מערכות הפעלה
Transcription
מערכות הפעלה
מערכות הפעלה סיכום הרצאות סיכם :אלעד ריכרדסון ערך והוסיף :אביחי כהן תודה לרז פקטרמן על תרומתו לסיכום ,ולכל מי שערך או תיקן . 1 הרצאה) 1 מצגת (1 מבוא למערכות הפעלה מערכות הפעלה ניתן לחלק לכמה סוגים ישנן מערכות הפעלה כמו DOS שהן פשוטות יחסית ותומכות במשתמש יחיד וישנן מערכות מתוחכמות יותר שתומכות בריבוי משתמשים ,הראשונה שעשתה זאת הייתה ,Unix אך כיום רוב מערכות ההפעלה הנפוצות תומכות בריבוי משתמשים . כמו כן מערכת הפעלה מסוימת מתאימה בד"כ לפלטפורמה מתאימה ,נעשה ניסיון בשלב מסוים לעשות מערכות הפעלה שמתאימות לפלטפורמות שונות ,למשל Microsoft טענו ש Windows NTיוכל לרוץ על כל חומרה ,מה שלא היה נכון בפועל Linux .לעומת זאת באמת רצה כיום על המון סוגים של חומרה ,החל ממחשבים ביתיים ,שרתים ועד לטלפונים ניידים . מדוע בעצם יש צורך במערכות הפעלה? כשאנחנו בתור מתכנתים הולכים וכותבים תוכנית חדשה אנחנו לא רוצים לחשוב על החומרה בה אנו משתמשים /היינו רוצים לעבוד על מעין מחשב אבסטרקטי ,אוניברסאלי ,בלי להתחשב בפרטים המדויקים של החומרה .כדי שנוכל לבצע הנחה כזאת צריכה להיות תוכנה שתבצע את הקישור בין המחשב האבסטרקטי לחומרה עצמה ,כאן נכנסת מערכת ההפעלה .מערכת ההפעלה הינה בעצם שכבה שמקשרת בין אפליקציה לבין המחשב הפיזי . Embedded Softwareתוכנה שהותאמה לחומרה מסוימת מערכת הפעלה חייבת להיות embedded שכן היא מותאמת לחומרה עליה אנו רצים .המון קוד במערכת ההפעלה הוא ספציפי לחומרה ,יש ממש ספריות שכתובות לארכיטקטורות השונות שעליהן תצטרך מערכת ההפעלה לרוץ .אם ננסה להתקין לינוקס על חומרה שאין ספריות בשבילה זה פשוט לא יעבוד ,צריך לעשות תהליך של .embedding חשוב להבחין כי כשאנחנו מתקינים מערכות הפעלה כיום ,אנחנו לא מקבלים רק את ה ,kernelהליבה של מערכת ההפעלה , אלא תוכנות נוספות שמגיעות עם ה ,distributionלמשל ספריות מתמטיות ,תוכנות המטפלות ב ,UIעורך טקסט ,קומפיילר ועוד .אנחנו נדבר בקורס רק על הגרעין של מערכת ההפעלה ,האלגוריתמים שבאמצעותם היא מנהלת את הזיכרון ואת התהליכים במחשב . מערכות ההפעלה הראשונות היו פשוט תוכנית אחת עצומה של קוד ספגטי ,ללא כל חוקיות ,מה שהקשה מאוד על הפיתוח , ובמיוחד על חלוקת העבודה בין מתכנתים שונים .באותה תקופה נוצר ה conceptשל .Microkernel לקחו את המרכיבים המרכזיים של מערכת ההפעלה ,ניהול תהליכים ,ניהול זיכרון וניהול דיסקים ,ושמו אותם בקובץ אחד קטן יחסית שלא עובר את ה 10,000שורות קוד .את השאר ה utilitiesהוציאו מחוץ לקרנל . די מהר גילו שאמנם זה הופך את חלוקת הקוד לפשוטה יותר אך יש בעיות ביצועים בשיטה הזאת .למשל כשאותו utility שמטפל ברכיב הרשת היה מקבל בקשות רבות היינו צריכים לבצע מעברים רבים בין התוכנה ברמה של המשתמש לבין הרמה של מערכת ההפעלה . בסופו של דבר הגיעו למסקנה שהדרך הנכונה היא כן להשאיר את הכלים האלה במערכת ההפעלה ,אך לחלק אותם ל .componentsכל קומפוננטה מטפלת בתחום אחר וממומשת כ ,ADTכך קל יחסית לקחת מנגנון מסוים ולהחליף אותו , בסה"כ צריך לדאוג שמממשים את כל ה interfaceכנדרש .זו השיטה שבה נהוג להשתמש כיום . מערכות הפעלה היום ובעתיד כיום אנחנו מתחילים לזנוח את המונח של" מחשב אישי" ,במקום לתת לכל אחד Desktop משלו ,מקימים Data Centers שבהם יש הרבה מחשבים יחסית חזקים .כשאני רוצה לעבוד אני מתחבר מהמחשב שלי ,שלא חייב להיות חזק במיוחד , למחשב המרוחק ומשתמש בכוח העיבוד שלו .מעבר לכך ,על אותו מחשב פיזי מרוחק ניתן לבצע סימולציה של מספר מחשבים חלשים יותר .כמובן שאני לא רוצה שמה שקורה במחשב המסומלץ שלי ישפיע על מה שיקרה במחשב מסומלץ אחר ,הפועל 2 באותו מחשב פיזי .לכן מה שעושים כיום הוא להתקין על המחשב מערכת הפעלה אחת הנקראת host או hypervisor והיא מסמלצת מספר מחשבים ודואגת לשמור על הפרדה ביניהם .אנו לא נדבר על מערכות כאלו בקורס . תפקידה של מערכת ההפעלה מה ההבדל המהותי בין מערכת ההפעלה לכל אפליקציה) תהליך( אחרת שאנו מריצים? לאפליקציה תמיד יש פעולה שברצונה לבצע ,או שהיא נמצאת במצב מנוחה .למערכת ההפעלה אף פעם אין משהו שהיא " רוצה" לעשות ,ישנן שתי סיבות מרכזיות שיכולות לגרום למערכת ההפעלה לבצע פעולה : ● ● בקשה מתוכנה מסוימת ,מתן שירות . חומרה חיצונית ברגע שהחומרה החיצונית אומרת למערכת ההפעלה שיש לה מידע חדש ,היא צריכה להגיב לה) .פסיקות חומרה( ישנן גם פסיקות תוכנה) חריגות ,תקלה בקוד שאנו מריצים כמו חלוקה באפס( .זו לא בקשה מתוכנה מסוימת וזו גם לא הודעה של חומרה חיצונית ,אך היא גם מטופלת ע"י מערכת ההפעלה .בד"כ מערכת ההפעלה פשוט תסיים את התהליך שגרם לשגיאה הזאת .אם הקוד שגרם לשגיאה הוא של מערכת ההפעלה נקבל את ה .Blue Screen Of Death System Callsקריאות מערכת איזה שירותים מערכת ההפעלה צריכה לתת למשתמש? נזכור שמערכת ההפעלה נותנת לתוכנת מחשב את ההרגשה שהיא חיה בתוך מחשב אבסטרקטי שבו היא התוכנית היחידה הקיימת ,לצורך כך מערכת ההפעלה לא נותנת לאפליקציה לעולם גישה ישירה לרכיבי החומרה ,למעט ביצוע פעולות אריתמטיות פשוטות ועבודה עם הרגיסטרים .כיום ,חומרה יכולה לדעת כאשר תהליך מנסה לגשת אליה באופן ישיר ולעצור אותו אם אין לו הרשאות . בין השאר מערכת ההפעלה מספקת את התכונות הבאות : ● ● ● ● היכולת לאכסן מידע בקבצים . היכולת לבצע תקשורת עם מחשבים אחרים . היכולת לבצע תקשורת עם אפליקציות אחרות באמצעות זיכרון משותף . היכולת לבצע הדפסה על המסך . בצורה זו מערכת ההפעלה מגבילה את האפליקציה מהשפעה מסוכנת על המחשב האמיתי ,ועל האפליקציות האחרות הרצות עליו .מערכת ההפעלה היא מעין הורה אחראי של כל התהליכים הרצים במחשב .צריך להבין את התוצאות ההרסניות של גישה ישירה של האפליקציה לחומרה חיצונית ,למשל האפליקציה הייתה יכולה להחליט שהיא מפסיקה את פסיקות השעון ובכך הייתה גורמת לכך שהיא תהיה האפליקציה היחידה שתרוץ על המחשב . בין אמצעי החומרה בהם תומכת מערכת ההפעלה נמצא את הבאים : ● שעון אין לשעון זה שום קשר לתדר של מחשב ,זהו פשוט אירוע שקורה בין 100 ל 1000פעמים לשנייה , בהתאם להגדרה ,ותפקידו למדוד זמן .אם יש לי שני תהליכים ששניהם רוצים לרוץ ,מערכת ההפעלה צריכה לתת לשניהם זמן ריצה ,אך במידה ויש מעבד אחד לא באמת ניתן להריץ אותם במקביל ,ולכן היא מקצה לכל אחד מהם פרק זמן מסוים לריצה ,ואז מחליפה אותו בתהליך השני וחוזר חלילה .זו אחת הסיבות המרכזיות לצורך בשעון . ● סנכרון כדי למנוע התנגשות בין חלקי קוד שונים במספר אזורים עדינים ,יש צורך בסנכרון פעולות שונות . למשל אם בעת הכנסת תהליך חדש לרשימת התהליכים ,מתבצעת גם הוצאה של תהליך אחר שהסתיים , חשוב לשמור על הסדר בין הפעולות .זו אחת הבעיות הקשות שבהן נתמקד .בד"כ סנכרון מתבסס על 3 תמיכה מהחומרה ,כל חומרה מגיעה עם פקודות מיוחדות המאפשרות למערכת ההפעלה לממש כל מיני מנגנונים של סנכרון שהמימוש בהם גם יעיל וגם פשוט ,ללא צורך בקוד מורכב . ● I/O Controllersכל חומרה חיצונית מתקשרת עם המעבד שלנו באמצעות פעולות .I/O ○ DMAבשיטה זו החומרה ניגשת ישירות למעבד וכותבת שם את המידע שהיא רוצה להעביר) .למשל כרטיס רשת( . הרשאות גישה לחומרה נזכור שאנו רוצים למנוע מהמשתמש לעשות פעולות אסורות כבר ברמת החומרה ,חייבים למנוע מצב שתהליך מנסה ומצליח בעצמו לגשת לחומרה חיצונית ,פוגע בתהליכים אחרים ועוד .הדרך לעשות זאת באופן וודאי וללא שגיאות היא באמצעות החומרה . בארכיטקטורה של אינטל למשל כשתהליך רץ מוגדרת עבורו הרמה שבה הוא רץ Current Privilege Level או Ring וישנן 4 רמות . ● ל Ring 3כמעט אין הרשאות ,אפליקציה ברמה זו יכולה לבצע חישובים ולגשת לרגיסטרים ,שם ירוצו האפליקציות של המשתמש . ● ● ל Ring 0יש את כל ההרשאות ,שם בד"כ תרוץ מערכת ההפעלה . Ring 1ו 2הן רמות ביניים שלא ממש משתמשים בהן . לכל פעולה מוגדרת רמה I/O PL) מהו ה PLשצריך כדי לבצע את הפעולה( .כשאנו ניגשים לזיכרון החומרה מסתכלת על ה) DPLמהו ה PLשצריך( ומשווה אותו עם CPL של הקוד ומוודאת ש .CPL<=DPL כשמבקשים שירות ממערכת ההפעלה ,עוברים מ CPL 3ל CPL 0מבצעים את הקוד של מערכת ההפעלה המטפל בבקשה של המשתמש וחוזרים ל .CPL 3 דוגמא : בחומרה יש טבלה בשם ,INTERRUPT TABLE ובה מופיעות הפונקציות המתאימות לכל פסיקה .בתא 128 נמצאת הפונקציה .sys_call() כאשר אנו מבצעים פקודת interrupt של שירות 128 תיקרא הפונקציה של שירות מערכת ,אך לפני כן החומרה מבצעת את המעבר ל .Ring 0כל קריאה למערכת ההפעלה נעשית באמצעות הפסיקה המסוימת הזאת ,קביעת השירות נקבעת לפי הערך ברגיסטר .eax כמו כן יש טבלה שאינה מוכרת לחומרה בשם ,SYS_CALL_TABLE ובה מוגדר השירות) הפונקציה( שיש לתת עבור כל ערך של .eax אחרי ביצוע הפונקציה חוזרים ל ,sys_callהפקודה האחרונה בה היא iret שמסיימת את הפסיקה ומחזירה את ה CPLמ 0ל .3 4 תהליכים מה מאפיין תהליך? מהו בעצם תהליך? ניתן להגיד כי תהליך הינו מופע של תוכנית הקיים בזמן ריצה ומנוהל ע"י מערכת ההפעלה . מעבר לקוד עצמו האחראי לביצוע התהליך ,התהליך מוגדר גם ע"י הנתונים ברגיסטרים ,במחסנית ובערימה ,למשל השורה הבאה לביצוע שמורה ברגיסטר .לכל תהליך מוגדר מספר תהליך (process id) המאפיין אותו ,וכן רשומה המתאימה למספר זה ומכילה מידע נוסף מתאר תהליך ,Process Control Block) או ב Linuxנקרא גם .(Process Descriptor תהליך יודע רק את מספר הת.ז .של עצמו ,ושל תהליכים אחרים .ה kernelמקבל את הת.ז .ולפיו מוצא את הרשומה של התהליך המכילה את כל המידע הדרוש לניהולו .בין השאר ,מצביע למחסנית של התהליך בזיכרון ,ערכי הרגיסטרים שלו בזמן הפסקת ריצתו ,קבצים שהוא פתח ,מי המשתמש שהריץ אותו ועוד . ב Linuxשעליו נעבוד יש 95 שדות ברשומה של תהליך . מרחב הכתובות כמו שאמרנו תפקידה של מערכת ההפעלה הוא לספק לתהליך מעין מחשב אבסטרקטי ופשוט עליו הוא עובד ,כחלק מכך כל תהליך חושב שיש לו מרחב כתובות רציף השייך לו בלבד . מרחב כתובות זה מחולק לסגמנטים באופן הבא : ● ● ● ● codeהקוד עצמו הדרוש לריצת התהליך . static dataמשתנים סטטיים ,גלובליים וקבועים של התוכנית . Heapהקצאות דינמיות שביצעה התוכנית Stackמשתנים מקומיים ,כתובת חזרה של פונקציה ,מצב הרגיסטרים לפני הקריאה לפונקציה ופרמטרים לפונקציה .כמו כן ,החזרה של struct נעשית על המחסנית . נבחין כי הערימה גדלה כלפי הכתובות הגבוהות ואילו המחסנית גדלה כלפי הכתובות הנמוכות ,מדוע? רוצים לתת לתהליך לנצל את כל המרחב שיש לו ,כחלק מכך נותנים לו את הגמישות ליצור ערימה גדולה או מחסנית גדולה בהתאם לצורך שלו ,ולכן הם צריכים לגדול בכיוונים הפוכים ,על מנת לנצל את אותו מרחב . 5 זמן חיים של תהליך את זמן החיים של תהליך ניתן לחלק למספר חלקים כפי שניתן לראות בדיאגראמה להלן . ● Newשלב יצירת התהליך והרשומה המתאימה לו .בסוף היצירה התהליך מועבר למצב ,Readyהוא אינו מתחיל ריצה באופן מיידי שכן יש תהליך אחר שנמצא במהלך ריצה . ● Readyהתהליך אינו רץ שכן יש תהליך אחר שרץ כרגע .מערכת ההפעלה יכולה להחזיק תהליך זמן רב במצב ,Ready עד להעברתו למצב .Running ● Runningמערכת ההפעלה מקצה זמן ריצה לתהליך ,ומאפשרת לו לבצע את עבודתו .אם התהליך הסתיים במהלך זמן הריצה שלו נעביר אותו למצב .Terminated במקרה שבו התהליך לא סיים את הריצה שלו ,אך חרג מה time sliceאשר הוקצה עבורו ,מערכת ההפעלה תחזיר אותו למצב .Ready אם תהליך מבקש לקבל נתונים ,לבצע פעולת I/O וכד' הוא יעבור למצב .Waiting נבחין כי תהליך שצריך לחשב חישוב מורכב על ה CPUיבצע מעברים רבים בין מצב Ready ל ,Runningבהתאם לזמנים שיוקצו עבורו . ● Terminatedמצב Zombie של התהליך ,בעת סיום ריצתו .למה לא פשוט מוחקים את התהליך אלא מחזיקים אותו במצב ?Terminated בלינוקס תהליכים יכולים לבקש לקבל הודעה על סיום של תהליך אחר ,המערכת מחזיקה את התהליך במצב Zombie עד שהיא מוודאת שכל תהליך אחר קיבל הודעה על סיומו של תהליך זה ורק לאחר מכן היא מחסלת אותו ,ומוחקת אותו מהזיכרון . ● Waitingה CPUלא יכול לספק באופן מיידי את נתוני ה ,I/Oיש לחכות עד להגעת הנתונים . מהרגע שהוצאנו בקשה לקבל את הנתונים יכולים לעבור חמישה מיליון)!( מחזורי שעון .ולכן נעביר אותו למצב המתנה ונטפל בתהליכים אחרים .ברגע שהאירוע התרחש ,נתוני ה I/Oהגיעו למשל , מערכת ההפעלה מעירה את התהליך ומביאה אותו למצב) Ready שהרי כרגע יכול להיות שיש כבר מישהו אחר שרץ( . Context Switch מה קורה כשרוצים להחליף תהליך אחד בתהליך אחר? בעצם רוצים שהקוד של תהליך 1 והמידע שנשמר עבורו יוחלפו בקוד ובמידע של התהליך האחר .בכל ארכיטקטורה קיים רגיסטר המציין את המיקום בזיכרון של הפקודה הבאה לביצוע) באת"מ זהו ה R7שמציין את ה (PCוכן ישנו רגיסטר המציין את המיקום של המחסנית) באת"מ זהו R6 שמציין את ה .(SPבעצם כל מה שצריך לעשות הוא להחליף את ערכי הרגיסטרים ,והם אלו שיקבעו איזה תהליך ירוץ לאחר העדכון . 6 כיצד זה יתבצע בפועל? בשלב הראשון ניקח את כל ערכי הרגיסטרים של התהליך שרץ כרגע ונשמור אותם ברשומה שלו) ה .(Process Control Blockלאחר מכן נטען את ערכי הרגיסטרים של תהליך 2 מתוך הרשומה שלו וכך בעצם נעביר אותו לתהליך הפעיל .על אותה הדרך נבצע את ההחלפה של תהליך ,2 חזרה . תהליך זה של ניהול התהליך המקבל זמן ריצה וגישה למשאבי המערכת נקרא . Scheduling תורי מצבים מערכת ההפעלה מחזיקה את התהליכים הקיימים בתורים ,כך שכל תהליך צריך להימצא בתור מסוים .בתור ה Ready ,Queueתור התהליכים שמחכים ל .CPUאו בתור המתנה כלשהו ,תור של תהליכים המחכים לאירוע מסוים . נבחין כי יש תור Ready יחיד),תחת ההנחה של מעבד יחיד( ,אך מספר תורי המתנה עבור אירועים שונים .מעבר לכך ,יש תורי המתנה שבעת הגעה של אירוע יתעורר רק תהליך אחד בתור) למשל בהמתנה להקשת מקלדת רק תהליך אחד יכול לטפל בהקשה( ,ויש כאלו שבהם נרצה להעיר את כל התהליכים ולהעבירם למצב) Ready למשל אם תהליכים מחכים לשעה מסוימת ,אפשר להעיר את כולם ביחד( . 7 קריאות מערכת וShell- Fork פונקצית השירות fork משמשת לפיצול תהליכים ,היא יוצרת עוד מופע של התוכנית ומקצה לו את המשאבים הנדרשים . התהליך יודע האם הוא התהליך המקורי או הבן לפי ערך ההחזרה של .fork בעצם הפונקציה" fork חוזרת פעמיים" ,כך שעבור כל תהליך היא מחזירה ערך שונה ,בתהליך של הבן היא מחזירה ) 0 זהו pid לא חוקי( ,ובתהליך של האב היא מחזירה את ה pidשל הבן . מרגע החלוקה לכל תהליך יש מחסנית PC ,ומשאבים משלו ומבחינת מערכת ההפעלה מדובר כבר בשני מופעים שונים , למרות כל זאת הם חולקים את אותו הקוד וכן יש לאב ולבן משאבים משותפים ,למשל כברירת מחדל הקבצים משותפים . Forkהינה הדרך היחידה שלנו ליצור תהליכים חדשים) לפחות ב Scopeשל הקורס( . execv כמובן שלעיתים קרובות נרצה להתחיל הרצה של תוכנית אחרת ,שאינה חלק מהקוד שלנו .פונקצית fork לא מאפשרת לנו לעשות זאת .לצורך כך נשתמש בפונקציה execv אשר עוצרת את ביצוע התוכנית הנוכחית וטוענת את prog שהועברה לה )ניתן להעביר גם ארגומנטים( נבחין כי קוד זה אינו מתחיל תהליך חדש ,אלא מחליף את התוכנית שמריצים בתוך אותו תהליך ואותו,PCB וכך ניתן להריץ איזשהו exe שרירותי . באמצעות שתי הפונקציות האלו נוכל לממש shell מינימאלי ,כפי שניתן לראות בקוד הבא : מה בעצם עושה ה shellבדוגמא? הוא רץ בלולאה אינסופית ומקבל בכל פעם פקודה חדשה ,עבור כל פקודה הוא מבצע fork כך שהפקודה שהתקבלה תרוץ על תהליך חדש וה shellיוכל להמשיך ולקרוא את הפקודות הבאות וליצור תהליכים גם עבורן . נבחין כי הקוד יודע האם הוא הבן או ה shellהמקורי לפי ה pidשהוחזר . כמו כן ,נבחין כי execv היא פונקציה שלא אמורה לחזור ,ולכן אם הגענו לשורה fprintf שמופיעה אחריה מדובר בשגיאה ,ויש לטפל בה . נשים לב לבעיה ב shellהזה ,נניח שפקודת execv נכשלה ,לאחר הדפסת הודעת השגיאה התהליך יישאר בלולאה אינסופית של ה shellוינסה לתפקד כ shellבעצמו ,הפתרון הנכון היה כמובן לבצע break או return במקרה של כישלון . 8 הרצאה) 2 מצגת (2 Scheduling כפי שהזכרנו בהרצאה הקודמת Scheduler ,הוא הקוד במערכת ההפעלה שתפקידו לקבוע איזה תהליך ירוץ כעת . כדי להבין את חשיבותן של שיטות Scheduling שונות נרצה לשנות קצת את צורת המחשבה שלנו ולעבור מחשיבה על מחשבים אישיים למחשבי על . ● בניגוד למחשבים האישיים שאנו מכירים ,מחשבי על מורכבים מכמות גדולה יותר בסדרי גודל של ליבות כך שכל אחת מהן בפני עצמה חלשה בסדרי גודל מליבות של המעבדים בשוק המחשבים האישי ,אך יחדיו הן חזקות משמעותית מהמחשבים האישיים שאנו מכירים ,על תקן" זו לא האיכות זו הכמות" . עוצמתן החלשה מאפשרת לאכסן את הליבות בצפיפות מאוד גבוהה ללא בעיות של עומס חום . ● בד"כ באותו מחשב על משתמשים מספר רב של משתמשים) עד 100 משתמשים שונים( ,המשתמשים בו במקביל . ● לכל עבודה לביצוע מוקצות מספר ליבות לצורך החישוב ,זהו הגודל של העבודה ,שמוגדר ע"י המשתמש . במהלך ביצוע העבודה הליבות יכולות לתקשר זו עם זו . לכל תהליך שרץ במחשב יש בעצם שני מימדים :זמן הריצה שלו ומספר המעבדים שהוא משתמש בהם ,בניגוד למחשבים אישיים שבהם בד"כ כל תהליך רץ על מעבד אחד .במחשבי על נרצה לעיתים קרובות להקצות מספר ליבות לאותו התהליך . Batch Scheduling בקבוצת שיטות תזמון זו תוכנית רצה בלי הפרעות ,ללא עצירה ,עד לסיומה .הרבה פעמים כותבים את התוכניות האלה כך שהן ייכנסו בזיכרון ולא יהיה צורך להשתמש בדיסק .בכל נקודת זמן יש רק תוכנית אחת שמשתמשת בליבות .רק בסוף ביצוע התהליך ניתן להקצות את הליבות לתהליכים אחרים .תהליכים שרצים באופן זה נקראים .nonpreemptive מטריקות שונות לצורך בדיקת מערכת ישנם מספר פרמטרים שמעניינים אותנו ,נגדיר אותם כעת : ● Average Wait Timeזמן ההמתנה הממוצע בין הגשת התהליך עד לתחילת הביצוע שלו . ● Average Response Timeזמן התגובה הממוצע בין הגשת התהליך עד לסיום הביצוע שלו . 9 ● ההפרש בין זמן התגובה הממוצע לזמן ההמתנה הממוצע הוא קבוע השווה לזמן הריצה הממוצע . נשים לב ששתי מטריקות אלו הן ממוצעות ,ביחס למספר רב של תהליכים שאנו מריצים ,כמו כן את שתיהן נשאוף לצמצם ככל הניתן ובכך לשפר את הביצועים . בתור מתכנן מערכת הפעלה אין לנו ממש השפעה על זמן הריצה ,הוא תלוי במרכיבי החומרה וביעילות האלגוריתמים בתוכנית ,אך זמן ההמתנה נתון להשפעתנו דרך ה .Scheduling ● Average slowdownהיחס בין זמן התגובה לזמן הריצה ,זו מטריקה נוספת שנשתמש בה לעיתים כדי לאפיין את המערכת .בעצם יחס זה אומר" כמה חיכיתי ביחס לכמה זמן לקח לבצע אותי" .נרצה לצמצם את ה slowdownבמטרה למנוע מצב בו זמן ההמתנה ארוך משמעותית מזמן הריצה . ● ) Utilizationניצולת( אחוז הזמן שבו המעבד מבצע עבודה פרודקטיבית ,נרצה למקסם . ● ) Throughputספיקה( כמה עבודה אנחנו מסיימים ביחידת זמן ,נרצה למקסם . המטריקות שמעניינות אותנו הן תלוית הקשר ,למשל כמשתמשים אכפת לנו מה response, waitו slowdown ואילו כבעלי המחשב נתעניין בניצולת ובספיקה ,שכן נרצה לנצל את החומרה שלנו באופן מירבי . נקודה מעניינת היא שתהליכים ארוכים לא משפיעים מאוד על המטריקה של ה ,Slowdownאלא על המטריקה של ה .Response Timeשכן נדיר מאוד שזמן ההמתנה שלהם גדול ביחס לזמן הריצה .בהתאם לכך התהליכים הקצרים הם בעלי ערכי ה slowdownהגדולים יותר ,שכן זמן ההמתנה שלהם יכול להיות פי כמה וכמה מזמן הריצה .אך ה Response Timeהכולל שלהם הוא יחסית קצר . FCFS Scheduling בשיטה זו של First Come First Served העבודות מתוזמנות לפי זמן ההגעה שלהם .העבודה שנמצאת בראש התור תבוצע ברגע שיהיו מספיק ליבות פנויות .שיטה זו הינה פשוטה מאוד ,אך קל לראות שהיא אינה יעילה במיוחד .במקרים רבים ליבות יהיו במצב של אבטלה בזמן שנחכה שיתפנו מספר ליבות ,כאשר בשיטה מתקדמת יותר היינו יכולים להשתמש בזמן זה כדי לבצע עבודה קצרה יותר שדורשת מספר קטן יותר של ליבות לעומת העבודה שיושבת בראש התור . EASY Scheduling מובססת על שיטת ,FCFS אך בזמן שמחכים שיתפנו מספיק ליבות לצורך ביצוע העבודה שבראש התור אנו מחפשים עבודה אחרת שנמצאת בהמתנה בתור כך שאם נריץ אותה הוא תסיים את פעולתה בזמן ההמתנה של העבודה הראשונה מבלי לעכב את פעולתה ,תהליך זה נקרא .backfilling הקושי המרכזי בשיטה זו הוא שהיא דורשת מאיתנו לחזות את זמן הביצוע של עבודה מסוימת ,לפני הביצוע שלה , הפתרון הנפוץ במחשבים אלו הוא פשוט להכריח את המשתמש לציין את זמן הריצה הצפוי לתוכנית ,כאשר אם התוכנית רצה יותר מהזמן הצפוי שלה היא תושמד .כך מבחינת המשתמש כדאי לו לציין חסם עליון ,על מנת למנוע את הריגת התוכנית שלו ,אך חסם עליון הדוק כדי לאפשר הכנסה של התהליך בזמן פנוי .זו באמת השיטה הנפוצה במחשבי על Easy = Extensible Argonne Scheduling System . 10 SJF Scheduling שיטת Scheduling זו ממיינת את העבודות לפי זמן הריצה שלהם ,ולא לפי זמן ההגעה שלהן .תוכניות שלהן זמן ריצה קצר יותר יתבצעו קודם .היתרון של שיטה זו שהיא מורידה באופן משמעותי את ה Wait Timeהממוצע ,שכן תהליכים קצרים לא נפגעים על ידי תהליכים ארוכים .הבעיה המרכזית של שיטה זו היא שיש מצב תיאורטי שבו עבודה מסוימת אף פעם לא תרוץ ,כאשר בכל פעם תגיע עבודה אחרת שהיא קצרה ממנה .ובכל מקרה אנו מרעיבים את העבודות הכבדות יותר ומעכבים את הריצה שלהן . הערה ,SJF Shortest Job First :נקרא גם SPTF Shortest ProcessTime First אפקט השיירה (Convoy Effect) ב Schedulingשהוא ,nonpreemptive בו לא ניתן לעצור את פעולתו של תהליך ,תמיד ייתכן מצב שבו נוצר תור עצום של תהליכים המחכים לסיומו של תהליך אחד המנצל את כל הזיכרון ,מצב זה מכונה אפקט השיירה .שיטת SJFמנסה לצמצם את אפקט השיירה בכך שהיא נותנת לתהליכים הקצרים יותר להתבצע קודם לכן ,אך זוהי עדיין סיטואציה שיכולה לקרות במצבים מסוימים) למשל כאשר מגיע תהליך המנצל את כל הזיכרון ואין אף תהליך אחר המחכה לביצוע ,התהליך הכבד יתחיל להתבצע ולא ניתן יהיה לעצור אותו לאחר מכן( . SJF Optimality בהינתן מערכת עם ליבה אחת המקבלת את כל התהליכים באופן סדרתי ,ותחת ההנחה כי כל התהליכים מגיעים ביחד וזמן הריצה שלהם ידוע :נרצה להוכיח כי זמן ההמתנה הממוצע של של SJF קטן או שווה לזמן ההמתנה הממוצע של כל Batch Scheduling אחר . רעיון ההוכחה : ● יהי S סידור כלשהו של התהליכים המחכים לריצה ● אם הסידור S שונה מהסידור שהיינו מקבלים מ SJFניתן להסיק כי קיימים לפחות שני תהליכים P i ו P i+1כך שזמן הריצה של , R(P i) P i גדול מזמן הריצה של . R(P i+1) P i+1 נחליף ביניהם ונקבל כי קיצרנו את זמן ההמתנה של P i+1 ב ) , R(P iומצד שני הגדלנו את זמן ההמתנה של P i ב ) . R(P i+1 מכיוון שמתקיים R(P i) > R(P i+1) קיבלנו כי בסך הכל שיפרנו את זמן הריצה . ● נמשיך באותה הדרך עד שנקבל את הסידור של .SJF 11 ובאופן יותר פורמלי : וריאציות מבוססות SJF 12 הרצאה) 3 מצגת (2 Preemptive Scheduling הערה :במהלך ההרצאה הקרובה נניח כי תהליך תמיד מוכן לריצה ,כלומר הוא במצב ready או running ולא מגיע ל .waiting Preemption ) Preemptionאו הפקעה( מתאר מצב בו התהליך לא סיים לעבוד ומערכת ההפעלה" מפקיעה" ממנו בכוח את זכות הריצה ,ומעבירה את המעבד לתהליך אחר . מתי הגיוני לבצע ?Preemption כאשר שני תהליכים הם בערך באותו האורך לא נרוויח מכך . נניח שיש לנו שני תהליכים הזקוקים ל 10שעות כל אחד ,אם נבצע ריצה לסירוגין ונחליף תהליך כל שניה ,אז נקבל כי במקום תהליך אחד שיגמור את פעולתו תוך 10 שעות ותהליך שני שיגמור תוך 10 שעות נוספות נקבל שני תהליכים שיגמרו את פעולתם כמעט יחדיו תוך 20 שעות ,ולכן ה Preemptionלא יסייע לנו כאן ,אלא להיפך . עם זאת ,כשתהליך אחד הרבה יותר קצר מהאחר נרצה לבצע Preemption ובכך לתת הזדמנות לתהליך הקצר לסיים את פעולתו במקום לחכות לסיום פעולת התהליך הארוך ,מבלי להשפיע באופן משמעותי על התהליך הארוך . דוגמא קלאסית אחרת לשימוש ב Preemptionהיא הדמיה של ,multitasking הנפוצה במחשבים הביתיים כיום . נניח לדוגמא שיש לנו תהליך כגון מעבד תמלילים שלא צורך משאבים רבים אך צריך להציג למשתמש תגובתיות גבוהה ויש תהליך אחר שרץ ברקע וזקוק למשאבים רבים .באמצעות preemption נוכל להריץ את התהליך הכבד ועם זאת באמצעות החלפה ביניהם כל פרק זמן קצר לאפשר למעבד התמלילים להציג למשתמש פעולה חלקה של מעבד התמלילים . מטריקות ומושגים לPreemptive Schedulers- חלק מהמטריקות שהצגנו בעבר קצת משתנות עבור Preemptive Scheduling ● Wait Timeלא משתנה . ● Response Timeכאן בא לידי ביטוי השינוי ,כאשר יש preemption במהלך הביצוע זמן זה יכנס גם ל response timeשל התהליך שלנו ,ולכן נקבל Response Time שהוא לפחות כמו במערכות batch Quantum פיסת הזמן המקסימלית שבה תהליך יכול לרוץ ללא הפרעה . ישנן מערכות הפעלה כגון Solaris שבהן לתהליכים בעלי עדיפות גבוהה ,למשל מעבד תמלילים ,ניתן Quantum קטן ,כך שהם ירוצו לעיתים קרובות אבל לזמן קצר ,ואילו התהליכים בעלי הקדימות הנמוכה ניתן Quantum ארוך יותר כך שאם הם כבר רצים הם ירוצו לזמן ארוך .במערכות Linux רגילות זה הפוך ,כך שזה לא באמת מהותי ונתון לוויכוח . תקורה overhead כשמחליפים הקשר יש את התקורה הישירה הנובעת מהחלפת הרגיסטרים ,כמו כן יש גם תקורה עקיפה הנובעת מכל מיני מבנים בחומרה שמבצעים אופטימיזציה של ביצוע התהליך ומאיצים את פעילותו .בכל פעם שאנו משנים הקשר אנו מלכלכים אותם והם צריכים להתאים את עצמם מחדש לתהליך .תקורה עקיפה יכולה לעלות לנו סדר גודל יותר גדול מתקורה רגילה וצריך להתחשב גם בה . 13 ) RoundRobin Scheduling (RR השיטה הפשוטה ביותר ,כל תהליך רץ עד שנגמר לו ה ,Quantumואז עוברים לתהליך הבא .כשעוברים את כל התהליכים נגמר) epoch עידן( ומתחילים עידן חדש באותה הדרך .כמובן שיש לנו צורך כאן בשעון כדי למדוד את ה .Quantumשעון זה מתבצע באמצעות פסיקת חומרה ,ניתן להגדיר פרמטרים שונים של משכי זמן לחומרה . מגדירים interrupt handler לשעון שתפקידו יהיה לבצע Context Switch לתהליך הבא . נבחין כי ככל שה Quantumשואף לאפס נקבל הדמיה של שני תהליכים הרצים במקביל בחצי המהירות של תהליך הרץ לבד ,ואילו כאשר ה Quantumשואף לאינסוף נקבל ,First Come First Served כמו ב .Nonpreemptive Scheduling האם עדיף שכל התהליכים יחכו בתור אחד משותף לריצה ,או שהתורים יחולקו לפי המעבדים השונים? הבחירה הטבעית היא לעשות תור אחד משותף לריצה ,בצורה זו לא ניתן לרמות את המערכת ,כשמעבד כלשהו מתפנה התהליך שבראש התור יתחיל לרוץ .בפועל יש היום נטייה לעשות תורים שונים למעבדים השונים ,מכיוון שבריצה עם מספר מעבדים השימוש בתור יחיד דורש סנכרון מאוד עדין בין המעבדים . אופטימליות של NonPreemptive Scheduling נניח שיש לנו קבוצת תהליכים הרצים באופן לא רציף ,כלומר במדיניות עם הפקעה .אם כל התהליכים כבר רציפים , אז באופן מיידי ניתן להריץ אותם .nonpreemptive אחרת ,נניח שיש תהליך לא רציף ,נתחיל מהתהליך האחרון ונחפש את התהליך הראשון שלא רץ בצורה רציפה , נאסוף את כל חלקי ההרצה של התהליך הנ"ל מלבד האחרון ונסיר אותם .לאחר מכן נקדים את שאר הפרוסות שרצות ונצופף את הרווחים .נקבל מקום המתאים בדיוק לחלקי ההרצה שהסרנו ,נכניס אותם שם ונקבל הרצה רציפה גם של תהליך זה .נמשיך על אותה דרך ונקבל הרצה רציפה גם של תהליכים אחרים .נתבונן בשינוי בזמנים? כמובן שזמן הריצה הממוצע לא השתנה לאף תהליך . מה לגבי זמן המתנה? ● כל מה שאחרי התהליך הלא רציף הראשון לא השתנה . ● כל מה שלפני הפרוסה הראשונה של התהליך הלא רציף הראשון לא השתנה . ● האם זמן ההמתנה של התהליך הלא רציף השתנה? נזכור כי זמן ההמתנה במקרה של preemption הוא זמן הסיום של התהליך פחות זמן הריצה שלו ,וכמובן שזה לא השתנה ,הוא קיבל בדיוק אותו זמן ריצה והסתיים בדיוק באותו מקום . ● מה לגבי התהליכים שצופפנו? הקדמנו כל אחד מהם לפחות בפרוסה אחת ולכן הוא בהכרח מסתיים יותר מוקדם ,כלומר זמן ההמתנה שלו קטן . ולכן זמן ההמתנה לא נפגע ,במקרה הטוב הוא אפילו השתפר .נמשיך באותו אופן על כל התהליכים עד שנקבל כי כל התהליכים רציפים ,וקיבלנו scheduling שהוא nonpreemptive לאחר לכל היותר n איטרציות ,כאשר n הוא מספר התהליכים שהרצנו . אז הוכחנו כי לכל אלגוריתם עם הפקעה יש אלגוריתם בלי הפקעה שהוא לפחות טוב באותה מידה .הראנו בהרצאה הקודמת כי לכל אלגוריתם ללא הפקעה SJF הוא האופטימלי ,ולכן תחת התנאים שכל התהליכים מגיעים ביחד ,יש מעבד יחיד וזמני הריצה ידועים אז SJF הוא האלגוריתם האופטימלי . 14 ובאופן יותר פורמלי 15 הוכחנו לפני מספר שורות כי לכל אלגוריתם preemptive קיים אלגוריתם nonpreemptive עם זמן המתנה שווה אם לא טוב יותר .אז למה בעצם אנחנו צריכים ?Preemptive Schedulers ● ראשית יש לכך שימוש חשוב במחשבים אישיים לצורך הפעלת תהליכים במקביל .בתור משתמשים נרצה לשפר זמן המתנה ,אבל לא זמן המתנה ממוצע ,אנחנו רוצים לשפר ספציפית זמני המתנה של תהליכים הדורשים למשל קלט מהשתמש .מערכת preemptive נותנת לנו את הגמישות לעשות זאת ,לאפשר את פעולתו של תהליך המקבל או מציג קלט למשתמש ,גם אם תהליך אחר כבר התחיל את ההרצה שלו . כמו כן ,נזכור כי ההנחות שהנחנו בשתי ההוכחות אינן עומדות תמיד בתנאים אמיתיים ,כדי ליצור סדר הפעלה יעיל באלגוריתם nonpreemptive נצטרך לדעת מראש את סדרת התהליכים הרוצים לרוץ במחשב ,מה שלא מובטח לנו ,ולכן זמן ההמתנה של nonpreemptive לא בהכרח יהיה טוב יותר .וכמו כן ברוב המחשבים כיום אין מעבד יחיד . דיברנו כבר על הבעייתיות שיכולה להיות באלגוריתמים preemptive בתחילת ההרצאה .בניסיון להריץ שני תהליכים ארוכים שנניח שזמן הריצה שלהם הוא ,1000 נקבל כי במקום זמן תגובה ממוצע של 1000) 1500 לראשון ו 2000לשני ב (SJFנקבל זמן תגובה ממוצע של כמעט) 2000 התהליכים ירוצו" במקביל" וסיימו את פעולתם באתו הזמן( . מתקיים כי זמן ההמתנה המקסימלי ב RRהוא לכל היותר פעמיים זמן ההמתנה של :SJF כלומר זמן הריצה של התהליך ה kהינו סכום העיכובים שלו ע"י תהליכים אחרים ,כולל הוא עצמו) כיוון שהעיכוב שהוא מעכב את עצמו מוגדר להיות זמן הריצה שלו ,וכל העיכובים שתהלכים אחרים מעכבים אותו הוא זמן ההמתנה שלו( . ובאופן יותר פורמלי : 16 Gang Scheduling RR in a parallel system כאשר במערכת מקבילית יש לנו תהליכים שזמן הריצה שלהם קטן משמעותית מזמן הריצה של תהליכים אחרים ישנו היגיון בביצוע .Preemptive Scheduling שיטה זו של ביצוע RR במערכות מקבילות נקראת Gang .Scheduling נחלק את הזמן ל ,slotsכל עבודה יכולה לרוץ במהלך slot מסוים ,ולאחר מכן פעולתה מופסקת .האלגוריתם מנסה למלא" חורים" בזמן הריצה ע"י הכנסת תהליכים קצרים יותר למקומות פנויים אלו .האלגורים מנסה להביא למינימום את מספר ה slotsע"י איחוד של " חורים" כאשר זה אפשרי .אלגוריתם זה הוא שימושי כי הוא לא דורש שנדע מראש את זמני הריצה שלה תהליכים . תהליכים שרוצים לרוץ ביחד צריכים לרוץ באותו ה ,time slotבתוך אותו time slot יכולים להיות כמה !quantumsהמערכת מנסה לנייד תהליכים ולבחור באיזה מעבד הוא רוצה לרוץ ,מטרתה היא לפנות מעבד ולהריץ עליו תהליכים חדשים של משתמש אחר . ככל שיש יותר slots המערכת איטית יותר ,שני slots מספקים לנו מערכת איטית פי שתיים . 17 עבור slot מסוים התהליך שרץ בו יהיה אותו תהליך עד לסוף הריצה שלו .אורך ה slotמוגדר כמשך הזמן שהתהליך יכול לרוץ עד ל .preemptiveמספר ה slotsהוא בעצם מספר התהליכים שרצים במקביל . שיטה זו נתמכת ברוב המחשבים אך לא משתמשים בה שכן היא נועדה להרצת RR על מחשבי על ,וידוע שבמקרים כאלה עדיף להריץ .Batch נבחר quantum כך שהתקורה קטנה מאוד באופן יחסי ,אחרת נשלם המון על תקורה . נניח שהתקורה עולה אפס ,במקרה זה תמיד נוכל לשפר תזמון preemptive קיים ,ל scheduleמסוג batch שהזמן שלו יהיה קצר יותר ,בהינתן שנוכל לסדרם בכל סדר שנרצה וזמני הריצה ידועים מראש ,וכמובן שאם ה Context Switchעולה אז נקבל שיפור אפילו טוב יותר . אז מה בכל זאת היתרון ב ?Preemptiveמאפשר סיום מהיר של תהליכים קצרים . SRTF אחת החולשות של SJF היא שהיא מניחה שכל התהליכים מגיעים באותה העת SRTF ,היא וריאציה של SJF שלא מניחה הנחה זו ומשתמשת ב Preemptionכדי להתמודד איתה באופן יעיל .בכל פעם שמגיעה עבודה חדשה ,או שעבודה כלשהי הסתיימה נתחיל את העבודה עם זמן הריצה הנותר הקצר ביותר .הדבר נותן לנו פעולה אופטימלית .וקל לראות שאם התהליכים מגיעים באותה העת אנו מקבלים בדיוק את .SJF זהו אלגוריתם אופטימלי גם ללא ההנחה של) .SJF אנחנו מניחים שהחלפת הקשר לא עולה לנו זמן( . Selfish RR עדיפות התהליכים מוגדרת לפי פז"ם ,התהליכים הותיקים יותר רצים ב ,RRואילו התהליכים החדשים רצים ב FIFOכשאף תהליך ותיק לא צריך לרוץ . ניתן לשחק עם פרמטר ההזדקנות) , (agingהזדקנות מהירה נותנת לנו ,RR ואיטית נותנת לנו .FCFS עדיפויות לכל תהליך מקצים חשיבות ,ב SJFלדוגמא ,העדיפות מוגדרת לפי אורך זמן הריצה ,ככל שאתה יותר קצר תרוץ מוקדם יותר ,הדבר מאפשר לנו לקבוע שתהליך מסוים הוא חשוב אפילו אם זמן הריצה הנותר שלו הוא עוד ארוך . Negative Feedback Principle נחשוב על תהליך שרוצה לרוץ הרבה ,אם תהליך זה יחכה טיפה יותר לא יקרה לו נזק משמעותי .לעומת זאת תהליך שזקוק רק לזמן קצר מאוד ,אפילו פחות מהזמן המוקצה ,כדי לטפל באיזשהו קלט .לכן נרצה לסווג את התהליכים ולהבין איזה תהליך הוא CPUBound ואיזה תהליך הוא .I/OBound דרך אחת לסווג היא לפי ניצול זמן הריצה ,מי שניצל פחות מהזמן שהוא קיבל הוא כנראה I/O Bound ולכן נרצה להקדים אותו ,ומי שניצל את כל הזמן שלו הוא כנראה CPUBound ולכן נעכב אותו . נבין כי בעיית התזמון היא קשה מאוד ,רוב ה schedulersשמערכות הפעלה השונות הם פשוטים למדי כאשר השיטה השולטת במתן עדיפויות היא : ● ריצה מורידה את העדיפות להמשיך לרוץ ● איריצה מגבירה את העדיפות לרוץ עוד כמו כן תהליכים הקשורים ל I/Oמקבלים עדיפות גבוהה יותר בד"כ . 18 Multilevel priority queue כל מערכות ההפעלה שאנו מכירים מבצעות תהליך הדומה לזה ,יש מספר תורי RR המתאימים לעדיפויות שונות , כאשר ככל שתהליך מקבל יותר זמן ריצה הוא הולך ויורד בתורי העדיפויות ,בהתאם ל Negative Feedback .Principle ב Windowsתורים גבוהים מקבלים Quantum מאוד קצר ,אך הם בד"כ בקושי מחכים . ב Linuxהגישה שונה ,התהליך החשוב מבחינתי הוא זה שרוצה לרוץ הכי הרבה זמן .הם מקבלים גם Quantum ארוך וגם זמן המתנה קצר . ה Scheduler-של לינוקס הגדרות) :להשלים הגדרות מהשקפים!!( ● ● ● ● ● ● ● Taskכל תהליך שרץ במערכת ההפעלה לינוקס ,לכל Thread גם כן נקרא .task epochכאשר מריצים RR על התהליכים במערכת כל תהליך רץ זמן מסויים (Quantum) שמוקצה לו , כאשר כל התהליכיםc מערכת מסיימים את ריצת ה Quantumשלהם נגמר ה) epochעידן( ומתחיל אחד חדש שבו שוב כל התליכים רצים את ה Quantumשהוקצה להם . Task’s priorityלכל תהליך מוגדר מספר שמייצג את העדיפות שלו ,ככל שמספר זה יותר גבוהה ישנה עדיפות ריצה יותר גבוהה לתהליך זה .לכל תהליך יש 2 סוגי עדיפויות דינמית וסטטית . Task’s static priorityעדיפות זאת לא משתנה במהלך הריצה אלא אם המשתמש מזמן את פונקצית המערכת ,nice העדיפות הסטטית מגדירה את ה Quantumהמקסימאלי עבור התהליך . Task’s dynamic priorityעדיפות זאת מייצגת את הזמן שנשאר לתהליך לרוץ ,עדיפות זאת קטנה עם הזמן ,כאשר היא מגיעה לאפס התהליך מפסק לרוץ כלומר הוא מוותר על המעבד .בכל תחילת עידן חדש העדיפות הדינמית מתחדשת לערך העדיפות הסטטית של התהליך . processorאיזה core אתה ,אין משימה שמקבלת יותר מליבה אחת . need_reschedדגל המסמן שיש צורך לבצע תזמון מחדש של התהליכים . הערות : * לכל תהליך מוקצה Quantum משלו ,המהווה את זמן הריצה שלו בטיקים .בכל טיק יורד זמן הריצה שנותר לנו . במערכת Linux שאנו לומדים שעדיפות שקולה לזמן הריצה ,ככל שיש יותר זמן ריצה מקבלים עדיפות גבוהה יותר .זמן הריצה מוגדר לפי ה ,Kernel's Nicenessוכתוצאה מכך גם זמן הריצה . *בביצוע fork זמן הריצה מתחלק ,על מנת למנוע ניסיונות לרמות את המערכת .ולהשיג ריצה ארוכה ע"י ביצוע .fork * בשקף 49 מגדירים את העדיפות כמספר הטיקים ב ,50msוזהו הקשר בין עדיפות לזמן אמיתי) .טיק אחד רץ ב .(10ms לכל task_Struct יש חמישה שדות שבהן משתמש ה :Scheduler ● niceקובע עדיפות סטטית של תהליך . ● counterכמה פסיקות שעון (ticks) נשאר לתהליך עד להחלפתו על ידי תהליך אחר . ● processorה idשל המעבד עליו התהליך רץ כרגע . ● need_reschedהאם התהליך יכול להמשיך לרוץ או שצריך להחליף אותו) מעבד אחר יכול לעיתים לזהות את הצורך בהחלפת הקשר של תהליך מהמעבד שלנו ,הוא אמנם אינו יכול להחליף אותו אך הוא יכול לסמן שיש לבצע החלפה( ● mmמצביע למרחב הזיכרון הוירטואלי ,מתאר את הזיכרון של התהליך . 19 :Nice ● משתמש יכול לקבוע את הערך בין מינוס 20 ל .19ערך קטן יותר נותן עדיפות גבוהה יותר ,והערך ה def הוא אפס ● ב Kernelמתבצעת המרה לערך בין 1 עד ,140 כאשר ככל שהערך גדול יותר יש עדיפות גבוהה יותר . :Task's counter ● כשלכל התהליכים שמוכנים לרוץ נגמר ה ,epochה ,time sliceעוברים אחד אחד ומחשבים עבורם את ה time_sliceבשדה counter ● נבחין כי הנוסחא לחישוב ה counterהינה .c = c/2 + alpha ● לכאורה ה c/2מיותר שהרי עבור תהליכים שנגמר זמן הריצה שלהם ה .c=0אך נזכור כי יש גם תהליכים שנמצאים במצב waiting ועבורם c שונה מאפס .נבחין כי אם תהליך כל הזמן ממתין ואף פעם לא מקבל זמן ריצה ה counterשלו מתכנס ל ,(nice_to_tick)*2וחסום על ידיו! בעצם זו הדרך שבה אנו נותנים בונוס לתהליכי I/O שנמצאים זמן רב בהמתנה . ● עבור תהליכים שרצים כל הזמן הערך תמיד .nice_to_tick תהליך אופייני עם ערך NICE דיפולטיבי ירוץ 6 פסיקות שעון ,תהליך עם ערך בעל העדיפות הנמוכה ביותר יקבל פסיקה אחת ,ואילו בעל העדיפות הגבוהה ביותר יקבל 11 פסיקות שעון . מימוש ה Scheduler ● ● ● ● ()goodnessכמה כדאי להריץ תהליך מסוים . ()scheduleהאם לבצע החלפת הקשר או לא ,תוך כדי שימוש ב .goodness __ ()wake_up_commonהערת תהליכים המחכים לסיומה של פעולה מסוימת ב .wait queue reschedule_idleבודקת עבור תהליך האם ניתן להריץ אותו על מעבד מסוים . להלן הקוד עצמו int goodness(task t, cpu this_cpu) { // bigger = more desirable g = t.counter //A task with more ticks left is more desirable if( g == 0 ) // exhausted quantum, wait until next epoch return 0 if( t.processor == this_cpu ) // try to avoid migration between cores g += PROC_CHANGE_BONUS //Reward the cpu if( t.mm == this_cpu.current_task.mm ) //Ask // prioritize threads sharing same address space // as contextswitch would be cheaper g += SAME_ADDRESS_SPACE_BONUS return g } void reschedule_idle(task t) { 20 next_cpu = NIL if( t.processor is idle ) // t’s most recent core is idle next_cpu = t.processor else if( there exists an idle cpu ) // some other core is idle next_cpu = least recently active idle cpu //Nowadays, There is more sense in choosing an active cpu and turning off this one to save power else // no core is idle; is t more desirable // than a currently running task? threshold = PREEMPTION THRESHOLD foreach cpu c in [all cpus] // find c where t is most desirable gdiff = goodness(t,c) goodness( c.current_task,c) if( gdiff > threshold ) threshold = gdiff //Choose the cpu that will have the best threshold next_cpu = c if( next_cpu != NIL ) // found a core for t prev_need = next_cpu.current_task.need_resched next_cpu.current_task.need_resched = true //When is this field checked? In user mode need_resched cannot be accessed, The change can only be done Kernel mode of the next_cpu,the flag is checked when getting back from kernel mode to user mode. we want to make sure the cpu will enter kernel mode, so if we are the first to ask it to resched we want to interrupt it and force it into kernel mode. (It will always be interrupted during clock interrupt, but we don’t want to wait without need). if( (prev_need == false) && (next_cpu != this_cpu) ) interrupt next_cpu //Note that we are not telling the next_cpu which task to run, it may decide to run some other task with an higher priority } void __wake_up_common(wait_queue q) { // blocked tasks residing in q wait for an event that has just happened // so try to reschedule all of them… foreach task t in [q] remove t from q add t to readytorun list reschedule_idle(t) } //When will we schedule different tasks on different cpus? It may seem that we will always choose the cpu with the least important running process but we’ve got to remember that the goodness gives a bonus for running a process on the original cpu, and therefore different process may choose different cpus If there are enough processes the first tasks may start running before we reschedule the last ones and therefore the cpu with the least important running process may change! 21 void schedule(cpu this_cpu) { // called when need_resched of this_cpu is on, when switching from // kernel mode back to user mode. need_resched can set by, e.g., the // tick handler, or by I/O device drivers that initiate a slow operation // (and hence move the associated tasks to a wait_queue) prev = this_cpu.current_task START: if( prev's state is runnable ) next = prev next_g = goodness(prev, this_cpu) else next_g = 1 foreach task t in [runnable && not executing] // search for ‘next’ = the next task to run on this_cpu cur_g = goodness(t, this_cpu) if( cur_g > next_g ) next = t next_g = cur_g if( next_g == 1 ) // no ready tasks end function // schedules “idle task” (halts this_cpu) else if( next_g == 0 )// all quanta exhausted => start new epoch foreach task t //for all tasks t.counter = t.counter/2 + NICE_TO_TICKS(t.nice) goto START; else if( next != prev ) next.processor = this_cpu next.need_resched = false // 'next' will run next context_switch(prev, next); if( prev is still runnable ) reschedule_idle(prev) // perhaps on another core // ‘next’ (which may be equal to ‘prev’) will run next…. } 22 המשך הרצאה 3 תהליכים וחוטים) מצגת (3+4 נרצה לעיתים קרובות להריץ עבודה מסוימת על פני כמה תהליכים שונים ,מוטיבציה אחת לכך היא לדאוג שהשירות ימשיך לתפקד בצד של המשתמש גם אם בינתיים מתבצעות גישות לדיסק וגם אם משימה מסוימת נופלת . שרתים ברשת משתמשים בדבר הדומה מאוד ל ,multiprocessingבניהול בקשות שמגיעות אליה .כל בקשה מקבלת תהליך משלה ,וכך מעבדים שונים יוכלו לבצע משימות שונות ומערכת ההפעלה תדאג לבצע החלפה בהתאם לצורך בין המשימות דוגמא : הכפלת מטריצות ניתן לחלק עבודה גדולה לתתי עבודות ולבצען במקביל .הקושי בביצוע חלוקה הוא שלכל תהליך יש מרחב כתובות וזיכרון משלו ,אז נצטרך לשכפל את המטריצה עבור כל תהליך ,ואין לדעת אם כל העותקים ייכנסו בזיכרון ,שלא לדבר על הבעיה בביצוע המיזוג בסוף החישוב .פתרון בסיסי יהיה לאפשר להם לגשת לאיזור משותף בו תישמר המטריצה ,אך במקרה זה קצב קריאת הנתונים יכול להיות יותר איטי .חלוקת החישוב לא בהכרח מועילה! Multithreading תהליכים החולקים ביניהם את אותו המידע נקראים חוטים (Threads) תהליכים אלה משתפים את כל הנתונים , הזיכרון ,המשתנים הגלובליים והסטטים .מה שלא משותף הוא המחסנית, הרגיסטרים ומבנה הנתונים שמתארים אותם ,על מנת לאפשר להם לרוץ בנפרד . הם יכולים לגשת למחסניות אחד של השני ,אבל זו לא התנהגות רצויה ,מידע משותף צריך לשבת ב !Heap תהליכים שלא ביקשו ליצור חוטים בפועל מריצים חוט אחד .singlethreaded process Standard API for Threads openMPמנסה ליצור מספר אופטימלי של חוטים ,אלגוריתם די יפה אבל אף אחד לא משתמש בו כי היה תקן אחר שהקדים אותו ,סיפור עצוב ונוגע ללב ): ב Javaוב C++11יש תמיכה מובנית בחוטים . POSIXספריה שרצה כמעט על כל פלטפורמה ,מאוד נפוצה Portable Operating System Interface . pthreadsספרית החוטים של .POSIX מה משותף לחוטים? מבחינת ה userה threadsחולקים את כל המידע ,למעט הדברים הייחודיים לריצתם . בתוך מערכת ההפעלה ,כל מערכת הפעלה מחליטה איך היא מחלקת את החוטים .בלינוקס כל חוט מיוצר על ידי תהליך נפרד .הפונקציה clone יוצרת חוט חדש fork) .הוא בעצם תהליך חדש שלא משתף שום דבר עם התהליך הקודם( . ההבדל המהותי בין חישוב מקבילי על ידי תהליכים לבין חישוב מקבילי על ידי חוטים ,הוא שבחישוב על ידי תהליכים צריך איזשהו ערוץ תקשורת בין התהליכים ואילו בחישוב על ידי חוטים פשוט משתמשים באיזור הזיכרון המשותף . ערוץ התקשורת נעשה באמצעות ,pipe אשר ניתן לכתוב אליו מידע מתהליך אחד ולקרוא אותו מתהליך אחר . דוגמא לתקשורות בין תהליכים :Pipe 23 ישנם הרבה סוגים של ערוצי תקשורת בין תהלכים ,אנו נציד את pipe שהוא דרך פשוטה לתקשר בין תהליכים . אנחנו נשתמש ב pipeלצורך העברת הודעה יחידה מתהליך אחד) תהליך האב( לתהליך אחר) תהליך הבן( . pipeמוגדר ע"י שני ,file_descriptor fd כלומר ע"י , [int pipe_fd[2 כך שהאיבר הראשון במערך הוא ה fd שמשמש לקריאה מה pipeוהתא השני במערך הוא ה fdשמשמש לכתיבה ל .pipe כתיבה וקריאה ל pipeנעשות ע"י קריאות המערכת read ו .writeהקרנל שומר את כל מה שנכתב ל ,Pipeכך שמאוחר יותר הוא יהיה מסוגל לתת שירות עבור קריאה של נתונים אלו . ) write( pipe_fd[1], src_buf, Nקריאת מערכת שתפקידה הוא להעתיק את ה src_bulלתא השני ב .pipe ) read( pipe_fd[0], dst_buf, Nקריאת מערכת שתפקידה לרשום ל dst_bufאת מה שנרשם בעבר ל Pipe ושמור עכשיו בתא הראשון ב) pipeכלומר ב .([pipe_fd[0 נשים לב שלא ניתן לקרוא מ pipeאם לא נכתב אליו עדיין שום דבר ,במקרה זה התהליך יקבל יחסם . המאקרו :DO_SYS הערה :אם נסתכל במאקרו DO_SYS ובמקרואים אחרים נשים לב כי הם בד"כ מוקפים ב .do whileהמטרה נועדה למנוע באגים בהם המתכנת עלול לשכוח שהמאקרו בפועל עלול להיות מתורגם ליותר משורה אחת בזמן .precompilation if (...) DO_SYS else במקרה זה למשל ה elseיתייחס ל ifהפנימי אם לא היינו משתמשים ב .do_while מקרה פשוט יותר הוא שהמאקרו פשוט מבצע מספר שורות ,והקפה ב ifתכלול רק את הראשון if DO_MACRO בעצם זו פשוט דרך לעטוף את הקוד בבלוק משלו ,מה שאי אפשר לעשות סתם כך ב .C נבחין כי DO_SYS לא מחזירה לנו ערך! 24 ולא שלו המערך לתוך קורא הבן ,הבן של המערך את ולא שלו המערך את מילא האבא : הנ"ל לקוד הסבר ולבן לאב ולכן !חוטים ולא תהליכים אלו כי נזכור !תהליכים בין משותף לא הגלובלי המשתנה !האבא של המערך .(בזבל )מלאים ריקים היו שניהם השכפול בשלב ,נפרד גלובלי מערך יש // Assume that filling g_msg requires a lot of computational work. // So we want to use 2 threads, one thread to fill the // first half of g_msg and another to fill the second half void fill_g_msg( void ) { pthread_t t1, t2; // launch the two threads pthread_create(&t1, NULL, thread_fill, “first"); //את להריץ הולכים שניהם thread_fill שונים פרמטרים עם pthread_create(&t2, NULL, thread_fill, “second"); //המחסנית הוא השני הפרמטר , NULL חדשה מחסנית ליצור רוצים שלא אומר // wait for both threads to finish pthread_join(t1, NULL); pthread_join(t2, NULL); } void* thread_fill(void *arg) //יעדכנו אך ,הזאת הפונקצה את יבצעו החוטים שני המערך של שונים חלקים { // assume I’m the “first” thread int i; int lo = 0; int hi = N/2; if( strcmp((char*)arg, “second”) == 0 ) { // I’m the “second” thread 25 lo = N/2 + 1; hi = N 1; } for(i = lo; i <= hi; i++) g_msg[i] = /*do some really hard work here*/ ; return null; } אם יש לנו מעבד יחיד אז אנחנו לא מרוויחים כאן שום דבר ,רק הוספנו overhead של מעבר בין התהליכים ,אבל כשיש יותר ממעבד אחד נוכל לשפר את הביצועים שכן הקוד יוכל תיאורטית לרוץ בקצב כפול . מסקנה :אם יש לנו צורך של ממש לשתף נתונים אז חוטים הם בחירה טבעית ,הרי הכל משותף ,בסך הכל צריך להקצות את הנתונים בערימה) (heapאו כמשתנים גלובליים וכל החוטים יראו אותם . בתהליכים ניתן להעביר הודעות באמצעות pipe או ערוץ תקשורת אחר ,או ליצור איזור זיכרון משותף וכך העברת נתונים תהיה דומה להעברה בחוטים ,למעט העובדה שהרבה יותר מסובך ומסורבל לבצע סנכרון בכתיבה והקריאה כשמודבר בתהליכים .לכן באופן כללי נרצה להשתמש בחוטים כדי לשתף מידע ,אם למשל אנו רוצים להשתמש בשתי תוכניות שונות אחת שקוראת ואחת שכותבת ,אין לנו מנוס מהלשתמש בתהליכים ולשתף את המידע) שכן חוטים מריצים אותו קוד ,גם אם קטעים שונים ממנו( . ככל שהיחס בין הזמן שהתהליך רץ לבין זמן החלפת ההקשר גדל ,כלומר ככל שהחלפת ההקשר זניחה ביחס ל quantumשל תהליך אז השפעתו באמת קטנה בהתאם לאינטואיציה . בהחלפת הקשר צריך להפריד בין שני דברים : ● פגיעה ישירה הזמן שלוקח למערכת ההפעלה לעשות את ההחלפה ,באמת ברוב המקרים הוא ממש זניח ,עבור אלגוריתם זימון טוב מספיק . ● פגיעה עקיפה פקטור נוסף שצריך להתחשב בו ,לעיתים הוא פוגע במערכת בכמה סדרי גודל יותר .זמן של הגעה של מערכת לקצב מקסימלי מתברר שאחרי החלפת הקשר החומרה מריצה את התהליך לאט מאוד ,ולוקח לה זמן" להתחמם" . רוב הזמן המערכת קוראת נתונים ומבצעת איתם איזשהו חישוב ,הבעיה היא שהרבה מאוד פעמים לא קוראים נתונים מתוך הרגיסטרים אלא מתוך הזיכרון הראשי .נניח שיש לנו נתונים שמגיעים מזיכרון ,הזיכרון צריך להיות מסוגל לספק נתונים בקצב שהמעבד רוצה לקרוא אותם ,לצערנו זה לא המצב ,הזיכרון לא מתקרב אפילו לקצב של מעבד ,יש הבדל של שנייםשלושה סדרי גודל .לכן מעבד לא יכול לקרוא נתונים בקצב הרצוי . מה הפתרון של בעיה זו? נבחין שכאשר מסתכלים על נתונים הם מקיימים שתי תכונות : ● לוקליות בזמן אם השתמשתי בכתובת מסוימת עכשיו ,סיכוי גבוה שארצה להשתמש בה שוב ולגשת לאותו מקום בזיכרון) .דוגמא קלאסית :עבודה על משתנה מסוים בתוך פונקציה ,ביצוע לולאה( . ● לוקליות במקום אם קראתי כתובת ,X סביר להניח שאקרא בקרוב את) X+2 דוגמא קלאסית :קריאת מערך( . למה אבחנה זו חשובה? למרות שלתהליך יש המון נתונים ,בפרק זמן מסוים הוא צריך כמות מזערית של נתונים . למה שלא נשתמש באיזו חומרה שהיא מהירה בהרבה מזיכרון ,עם מעט מקום ונשמור רק את הנתונים ה"חמים" במקום ובזמן .הפתרון הוא באמת להשתמש ב .Cache 26 Cacheזיכרון מטמון : ● ● ● ● ● יש את רמת הרגיסטרים שהם מהירים ביותר . L1בגודל ,64KB מחולק לשני חצאים 32KB ,ל dataו 32KBל .instructionsהחומרה הזאת מהירה . L2בגודל ,256KB יותר איטי מ ,L1צריך כ 12מחזורי שעון לקרוא ממנו . L3בגודל ,8MB לוקח 36 מחזורי שעון . DRAMהזיכרון עצמו ,עד ,32GB לוקח 230 מחזורי שעון . נסתכל למשל על המעבד הזה ,לכל ליבה יש L1 ו L2משלה ,כך הזמן הפיזי שלוקח להגיע למעבד הוא קטן משמעותית L3 .לעומת זאת משותף לכולם וחיצוני ,ולכן הוא איטי יותר . כעת ,ננסה להבין ,מהו המחיר העקיף הזה? למה לוקח למעבד זמן להתחמם? אחרי שאנו מחליפים תהליך אחד בתהליך האחר הCacheים לא מעודכנים ,ולכן כל פעם שנרצה לקרוא נתון נצטרך להגיע עד ל .DRAMעם הזמן הCacheים מתמלאים ואז המערכת מגיעה לזמן האופטימלי הזה .זמן המילוי הוא משמעותי לביצועים . כשמחליפים בין חוט לחוט אמנם יש ערכים שהם אינם רלוונטיים לשני החוטים ,אך רוב המידע) הקוד ,משתנים גלובליים וכד'( רלוונטי לשני החוטים ולכן הזמן שלוקח למערכת להתחמם במעבר מחוט אחד לאחר הוא הרבה יותר קצר שכן אנו מתחילים ממצב שיש הרבה נתונים רלוונטיים ב ,Cacheומתחילת הביצוע הוא יותר מהיר שכן ה Cacheכבר מעודכן באופן חלקי . אם ניזכר בהרצאה על Schedulers אחד הנתונים שבדקנו בכדאיות להחלפת הקשר היא האם התהליך משתף זיכרון עם התהליך שרץ כרגע במעבד) יכול להיות שהתהליך החדש שאנו רוצים להביא הוא חוט אחר של אותו 27 התהליך( ,ולכן נתנו בונוס למקרה כזה שכן אם נבצע את החלפת ההקשר עם המעבד הזה אז הCacheים יהיו מעודכנים יותר ונקבל תקורה עקיפה קטנה יותר . ככל שיש יותר ליבות הביצועים יותר גרועים ,כי כל ליבה בולמת את הגישה של ליבות אחרות . אפשר לשתף זיכרון גם בין תהליכים באופן ישיר ,אפשר לבקש ממערכת הפעלה להגדיר בלוק בזיכרון שיהיו כמה תהליכים שיוכלו לגשת לשם ,חשוב להבין שגם אם מדובר באותה כתובת בזיכרון הפיזי ,הכתובת של הבלוק בזיכרון הוירטואלי של כל תהליך יכולה להיות שונה . Shared Memory ● ● ● ● ● ● shmgetיוצרת את הבלוק ומחזירה איזשהו .identifier בד"כ משתמשים בו על מנת לשתף זיכרון בין אב ובן .האבא יוצר זיכרון מבצע fork ואז הוא והבן יכולים ל"דבר" להשתמש בזיכרון המשותף . shmatלקשר את הבלוק לזיכרון הוירטואלי של תהליך . shmdtלבטל את הקישור . shmctrlמאפשר לשנות תכונות של ה ,shared_memoryלמשל בהתחלה שיהיה ספציפי לתהליך ואחרי עדכון מסוים להפוך אותו ל .public shm_openיוצר קובץ במערכת קבצים מיוחדת ,מאפשר לשני תהליכים בלתי תלויים לשתף זיכרון .כל תהליך פותח את הזיכרון בנפרד) הוא יווצר רק בפעם הראשונה( וכל אחד כותב לשם בזמנו . shm_unlinkכל תהליך יכול לסגור בצורה בלתי תלויה ,ברגע שהאחרון סוגר ה shmנעלם לגמרי . copy_on_write optimization כשיוצרים תהליך חדש כל הזיכרון של תהליך האבא משוכפל לכאורה לתהליך הבן ,את כל הנתונים שיש לאבא צריך להעתיק .לכן היינו מצפים ש forkיהיה מאוד יקר ויימשך זמן רב .בפועל מערכת ההפעלה מעתיקה רק את ה descriptorsשל הזיכרון ,ומכבה את הרשאות הכתיבה לזיכרון הזה .נניח שהאבא מנסה לכתוב לזיכרון שלו , מתרחש .page fault מערכת ההפעלה בוחנת את ה page faultומסיקה שהאיזור אכן צריך להיות נגיש לכתיבה )למשל במחסנית( ,אך מסיבה כלשהי ה pageלא נגיש לקריאה ,היא בודקת כמה תהליכים משתפים את הדף הזה ומגלה שיש יותר מתהליך אחד כזה ,מה שלא צריך לקרות לדף מחסנית .מכאן יכולה מערכת ההפעלה להסיק שזה מנגון ה copy_on_writeולכן עליה להעתיק את הזיכרון לבן ולהוריד את החסימה . ברוב המקרים אין בכלל העתקה ,שכן לעיתים קרובות אחרי fork יש execv ולכן אין צורך להעתיק את המידע של האב ,והרשאות הגישה יוחזרו לו מבלי לגרור העתקה של הזיכרון . Multitaskingשיטה המאפשרת למערכת ההפעלה להריץ מספר תהליכים על אותו המעבד באמצעות חלוקת הזמן . Multiprogrammingתוכנית מאוד גדולה שמכילה הרבה תוכניות ב"ת בתוכה Multiprocessing 0) תהליך אחד שאנו מנסים לחלק לבלוקים שצריכים לבצע משימות שונות( . חוטים ברמת המשתמש עד עכשיו דיברנו על חוטים שמערכת ההפעלה מכירה ,ב Linuxכל חוט הוא בעצם תהליך שמשתף את כל המידע עם תהליך אחר) אך מבחינת מערכת ההפעלה הוא עדיין תהליך( . בחלק גדול מהמקרים המשתמש שכותב את הקוד שלו רואה שהוא יכול להריץ חלק מהקוד כחוט נפרד ,אבל עלות יצירת החוט לא שווה את המאמץ .עם זאת ,נניח שעלות יצירת החוטים הייתה אפסית וכן לא היה צורך בסנכרון אז ניתן ליצור חוטים ברמת המשתמש . 28 מה זה בעצם חוט ברמת המשתמש? המשתמש בעזרת ספריה או קוד שהוא כותב בעצמו מדמה בתוך התוכנית שלו חוטים שונים באמצעות מעבר בין קטעים שונים בקוד ושינוי הנתונים המתאימים ברגיסטרים וב structs )למשל באמצעות .(setjmp, longjmp מבחינת מערכת ההפעלה יש רק תהליך אחד ,שרץ על אותו timeslice אבל ה userמשתמש ב timesliceהזה ומחלק אותו בין מספר מטרות) למשל המשתמש רוצה במהלך ביצוע התוכנית לקרוא איזה ,database אבל התהליך יכול להיתקע או לקחת זמן רב אז הוא רוצה במהלך זמן זה להמשיך בביצוע החישוב( . בד"כ נבחר להשתמש בחוטים ברמת המשתמש כאשר נרצה שליטה על ה schedulingבין חלקי התוכניות השונים ,או במקרים בהם לא נרצה לשלם את העלות של שימוש ב schedulerבעת החלפת ההקשר בין החוטים שלנו . OmpSsתומך גם בחוטים ברמת המשתמש ,מכריזים על הפונקציה כ ,taskמגדירים כתובות שהולכים לקרוא ולכתוב) ב ,(inoutכך ה ompיודעת לקבוע מה יוכל לרוץ במקביל ,ומגדירים את הפונקציה עצמה . נשים לב בדוגמא שבגלל ההגדרה של פונקציית sort כל קריאה ל sortהיא task נפרד ,כאשר שני ה sortב"ת לפי ה inoutשלהם . הספריה הזאת חדשה יחסית ויש לה כל מיני מגבלות ,או שהכתובות צריכות להיות ממש זהות) ממש אותם חלקים של מערך ואז לא ניתן להריץ במקביל( או כתובות זרות לגמרי ,לא ניתן לתת חיתוך של כתובות במקרה זה הוא ינסה להריץ אותם במקביל אפילו שהדבר אסור . setjmpפונקציה ששומרת את הערכים של הרגיסטרים במיקום מסוים . longjmpפונקציה שמקבלת כתובת בזיכרון וטוענת משם את הריגסטרים . 29 הרצאה) 4 מצגת(4 סנכרון הקדמה יש חשיבות גדולה בסנכרון בין גישות שונות לזיכרון . נניח ששני אנשים ניגשים ל serverורוצים למשוך משם כסף אם אין סנכרון אנחנו יכולים לגרום למצב בעייתי בו שתי משיכות שמתבצעות ביחד גורמות לעדכון יחיד של אחת מהן ,מה שיוצר בעיה ברורה . ננסה להמחיש את הבעיה באמצעות בעיית החלב ,נניח שבן אדם א' רואה שאין חלב בבית והולך לקנות חלב ,בן אדם ב' רואה שאין חלב כמה דקות אחריו והולך לקנות חלב גם הוא ,אחרי ששניהם יהיו במכולת נסיים עם שני חלב ): פתרונות : .1הפתרון הפשוט ביותר בעולם האמיתי הוא שאדם א' ישאיר פתק כאשר הוא רואה שאין חלב וככה אדם ב' יידע שכבר הלכו לקנות חלב ולא יילך גם הוא . ○ פיתרון זה לא עובד במקרה של תהליכים אם הם מגיעים ביחד הם יחליטו לשים פתק לפני שהם יראו את הפתק של האחר ,וילכו שניהם . .2הפתרון השני הוא להשאיר פתק לפני שבודקים אם אין חלב . ○ הבעיה : ייתכן שאף אחד לא יקנה חלב אם שניהם ישימו פתקים ויראו שהשני גם שם פתק . .3הפתרון השלישי הוא פתרון שלכאורה עובד ,אדם א' ישאיר פתק ואז יבדוק כמו בפתרון 2 האם אדם ב' שם פתק .ואילו אדם ב' שם פתק הוא יחכה עד שלא יהיה פתק של אדם ב' ,בסופו של דבר אם אדם א' לא יצא לקנות אז אדם ב' יעשה זאת . ○ גם פתרון זה לא עובד : נבחין כי אין סנכרון בין ה Cacheשל מעבדים שונים ,וכך ניתן להגיע למצב שערך מסוים בזיכרון כבר השתנה אבל מעבד אחר חושב שהוא עוד לא השתנה .נניח ששני מעבדים קראו ערך מסוים בזיכרון ,שניהם הוסיפו את הערך ל .Cacheכעת מעבד אחד מעדכן אותו .במעבד השני עדיין יש את הערך הישן ב ,Cacheולכן המעבד לא ייגש לערך המעודכן ,ולא מוגדר מתי הערך החדש יגיע! הדבר הורס את פתרון 3 כי גם כשנעדכן את הפתקים שלנו ,לא ברור מתי העדכון יגיע לתהליך האחר . ניתן לפתור בעיה זאת באמצעות כל מיני דרכים ,למשל volatile : ערכים שניתן לגשת אליהם ממקום אחר ולכן לא שמים אותם ב Cacheאלא ניגשים אליהם ישירות . כיצד נפתור את הבעיה? ) : Mutual exclusion (mutexהגרעין של הבעיה הוא שיש כאן חוטים שמשנים ערכים מסוימים באופן לא סנכרוני .לכן אנו רוצים להגדיר משימות מסוימות שפועלות באופן סדרתי . קטע קריטי באטומיות של קטע קריטי הכוונה היא שאף חוט אחר לא יוכל לשנות את הערכים שמופיעים בקטע הקריטי ולהפריע לחישוב זה ,מבחינת כל חוט אחר קטע הקוד הקריטי הוא כמו פקודה אטומית אחת ואין ערכי ביניים .אז בעצם אנחנו רוצים לפתח שיטה לשמור על קטע קריטי . משל למה הדבר דומה? יש לנו מחשב בחדר עם מנעול ,בכניסה לחדר יש מפתח ,כאשר אדם נכנס לחדר הוא נועל את עצמו בפנים ,מריץ את הקוד מבלי שאף אדם אחר יפריע לו . 30 נרצה להגדיר משאבים ,שרק חוט אחד יוכל לטפל בהם .כל רכיב חומרה או כל חלק קוד יכול להיות משאב . לדוגמא ,מדפסת רק תוכנית אחת יכולה להדפיס בשלב מסוים ,פונקציה ניתן להגדיר פונקציה שצריך שרק חוט אחד ישתמש בה בפרק זמן מסוים . Deadlock R1,2משאבים ,למשל קבצים . P1,2תהליכים) חוטים( . נניח P2 פתח את R1 ורוצה לפתוח את ,R2 ואילו P1 פתח את R2 ורוצה לפתוח את .R1 אך הם לא יתקדמו כי כל אחד רוצה לפתוח את שני הקבצים ויש לו רק אחד מהם ,בעצם יש לנו כאן תלות מעגלית דבר זה נקרא .deadlock Livelockתכונה של תכנות ,המערכת לא במצב דפוק אבל הקוד שלנו כן . דוגמא קלאסית" :אחריך" הדלת פתוחה ,אבל כל תהליך נותן קדימות לשני ,ולכן בפועל אף אחד לא נכנס . אם במקום פשוט לנסות לנעול את הקובץ ולחכות עד שייפתח כמו בדוגמא הקודמת היינו מנסים לנעול קובץ אחד עד להצלחה ,מנסים לנעול את השני ואם הוא תפוס אז משחררים אותו ואת הראשון ומתחילים מהתחלה . בדוגמא הקודמת היינו יכולים להגיע למצב שכל אחד מהם תופס רק קובץ אחד ומשחרר ,נבחין כי זהו מצב שניתן לבלות בו זמן ניכר אך ניתן לצאת ממנו אם תהליך אחד מספיק זריז . שיטות תכנות ● Lock freeלא משתמשים במנעולים לא מגיעים למצב שמחכים לכך שמשאב מסוים ייפתח ,אלא כותבים קוד שמנסה לעבוד מבלי לחכות .זה אמנם חוסך לנו את הצורך בעבודה עם המנעולים אבל בשיטת תכנות זו ייתכן שנבצע את אותו קטע קוד מספר פעמים ,ובכך נבצע עבודה מיותרת) .נניח נעדכן איזשהו ערך ברשימה ואז נעדכן אותו שוב לאותו הערך( . ● Wait Freeקוד שבניגוד ל Lock Freeגם לא מבצע עבודה מיותרת ,מצליח לבצע את עבודתו תמיד . )ב Lock Freeיכול להיות מצב שלא נחכה ונגלה בדיעבד שעשינו עבודה מיותרת ,תחום שנמצא בחיתוליו( .בעצם Wait Free מכיל את .Lock Free הוגנות מנעול יוגדר כהוגן אם אין בו הרעבה נצחית – כלומר אין מצב שתהליך) חוט( מחכה לנצח למנעול בזמן שתהליכים) חוטים( אחרים נכנסים לקטע הקריטי ובעצם" תופסים" את המנעול לעצמם .במנעול לא הוגן נוכל לקבל מצב שבו אחד מהתהליכים מורעב ע"י שאר התהליכים שמבקשים לנעול את המנעול) תהליכים ישנים ותהליכים חדשים( .ניתן לחזק את ההוגנות עוד יותר ע"י הדרישה שתהליכים) החוטים( ייכנסו לקטע הקריטי לפי סדר ההגעה שלהם ,כלומר .FIFO 31 חיזוק נוסף של ההוגנות הוא קביעת זמן סף מסויים ,שמרגע שהתהליך מבקש לגשת למנעול לא יוכל להיות מצב שהוא יחכה יותר מזמן הסף שהוגדר כדי לקבל את הגישה למנעול) .זה סוג של הבטחה" לא תחכה יותר מככה וככה זמן" ,או לפחות נבטיח שלא נרעיב אותו לחלוטין( . מנעולים מי שמגיע כשמשאב חופשי משתמש בו ונועל . אם המשאב נעול צריך להחליט האם מחכים כ ,busywaitאו שמוותרים על המעבד ונותנים למישהו אחר את המעבד) שיכול להיות שהוא האחד שנעל וכך הויתור ישחק לטובתנו( . נבחין כי לא כל כך קל לנעול משאב נניח שהיינו נועלים באמצעות דגל ,אנו חוזרים לבעיה המקורית של קניית החלב איך דואגים שלא שני אנשים יסמנו את המשאב ? כלומר 2 משאבים ינעלו את המנעול בו זמנית? כיצד מנעול פותר את הבעיה בעצם? לפני ביצוע ה critical_sectionנועלים את המנעול ומבצעים את החישוב ,כאשר בסופו משחררים אותו . אם יגיע חוט חדש והוא ינסה לנעול את המנעול הוא לא יוכל להתקדם ויעבור לתור המתנה ,מתישהו החוט המקורי יסיים את ה criticalsectionויפתח את המנעול ,ברגע שהוא פתח את המנעול החוט המקורי יעבור מתור המתנה לתור .ready יכול להיות שהוא יתחיל לרוץ מיד) ,אם לא אז אין בעיה( ינעל את המנעול מצידו ויתחיל בביצוע החישוב . נחזור לבעיית הבנק נשים לב שהערך שנחזיר של היתרה בחשבון מוחזר אחרי פתיחת המנעול ולכן תחזיר את הערך הלא מעודכן .כדי לפתור זאת ה critical_sectionצריך להכיל גם את ה ,returnמה שכמובן לא ניתן לעשות מתוך הפונקציה ולכן ה criticalsectionצריך להכיל את כל הפונקציה . ניתן לראות כי גם לצורך מימוש המנעול יש צורך בנעילה של המערכת כדי להתמודד עם ה .critical_sectionישנן שתי גישות לביצוע הנעילה הזו :ביצוע באמצעות תוכנה או ביצוע באמצעות חומרה . FINEGRAINED SYNCHRONIZATION ביצוע באמצעות תוכנה החלפות הקשר : החלפת הקשר מתבצעת במקרים הבאים : קוד חוסם שקורא ל ,waitקריאה מפורשת להחלפת הקשר . אירוע חיצוני בד"כ פסיקת שעון או פסיקת חומרה אחרת . אנחנו בעצם רוצים לעצור את החלפת ההקשר ,הדבר ימנע את הבעיות ב ,critical_sectionכך נדע בוודאות שבמערכת הנוכחית לא יהיו החלפות הקשר שלא בשליטתנו . אנחנו לא נראה דוגמאות לכיצד לבצע נעילות באמצעות תוכנה! ביצוע באמצעות חומרה כל חומרה מאפשרת תמיכה בפקודת מכונה כזו או אחרת המאפשרת לנו לבצע סט פעולות באופן אטומי אשר באמצעותו נוכל לממש מנעול . xchgקריאה לפקודת מכונה שכותבת ערך מכתובת מסוימת ומחזירה את הערך שיישב שם ,פקודה זו קוראת גם ל lockלפני ביצוע הכתיבה ,ובכך מבטיחה לנו את סדר ביצוע הפעולות ,מה ש xchgנותנת לנו זו בעצם 32 האפשרות לבצע את פקודת ההחלפה באופן אטומי ,כך שבוודאות לא תהיה החלפת הקשר בין הכתיבה להחזרת הערך . כשאנו מריצים פקודות הן לא בהכרח רצות בסדר שבו הן נכתבו ,הן רצות במקביל ומתבצעות בסדר שונה .אנו לא רוצים שאחרי xchg יקרה מצב שבגלל אופטימזיציה של המעבד פקודה שאמורה לרוץ עם מנעול נעול תרוץ עם מנעול פתוח ,לכן יש צורך בקריאה ל .lock ב acquireאנו בעצם מנסים לכתוב למנעול ערך 1 ומחכים שנקבל ערך חזרה שונה מאפס . ● נניח הגענו למנעול פתוח ,אנו מנסים לכתוב כותבים 1 לשדה ,lock אם המנעול היה פתוח הערך שיוחזר לנו הוא 0 ולכן נוכל לסיים את הלולאה ,אנחנו יודעים שעכשיו הערך הוא 1 ושבעבר היה שם ערך 0ומכאן כי נעלנו כנדרש . ● נניח הגענו למנעול נעול ,אנו מנסים לכתוב כתובים 1 לשדה ,lock אך יוחזר 1 שכן המנעול כבר היה נעול ,ונתקע בלולאה עד שמישהו אחר לא ישחרר את המנעול . releaseמיד כותבים ערך ,0 אנחנו יודעים בוודאות שהיה שם כבר ,1 אם אני נעלתי רק אני יכול לפתוח , ומתירים את פעולת הפסיקות . למה גם כשכותבים עושים את זה עם ?xchg למה אנחנו לא רוצים שפקודות שרצות אחרי המנעול ירוצו לפניו? מה קורה בשאר המעבדים? בטוח שבמעבד שלנו רק אותו תהליך רץ ,אבל בשאר המעבדים יכול להגיע תהליך או חוט שרוצה לנעול מנעול ,מנסה לנעול אותו במעבד נוסף ,ומחכים בלולאה .כלומר עד שלא נשחרר את הנעילה מעבדים אחרים לא מתקדמים .אם פתאום נאפשר לפקודות שבאות אחרי פתיחת מנעול לבוא לפני פתיחת מנעול אז אמנם נשפר את הפעולה במעבד שלנו אבל נתקע עוד יותר את כל המעבדים האחרים . הדבר ההרסני הוא ש enable_Interruptsיתבצע לפני ,xchg כלומר כשהמנעול עוד נעול ,ותתבצע החלפת הקשר .זה דבר בעייתי בפני עצמו ,אבל כל עוד נחזור רק נעכב את המעבד .מה יקרה אם נעבור בהחלפה לקוד שרוצה לנעול את המנעול? הוא ייתקע בלולאה אינסופית שלא ניתן לצאת ממנה כי כבר אין החלפות! מה קורה אם תהליך מגלה באמצע עבודה עם משאב שהוא צריך לחכות למשהו? הגיוני לעשות החלפת הקשר ולהשאיר את המנעול נעול ,אבל אז אנחנו עלולים להגיע לתהליך אחר שרוצה לנעול את המנעול והוא גם ייתקע . לכן נרצה לאפשר החלפת הקשר רק אחרי פרק זמן מסוים . נוסיף תיקון ל acquireכך שהוא פותח ונועל את הפסיקות .זה מאפשר לנו בעצם לקבל פסיקות שעון ,אם תהליך מגיע ומנסה לנעול את המנעול ונגמר לו ה time sliceאז נאפשר החלפת הקשר .נבחין כי במערכת לינוקס שבה פסיקת שעון רק מוסיפה need_resched הדבר לא יפתור את הבעיה ונצטרך לעשות שינויים נוספים . 33 הערה :אם אנחנו מגיעים לקוד עם פסיקות חסומות לא ניתן לפתוח אותו סתם .פסיקה לא יכולה לתפוס מנעול שהוא נעול . האם יש בעיה מקבילה ב ?user space סיגנלים נוצרים כתגובה לאיזה מאורע חיצוני ,רוב הפסיקות בחומרה שולחות בסוף סיגנל ל .user spaceבכל רגע נתון אנחנו עלולים להריץ תהליך אחר ,חוט אחר ,או פונקציה שמטפלת בסיגנל . אין לנו את היכולת להגיד אני רוצה להריץ קטע קריטי בלי שאף אחד יפריע לי .מצד שני ניתן להשתמש בשירותים של מערכת ההפעלה כדי לכתוב קוד מסונכרן . מימוש מנעול באמצעות) atomic ops במקום xchg שכבר ראינו( ● , CAS cas(addr,val1,val2) Compare and Swapמקבלת כתובת בזיכרון ושני ערכים .היא בודקת האם הערך בזיכרון שווה ל ,val1אם כן היא כותבת val2 ומחזירה ,val1 אחרת היא מחזירה את הערך האמיתי מהכתובת ולא מבצעת עדכון . struct lock { uint lock; } lock (lock* l) { disable_int(); while (cas(l>lock,0,1) != 0) { enable_int(); disable_int(); } } unlock (lock* l) { cas(l>lock,1,0) //We know for sure that the value is 1 right now enable_int(); } LL & SC Linked load & store condition הן תמיד מגיעות ביחד . ○ LLמקבלת כתובת בזיכרון ומחזירה את הערך של הכתובת . ○ SCמקבלת כתובת בזיכרון וערך שרוצים לכתוב לשם ,היא בודקת האם הייתה כתיבה לאותה כתובת מאז שהפעלתי LL בפעם האחרונה ,אם הייתה כתיבה SC לא כותבת שום דבר ומחזירה כישלון ,אם לא הייתה אז SC כותבת כנדרש . lock (lock* l){ disable_int(); while (LL(l>lock) != 0 || SC(l>lock,1) ==0) { //short circuit or, if we reached second condition than lock=0 and we’ll try writing, if we failed we will stay in the loop enable_int(); disable_int(); } 34 } הערה : מעשית משתמשים בד"כ ב .CAS COARSE-GRAINED VS FINE-GRAINED SYNCHRONIZATION ראשית נרצה להגדיר את המושגים coarsegrained ו ,finedgrainedשני מושגים אלו מתייחסים לרמת הגרעיניות של המידע אותו אנו נועלים . Fine Grained Synchronization בשיטת נעילה זו נרצה לפרק את המשאבים שלנו ליחידות גרעיניות קטנות ככל הניתן ולנעול כל אחת מהן בנפרד , בצורה זו נוכל לשפר את המקביליות של הקוד ולאפשר לתהליכים רבים יחסית לגשת לאותו מבנה נתונים כל עוד הם מעוניינים בגישה לאיזורים שונים במבנה ,עם זאת נקבל תקורה גבוהה יחסית של מנעולים . דוגמא קלאסית :נעילה של חוליות ברשימה או בעץ חיפוש בינרי . הערה :כאשר אנו עובדים עם fine grained נרצה הרבה פעמים להשתמש בנעילה בשיטת ,spinlock זאת מכיוון שבד"כ זמן הנעילה ב fine grainedהינו קצר יחסית . דוגמא למנעול שנועל בשיטה זאת הוא .spin lock Coarse Grained Synchronization בשיטת נעילה זו נרצה להסתכל על מבנה הנתונים כיחידת גרעיניות גדולה ,כאשר כל תהליך צריך לנעול את כל מבנה הנתונים על מנת לקבל אליו גישה ,שיטה זו אמנם פוגעת במקביליות אך היא מפשטת את השימוש במנעולים . דוגמא קלאסית :שימוש במנעול אחד עבור רשימה מקושרת . הערה :כאשר אנו עובדים עם coarse grained נרצה הרבה פעמים להשתמש בנעילה בשיטת ,block זאת מכיוון שבד"כ זמן הנעילה ב coarse grainedהינו ארוך יחסית . דוגמא למנעול שפועל בשיטה זאת הוא .semaphore *הערה : נציין כמובן כי ישנן דרגות שונות בין שתי השיטות . ?Spin or block מה קורה כשמעבד אחד נעל מנעול ומעבד אחר מריץ תהליך שמנסה לנעול את אותו מנעול ? התהליך מבצע .busy wait האלטרנטיבה היא לעשות החלפת הקשר ,לתת למישהו לרוץ במקומו בתקווה שאותו תהליך שרוצה לנעול את המנעול יחזור בסוף לריצה ואז המנעול כבר ייפתח) .נזניח את המקרה שבו מי שמנסה לנעול היא פסיקה ואין החלפת הקשר בפסיקה( . למה לא עושים החלפת הקשר כזו? השאלה היא מה גורם ליותר נזק . נניח שיש לנו קוד שמבצע N פעמים חישוב ו Nפעמים סנכרון .מה יותר יקר לנו? אם הסנכרון זו פעולה כל כך ארוכה שהיא הרבה יותר ארוכה מהקוד שהוא רוצה להריץ אז לא כדאי לנו לעשות החלפת הקשר . נניח שיש שני חוטים אחד מנסה להריץ את הקוד הקצר והשני מחכה שהחוט הראשון יסיים את החישוב כדי שגם הוא יוכל להתחיל .אם זמן החישוב כל כך קצר שהחלפת הקשר יותר יקרה אז אין טעם להחליף ,אם החוט הראשון מבצע איזשהו חישוב מאוד ארוך אז כדאי לבזבז את החלפת ההקשר ולא לחכות ב .busy_wait נשים לב שב kernelקוד קריטי הוא בד"כ קצר מאוד ,אסור לחסום פסיקות להרבה זמן .מכיוון שהקטע תמיד קצר אם נתחיל לעשות החלפות הקשר בגלל מנעול נעול אנחנו רק נאבד זמן ,ולכן ב kernelכמעט תמיד משתמשים ב.busy waitב user spaceהמצב שונה ,יכול להיות קטע קריטי מאוד ארוך ,ויותר מכך אנחנו בכלל לא יודעים מתי יוחלף התהליך . 35 כלל אצבע :ב user spaceלא משתמשים) כמעט( ב ,busy waitאלא גורמים לתהליך לצאת להמתנה וב kernelרוב השימוש הוא ב spin lockשכן גורם ל .busy wait ) Semaphoreאיתות בדגלים( waitניגשת למונה של semaphore ומקטינה אותו באחד ,אם אחרי ההמתנה הוא לא שלילי הכל ממשיך לרוץ הלאה ,אם אחרי ההמתנה הוא הפך להיות שלילי) או שנשאר שלילי( ,תהליך נחסם ועובר לתור המתנה . signalאם המונה חיובי אחרי ההמתנה לא עושה כלום ומסתיימת ,אם הוא לא חיובי אחרי ההגדלה) קטן שווה לאפס( היא לוקחת תהליך שנמצא בתור המתנה ומחזירה אותו למצב .ready הערה : ניתן להשתמש ב semaphoreבתור מנעול ע"י כך שנאתחל אותו ל ,1כדי לנעול נעשה ,wait כדי לפתוח נעשה .signal מה ההבדל המהותי? ● לא ניתן לפתוח מנעל פעמיים אבל ניתן לפתוח signal פעמיים ,שני תהליכים יכולים אחרי זה לעשות wait במקביל . ● בעלות :במנעול רגיל ברגע שנעלתי אותו רק אני יכול לפתוח אותו ,ואילו ב semaphoreכל אחד יכול בכל רגע נתון לעשת wait או signal מבלי שעשה wait בעבר .זה אמנם הגיוני בחלק מהמקרים אבל צריך להבין שאין מישהו שימנע מה semaphoreלעבוד לא נכון .לכן צריך להיות זהירים! מה משמעות המונה? אם הוא חיובי זה בעצם כמה עוד יכולים לעבור ,אם הוא שלילי זה כמה מחכים בתור ואם הוא אפס אף אחד לא יכול לעבור ואף אחד עדיין לא ממתין . הערות : קוד שעושה סיגנלים מיותרים הוא מסוכן . שימוש אחר ב semaphoreהוא מונה סנכרוני מונה שאפשר לשנות אותו מכמה חוטים מבלי לחשוש שייכתב ערך זבל . בעיית producer/consumer ● יש חוטים שמוסיפים items מפס הייצור מפיקים . ● יש חוטים שמורידים items מפס הייצור צרכנים . מן הסתם אסור לקחת item מפס ייצור ריק או לשים item על פס ייצור מלא . אם ידוע מספר החפצים מממשים באמצעות מערך ציקלי . ● producerמוודא שיש מקום פנוי בפס הייצור ,שם את ה itemהחדש במקום המתאים ,מקדם בצורה ציקלית ומגדיל את מספר החפצים 36 ● consumerהפעולה המקבילה . נבחין כי יש לנו שני חוטים שמנסים לשנות אותו מונה ולא יודעים מה יהיה הערך שם ,לכן נרצה לתקן את הבעיה באמצעות שני : semaphores free_spaceשומר כמה מקום פנוי יש לנו . avaiitemsשומר כמה איברים יש במערך . ה producerמבצע wait ל ,free_spaceאם free_space>0 מקטינים אותו וממשיכים ,אחרת נחסמים לבסוף עושים signal ל avai_itemsכדי לסמן שיש עוד איבר פנוי . אם יש מספר חוטים שמריצים קוד של producers נבחין כי הם משתפים את המצביעים ,אם שלושה producersנקראים בבת אחת זה רעיונית בסדר אבל שלושתם עלולים להשתמש בדיוק באותו ה itemוזה לא תקין . מנעול קריאות וכתיבות Concurrent readers, exclusive writer (CREW) יש מקרים רבים שבהם נרצה רק לקרוא או רק לכתוב) מקובץ למשל( ,יש מצבים בהם יש הרבה קוראים במערכת . למה זה משנה לנו? אם כולם מנסים לכתוב ,אין ברירה אלא לנעול את המנעול ,אבל אם יש הרבה קוראים במערכת אפשר לתת לכולם לקרוא ורק לסמן שלא ניתן לכתוב ל databaseכרגע וכך נוכל לאפשר קריאות במקביל . 37 חייבים קודם לפתוח את המנעול ואז לחסום תהליך ,אבל אז מגיעים לבעיה של /"problem of lost wakeup" ראשית ,חשוב שנזכור כי פונקציות wait ו signalאינן אטומיות ,ולכן יכולה להתבצע החלפת הקשר בין שחרור של מנעול לבין חסימה של תהליך . נניח כי המונה ,0 = מגיע תהליך מבצע wait ורואה שהוא צריך להיכנס לתור ההמתנה ,לשם כך הוא פותח את המנעול ולפני שהוא הספיק להיחסם מתבצעת החלפת הקשר ומגיע תהליך אחר עושה ,signal מגדיל את המונה באחד ומוציא תהליך מתור המתנה) אותו תהליך שעוד לא הספקנו לחסום( ומבצע לו ,wakeup שהוא ממילא כבר ער אז זה סתם מציק לו .מתישהו מערכת ההפעלה תחזור לתהליך המקורי ואז הוא חוסם את עצמו , אבל הוא כבר לא בתור המתנה ואף אחד לא יעיר אותו יותר ): אם נזכר בקוד שראינו WAKEUP תסמן שיש סיגנל שממתין לו ולכן כשנעשה לו block ה schedמיד יעיר אותו , וכך פתרנו את הבעיה ב schedulerשלנו . אבל אם תהליך נכנס למצב uninterruptable המצב יותר בעייתי! חוק אמדל עבור n חוטים T_n ,הזמן שלוקח להריץ את אלגוריתם עם n חוטים ו sהוא החלק מתוך האלגוריתם שמורץ באופן סדרתי רציף ,אזי מתקיים : , Condition Variablesמשתנה תנאי זהו מנגנון המאפשר הוספת לוגיקה לסנכרון ,כשדיברנו על semaphore ראינו שזה מנגנון גנרי שננעל או נפתח לפי פעולה שאנו עושים ,אין שם שום לוגיקה של מצב המערכת . ב Condition Variablesמנסים לעשות החלטות יותר חכמות ,בניגוד למנגנונים שראינו עד עכשיו שיש להם שתי פונקציות ,למנגנון זה יש שלוש : ● waitבניגוד ל semaphoreשם בדקנו את המונה ולכל היותר שינינו אותו .כאן אם מבצעים wait התהליך תמיד נחסם .כשעובדים עם condition variables חייבים להעביר מנעול שאותו עלינו לנעול לפני ביצוע ה ,waitכשמצבעים wait מוותרים על המנעול ונכנסים לתור המתנה של ה ,conditionזו פעולה אטומית! לא ניתן לראות תהליך שויתר על המנעול ועוד לא הספיק להיכנס לתור המתנה של .condition ● signalבוחרת את אחד התהליכים בתור המתנה של ,condition מוציאה אותו משם ומעבירה אותו לתור ההמתנה של המנעול אותו הוא איבד ברגע שהוא עשה ,wait לפני שהוא חוזר לריצה הוא בהכרח יחזיק את המנעול .נהוג שמי שבמצע signal מחזיק כרגע במנעול אך זה לא הכרחי בניגוד ל .wait ● broadcastדומה מאוד ל ,signalאך היא מעירה את כל התהליכים הממתינים ושולחת את כולם לנסות לנעול את המנעול שלהם) יכול להיות שהם איבדו ב waitאותו מנעול ,אך יכול להיות שלכל אחד יש מנעול שונה( הבדל חשוב ביחס ל semaphoreהוא שאם אין אף אחד בתור המתנה ניתן להביא את המונה ל 30באמצעות ,signalואז 30 תהליכים יוכלו לעבור .זאת לעומת הפעולות אצלנו שהן חסרות זיכרון . למה זה טוב? מימוש תור אפשר להכניס ולהוציא איברים ,אך אסור להוציא איברים כשהתור ריק . מגדירים מנעול ,ב enqueueמיד נועלים אותו ובסוף משחררים אותו כלומר שמים את העצם ונועלים . בהוצאה ,נועלים מנעול בודקים אם התור ריק ואם לא מאבדים את המנעול ומחכים ,מתישהו נקבל את ה signal ואת המנעול ואז נוכל להוציא עצם . 38 מה היה קורה אם היינו הופכים את את signal ו ?put item כלום ,עדיין המנעול בידנו ולכן dequeue לא יוכל להשפיע ולהוציא איבר ,ונכונות הקוד לא תיפגע . למה צריך while ולא ?if נניח יש שלושה חוטים) למנעולים צריך חוטים( A,B ,עושים dequeue ו Cעושה .enqueue נניח מגיע ,A נועל ועובר ל ,waitמגיע C מכניס איבר לתור ויש Context Switch כעת מגיע ,B רואה שהמנעול נעול ונכנס לתור המתנה היחיד שיכול לרוץ כעת זה חוט ,C הוא שולח signal וחוט A עובר לתור המתנה של מנעול ,פותח את המנעול . כעת יש שני תהליכים בתור המתנה ,תהליך A ותהליך B או הראשון הוא ,FIFO אז B יוצא קודם ,אחרת אקראי . כעת B נועל מנעול ,מוציא איבר מהתור ומשחרר את המנעול . כעת תהליך A יכול לנעול את המנעול ולהוציא איבר מתור ריק! לכן נרצה לשים שם !wait למה בעצם זה קרה? A התעורר כי התור כבר לא היה ריק ,אבל עד שהוא חזר לריצה יכול להיות שהתור כבר לא ריק ולכן יש צורך בבדיקה חוזרת . producer/consumerעם משתני תנאי בדוגמא זו יהיה לנו מנעול בשם bLock ושני משתני תנאי .notFull, notEmpty : ה producerנועל את המנעול ובודק האם ה bufferמלא ,אם כן הוא נכנס להמתנה ,אחרת הוא מוסיף את ה itemומודיע . ה consumerבודק אם יש מה לצרוך ואם אין הוא הולך לישון ,אם כן הוא מודיע שיש מקום חדש שהתפנה . נבחין כי consumer, producer לא רצים במקביל שכן יש מנעול ,ולכן זה קוד פחות יעיל . אם היינו מוסיפים לולאה מקיפה בתוך כל נעילה היינו מקבלים שמתמלא לגמרי ומתרוקן לגמרי לחילופין . אז מה נותן לנו ה ?condition variables לכאורה אין בו צורך והוא מוסיף בעיות כפי שראינו עם .A,B,C אך בעולם המודרני משתמשים בו בצורה מאוד חזקה :בשפת Java יש תמיכה מובנית בסנכרון .ב Javaהם הגיעו למסקנה שהכל צריך להיות מסונכרן ,המון עבודה עם מנעולים ולכן השפה הייתה איטית מאוד ,למרות שברוב המקרים אין צורך בסנכרון ,למשל במבנה .Vectorלכן הוסיפו מבני נתונים אחרים דומים מאוד ללא סנכרון .בכל זאת מתכנתים משתמשים ב Vector במקום ב ArrayListגם על תוכניות של תהליך אחד .ניתן להגדיר גם Class או function בתור מסונכרן .כל המנגנון ב Javaמבוסס על Condition Variables כי ברוב המקרים זה יותר קל ומהיר . 39 הרצאה) 5 מצגת (5 Deadlocks כשמשתמשים לא נכון בסנכרון עלולים להגיע למצב של .Deadlock Deadlockזה מצב שיש קבוצה של חוטים או תהליכים שאנחנו יודעים בוודאות שהם לא יצליחו להתקדם . ייתכן במערכת שיש זמנית מצב שבו קבוצה של תהליכים נתקעים לזמן רב ,אבל כל עוד הם יוכלו להשתחרר בהסתברות כלשהי זה לא !Deadlock Deadlockקורה כשכל התהליכים מחכים לאיזשהו אירוע שאחד התהליכים המחכים צריך ליזום) למשל קבוצה של תהליכים שכל אחד מהם מחכה לאיזשהו משאב שהוא בשימוש של תהליך אחר באותה קבוצה( . הגדרה מדויקת :קבוצה של תהליכים שבה כל תהליך בקבוצה מחכה לתהליך שמוחזק ע"י תהליך אחר באותה קבוצה . The Dining Philosophers בעיה הממחישה את מושג ה ,Deadlockלכל אחד מהם יש קערת אורז מולו ו Chopstickמכל צד . כל אחד באופן ב"ת חושב ,תופס Chopstick מצד ימין ,ולאחר מכן Chopstick מצד שמאל ,אוכל ,מניח אותם וחוזר חלילה . ניתן להגיע למצב של ,deadlock כל אחד תפס Chopstick אחד ,והם מנסים לתפוס את השני אבל הוא כבר תפוס .הם לעולם לא יצליחו להתקדם! במצגת מוצג" מימוש" כקוד עם סמפורים ,כל שמוגדרים 5 סמפורים ,סמפור לכל . Chopstick philosopher(i): while(1) do think for a while )] wait( chopstick[i )] wait( chopstick[(i+1) % 5 eat )] signal( chopstick[(i+1) % 5 )] signal( chopstick[i :resource allocation graph ננסה להציג דוגמא מורכבת יותר ,ראשית נגדיר סימונים : עיגול כחול תהליך ,ריבוע משאב עם מספר עותקים . קשת מעותק של משאב לתהליך העותק בשימוש של תהליך . קשת מתהליך למשאב התהליך מחכה למשאב . כאשר יש עותק יחיד לכל משאב ניתן להחליף את הקשתות כך שילכו בין התהליכים עצמם . ננסה להבחין בקשר בין מעגלים בגרפים ל .Deadlocks * במצגת ישנו הסבר מפורט על הגדרת ה deadlockעפ"י סימון זה . 40 מה צריך להתקיים בדיוק כדי שיהיה ?Deadlock ארבעת התנאים הבאים צריכים להתקיים כולם : mutual exclusion .1גישה למשאב כלשהו היא מוגבלת למספר תהליכים חסום . hold & wait .2אני תופס משאב ונכנס להמתנה) למשל כי אני רוצה לנעול משאב אחר( . Circular wait .3ניתן לסדר את התהליכים כך שהתלויות הן מעגליות) תנאי הכרחי אך לא מספיק( . No resource preemption .4אין מצב שניתן לקחת בכוח משאב נעול ,אחרת מישהו היה יכול לשבור את המעגל . Dealing with Deadlocks ב 99%מי שאחראי לטפל בכך זה אנחנו ולא מערכת ההפעלה! התמודדות עם Deadlock מתחלקות לשתי גישות : .1אנחנו נכתוב קוד כך שהמערכת לא תיכנס ל ,Deadlockפשוט כותבים קוד נכון . .2אני כותב קוד שיכול להגיע ל deadlockאבל אני יודע איך להיחלץ משם) למשל מוסיף מנגנון שבודק אם יש סיכוי להיכנס ל deadlockואז תמנע מצב זה( . איך כותבים קוד כמו שצריך? מספיק שאחד מארבעת התנאים לא יתקיים ,ננסה למנוע מאחד מהם להתקיים : Hold & Wait .1לפני שמתחילים לרוץ מסתכלים על כל המשאבים ומבקשים את כולם ,בסוף הריצה משחררים את כולם ,וכך אין מצב שנעלתי רק חלק מהמשאבים! חסרונות :מה אם לא צריך את כל המנעולים ביחד ,אולי יש צורך בהם אחד אחרי השני .חסרון נוסף הוא שצריך לממש מנגנון נעילה של מספר מנעולים בו זמנית . שיפור :אני נועל את המשאבים לפי הצורך ,משחררים כל מה שהיו לנו עד עכשיו ,ומבקשים את המשאב החדש יחד עם כל המשאבים הקודמים) וכך אם המשאב החדש לא פנוי שיחרננו את אלו שכבר תפסנו( . no resource preemption .2אם במשך x זמן לא משתמשים בקובץ ,התהליך מאבד בעלות על הקובץ ולוקחים ממנו את המשאבים בכוח) אם נכנס להמתנה הוא לא ישתמש במשאב( . mutual exclusion .3יש המון מבני נתונים קיימים ,כמו רשימות מקושרות שאין הגבלה למספר התהליכים שיכולים להשתמש בה ואפשר לממש אותם ללא צורך במנעולים .ניתן לעשות זאת ע"י שימוש ב HWשתומך בפעולות אטומיות בלבד .בכל מקרה אם המערכת באמת מסובכת יש צורך במנעול בסופו של דבר . circular wait .4לכל משאב נותנים מספר ,כשנועלים משאב אפשר לנעול רק משאב העל מספר יותר גבוה ממה שיש לי עכשיו ,נועלים דברים לפי הסדר . 41 מעשית עבור הפילוסופים למשל נוכל להציע את התיקון הבא : הבעיה היא רק בפילוסוף האחרון שנועל קודם chopstick עם מספר גבוה ולאחר מכן chopstick עם מספר נמוך , ולכן ננעל עבורו קודם את 0 ואז את 4 ולא נקבל המתנה מעגלית! איך מגלים ?Deadlock קיים אלגוריתם שעושה אנליזה של הגרף (resource allocation graph) ומוצא ,Deadlock אם האלגוריתם מוצא מעגל בגרף אז קיים ,deadlock אלגוריתם זה לא ב scopeשל הקורס ,אבל נניח שאנחנו משתמשים בו . גישה אחת מציעה שנהרוג כמה מהתהליכים עד שה Deadlockישתחרר ,אבל עלולים בטעות להרוג תהליכים מאוד חשובים .גישה אחרת מציעה שניקח תהליכים בכוח ,אבל זה מסוכן .לכן בד"כ מערכת ההפעלה לא עושה שום דבר כדי לטפל ב ,Deadlockקשה לזהות מה צריך לעשות . המנעות מ Deadlock )לכן נרצה שמערכת ההפעלה תזהה מצב של Deadlock לפני שהוא יתרחש ותמנע כניסה אליו . כשתהליך בא ומבקש משאבים מהמערכת ,המערכת צריכה לחשוב האם ההקצאה הזאת יכולה לגרום ל Deadlockבעתיד ,אם יש חשש כזה היא לא תאפשר לבצע את ההקצאה ,היא יכולה להחזיר כישלון או לחסום אותו עד שההקצאה תהיה חוקית .מתי נקצה אותו? כשנדע שההקצאה לא יכולה לגרום ל (.Deadlock 42 43 סיכום ישן : אלגוריתם הבנקאי נניח אני צריך x שקלים מהבנק ,ואני מבטיח להחזיר 2x שקלים . נניח שלבנק יש רק x שקלים בסך הכל .ואני מבקש את העסקה בחלקים ,בכל פעם .1/2x וכעת מגיע אדם אחר שגם רוצה לבצע אותה עסקה בדיוק . כעת לאחר חודש רוצים לבצע את החלק הראשון ,כל אדם מקבל 1/2x ולבנק כבר אין כלום .כעת הבנק לא יוכל לבצע את החלק הבא של העסקה וכן אף לקוח לא יחזיר את הכסף ,שכן הוא עדיין לא הקים את העסק שלו . ונוצרה לנו כאן בעיה) זה אגב מה שקרה במשבר של .(2008 ניתן לדמות את זה כתהליכים באופן הא : R1מוקצה ל P1 R2מוקצה ל P2 נראה שעד עכשיו אנחנו בסדר אבל ייתכן ש P2רוצה לקבל את R1 ו P1רוצה את R2 וקיבלנו .deadlock מה נעשה? לא נקצה ל P2את ,R2 זו הקצאה לא חוקית! זה יכניס אותנו למצב בעייתי שניתן להגיע ממנו ל .deadlockזו דוגמא שמראה את הבעיה של השיטות האלו ,הן מחמירות ומניחות את הדבר הגרוע .הוא מניח שכל המשאבים שתהליך יכול לצרוך הוא צורך ביחד ,והוא אינו מאפשר ריצה אופטימלית . כל תהליך מתאר את מספר העותקים המקסימלי שנרצה מכל משאב ,שומרים את המידע על כל התהליכים במטריצה .כמו כן שומרים כמה בפועל עותקים של כל משאב הקצתי לכל תהליך ,במטריצה נוספת .ומתקיים .[cur[i,j] < max[i,j הווקטור R מתאר את המשאבים שנדרשים להרצה כרגע ע"י תהליך .P הווקטור avail מתאר כמה עותקים של כל משאב זמינים במערכת . נניח שקיבלנו את המשאבים שרצינו ב Rונתבונן בשינוי בוקטורים והמטריצות : הבדיקה הראשונה היא שלא עברנו את max וכן ש availאינו מכיל ערך שלילי . נבדוק האם נוכל לספק את הדרישות של כל התהליכים ,מחפשים אם יש תהליך שניתן לסיים עכשיו ,כל עוד לא סיימנו את כולם .אם תמיד מצאנו תהליך שיכול להסתיים נחזיר הצלחה ,אחרת כישלון . נבדוק אם כמה שאנחנו עוד נוכל לבקש פחות ממה שיש לנו פנוי אז הוא בהכרח יוכל לסיים ,אם כן נניח ששיחררנו אותו ונחפש את התהליך הבא שניתן לסיים .כמו כן האלגוריתם מחזיר סדר אפשרי שלפיו נרצה להריץ על מנת למנוע .deadlock 2 נבחין :סיבוכיות האלגוריתם היא , N אבל זהו אלגוריתם חמדן ,לא בדקנו את כל האפשרויות ויכול להיות שפספסנו את הצירוף הנכון מתוך N עצרת הצירופים . נכונות האלגוריתם נובעת מכך שאם שיחררנו בסדר שונה ,אז לכל היותר שיפרנו את המצב שלנו) כי בשלב מסוים באלגוריתם שיחררנו מישהו שבסדר האחר עוד לא היה משוחרר( ,ואם לא מצאנו את הפתרון לא קיים סידור אחר! השאלה היא אם יש false negative הוא אומר שאין סידור ובפועל יש false positiveלא ייתכן אם הוא אומר שיש סידור אז יש . חשוב לדעת לעשות את זה פורמלי לבחינה! באינדוקציה ולא בנפנופי ידיים! 44 הרצאה) 6 מצגת (6 כמו שאמרנו בהרצאה הראשונה אחד המאפיינים של מערכת ההפעלה הוא שהיא לא רצה מעצמה ,אלא מגיבה ל ,eventsכלומר לפסיקות חומרה ותוכנה שהיא מקבלת . סוגי פסיקות מי מייצר את הפסיקות? פסיקות חומרה נוצרות ע"י חומרה חיצונית ,לחיצה על מקש מקלדת ,פסיקת שעון Nic Network Interface , Cardוכד' ,פסיקות כאלה יכולות להגיע בכל רגע נתון בלי שום קשר וסנכרון לשעון הפנימי של המעבד . פסיקות סנכרוניות ,פנימיות המעבד מריץ קוד וכתוצאה מכך הוא גורם לפסיקת תוכנה ,זו פסיקה סנכרונית שכן היא נוצרת כתוצאה מקוד של המעבד . נבחין בין שני סוגים של פסיקות פנימיות : ● פסיקות מפורשות (explicit) המעבד רוצה ליזום פסיקה ,למשל קריאות מערכת הפעלה במטרה לקבל שירות שאנו זקוקים לו .סיבה נוספת היא ,Debugging ה Debuggerממומש באמצעות פסיקה מפורשת . ● פסיקות מרומזות (implicit) פסיקות שלא היינו רוצים לקבל אבל בלית ברירה הן מתבצעות ,פסיקות אלו קורות כאשר אנו מבצעים פעולה לא חוקית) גישה לכתובת לא חוקית ,ניסיון לבצע פקודה המותרת לביצוע רק מ ,Kernel Spaceניסיון לשינוי] CS ה ringשאנו נמצאים בו שמור ב Code Segmentבשני הביטים הראשונים ולכן אסור לשנות אותו אבל מותר לשנות את ה .([eipההתנהגות ברירת המחדל היא ללכת ולהרוג את תהליך שביצע את הפעולה הלא חוקית . יש פקודות לא חוקיות שבהן אשמה מערכת ההפעלה ולא התהליך page fault ,למשל מנסים לגשת לזיכרון חוקי לגמרי אבל הוא לא נמצא בפועל ב RAMאלא בדיסק בגלל שלמערכת ההפעלה נגמר המקום ולכן עבור פסיקה זו לא נהרוג את התהליך אלא נדאג לתקן את ה .fault טיפול בפסיקות לכל פסיקה מוצמד מספר המציין את המיקום שלה ב ,INTERRUPT_TABLEכאשר המיקום שלה בטבלה מצביע על הפונקציה לביצוע . מי טוען את הטבלה הזאת? כשהמחשב נדלק הדבר היחידי שמחשב יודע לעשות זה להתחיל לטעון את ה ,BIOSכאשר הדבר האחרון ב BIOSהוא קריאה לטעינה של מערכת ההפעלה .עם זאת הקריאה מה BIOSלא מאוד אמינה לכן הדבר הראשון שמערכת ההפעלה עושה היא למחוק את הטבלה שה BIOSטען לשם ,ואז היא מעדכנת את הטבלה לפי ה driversשלה על מנת להבטיח התנהגות נכונה . כל פסיקה ,לא משנה אם הפעלנו אותה בצורה ישירה או עקיפה אנו תמיד שומרים את ה Contextשל התהליך שרץ עכשיו) חמישה שדות( ,עוברים לפונקציה המוצבעת ,מחליפים מחסנית למחסנית של ה Kernelומבצעים את קריאת המערכת . חשוב להבין שהתהליך שעל חשבונו מתבצעת הפסיקה הוא לא בהכרח התהליך שקשור לפסיקה הזאת ,גם תהליך חישובי יכול להיות זה שיקבל את פסיקת המקלדת . 45 נניח שחסמנו פסיקות ,מה קורה בכל זאת כשמגיעה פסיקה? אם המעבד מתעלם מהפסיקה הזאת לגמרי יש לכך כל מיני חסרונות למשל לא נספור כמו שצריך את זמן הריצה של התהליך ,לא יתבצע סנכרון עם חומרות חיצוניות והן יתנתקו ,לכן אסור סתם להתעלם מהפסיקות .באינטל ,32 מעבד יכול לזכור על קיום של 2 פסיקות לא מטופלות מכל סוג ,לכן ברגע שנאפשר שוב פסיקות נטפל בשתי הפסיקות הללו ,כמובן שאנו עדיין בבעיה אם מגיעות יותר פסיקות ,לכן לא נרצה לחסום פסיקות לזמן רב . חלוקת פסיקה פסיקה מחולקת לשני חלקים : ● חלק קריטי Tophalves First Level Interrupts Handler החלק שצריך לבצע עכשיו לפני כל פסיקה אחרת ,למשל עבור מקלדת זה לדעת על מה הקשתי ,כי אם נמתין המשתמש עלול להקליד שוב ונאבד את ההקשה הקודמת ,לכן קריטי לגשת ל driverשל המקלדת ולקרוא את האות , ● חלק לא קריטי Bottomhalves עבור המקלדת למשל המשך הטיפול בפסיקה בדיקה של איזה תהליך מחכה לאות הזאת ,להעיר אותו וכו' זה החלק הלא קריטי .בד"כ יש רשימה של משימות לביצוע ב Bottomhalvesואנו מכניסים אותה לשם . מתי מבצעים את המשימות הללו? תלוי במערכת ההפעלה ,יכול להיות במעבר מ Kernelל ,User יכול להיות כשהמערכת לא עמוסה ,אבל בכל מקרה אנו לא חוסמים פסיקות להרבה זמן ולא מענישים את התהליך הנוכחי . מה נעשה אחרי שנסיים את הפסיקה? עבור פסיקת חומרה זה פשוט לא נעבור לפקודה הבאה לביצוע אלא נטפל בפסיקה באופן מלא ורק אז נחזור לקוד ונחזיר את הפקודה הבאה לביצוע .גם עבור פסיקות תוכנה מפורשות מתבצע אותו תהליך . לעומת זאת עבור פסיקות תוכנה מרומזות שנוצרו כתוצאה מתקלה הפקודה האחרונה שהרצתי היא זו שגרמה לתקלה ולכן במקרה כזה מה שנריץ אחרי סיום טיפול בפסיקה זו אותה פקודה שוב ושוב .זה מאוד הגיוני בנושא של ,page fault אם הכתובת לא חוקית נרצה לבצע את אותה הפקודה שוב בתקווה שכעת מערכת ההפעלה טענה את המידע .עבור פקודות לא חוקיות אחרות ה signalברירת המחדל גורם להריגת התהליך .צריך להיות מאוד זהירים בשינוי ה ,signalאם למשל נחליט שאחרי חלוקה באפס ממשיכים את ריצת התוכנית נקבל לולאה אינסופית ,יש חלוקה ,מערכת ההפעלה שולחת ,signal הוא מתעלם ומערכת ההפעלה מנסה להריץ אותה פקודה שוב . ניהול פסיקות במערכת מרובת ליבות : פסיקות מקומיות קורות תמיד בליבה שגרמה להם . פסיקות חיצוניות יש מספר גישות לעניין זה : באופן סטטי למשל כל הפסיקות הולכות לאותו מעבד ,מחלקים את הפסיקות השונות לפי תחומים בין המעבדים . באופן דינמי ,RR שליחת פסיקה למעבד הכי פחות עמוס . Polling ישנן חומרות חיצוניות שמייצרות פסיקות בקצב מאוד גבוה ,למשל כרטיס רשת שיוצר פסיקה עבור כל פקטה וכך המעבד יצטרך כל הזמן לטפל בפסיקות ונקבל בזבוז מאוד גדול של זמן חישוב של מעבד . לכן ,יש גישה אחרת לניהול פסיקות ,ניתן להגיד לחומרה מסוימת שלא תשלח פסיקות כלל) או פשוט להתעלם( ופעם בפרק זמן מסוים ניגשים לחומרה ובודקים אם יש לה משהו חדש להביא לנו . החסרון הוא שהנתונים עלולים לחכות יותר מדי זמן ,ולכן יש כל מיני שיפורים לרעיון ה .Polling 46 השיפורים העיקריים מתחלקים לשני סוגים : מבוסס חומרה : ● כרטיס רשת לא מודיע מיד על קבלת פקטה אלא הוא מנסה לאגור כמות גבוהה יותר . ● כרטיס הרשת מחכה פרק זמן מסוים לפני שליחת ההודעה . רוב כרטיסי הרשת לא תומכים בזה ,ולכן יש פתרון מבוסס תוכנה : ● בלינוקס למשל מתחילים לעבוד עם כרטיס הרשת כהתקן חיצוני רגיל ,ברגע שמגלים שקצב הפסיקות עובר איזה גבול מסוים מערכת ההפעלה מתחילה לעבוד על כרטיס הרשת בצורה של ,Polling מתעלמים לגמרי מפסיקות שמגיעות מהכרטיס הזה וניגשים בעצמנו לחומרה פעם ב Xזמן .אם מערכת ההפעלה רואה שהיא ניגשה פעם או פעמיים ב pollingולכרטיס לא היה מידע חדש היא חוזרת לטפל בו כחומרה רגילה מתוך הנחה שהוא עבר את השלב בו היה מספר גבוה של פסיקות . DMA Direct Memory Access לא רוצים שההתקן ישמור את כל הנתונים שהוא צריך להעביר על עצמו כך שבכל פעם נצטרך להעתיק את המידע ,ולכן בד"כ אומרים להתקן לאיזה איזור בזיכרון הוא יכול לכתוב את הנתונים שלו .זו גישה מאוד נפוצה . החיסרון הוא שההתקן צריך להודיע למערכת ההפעלה גם כאשר הוא מסיים לכתוב ,מערכת ההפעלה צריכה לדעת שהוא סיים לכתוב כדי להגיד שה bufferהתפנה . דוגמא קריאה מדיסק process מבצעים פסיקה מפורשת=> read() מוציאים בקשה לדיסק להוציא נתונים לזיכרון=> OS משהים את התהליך=> process is suspended, others will run until the I/O arrives => find blocks from which data should be read => initiate DMA transaction מסתיימת הקריאה והנתונים מגיעים לזיכרון=> DMA done => device fires interrupt to notify DMA is done Bottom half בגדול רק ליצור משימה של=> OS tophalf adds relevant bottom to relevant list => later, bottom half runs & copies data to user => makes process runnable => process scheduled and can now read the data סיגנלים סיגנל הוא ערוץ תקשורת בין מערכת הפעלה לתהליכים ,זהו event שנוצר ע"י מערכת ההפעלה ומטופל בהקשר של תהליכים .לכל תהליך יש Vector של טיפול בסיגנלים שאומר מה צריך לבצע עבור כל אחד) יש 31 סיגנלים בלינוקס( ,השמות של הסיגנלים הם סטנדרטיים ולא ניתנים לשינוי .מה שניתן לשינוי הוא התגובה של התהליך לסיגנל . 47 P: טיפשית דוגמא #include <signal.h> #include <stdio.h> #include <stdlib.h> void sigfpe_handler(int signum) { fprintf(stderr,"I divided by zero!\n"); exit(EXIT_FAILURE); } int main() { signal(SIGFPE, sigsegv_handler); // ערך שמשנה הפעלה מערכת קריאת בוקטור מסוים int x = 1/0; return 0; } P: טיפשית פחות דוגמא #include <signal.h> #include <stdio.h> #include <stdlib.h> void sigint_handler(int signum) { printf("I'm ignoring your ctrlc!\n"); } int main() { // when pressing ctrlc in shell // => SIGINT is delivered to foreground process signal(SIGINT,sigint_handler); for(;;) { /*endless look*/ } return 0; } עם kill 9 באמצעות אותו ולהרוג CTRL+Z באמצעות אותו לעצור ניתן ? התהליך את להרוג זאת בכל ניתן איך .SIGKILL סיגנל לתהליך ששולחת התהליך של pidה מתהליך למנוע מנת על SIGSTOPו SIGKILL :שלהם handlerה את להחליף ניתן שלא סיגנלים משני אחד זהו .שליטה ללא לרוץ SIGCONT באמצעות נעשה הריצה חידוש .breakpointל הגעה בעת Debuggerב בד"כ משתמשים STOPב .שלה handlerה את לשנות ניתן אבל אותו למנוע ניתן שלא signal בעצם גם זה 48 דוגמא שאפילו יש בה היגיון מסוים : יש שני סיגנלים sigusr1 ו sigusr2שניתן להשתמש בהם לצורכנו . int g_count=0; void do_work() { for(int i=0; i<10000000; i++); } void sigusr_handler(int signum) { printf("Work done so far: %d\n", g_count); } int main() { signal(SIGUSR1,sigusr_handler); for(;;) { do_work(); g_count++; } return 0; } כשנרצה לדעת את מצב התוכנית נשלח לה .kill USR1 מה הבעיה בקוד הזה? צריך סנכרון ברמת העיקרון של ,g_count אם g_count היה מבנה יותר מורכב היינו יכולים לקבל את ה signalבמהלך טיפול במבנה הנתונים ולכן יש צורך בסנכרון . סיגנלים נפוצים ● SIGSEGVנשלח ע"י bottom half של segv בפועל נקרא כאשר עושים גישה לא חוקית לזיכרון . ● SIGILLנשלח כאשר מנסים לבצע פקודה לא חוקית מבחינת הרשאות ב .user space ● SIGFPEחלוקה באפס . הערה :הפקודה halt עוברת בהצלחה בלי שום signal אבל לא עושה דבר ,זו בעיה גדולה בוירטואליזציה מצפים לקבל תגובה ממערכת ההפעלה שעשינו משהו לא חוקי אבל בפועל לא מקבלים שום דבר . ● SIGCHLDכאשר אחד הבנים מקבל SIGSTOP או מת . ● SIGALRMסיגנל שנשלח כעבור זמן מסוים ,כמו ע"י alarm ו .settimer ● SIGTRAPכשרוצים לעשות step ב .debugger ● SIGUSR1,SIGUSR2אין להם משמעות מיוחדת אפשר לתת להם איזו משמעות שבא לנו . ● SIGXCPUבודק האם תהליך מנצל את כל הזיכרון שלו ,אם הוא מתקרב לניצול של כל הזיכרון שלו הוא מקבל סיגנל של SIGXCPU שהוא עלול לחרוג בקרוב מדרישת הזיכרון שלו ,אם הוא לא דואג לצמצם את ניצול הזיכרון הורגים אותו . ● SIGPIPEנשלח כשמנסים לכתוב ל pipeשאין לו שום קורא פעיל והנתונים הללו סתם ילכו לאיבוד . ● SIGIOנשלח כשבאיזהו התקן שאפשר לעשות ממנו קריאה נוצרו נתונים .למה זה שימושי? קריאה מקבצים בצורה לא חוסמת .הדרך הפשוטה היא כזכור לפתוח קובץ ולנסות לקרוא ,ואם הקובץ ריק התהליך ייחסם .יש מקרים שבהם אם אין שום מידע בקובץ לא נרצה להיחסם ,אלא לקבל התרעה שאין מידע .ניתן לבקש זאת ממערכת ההפעלה וכך אם הקובץ ריק נקבל,EAGAIN אמנם אנו יכולים לבדוק שוב בעצמנו אבל אפשר גם לחכות ל SIGIOשיגיד שיש שם נתונים . 49 Signal System Calls int kill(pid_t pid, int sig) ● מאפשר שליחת signal לתהליך מסוים . ● int sigprocmask(int how, const sigset_t* set, sigset_t* oldset) שינוי ה maskשל הסיגנלים שבהם מטפלים ,מוסיפים עוד סיגנלים שאנו רוצים לחסום או לשחרר,או קביעה מחודשת לפי ה .how ● int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact) כזכור קריאת המערכת signal מחליפה את ה ,handlerלא להשתמש בקריאת המערכת אף פעם! זו קריאת מערכת שלא מוגדרת היטב ,בכל הפצה של לינוקס זה יכול לעשות משהו שונה) למשל יכול להיות חד פעמי או לתמיד( ולכן נשתמש ב sigactionsונעביר לה מצביע לפוקציה החדשה ומוחזר מצביע לפונקציה הקודמת למקרה שנרצה לשחזר אותה . רצוי לדעת ש signalsמשפיעים על קריאת מערכת הפעלה אחרות למשל אם ביקשנו לעשות read ונחסמנו ברגע שנשלח signal כלשהו לאותו תהליך הפקודה החוסמת נכשלת) אם התהליך במצב (INTERRUPTIBLE ואז read תסתיים עם שגיאה כאשר ההערך המוחזר היא .ERROR_INTERRUPT סיגנלים לעומת פסיקות מתי הם קורים? שניהם יכולים להיות לא סנכרוניים ביחס לפעולתו של התהליך . מי מייצר אותם? ● פסיקות חומרה או תהליכים . ● סיגנלים מערכת ההפעלה או תהליכים . מי מטפל בהם? ● פסיקות מערכת ההפעלה . ● סיגנלים המשתמש . מי מייצר אותם ומגדיר את אופן פעולתם? ● פסיקות החומרה מייצרת אותם ומערכת ההפעלה מחליטה על הלוגיקה . ● סיגנלים מערכת ההפעלה מייצרת אותם והתהליך מגדיר את הלוגיקה . מי חוסם אותם? ● פסיקות מערכת ההפעלה . ● סיגנלים התהליך . 50 הרצאה) 7 מצגת(7 זיכרון וירטואלי מבוא הזכרנו כבר בהרצאה את ה ,Cacheרכיבי זיכרון שהינם חלק מהמעבד ,אשר משמים אותנו לצורך חיסכון בגישות לזיכרון .ה ,DRAMהזיכרון החיצוני ,הינו חיצוני למעבד אך נמצא כמה שיותר קרוב לו על מנת לשפר את זמן הריצה . לידע כללי :בד"כ משתמשים בכרטיסי זיכרון בזוגות ,ישנן מערכות הפעלה שיתלוננו על צירוף אחר ,וישנן מערכות הפעלה שפשוט יעבדו באופן מזוויע . דוגמא לדיסק :תדר המעבד (1600MHz) כמה פעמים המעבד יכול לשדר מידע לזיכרון בשניה .הוא יכול להעביר 64bit בשניה ,וסה"כ נקבל כי מועברים 12.8GB לשניה .ה Latencyשל רכיבי זיכרון הוא בד"כ סדר גודל של 100 ננו שניות במקרה האופטימלי ,עד לאלפי ננו שניות . מה ההבדל בין DRAM ל ? HDD הגישה ל DRAMדורשת מתח ואילו הגישה ל HDDלא דורשת מתח) חשמל( ,המידע ב DRAMאינו נשמר לאחר אובדן מתח ,ובפרט לאחר כיבוי המחשב . כמו כן ,הגישה ל DRAMהרבה יותר מהירה והעיכוב קטן יותר .נבחין בהבדל המשמעותי בין זמני הגישה הרציפה ב HDDלגישה אקראית ,הבדלים שאין לנו ב) .DRAMשכן כפי שניתן להבין משמו זהו זיכרון שהעבודה הינו הינה בד"כ בגישות אקראיות .(Random Access Memory ניהול זיכרון ניזכר כי בתחילת הסמסטר הצגנו כיצד התהליך חושב שהזיכרון נראה רציף החל מכתובת .0 אך ברור שאם כל תהליך חושב שזוהי מפת הזיכרון יש לנו כאן בעיה ,ראשית תהליכים יכולים להתנגש בגישות לזיכרון מבלי להתכוון לכך .ברור לנו שלא נרצה שתהליכים יגשו לאותם איזורי זיכרון סתם כך ,ולכן צריך להגביל את התהליך מלגשת לאיזור זיכרון ששייך לתהליך אחר או למערכת ההפעלה . כמו כן ,ייתכן שכל תהליך חושב שיש לו יותר זיכרון ממה שיש בפועל במערכת ,ולכן גם את בעיה זו נרצה לפתור . זיכרון וירטואלי ישנן כמה פתרונות לבעיה ,אנו נלמד את השיטה הנפוצה היום זיכרון וירטואלי . תחילה נגדיר מספר מושגים : ● כתובת וירטואלית VA כל תהליך עובד עם כתובות וירטואליות ,כתובות רציפות החל מכתובת .0 ● כתובת פיזית PA הכתובת האמיתית של המידע בזיכרון הראשי ,מוכרות רק למערכת ההפעלה ולחומרה . נרצה בעצם שיטה שבהינתן כתובת וירטואלית תיתן לנו כתובת פיזית ,ובכך תאפשר לנו לקשר בין האיזור הוירטואלי של התהליך לבין איזור פיזי בזיכרון הראשי . הזיכרון הוירטואלי מדמה לתהליך את התנאים שהוא צופה שהתקיימו ,התהליך לא צריך בכלל לדעת לאן בפועל הוא ניגש ,ומידע זה מוסתר ממנו . 51 ● Pageהזיכרון מחולק לבלוקים בגודל קבוע ,כאשר Page הוא בלוק של מידע רציף ,במערכות שאנחנו נעבוד איתן גודלו יהיה בד"כ .4KB ● Frameבלוק בזיכרון הפיזי שמתאים בגודלו להחזקת .Page כל Page שיושב בזיכרון הראשי ימופה ל Frameמסוים ,ונצטרך לשמור את המיפוי הזה במקום מסוים . יכול להיות ש Pageמסוים לא ממופה ל Frameפיזי .מתי למשל? יכול להיות שה Pageעוד לא הוקצה , התהליך לא היה זקוק לו עד עכשיו .יכול להיות גם שה Pageיושב בדיסק ,זאת משום שאם נגמרים ה Frames בזיכרון הראשי מעבירים חלק מה Pagesלזיכרון המשני ,עד שיש בהם צורך . איך ניקח כתובת וירטואלית ונמפה אותה לכתובת פיזית? לצורך הדיון נניח שיש בידנו 32bit של כתובת וירטואלית ו 32bitשל כתובת פיזית . נזכור ,הזיכרון הפיזי הוא רכיב יחיד ומשותף בין כל התהליכים בפועל .נשתמש במיפוי של ה Pagesעל מנת לקבוע אם תהליכים ישתפו ביניהם מידע מסוים או לא ,אם נרצה שיתוף נמפה לאותה כתובת פיזית ,אחרת נדאג למפות לכתובות שונות . מה המיפוי שלנו צריך בעצם לעשות? ראשית הוא צריך לתרגם Page ל .Frameלאחר מציאת ה Frameנוכל להשתמש ב Offsetישירות מבלי לתרגם אותו . מה עושים אם הזיכרון הוירטואלי הוא גדול יותר מהזיכרון הפיזי ,למשל 64bit ל ?32bitעדיין ניתן לשניהם אותו גודל של ,offset בהתאם לגודל הדף .רק נצטרך לעבוד קשה יותר כדי למפות מ Pageל .Frame איך עושים את המיפוי הזה? הפתרון הפשוט הוא טבלה לכל תהליך שממפה באופן ישיר מכתובת דף וירטואלית לכתובת frame פיזי . כל entry בטבלה הוא בגודל ,32bit עשרים מהם ישמשו ל Mappingוהנותרים ישמשו למידע נוסף ,האם המידע רק לקריאה ,האם הוא שייך למערכת ההפעלה וכו' . למה בעצם החומרה צריכה לעשות את כל הבדיקות כאן ולא מערכת ההפעלה? נזכור שמערכת ההפעלה היא לא חלק מהסיפור הזה ,היא מתערבת אך ורק כשהחומרה אומרת שיש בעיה .אנחנו לא רוצים שבכל פעם שתהליך ינסה לגשת לזיכרון שלו מערכת ההפעלה תתערב ולכן החומרה צריכה להיות זאת שמגלה את הבעיות והיא זקוקה לאינדיקטורים ממערכת ההפעלה לגבי חוקיות הגישה . 52 ● Validנקרא גם .Present תפקידו של הביט הוא להגיד האם המידע נמצא בזיכרון או שהוא לא נמצא שם ,כלומר Valid הינו 0 בין אם דף לא הוקצה כלל או שהוא כרגע שמור בדיסק . כשהחומרה ניגשת לתרגם כתובת היא בודקת את ,Valid אם הוא שווה לאפס היא אינה ממשיכה בתרגום וגורמת לפסיקת .Page Fault מערכת ההפעלה צריכה להחליט מה לעשות עם הפסיקה ,היא מקבלת את סיבת התקלה ברגיסטר ובודקת למה התקבל .Page Fault אם למשל המידע נמצא בדיסק ,על מערכת ההפעלה למצוא Frameפנוי ולבקש להביא את ה .Page כיצד יודעים מאיפה להביא את ה ?Pageלוקחים את 31 הביטים הנותרים ומהם צריך להסיק איפה נמצאת הכתובת ,כל מערכת הפעלה עושה משהו אחר .בלינוקס למשל 7 ראשונים אומרים מה ה ,Swapperבלינוקס יש תמיכה בכ .Swappers 27הביטים הנותרים 24) ביטים( אומרים מהו ה Offsetבתוך ה .Swapper ● Page inיש מידע שהעברנו מתישהו לדיסק ועכשיו קוראים אותו חזרה לזיכרון ● Page Outיש מידע שאנחנו לא זקוקים לו כרגע ולכן נעביר לדיסק . בנוסף יש גם Swap In ו Outשמשמעותם הפוכה . Major Page Fault כאשר הדף אינו בזיכרון ויש להביאו מהדיסק נקבל Major Page Fault .1המעבד מזהה מידע לא ולידי ,נניח כי בגלל שהוא בדיסק . .2המעבד גורם לפסיקה . .3מערכת ההפעלה מבקשת את המידע מהזיכרון . .4מערכת ההפעלה עושה החלפת הקשר ,שמה את התהליך כ .TASK UNINTERRUPTABLE .5ברגע שהמידע יגיע הדיסק תשלח פסיקה ,היא תגלה שהגיעו הנתונים שחיכו להם ותחזיר את התהליך למצב .ready .6מתישהו בהמשך התהליך יחזור לרוץ ויבצע את אותה פקודה שגרמה לפסיקה ,(restartable interrupt) כיוון שכעת יש לו את הדף שהוא חיפש . Minor Page Fault לא כל ה Page Faultמתרחשים מסיבה כזאת . למשל ב ,COW Copy On Writeכפי שתיארנו בביצוע fork משתמשים באותם דפים והופכים אותם ל read ,onlyברגע שאחד מנסה לכתוב לדף ,מערכת ההפעלה משכפלת את הדף ונותנת לתהליך שביקש לכתוב את ההעתק של הדף ,וכך חוסכים את ההעתקה של מרחב הזיכרון .זהו גם סוג של ,Page Fault אבל הוא לא דורש גישה לדיסק וגם גישות מחוץ למערך null pointers ,וכאלה . יש מקרים שבהם אמנם ה Frameלא נמצא בזיכרון אבל תהליך אחר כבר הביא אותו) למשל מידע מהדיסק( ולכן מערכת ההפעלה יכולה להביא לו את המידע באופן מיידי מה I/O Cacheמבלי אפילו להעבירו להמתנה . mmap כזכור בד"כ קוראים קבצים באופן סדרתי מהדיסק .ישנם מקרים שבהם רוצים לגשת לאיזורים שונים בדיסק תוך כדי קפיצות ,דרך אחת לעשות זאת היא פשוט לקרוא את כל הקובץ ולהתעלם ממה שלא מעניין אותנו . mmapהיא קריאת מערכת המאפשרת לנו להתייחס לקובץ כאל מערך של בתים ,וכך מקבלים כתובת וירטואלית שניתן לגשת אליה .נניח למשל שרוצים לגשת ל ,6kאז 6k/4k ייתן לנו את מספר הדף מתחילת הקובץ ו 6kmod4kנותן לנו את ה .offset 53 כשאנחנו רוצים להוציא מידע החוצה מהזיכרון הוירטואלי צריך להחליט לאן הוא הולך ,אם הוא הגיע כ memory mapמקובץ בד"כ נחזיר אותו חזרה לקובץ .ניתן לבצע מיפוי" פרטי" שאומר שאם משנים את המידע אנחנו לא רוצים שיחזור לקובץ ואז עושים סוג של ,COW כל עוד לא משנים הוא מגיע מהקובץ ,וברגע שמשנים עושים העתק ולא מעדכנים את הקובץ המקורי . נניח שיש לנו Page שמקורו בזיכרון ,ורוצים לזרוק אותו .אם המידע לא השתנה בפועל אז לא צריך לעשות לו ,page outאם שיניתי אותו אז יש צורך לעשות לו .map out אם זה עותק פרטי שלנו נצטרך למצוא לו מקום בדיסק ,אם זה עותק כללי נצטרך לעדכן את המקור . Demand Paging מה קורה כשקוראים נתון מהדיסק? מתי הוא יגיע לזיכרון? השיטה הבסיסית היא שרק כאשר ניגשים לנתון והוא לא נמצא שם יוצרים Page Fault ומביאים את המידע . הבעייתיות היא שבכל פעם שניגש לזיכרון בפעם הראשונה נקבל ,Page Fault מה שמשמעותי לזמן הריצה של התוכנית . יש כל מיני שיפורים שמנסים לפתור זאת ,למשל מנסים לזהות ,Patterns אם רואים שאנו עושים גישות סדרתיות לדיסק נביא גם את הדפים שלידו ,כלומר גם את הדף שלפניו וגם את הדף שאחריו וזה משפר משמעותית את הביצועים .כלומר בודקים האם יש לוקליות במקום מביאים עוד דפים מהאיזור שממנו קראנו הרבה . readahead prefetchingהפונקציות mmap ו read מבצעות prefetching כאשר הם מזהות גישה סידרתית . גישה זאת משלימה את demand paging כדי לנסות למזער את מספר חריגות הדף . Working Set ניסיון של מערכת ההפעלה להבין באיזה דפים תהליך מסוים משתמש .הוא מבוסס על טבלה ומסתכלים על ההיסטוריה של שלושת הגישות האחרונות לדיסק . מה נותנת לנו הסטטיסטיקה הזאת? מערכת ההפעלה יכולה ,לפני שהיא נותנת לתהליך לרוץ ,לבדוק האם כל הדפים ב Working Setשלו בזיכרון ,ואם לא היא לא תיתן לו לרוץ עד שהם יהיו בזיכרון .כך מערכת ההפעלה תתדאג שלפחות הדפים שהוא השתמש בהם לאחרונה יהיו בזיכרון וכך נימנע מ Page Faultבתחילת ריצת התהליך Microsoft .משתמשים בשיטה הזאת בצורה מאוד חזקה . Thrashingכשהקצנו הרבה יותר זיכרון ממה שיש לנו אנו עלולים להגיע למצב שכל תהליך מבצע ,Page Fault מעבירים לריצה של תהליך אחר שמבצע Page Fault וכן הלאה ,ללא התקדמות בביצוע הקוד .זו הייתה בעיה ממש חמורה בגרסאות ראשונות של ,Windows למשל .Windows ME האם הצלחנו כלומר ,האם הזיכרון הוירטואלי פותר את הבעיה? ● ניתן בקלות לתת לתהליך זיכרון רציף ,פשוט מגדירים מרחב כתובות וירטואלי רציף . ● הצלחנו לאפשר לכל תהליך לקבל יותר זיכרון מהמקום שיש בפועל בזיכרון הראשי באמצעות דפדוף , אמנם אנו עלולים לקבל ריצה איטית יותר ,אבל מעבר לכך זה עובד . ● הצלחנו להפריד ולבודד תהליכים שונים ,ע"י כך שפשוט לא ממפים אותם לאותו בלוק פיזי . ● יש לנו Access Control בטבלת התרגום כנדרש ,כבר ברמת החומרה . אבל איך הביצועים? הביצועים הם נקודה מאוד בעייתית ,אם מקודם) כשיש רק כתובות פיזיות( היינו ניגשים לזיכרון באופן ישיר באמצעות הכתובת ,כעת אנחנו צריכים לגשת ראשית לזיכרון כדי לקרוא את טבלת התרגום ופעם שניה בשביל הנתונים.כלומר ,הדבר הכי קריטי במערכת ,גישה לנתונים ,ייקח כעת פי 2 יותר זמן . 54 איך מטפלים בזה? כמו שאמרנו קודם 90% ,מהזמן אנחנו ניגשים ל 10%מהנתונים) לוקליות במקום( .לכן ,בהרבה מקרים ה Cacheיעזור לנו ,לא נצטרך לבצע את הגישה לדיסק עצמו ,אלא ל .Cacheכדי לשפר את מהירות התרגום לא נרצה להסתמך על L1 ו ,L2זה יגרום לנו לבזבז 2 מחזורי שעון נוספים על תחילת ביצוע פקודה ,במקרה הטוב . לכן מגדירים ,TLB חומרה חיצונית מאוד קטנה ומהירה ,בין 32 ל 128כניסות , שמכילה אך ורק תרגומים במיוחד את התרגומים האחרונים שעשינו ,זה בעצם Cache לכתובות וירטואליות . כמה הוא עוזר? מאוד! בתוכנית אופיינית ה hit rateהוא מעל .99% נבחין שבכל פעם שעושים החלפת הקשר התוכן של ה TLBכבר לא רלוונטי ולכן יש לעשות לו ,flush זו אחת הסיבות לכך שאחרי החלפת הקשר תהליך מתחיל לרוץ לאט ,ולוקח לו זמן" להתחמם" .מנסים לצמצם את ה Cold Startהזה , אחד הדברים שעושים הוא שיש כניסות ב TLBשאף פעם לא נפסלות ,למשל נתונים שקשורים למערכת ההפעלה לא זזים .לכן ליד כל כניסה רשום האם זו כתובת שצריך לפסול אותה או שאין בכך צורך ,(global) כנ"ל בהחלפה בין חוטים שאיזור הזיכרון הפיזי שלהם משותף . מה עוד משפיע על הביצועים? מדיניות החלפת הדפים) אלגוריתם דפדוף( יכולה להשפיע כמובן באופן משמעותי על מספר ה Page Faults וכתוצאה מכך על זמן הריצה . אלגוריתמי דפדוף ● ● ● ● ● ● חמדן / בלדי / אופטימלי כשיש צורך להוציא דף אנו בודקים איזה דף נצטרך בעוד הכי הרבה זמן ונזרוק אותו ,אבל אין דרך קלה לבצע את ההחלטה הזו בלי איזו מכונת זמן . FIFOאין יותר פשוט מזה ,צריך לשמור עבור כל דף את זמן הכניסה שלו ,נניח באמצעות רשימה . LRUנעיף את זה שניגשנו אליו הכי הרבה זמן) כמו בלדי על העבר( ,Least Recently Used ,ברוב המקרים הוא מתאים לנו ,אבל איך נממש אותו? ○ בכל פעם שניגשים לדף צריך להעביר אותו ל"ראש הרשימה" ,או לשמור את זמן הגישה האחרון אליו .אי אפשר לממש את זה בצורה יעילה ולכן לא משתמשים ב .LRUאלא בד"כ עושים קירובים ל .LRU Second Chanceדומה מאוד ל ,NRUלכל דף אנחנו מצמידים דגל של ,Accessed מודלק ע"י החומרה בכל פעם שהיא ניגשת לדף .כשרוצים להעיף דף מהזיכרון עושים סריקה של כל הדפים באיזשהו סדר ,מסתכלים על .Accessed אם הוא ,1 הוא לא מעיף את הדף אבל מסמן שכבר נתנו לו צ'אנס ונכבה את ,Access ונמשיך לחפש עד שנמצא מישהו מכובה .נניח שחזרנו כבר לאחר כמה גישות לדף הראשון שלו ,אם מערכת ההפעלה כבר ניגשה אליו פעם אחת ,אז נמשיך כמו קודם .אם הוא עדיין 0 אז נזרוק אותו) זה קירוב ל ,(LFU Least Frequently Used אלגוריתם שעון וריאציה של Second Chance מסתכלים על הדפים בזיכרון כמעגל ,יש לנו מצביע לדף הבא שצריך לסרוק .לכל דף יש את ביט ה ,Accessedששווה לאחד או אפס . אלגוריתם LFU OPT באלגוריתם זה קיימת הנחה שאנו יודעים מראש מה הולך לקרות .כלומר נדע מראש מתי נזדקק לכל דף) דבר שכמובן לא קורה במציאות( .במקרה זה נוציא מהזיכורן תמיד את הדף שלא יהיה בשימוש הכי הרבה זמן . 55 הירכיית -2רמות נבחין כי לכל תהליך נצטרך לפחות 4MB נתונים רק בשביל הטבלה שלו) מספר הדפים הוא .(2^20 יש כאן בזבוז אדיר! איך מונעים את הבזבוז הזה? צריך להבין שרוב הכניסות בטבלה הזאת הן ריקות ,ה Pagesשם לא מוקצים בכלל ,שכן נדיר שתהליך משתמש בכל הזיכרון שלו .נרצה שתהיה לנו יכולת לא להקצות את כל הטבלה הזאת . סתם לא להקצות נתונים לא יעזור לנו ,אנחנו נראה כתובת זבל וננסה לגשת אליה .לכן נפתור את זה בצורה דומה לרעיון של הקצאת הדפים ,ניצור טבלה שממנה נצביע ל Page Tableהמקורי .זה בעצם מערך דו מימדי! פשוט לא צריך להקצות כל תת טבלה אם לא ניגשים לכתובות שם! במקרה הטבלה שלנו נכנסת בדיוק לדף אחד ,זה לא בהכרח ככה ,ניתן היה לבחור גודל אחר ל .Page Table רמה שונה נקבעת לפי גודל של PTE ומספר הכניסות שהיא צריכה להכיל . איך נמפה את הכתובת הוירטואלית? הערה :הסבר מפורט יותר בשקפים ,בדיוק כמו בממ"ס . היחס בין החומרה והתוכנה חשוב להבין שהגודל של כל הטבלאות ,הפורמט והמבנה של הטבלאות ,נקבע לפי החומרה .מי שאחראי לתחזק ולמלא את הטבלאות זה מערכת ההפעלה .מי שמשתמש בנתוני העזר שנמצאים שם זה בעיקר החומרה ,וכן מערכת ההפעלה שצריכה לתקן את הנתונים שיש שם . בנוסף לניהול הטבלאות שעושה מערכת ההפעלה גם החומרה עושה עדכונים בתוך הטבלאות הללו ,כל פעם שהיא ניגשת לאיזשהו PTE היא יכולה לשנות אותו ,להדליק את הדגל Accessed, Dirty וכד' . נבחין כי במודל הנוכחי יש לנו שלוש גישות לזיכרון בכל פעם ,בגלל שיש שתי רמות של טבלאות תרגום ,לכן תפקוד ה TLBהופך להיות אפילו יותר קריטי! עוד נקודה שמאוד חשובה במערכת זה התפקיד שמשחקים הcacheים , בעיקר L2 אך גם .L1 56 למה? ניזכר שאמרנו שהבלוק ששמים ב ,L2זה לא מילה (32bit) אלא 32 בתים ,כלומר ל L2מגיע בלוק יותר גדול ,מביאים לא רק את המילה אלא 8 מילים . למה זה חשוב? הוא בעצם משפר לנו את פעולת ה ,TLBנניח כי תירגמנו ב TLBכתובת של דף מסוים ,ה PTE שהבאנו נמצא ב ,cacheולכן ב L2יש לנו גם את ה PTEsהבאים בטבלה ,וכשנרצה לקרוא דף שנמצא אחריו בזיכרון הוירטואלי נקבל כי ה PTEשלו עצמו כבר נמצא בזיכרון ולכן נחסוך . שאלה לדוגמא) מבחינה( נתון זיכרון שמנוהל ע"י טבלאות הדפים ב 4רמות .מרחב הזיכרון הוא ,64bit גודל הדף הינו ,64KB וגודל כניסה ברשימת הדפים .128bit מהו הגודל של הרמה הראשונה וכמה דפים היא מכילה? פרטו את החישובים . פתרון : טיפ לשים לב ליחידות ביטים הם לא בתים! נשים לב כי גודל דף 216 בתים ,וגודל PTE הוא 24 בתים . דרך ארוכה : קל לראות שצריך 16 ביטים ל offsetכדי להגיע לכל בית בתוך בלוק . כמה דפים יש לנו? 64 48 2 גודל זיכרון וירטואלי לחלק לגודל דף .כלומר , 216וקיבלנו . 2 לכן מספר הכניסות הכולל ב L4הוא . 248 והגודל הכולל הוא . 248 כפול הגודל של PTE סה"כ . 252 38 כמה דפים צריך ל ?L4הגודל שלו לחלק לגודל דף סה"כ . 2 16 ולכן על אותה הדרך מקבלים את הגודל של L3 וכן הלאה עד שנגיע שגודל L1 הוא . 2 וסה"כ היא מכילה דף אחד . הערה :מקובל שגודל הבלוקים בטבלאות הינו כגודל דף ,פשוט כי זה נוח יותר) וכך נניח( ,אבל ברמת העיקרון זה לא נכון . דרך קצרה ופשוטה : נבחין ,כל שכבת ביניים אליה מגיעים היא בגודל דף ,וגודל PTE בה הוא קבוע ,מכאן ניתן להסיק באופן מיידי את מספר הביטים שצריך בכל שלב כדי למפות בתוך הטבלה ,במקרה הזה .12 זה נכון רק לשלבי ביניים ולא ל !L1 12 מספר הביטים ב L1הוא פשוט מה שנותר ומכאן נקבל את גודלו של L1 שבמקרה זה יוצא 2 כפול גודל של .PTE היו יכולים לקבוע גודל של בלוקים לכל רמה . שאלה נוספת) מבחינה( אנומלית בלדי ייתכן שאני מגדיל זיכרון וכתוצאה מכך מקבלים יותר .Page Faults . קיימים d,r כך ש כאשר P F A הוא מספר ה page faultsעבור אלגוריתם .A הגדלת הזיכרון עושה לנו נזק! האלגוריתם המפורסם שסובל מהבעיה הזאת הוא FIFO ו LFU 57 תכונת המחסנית של אלגוריתמים כשמריצים אלגורתם A אומרים שהוא מקיים תכונת מחסנית אם בעת ריצת האלגוריתם עם זיכרון גדול יותר איזור הזיכרון מכיל את כל הדפים שהיו בריצה הקודמת לכל d,r אלגוריתמים שמקיימים זאת הם בלדי ו LRU אלו תכונות סותרות מי שיש לו את האחת אין לו את השני . האם ל LFUיש אנומליית בלדי? כן ,צריך להביא דוגמא : למשל ,d=3 נרצה לקבל בזיכרון בגודל 3 תוצאה יותר טובה גישה א' לנסות למצוא את הסדרה הזאת ,זה משחק עם המודולו גישה ב' רוצים להראות ש LFUיכול להתנהג כמו ,FIFO ידוע שיש סדרה שדופקת את .FIFO נניח שמסתכלים על הסדרה ורואים ש FIFOזורק את הדף E ומשאיר את A,B נשנה את הסדרה באופן הבא : נוסיף רצף ארוך של A ו ,Bכשיגיע זמן להכניס את D מבחינת FIFO לא עשינו שום דבר ,מבחינת LFU אנחנו מוודאים שהם לא יעופו עושים זאת עבור כל page fault וקיבלנו סדרה מתאימה ל !LRU זיכרון וירטואלי -כתובות פיזיות של 64bit כעת נחזור על הדוגמאות שעשינו קודם לכן ,אך הפעם עם מחשבים בעלי 64bit של כתובת פיזית . 64ביטים הם הרבה יותר ממה שאנחנו בפועל צריכים Exabyte) של כתובות ,(RAM לאחר הערכות שונות הגיעו למסקנה ש 48ביטים צריכים להספיק לכל המחשבים היום Petabyte) של כתובות RAM ניתנות לייצוג( ,מה שנקרא 48 2 bit ought to be enough for anybody לכן נגדיר את הכתובות הוירטואליות שלנו כ 64ביטים ,ומהם נגיע ל 48ביטים של כתובת פיזית . משיקולים כאלה ואחרים החליטו שגודל של דף לא צריך להשתנות ,אם נחלק את כמות המידע הפיזי בגודל הדף נקבל כי יש לנו 236 מסגרות ,נבחין כי כל PTE צריך להכיל 36 ביטים שמייצגים את המסגרת ,לכן במערכת הזאת PTE הוא 8 בתים ,מה שיתן לנו .64bit חישוב כתובות בהיררכיית 4 רמות ● offsetגודלו לא ישתנה ויישאר 12 ביטים ,כי גודל דף הוא .4kb ● את שאר החישובים נעשה בדומה לתרגיל שפתרנו ,הוחלט כי גם בתרגיל זה גודל הטבלה הינו גודל דף , גודל PTE הוא 8bits ולכן יש 2 בחזקת 9 כניסות בכל רמה . ● מתעלמים מהביטים העליונים שכן אין בהם צורך . נבחין כי החישובים עצמם עובדים באותו האופן ,רק היינו צריכים להתייחס להבדלים בגודלי הטבלאות . 64 bit PowerPc מריץ אפליקציות עם כתובות וירטואליות של ,64bit אך הוא נועד כדי להריץ הרבה אפליקציות במקביל ,ולכן נרצה למנוע מצב בו תהליך אחד שרץ לא יעיף בטעות דפים של תהליך אחר ,לכן הרכיבו ארכיטקטורה שונה לגמרי ממה שאנחנו מכירים מהמערכות של אינטל . 58 במערכת של PPC יש שלוש רמות שונות ● רמה עליונה effective space כל כתובת שם היא 64bit והיא בדיוק הכתובת הוירטואלית שאנו מכירים ב ,x86לכל תהליך יש מרחב כתובות ייחודי לו ,ההבדל הוא במיפוי . ● הכתובות הללו ממופות למרחב וירטואלי שונה לחלוטין מזה של ,x86 הכתובת של המרחב הוירטואלי היא של 80bits והיא משותפת לכל התהליכים ,כלומר כל כתובת וירטואלית פרטית של תהליך ממופה לכתובת במרחב הוירטואלי הגלובלי . ● הכתובת במרחב הוירטואלי ממופות לכתובת פיזית של ,62bit כפי שמוגדר בארכיטקטורה . מהו גודל הדף? בארכיטקטורה של אינטל בחרנו בגודל דף קטן על מנת למנוע שברור ,זאת למרות שבלוקים גדולים היו משפרים את פעולת ה .TLB נבחין כי גודל דף ברמה העליונה יכול להיות גדול יותר שכן יש עוד רמת ביניים שמפרידה בין הזיכרון האפקטיבי של תהליך לבין הרמה הפיזית של החומרה ,ולכן כדי לשפר את הביצועים של TLB נתעסק ברמת ה effective בדפים ענקיים של ,256MB מתוך 64bit של כתובת וירטואלית 28 הם ,offset ושאר הביטים מייצגים את מספר הדף במרחב הוירטואלי . כלומר כשרוצים לתרגם כתובת וירטואלית לכתובת של המרחב הוירטואלי 28 ,תחתונים לא מתורגמים . 36הביטים הנותרים צריך לתרגם ל 52ביטים) שיהיו משותפים לכל התהליכים( ,ע"י ה .SLB שאלת הבנה :מדוע אנחנו צריכים 52 ביטים של דפים וירטואלים אם יש לנו רק 36 ביטים כאלה בכתובת הוירטואלית המקורית של תהליך? נזכור כי המרחב הוירטואלי משותף לכל התהליכים ונרצה שיהיה מקום ליותר מהכתובות של תהליך אחד . השלב השני הוא תרגום של כתובת וירטואלית לכתובת פיזית ,כאן באמת נרצה לבחור בגודל דף קטן יותר ולא לעבוד בבלוקים ענקיים ,לכן גודל כל frame פיזי הוא 4KB כמו ב ,x86ונתסכל על 12 ביטים תחתונים כעל ה offsetו 68ביטים שהם מספר ה ,frameאותם נרצה לתרגם ל 50ביטים של מספר ה ,frameע"י ה .TLB 68 שאלת הבנה :איך זה שיש לנו כתובות וירטואליות הרבה יותר גדולות ,כלומר כיצד ייתכן שאנו רוצים לתרגם 2 מסגרות ל 250מסגרות? נזכור כי לא כל הכתובות הללו צריכות לשבת בפועל בזיכרון הפיזי ,יכול להיות שחלקן כלל לא הוקצו או שהן יושבות בזיכרון המשני ,ומרחב הכתובות הוירטואלי יכול להיות גדול יותר ממרחב הכתובת הפיזי . נבחין כי לכתובת במרחב הוירטואלי מסתכלים בתרגום הראשון כ 28ביטים של ,offset ו 58של מספר דף ובתרגום השני כ 12ביטים של offset ו 68ביטים של מספר דף . SLBמנגנון חומרה ,יש אחד ויחיד כזה ,(singleton) שומרת מיפויים אחרונים מכתובת במרחב אפקטיבי למרחב וירטואלי ,מכיל משהו כמו 32 כניסות והגישה אליו לוקחת בערך מחזור שעון אחד . TLBלא להתבלבל עם האחד של אינטל ,ה TLBהזה אף פעם לא נפסל בהחלפת הקשר ,שכן המרחב הוירטואלי אף פעם לא משתנה כשמחליפים בין תהליכים! יש בו 1024 כניסות . ה TLBנותן לנו מיפוי מכתובת וירטואלית לכתובת פיזית + וירטואלית ולכן כל שורה מכילה 68+50 ביטים . אחד היתרונות של חלוקת המיפוי למיפוי ממרחב אפקטיבי לוירטואלי וממנו לפיזי היא שיפור משמעותי של פעולת ה Cache עבור ה ,SLBהדפים הם הרבה יותר גדולים ולכן נקבל מספר קטן בהרבה של .misses עבור ה ,TLBמכיוון שהמרחב הוירטואלי משותף לכל התהליכים נקבל כי אין צורך בביצוע flush לאחר החלפת הקשר ,ולכן גם מיד לאחר החלפת ההקשר נוכל לקבל hits בסבירות גבוהה יותר . 59 מה שיהיה מאוד משמעותי במערכת שאמורה להריץ מספר רב של תהליכים . נתאר תהליך תרגום ● חומרה ניגשת ל ,SLBאם לא הצליחה להוציא משם מידע נקבל ,page fault זאת בניגוד לארכיטקטורה של אינטל שם היינו עושים ,page walk זאת משום שלחומרה אין שום מידע לאיך מערכת ההפעלה מנהלת את המיפוי הזה ,היא הולכת לטבלאות התרגום שלה ופשוט מעדכנת ב SLBאת הכתובת המתאימה) נזכור כי בשלב זה אין בכלל עניין של דפדוף והבאת כתובות מהזיכרון ,מדובר בו מתרגום מוירטואלי לוירטאלי ולכן החומרה לא חייבת לשחק תפקיד ,כמו כן אנחנו עובדים עם דפים הרבה יותר גדולים ויש היגיון בלנהל אותם מצד מערכת ההפעלה( ● חומרה ניגשת ל ,TLBאם היא לא הצליחה להוציא משם מידע החומרה מנסה לבנות את התא ב TLB בעצמה . הכיצד? בנוסף ל TLBיש HTAB טבלת hash שמכילה כל מיני תרגומים ,החומרה מפעילה פונקציית hashידועה מראש ,הטבלה עצמה גם קבועה ומוקצת בעליית המערכת .החומרה ניגשת לתא המתאים ובו 8 תרגומים8 ,כתובות וירטואליות ועבור כל אחת מהן כתובת פיזית מתאימה) זו הסיבה שאנחנו שומרים את התרגום כזוג של כתובת וירטואלית ופיזית( ,מחפשים צמד עם כתובת וירטואלית מתאימה , ואם מוצאים מעבירים אותו ל TLBומחדשים את הריצה ,אם לא מצאנו את התרגום שחיפשנו אז נהיים עצובים ): ואז מפעילים פונקציית hash שניה ומחפשים בתא אחר ,אם מצאנו ,(: אחרת .),: מה נעשה? נזרוק .page fault מערכת ההפעלה הולכת לדפי התרגום שלה ,היא שומרת דפי תרגום מלאים ממרחב וירטואלי למרחב פיזי ,טבלה ענקית ,היא מוצאת את התרגום הנכון מכתובת וירטואלית לכתובת פיזית והיא מעדכנת את הHTABע"י אחת פונקציות ה) hashכי אין לה גישה ל ,(TLBודורסת את אחד התרגומים שיושבים שם , ומחדשת את הריצה .כעת החומרה תוכל למצוא את הנתון ,לעדכן את ה TLBולהמשיך את הריצה כרגיל (: התרגום הזה הוא כבד וארוך יחסית ,גם ללא ,misses מדוע? SLB הוא די יעיל ,אבל ה TLBהוא גדול ולכן הוא איטי יותר מהאחד של אינטל . לכן יש לנו buffer נוסף שנקרא ,ERAT שמבצע תרגום ישיר באופן מאוד יעיל מכתובת אפקטיבית לכתובת פיזית )מדלג על שלב הביניים( ,נבחין כי זה אומר שהוא צריך לתרגם דפים של 256MB לדפים של ,4KB לכן אם למשל נרצה למחוק דף של ,256MB נצטרך לעבור על 216 דפים שונים ולמחוק את כולם מה ,ERATכמובן שהוא לא מחזיק את כולם אבל חייבים לוודא שמוחקים אותם ,ולכן הבנייה שלו אינה וירטואלית . ERATרץ מאוד מהר ,באותו cycle אפשר לפנות אליו ,לקבל כתובת פיזית ולגשת באותו מחזור לכתובת הפיזית ,כמעט כל התרגומים מצליחים באמצעותו .הוא משחק בדיוק את אותו תפקיד כמו TLB אצל אינטל ,שגם אצלו היה hit rate מאוד גדול אפילו עבור TLB קטן . הירכיית TLB גם באינטל הגיעו למסקנה שה TLBמתחילים להיות פחות ופחות יעילים ,מדוע? גודלו לא השתנה ,שכן זה ישפיע על הביצועים ,אבל הצריכה של זיכרון ע"י תהליכים השתנה באופן משמעותי .תהליכים רגילים לדרוש כמויות גדולות מאוד של זיכרון .עבור תהליכים שרוצים 0.5GB של זיכרון ה TLBפשוט לא יצליח לשמור על אפקטיביות , לכן הגיעו לשתי מסקנות .1הגיע הזמן לעשות TLB היררכי ,כמו שעשינו ברמות ה cacheשל ,(data (L1,L2,L3 ולכן יש לנו כעת .TLB1, TLB2 60 superpages .2מי אמר בכלל שכל הדפים צריכים להיות באותו גודל? נניח שאנחנו מסתכלים על איזור ששייך ל heapואנחנו מצפים שהתהליך ישתמש בהמון זיכרון דינמי ,אז מה ההיגיון לחלק 0.5GB של זיכרון וירטואלי לדפים של ,4kb שעבור רובם נקבל ?miss לכן יש לנו ,superpages שבמקום הרמה הרביעית מצביעים לבלוק גדול ,למשל של 1GB של .data זה לא טריויאלי למימוש ,כי כמעט כל דבר בתרגום צריך להשתנות ,וצריך תמיכה מצד החומרה . כמו כן לא טריויאלי להחליט מתי צריך להשתמש בהם ומתי לא . ישנם פתרונות שונים שמנסים לעשות ,למשל לשנות את הגודל של הטבלאות בתהליך התרגום ,למשל PGD של ,4MBו PTשל ,512kb או אולי אפילו להיפך מבחינת גדלים ,יש וריאציות שונות שאפשר לעשות במטרה לקבל פתרונות אופטימליים יותר ולקבל ביצועים משופרים . 61 הרצאה 8 מערכות קבצים מבוא אז למה צריך דיסקים חיצוניים? נזכור כי הרמות שאיתן עבדנו עד כה הן הרגיסטרים ,ה ,cachesוה ,RAMהרמות הללו קטנות יחסית ,וחשוב מכך המידע שלהן לא נשמר מהדלקה להדלקה . יש לנו צורך באיזושהי חומרה שתכיל כמות גדולה הרבה יותר של מידע ותוכל לשמור אותו לאורך זמן . זה מוסיף לנו את הרמות של SSD, HDD ו) tape drivesגדולים למידע שהוא .(Write Once Read Never מה הדרישות שלנו ממערכת הקבצים? ● ● ● ● ● ● ● נרצה שהמידע יישאר שם לאורך זמן נרצה שהמידע יהיה מאורגן בצורה המאפשרת מציאה קלה של המידע . נרצה תמיכה בבעלויות על קובץ נרצה מערכות קבצים שיוכלו להתמודד עם שגיאות ונפילות של חלק מהדיסק . נרצה ביצועים טובים . נרצה תמיכה לגישה מקבילית ,עם צפי מוגדר הן לקריאה והן לכתיבה . נרצה עבודה עם דיסק באופן אבסטרקטי שלא תלויה בסוג הדיסק SSD vs. HDD vs. CD) .(vs. tape איך מערכת ההפעלה תאפשר לנו זאת? נעבוד בצורה אבסטרקטית ,מבחינתנו יש קובץ ואיתו אנו עובדים ,מה קורה שם בפועל מבחינת הייצוג של המידע לא מעניין אותנו . איזה סוג של קבצים יש לנו? ● קובץ ● ספריה ● קישור) לקובץ או ספריה( ,בהמשך נרחיב על ההבדל בין soft link ו hard link ישנן מערכות קבצים שבהן קובץ וספריה הם כמעט אותו דבר ואילו יש מערכת קבצים שבהן אלו ישויות שונות לגמרי . קובץ יחידה לוגית של מידע ,מבחינתנו זה איזשהו ADT המאפשר פתיחה ,סגירה ,כתיבה וקריאה של מידע . מה מגדיר קובץ? ● ● ● שם . תוכן בד"כ רצף של בתים ,או טקסט שמקודד ע"י טבלת .ASCII meta dataמידע המתאר את הקובץ ,הרשאות גישה ,סוג הקובץ ,גודלו ,המיקום שלו וכו' . מתקיים שהקובץ הוא קבוע : הוא שורד נפילות מתח ,ונשאר גם אחרי שתהליך מסוים מת/הפסיק להשתמש בו . 62 מקביליות (concurrency) לפי POSIX ● קריאה מקובץ אין בעיה ,כל עוד קבצים רוצים רק לקרוא לא ניתקל בבעיה של סנכרון . ● כתיבה מקובץ נניח שיש לנו שני תהליכים שרוצים לכתוב לקובץ ,לפי דרישות ,POSIX אמנם סדר הכתיבה אינו מובטח אך מבחינת שאר התהליכים כל פעולת כתיבה של תהליך הינה פעולה אטומית ,או שקוראים את הקובץ לפני כתיבה ,או שקוראים אותה אחרי כתיבה ,אין תוצאת ביניים .במערכת קבצים לוקלית בד"כ יש תמיכה מלאה בסטנדרט בעזרת ה ,IOCache במערכות אינן לוקליות כמעט אף פעם אין תמיכה בזה . File Metadata ● גודל . ● בעלים . ● הרשאות בד"כ מיוצג ע"י ,9bits שלושה של ,self שלושה של group ושלושה של ,universe כאשר כל שלשה היא ביט לקריאה ,ביט לכתיבה וביט להרצה . ● timestampזמן יצירת קובץ) הדברים האלה אינם מחייבים ,למשל זמן זה לא קיים במערכת של ,(native Unix זמן שינוי וכד' . ● מיקום איפה הבלוקים של התהליך נמצאים בפועל בדיסק . ● סוג הקובץ תיקייה ,קובץ בינארי ,טקסט וכו' . נבחין כי שם אינו metadata של הקובץ! זהו חלק מה dataשל .directory תוכן של ספריה הוא בעצם מערך של שם קובץ ומיקום ה metadataשלו ,כשרוצים לקרוא קובץ הולכים לספריה , קוראים את התוכן שלה ,מתוכה מסיקים את מיקום ה metadataומתחילים לקרוא את הקובץ . File Descriptors כשפותחים קובץ בהצלחה מקבלים איזשהו ,FD מספר אי שלילי) ב Linuxעד (255 שמאפשר למערכת ההפעלה לגשת לאובייקט ניהול של הקובץ . ישנן פקודות שעובדות עם השם של הקובץ ולא עם ה ,FDויש אפילו כאלה שעובדות עם שניהם ,אז עם מה כדאי לעבוד? עדיף בד"כ אם יש לכך אופציה לעבוד עם ,FD מדוע? יש לכך כמה סיבות ,ביניהן ישנם כמה דברים מאוד מרוכבים בהתנהגות של ה ,IOCacheשהופכים את העבודה של השמות של הקבצים למורכבת ,לכן באופן כללי נעדיף לעבוד עם .FD פקודות קנוניות לעבודה עם קבצים ● ● ● ● ● ● ● createיצירת קובץ ,לא מחזירה .descriptor openפתיחה של קובץ קיים ,בד"כ open יכולה לבצע .create deletionביצוע מחיקה של קובץ או ספריה renameמשנה את השם של הקובץ לפי השם הנוכחי statקבלת metadata של קובץ . chmodמאפשרת שינוי הרשאות גישה . chownביצוע שינוי בעלות ,נבחין כי לא ניתן להחזיר בעצמנו את הבעלות חזרה אלינו . 63 ● ● ● ● ● write, readכשפותחים קובץ מקבלים אינדקס שבאמצעותו אפשר לגשת לאובייקט ניהול , שמכיל בין השאר את ה ,file pointerהמיקום שממנו צריך לקרוא ,פעולות read ו writeעובדות בהתאמה מהמיקום הזה seekשינוי המיקום של ה file pointer syncמכריחה סנכרון בין ה IObufferלדיסק ,למשל פונקצית flush מרוקנת את ה .buffers lockישנה אפשרות לבצע נעילה של קובץ באמצעות ,flock אם מישהו אחר ינסה לעשות flock אחרי שאני נעלתי אותו הוא ייחסם ,אך הוא בכלל לא חייב לנסות ולנעול ,הוא יכול לנסות ולגשת למידע בלי לנעול ואז הוא לא ייחסם .זה נקרא .advisory lock יש גם mandatory lock במערכות הפעלה שונות כשפותחים קובץ לא צריך לבקש מנעול , מערכת ההפעלה תדאג לנעול אותו בשבילנו . סוגי קבצים בד"כ מערכות קבצים מפרידות בין טקסט וקוד בינארי ,ישנן מערכות שבהן סוג הקובץ נקבע לפי הסיומת שלו , החיסרון בכך הוא שלא ניתן לדעת אם תוכן הקובץ אכן מתאים למידע שבו . ב Unixיש לנו את הטיפוסים הבאים : ● קובץ ● תיקייה ● soft link ● pipe FIFOכפי שראינו בתרגולים זה סוג של קובץ אבל הוא מתנהל בצורה שונה לגמרי . ● socketגם הוא מתנהג בצורה שונה ,עובדים איתו בצורה אחרת . ● device fileגם ראינו בתרגול Magic Numbers in Unix בזמנו החליטו לנסות לקבוע איזשהו מספר קסם בתוך הקובץ שיציינו מהו סוג הקובץ בפועל . במקור היו רק שני בתים לשם כך ,כיום יש אפילו יותר בתים לעיתים ,הקובץ מתחיל בזה שמציינים איך אפשר להריץ את הקובץ .זהו אינו סטנדרט ,הבעיה המרכזית היא שקשה להמציא מספר חדש כי צריך לתאם את זה עם כולם . memory mapped files מנגנון המאפשר לנו למפות מידע רציף מקובץ לבלוקים רציפים בזיכרון הווירטואלי) או להיפך( . הוא נוח מאוד לעבודה עם ,code הוא חוסך תקורה של שימוש בקריאה וכתיבה רבים מקובץ מסוים . צריך להיזהר איתו שכן ישנן פעולות שאינן מוגדרות : נניח שפתחנו איזשהו קובץ כ memory mapped fileוכן באמצעות פתיחה רגילה ,והם לא מודעים זה לזה .כעת נניח שכתבנו משהו לתוך הקובץ בפתיחה הרגילה ,לא מובטח לנו מה יהיה הערך בקריאה מה .memory mapped fileלכן בד"כ נשתמש ב memory mappingרק לקבצים שלא ישתנו ,כמו הקוד של התוכנית . 64 ביטים של :ls l ● ● ● ● ● ● ● ● ● ● ביט ראשון סוג הקובץ : ○ b block device ○ c character device ○ p pipe ○ d directory לאחר מכן יש את 9 הביטים של ההרשאות . המספר שאחרי ההרשאות מייצג את המספר ה hard linksשל קובץ) מינימום 2 לתיקייה , מינימום 1 לקובץ( . שם הבעלים . שם הקבוצה . גודל הקובץ . עבור block device, or character device יש לנו במקום זאת major ו .minor ב FIFOהגודל במערכת הקבצים לא בהכרח מייצג את הגודל שלו בפועל ,אלו הם רק הנתונים שנמצאים בדיסק . זמן גישה אחרונה לקובץ . שם הקובץ עבור קבצים וספריות ,עבור soft links על מה הוא מצביע . Access Control Lists כל מערכת הפעלה יכולה לתמוך בהגנה הרבה יותר עדינה על קבצים ,לתת הרשאות שונות לקבוצות שונות וכד' , זה לא סטנדרט אלא תכונה של מערכת ההפעלה . ב Macיכלו לתת לכל user הרשאות משלו ,זה לא כל כך עבד… היום מערכות לא ממש תומכות בזה מכיוון שיש הרבה מעבר של מידע בין מערכות שונות ואין שום הבטחה לתמיכה בכך . תיקיות היסטורית מערכות קבצים היו מאורגנות בצורה של תיקיות ,כאשר כל תיקיה יכולה להכיל קבצים או תתי תיקיות נוספות ,הבחירה בעץ נעשתה מכיוון שיש הרבה אלגוריתמים שנוח מאוד להריץ על עצים ,וזה נעשה יותר מורכב כאשר מדובר בגרף עם מעגלים ,דוגמא קלאסית היא פעולת חיפוש שפשוטה למדי בעץ אבל נעשית יותר מורכבת במקרה של גרפים עם מעגלים . היום יש כל מיני תכונות של מערכות קבצים שגורמות להן להיראות יותר כמו גרף ,עדיין חסר מעגלים ,ופחות כמו עץ . 65 File Paths כשאנו עושים פעולה כלשהי על קובץ אנחנו צריכים להעביר" שם של קובץ" או" מסלול לקובץ" ,יש כמה דרכים לציין מסלול זה : ● מסלול אבסולוטי דרך אחת לציין מסלול ,מהשורש של מערכת הקבצים עד לקובץ הדרוש . ● מסלול יחסי מסלול ביחס לספריית העבודה ,WD Working Directory ,הנוכחית . פעולות על תיקיות : ● mkdirיוצרת תיקייה חדשה . ● rmdirמוחקת ספריה קיימת . שמות ייחודיים של ספריות : ● .קישור לספריה הנוכחית ,זהו קישור אמיתי שקיים במערכת הקבצים . ● ..קישור לספריית האב ,גם זהו קישור אמיתי שקיים במערכת הקבצים . מעבר על ספריה קריאת המערכת readdir קוראת לנו בכל פעם direntry נוסף של הספרייה ,עד שנעבור על כולם ,כך ניתן לעבור על כל תתי הספריות והקבצים שנמצאים בתיקיה מסוימת ,ניתן להמשיך ולעבור על מערכת הקבצים באופן רקורסיבי . כדי לבצע את ההדפסה של כל התיקיות והקבצים באופן רקורסיבי צריך לבדוק האם de הוא תיקייה ובמידה וכן להפעיל עליו את הפונקציה כולה באופן רקורסיבי . Hard Links לכל קובץ יש שם אחד לפחות ,אבל ייתכן שיש יותר .איך לקובץ יש יותר משם אחד? יש מה שנקרא .Hard Link נזכור כי שם של קובץ לא מופיע ב metadataשלו ,הוא מופיע בסך הכל בספרייה שבה נמצא הקובץ ,אין לנו שום בעיה לשים אותו קובץ בכמה תיקיות ,כלומר כניסה עם איזשהו שם שמצביע לאותו .metadata בפועל אין שום דבר שמפריד בין השם המקורי של קובץ לשמות שלו בתיקיות אחרות ,כל שם נוסף נקרא ,Hard Link מרגע היצירה הקבצים הם שווי ערך לחלוטין . מספר ה Hard Linksנשמר ב metadataשל הקובץ .צריך לזכור ש Hard Linkעובד אך ורק באותה מערכת קבצים ,לא ניתן לבצע קישור בין מערכת הקבצים של המחשב למשל וזו של התקן חיצוני . איך יוצרים ?Hard Link פונקצית link מקבלת שם של קובץ מקורי ושם חדש שנרצה לתת לקובץ ויוצרת אותו בתיקייה המתאימה) .ב shell באמצעות פקודת .(ln פונקצית unlink מוחקת את הקישור) ב shellבאמצעות פקודת .(rm 66 Reference Counting לכל קובץ שומרים את מספר הקישורים אליו .מדוע יש צורך בשמירה של מספר הקישורים ,כשאין יותר אף מצביע ל metadataשל קובץ מסוים אז נוכל לשחרר אותו שכן לאף אחד אין גישה אליו . מה קורה כאשר מנסים לסגור קובץ שהוא כבר פתוח? אנחנו עדיין יכולים לגשת לקובץ בעזרת ה FDשכבר פתוח . אם הקובץ פתוח אצל יותר מתהליך אחד כאשר הקישור האחרון אליו נמחק אז הקישור ימחק לפני שפונקציה unlinkתחזור ,אבל ההסרה של תוכן הקובץ תחכה עד שכל התהליכים שפתחו אותו יסגרו אותו . מה Hard Links עושים להיררכיה? הם הורסים את העץ שלנו! אם למשל יש Hard Link לספריית השורש אז נקבל מעגל . כמעט כל מערכות הקבצים תומכות ברמת העיקרון ב Hard Linksלספריות ,אבל הן חוסמות את האפשרות לבצע אותם ,וכך מונעת יצירה של מעגלים ,שכן Hard Links לקבצים לא יוכלו ליצור מעגל . למה בכל זאת יש לנו צורך בתמיכה ב ? Hard Links .ו ..הינם ,Hard Links ומערכת הקבצים משתמשת ב Hard Linksלצרכיה . כמה Hard Links לכל היותר ולכל הפחות יש לספריה במערכת? יש לכל הפחות שניים ,המצביע עליה מתוך התיקייה המכילה אותה ,והמצביע . המספר המקסימאלי תלוי במספר תתי התיקיות ,שכן כל תת תיקיה מכילה Hard Link לתיקיה שלנו תחת .. לכן ,עבור n תתי תיקיות נקבל n+2 מצביעים . Symbolic Link בדומה ל Shortcutשאנחנו מכירים ב ,Windowsזהו אינו שם אלטרנטיבי לקובץ ,זהו בסה"כ קובץ המכיל את ה Pathאל הקובץ ,אין לנו שום בעיה לבצע קישור כזה אל תיקיות ואל קבצים .מה יקרה בקוד C למשל אם ננסה לקרוא קובץ ?lnk נקרא את התוכן הבינארי המקודד את ה pathולא את הקובץ המוצבע! שכן זהו אינו שם אחר , זה בסך הכל קובץ שמכיל מסלול לקובץ אחר . ההבדל המהותי בין קובץ lnk לבין ,soft link הוא שכאשר מנסים לקרוא קובץ soft link התוכן שנקבל כן יהיה התוכן של הקובץ שהוא מצביע אליו ,ומי שאחראי על כך ,כלומר על לקרוא את ה ,soft linkלראות את ה path ולקרוא את הקובץ שיש שם ,זה מערכת ההפעלה .אם בכל זאת רוצים לקרוא את התוכן של ה soft linkניתן לעשות זאת עם .readlink קריאת המערכת symlink יוצרת) symbolic link ב .(shell ln s נבחין כתוצאה מכך שה sym linkבסך הכל מכיל את המסלול לקובץ המקורי ,נקבל כי אם נמחק את הקובץ המקורי מערכת ההפעלה תנסה לגשת אל המסלול ותגלה שהוא לא קיים ,כלומר הוא יכול להיות תלוי באוויר .זאת לעומת hard link שלא יכול להגיע למצב כזה . inode inodeהוא מבנה הנתונים שעל ידו מערכת ההפעלה מייצגת את הקובץ .לכל קובץ יש inode ייחודי לו .באופן כללי ,שם הקובץ מצביע על ה inodeשלו . 67 ה inodeמכיל את ה metadataשל הקובץ בעלים ,זמנים מיוחדים ,מיקום הבלוקים בדיסק וכו' . כל מערכת הפעלה בוחרת איזה מידע היא שומרת ב inodeאבל לפי הסטנדרט יש כמה דברים שחובה לשמור . נבחין כי אנחנו שומרים גודל קובץ ,גודל של בלוק ומספר בלוקים . למה אנחנו צריכים את זה? הרי מספיק לנו שניים מתוך שלושת הפרמטרים הללו שכן מתקיים : ) (block * sizeof(block)) = sizeof(file# נזכור כי אנחנו לא בהכרח משתמשים בכל הגודל של הבלוק! גודל של קובץ לא חייב להיות כפולה שלמה של בלוקים! אנחנו חייבים לשמור את גודל הקובץ כדי למנוע קריאה מסוף הבלוק האחרון שאינו תפוס . מימוש של מערכת הקבצים נזכור כי הרעיון הכללי שציינו הוא שיש ספריה שתוכנה הוא dir entries שהם לפחות השם של הקובץ ואיפה הוא "נמצא" ,כלומר הוא מצביע לאיזשהו בלוק ב Hard Diskשמצביע ל metadataשל הקובץ . אם נרצה לממש Hard Link זה פשוט למדי ,ניקח את הספרייה שנרצה להוסיף לה את ה Hard Linkונשים שם direntryחדש שמצביע ל metadataהרצוי . אם נרצה לממש Soft Link ניצור קובץ חדש ,וב dataנכתוב בו את המסלול שאליו נרצה להגיע .במערכות קבצים מבוססות ,Unix החליטו שזה מיותר ליצור קובץ שהתוכן שלו יהיה המסלול ולכן יש שתי גישות שונות : ● ב Dir Entryעצמו אנחנו נשמור את המצביע לקובץ המטרה עצמו ,הבעיה בכך היא שאין לנו אפשרות לשמור מידע נוסף .חלק ממטרת השימוש ב Soft Linkהיא שינוי של הרשאות גישה במעבר במסלול מסוים . ● יותר נפוץ הוא ליצור קובץ חדש ,בעל metadata ובו נשמור באמת את ההרשאות ,את המסלול לקובץ לא נשמור ב Dataשל הקובץ אלא ב metadataשלו .נזכור גישה לזיכרון מורכבת מקריאת ה ,metadataביצוע parsing ורק לאחר מכן קריאת המידע עצמו metadata ,הוא בד"כ ממש קטן ואילו הקובץ עצמו הוא גדול .עבור soft link ה dataבד"כ כל כך קטן שניתן יהיה להכניס אותו ל .metadata מימוש של ספריות עבור כל ספריה אנחנו שומרים את ה structשנקרא direntry שמכיל את השדות : ● d_inoהמספר הייחודי של ה inodeשל הספריה . ● d_offה offsetל direntryהבא ,הספרייה מיוצגת ע"י מערך של ,dir entries הם לא בהכרח נמצאים ברצף ולכן יש צורך בפרמטר זה . ● d_reclenהגודל של ה entryהנוכחי ,גם בו יש צורך על מנת לבצע את המעבר על מערך ה .dir entries ● d_typeסוג הקובץ . ● d_nameשם הקובץ ,בניגוד לרשום הקובץ מערכות קבצים כנראה משנות את גודל השם . Path Resolution Processאלגוריתם : Namei בהינתן מסלול ,כיצד נגיע לקובץ המיוצג על ידו? נפרק את המסלול לאטומים ,לפי המסלול ,עוברים אטום אחרי אטום u מוודאים שיש הרשאות גישה והרצה לכל הספריות לאורך הדרך .בכל פעם ניגשים לאטום ,בודקים שיש לנו את ההרשאות ,מבקשים את האטום הבא וכן האלה עד להגעה לקובץ . 68 מה יכול לקרות בדרך? ייתכן שנפגוש ,Hard Links אבל מבחינתנו זה לא מקרה מיוחד ,מתייחסים אליהם כאל אטום valid לחלוטין .כשפוגשים soft link אנחנו צריכים לקבל ממנו את המסלול אותו הוא מכיל ,נפרק אותו לאטומים ונטפל בהם לפני המשך הטיפול . הזמן שייקח לנו לטפל במסלול הזה הוא לפחות ליניארי במספר האטומים ,אבל נזכור שכל אחד מהאטומים יכול להכיל מספר רב של אטומים בתוכו .למה זמן הריצה של האלגוריתם יכול להיות ארוך אפילו אם אין לנו ?soft link נזכור שכדי למצוא את האטום הבא בהינתן ספריה צריך לעבור על כל ה dir entriesעד שנגיע אליו . בתרגול למדנו איך אפשר להאיץ את החיפוש ע"י שימוש במטמון של ה .dentries דיסקים לכל דיסק יש לפחות ראש קורא אחד . Trackמסילה מסלול מעגלי שמעבר עליו לא דורש הזזה של הראש הקורא . Sectorבלוק יחיד של data על מסלול מעגלי ,עובדים בד"כ בגרעיניות של Cluster המוגדר כארבעה .sectors יש הבדל משמעותי בין זמן של גישה אקראית לבין זמן של גישה רציפה . זמן הגישה מורכב מהזזת הראש הקורא ,והזמן שלוקח עד שהסקטור המתאים מגיע לראש הקורא . מידע נוסף במצגות של מערכות קבצים :P למה הדיסק נקרא ?''3.5 הוא נכנס בדיוק ל ''3.5של הקורא דיסקטים!! גישה לדיסק בדרך כלל אנחנו אומרים איזה סקטור ,משטח וגליל אנחנו רוצים לקרוא ,ייצוג זה לא נוח מבחינת התוכנה .מאוד לא נוח לכתוב קוד שמשתמש בייצוג הזה ,הרבה יותר נוח לחשוב על דיסק כמערך של בלוקים ,כאשר ה Disk Driveמתרגם את מספר הבלוק לסקטור ,משטח ,גליל .הייצוג הזה ע"י מערך של בלוקים נקרא Logical Block ,Addressהייצוג הפנימי לא מעניין אותנו . הגודל הנפוץ של בלוק ב LBAהוא 512 בתים ,זה כמובן לא מחייב את מערכת הקבצים להשתמש בבלוקים באותו הגודל ,שבד"כ הינו בין 2 ל ,KB 4כל בלוק של מערכת הקבצים יתפוס מספר בלוקים רציפים של הדיסק . למה בכלל להסתבך? למה לא להגיד שכל קובץ יהיה רציף בזיכרון הפיזי? 69 בעיקרון זה אפשרי .בגישה זאת נקבל שייצוג הקובץ הוא מאוד פשוט וזמן החיפוש הוא מינימאלי .אבל נקבל בעיה של שברור חיצוני . שברור חיצוני יש לנו בלוקים פנויים אבל הם מפוזרים ,ולא ניתן להקצות אותם באופן רציף .כמו כן זה מונע מאיתנו להגדיל קבצים קיימים במידה ואין עוד זיכרון רציף אחריהם . VSFS - Very Simple File System זוהי שיטה לניהול הזיכרון על הדיסק ,להלן דוגמא עבור דיסק קטן ,נניח של 64 בלוקים ,נצטרך לשמור את המידע הבא : superblockמכיל מידע חשוב על מערכת הקבצים ,כגון :מספר ה ,inodesמספר ה ,data blocksמיקום טבלת ה .inodesמספר קסם בדומה למה שלמדנו על התקנים בתרגול ומספר ה inodeשל ה .root directory מיקום של ה superblockחייב להיות ידוע ,בד"כ זהו בלוק 0 בדיסק . ניתן להגדיר שאנו מחלקים את הדיסק לכמה חתיכות כך שכל חתיכה מהווה" כביכול" דיסק קטן יותר .הסבר : 70 הערה :נבחין כי מערכות הקבצים אינן מתחילות מכתובת 0 שלהן ,לפי ,Start זה בגלל המידע ששמור לפני כן . מדוע לא ניתן ליצור hard link ממערכת קבצים אחת לאחרת? ב metadataרשום מספר ה ,inodeהוא תמיד ילך למספר ה inodeשל מערכת הקבצים שבה הוא נמצא ,ולכן לא ניתן לקשר אותו למערכת קבצים שונה לגמרי עם מספר שונה של .inodes לעומת זאת עבור soft link יש לנו מסלול ,ואיתו נוכל להסתדר ,הוא לא מסתמך על המבנה הפנימי של מערכת הקבצים ,לכן במסלול החיפוש יש לבדוק האם עברנו מערכת קבצים . הרכבה של מערכות קבצים כשרוצים לחבר מערכת קבצים חדשה למערכת קבצים קיימת מבצעים פעולת .mount לדוגמא :נניח שמבצעים פעולת .mount VFAT /usr2 כל הקבצים שנמצאים ב directory user2יוסתרו ,מערכת ההפעלה מבינה שזו mounting point והיא עוברת לעבוד לפי ה superblockשל מערכת הקבצים החדשה .ניסיון לגשת לקבצים הישנים פשוט לא יצליח עד לביצוע .umount מציאה של inode נניח שברצוננו למצוא בלוק של inode מסוים ,לשם כך נצטרך לחשב את הסקטור שלו ,את מיקומו בתוך הדיסק . ראשית נצטרך לדעת איפה מתחיל המערך של ,inode ולהתקדם לפי גודל של .inode לפי ה inodeנרצה לדעת איפה ה ,dataהפתרון הפשוט הוא לשמור ב inodeאת רשימת הבלוקים של ה data אך נצטרך לשמור יותר מדי ביטים על מנת לאפיין את הבלוקים ולא יהיה לנו מספיק מקום לכך . לכן נרצה להציע מנגנון חכם יותר ,שמבצע מיפוי . Multi-level Index איך אנחנו שומרים את הבלוקים של קובץ מסוים בדיסק? ואיך אנחנו ניגשים אליהם אח"כ? יש לנו 12 מצביעים ישירים שאומרים איפה נמצאים בלוקים של זיכרון .כלומר אם הקובץ שלנו מכיל לכל היותר 12 בלוקים אז מה שנוכל לעשות זה לשמור את המספרים הללו במערך האינדקסים של הבלוקים ,זה יספיק לכל קובץ של עד ,48KBפשוט על ידי ציון של מספרי הבלוקים . מה קורה אם אנחנו צריכים יותר מזה? לשם כך יש לנו את אינדקס ,13 כניסה זו אינה מצביעה לבלוק של הקובץ ,אלא היא מצביעה לבלוק המכיל מצביעים לבלוקים נוספים של ,dataרעיון זה נקרא .singleindirect pointer איזה קבצים נוכל לייצג בצורה כזאת? הבלוק החדש הוא ב ,4KBכל מצביע הוא 4 בתים וסה"כ 210 מצביעים ,לכן נוכל לייצג . 4M ≈ 4KB * (210 + 12) 71 דבר נוסף שיש לנו זה ,Double indirect pointer עם שתי רמות של קינון ,לאחר רמה אחת נגיע לבלוק של מצביעים למצביעים לבלוקים של .data יש לנו 210 מצביעים שכל אחד מהם מכיל 210 מצביעים לבלוקים של ,4K סה"כ .4GB על אותה הדרך נגדיר ,triple indirection שייתן לנו ,4TB וכך נוכל לייצג כל קובץ ריאלי . הערה : הבלוקים של ה indirectהם בדיוק כמו בלוקים אחרים שאנו מקצים במערכת .בלוקים אלו לא נמצאים ב ,inodeמערכת הקבצים מקצה אותם בדיוק כמו ההקצאה של בלוקים רגילים במערכת הקבצים ,ואנו מפרשים אותם כבלוק של מבציעים . בשיטה זאת הגישה לבלוקים שהם עד 48K היא מאוד מהירה . MassCount Disparity מדוע ההיררכיה כל כך לא מאוזנת? הנתונים עבור הגרף נאספו מ 12מיליון קבצים ביותר מ 1000מערכות קבצים של .UNIX ● ציר ה Xמייצג גודל של קובץ בבתים . ● ציר ה Yמייצג פונקצית התפלגות מצטברת . ● העקומה האדומה מייצגת את אחוז הקבצים במצטבר שגודלם קטן או שווה לערך המתאים בציר ה .x ● העקומה הכחולה מייצגת את אחוז הבתים המצטבר ששייכים לקבצים שגודלם קטן או שווה לערך המתאים בציר ה .x ● הפונקציות מתלכדות כאשר אין אף קבצים בגודל שקטן או שווה ל xאו כאשר כל הקבצים הם בגודל שקטן או שווה ל .x ● החץ הירוק האמצעי מסמן את המקום בו סכום ערכי ה Yשל שתי העקומות שווה ל .1החץ מלמד על חוסר איזון ,שכן הוא מראה כי 90%~ מהמידע מוכל ב 10%~ מהקבצים בלבד . בפירוט ,החץ האמצעי מלמד אותנו ש ~90% מהקבצים אלו שגודלם קטן או שווה ל 16KB מכילים ~10%בלבד מכלל הבתים ששמורים על הדיסק ,בעוד ש ~10% הקבצים הנותרים שגדולים מ 16KB מכילים את יתר ~90% הבתים השמורים על הדיסק. המטריקה נקראת" יחס משותף" .(”joint ratio“) * ניתן לראות שכמעט 99 אחוז מהקבצים מכוסים ע"י direct ו .single ● מה מלמד החץ השמאלי? חוסר איזון המתבטא בכך שמחצית הקבצים) אילו הקטנים יותר( מכילים רק 12%מהמידע השמור בבתים . ● מה מלמד החץ הימני? חוסר איזון המתבטא בכך שמחצית המידע השמור מוכל בפחות מ 1% מהקבצים בלבד) הגדולים יותר( . 72 תופעה זאת של masscount disparity נפוצה במערכות מחשב ● אם נבחר קובץ כלשהו מסך הקבצים רוב הסיכויים שהוא יהיה קטן ,אך אם נבחר בית כלשהו מסך הבתים רוב הסיכויים שהוא יהיה שייך לקובץ גדול . ● אם נבחר תהליך כלשהו מסך כל התהליכים רוב הסיכויים שהוא יהי קצר ,אך אם נבחר יחידת זמן מסוימת מכל הזמן אז רוב הסיכויים שהיא תהיה שייכת לתהליך ארוך . ● באופן כללי : מספר קטן של פריטים מהווה את רוב המסה הכוללת ,בזמן שכל הפריטים הקטנים ביחד נחשבים לחלק קטן מכלל המסה . איך ניתן לנצל את התופעה הזאת בתכנון של מערכות קבצים? למשל ע"י כך שנתכנן את עץ היררכיית הבלוקים של מערכת הקבצים כך שתוכנו של קובץ קטן נמצא קרוב לשורש העץ ,ובכך נשפר ביצועים ע"י הקטנת מספר פעולות הקלט/פלט שנדרשות לאחזור המידע ,תוך שאיננו מבזבזים הרבה מקום על מצביעים .או ע"י כך שנבחר גודל בלוק שחלק ניכר מהקבצים נכנסים בתוכו ובכך נקטין את מספר פעולות הקלט/פלט וכן נבטיח את רציפות הקבצים הללו על הדיסק) .זה מראה גם את היעילות של המבנה של .(Multilevel Index Extents מבנה היררכי של הבלוקים יכול להקשות את הגישה הסדרתית ,כי כשצריך לעבור מבלוק לבלוק אחר צריך לקרוא את כל ההיררכיה וזה פוגע בביצועים בצורה ניכרת )במיוחד בקבצים גדולים( .נרצה שהבלוקים יהיו בצורה רציפה על הדיסק ובכך הקריאה הסדרתית תהיה מהירה ,לכן הוצע הרעיון של extent איזור רציף שמורכב ממספר משתנה של בלוקים .אנו שומרים ב inodeרשימה של מצביעים וגדלים שמייצגים את הקובץ עצמו .כאשר ה extentמאוד גדולים אפשר לקרוא את כל הקובץ באופן סדרתי .אם רוצים קבצים גדולים שלא ניתן להכיל ב extentאז נשמור את המידע הנוסף) שלא נכנס ב (extentבמבנה ההיררכי . יתרונות : מפשט את הגישה הסדרתית ,החיפוש של בלוק הוא יותר יעיל . חסרונות : הניהול בזיכרון יותר מסובך במקרה של מחיקה ,צריך לחזות את הגודל של הקובץ יכול לגרום לשברור פנימי במקרה שהקצנו יותר מידי ויכול לגרום להקצה נוספת במקרה שהקצנו קצת מידי . במערכת הקבצים Ext4 של LINUX מתקיים שגודל כל בלוק הוא 4KB ולכן הגודל של extent יכול להיות בין 4KB ל 128MBויכולים לשמור לכל היותר extents 4 בכל .inode את האינדקסים במערכת זאת שומרים ב Htree זהו מבנה שדומה לעץ B רק שהגובה שלו הוא לכל היותר .2 השימוש במבנה זה מספק לנו חיפוש יעיל של בלוק . linked list block allocation ניתן לשמור את הקובץ ע"י רשימה מקושרת של בלוקים ,כאשר כל בלוק מצביע לבלוק הבא ברשימה .בייצוג כזה אנחנו צריכים לשמור לכל קובץ את המצביע להתחלה ולסוף הרשימה . בשיטה זאת אין גישה סדרתית כי כל בלוק נמצא במקום אחר בדיסק . בייצוג זה חיפוש) גישה אקראית( של בלוק הוא יותר יקר כי במקרה הגרוע צריך לעבור על כל הרשימה) .ניתן לבצע אופטימיזציה ע"י הוצאה של המצביעים מחוץ לבלוק ,למשל ע"י שמירה שלהם בטבלה חיצונית( . 73 בעיה נוספת היא שבמידה ובלוק מסוים נמחק) איבוד מידע( כל הבלוקים שאחריו נעלמים ואי אפשר לאתר אותם , לכן הגיעו למסקנה שצריך את .FAT FAT filesystem עבור הייצוג של הקובץ כרשימה של בלוקים שהצגנו לעיל ניתן להגדיר את המושג הבא .FAT file allocation table : זוהי טבלת ההקצאות של הבלוקים עבור הקבצים במערכת הקבצים .עבור כל בלוק אנו שומרים לו בטבלה את הבלוק הבא ברשימה שמייצגת את הקובץ) כאשר 1 מייצג את סוף הרשימה( . זוהי טבלה אחת שמשותפת לכל הקבצים .לכל בלוק יש כניסה אחת בטבלה .טבלה זאת נשמרת גם בזיכרון וגם בדיסק . יתרונות : מימוש פשוט ,אין שברור חיצוני ,הגישה הרנדומאלית גם היא מהירה ע"י הגישה מהטבלה שנשמרת בזיכרון וככה לא צריך לגשת לקובץ . במקרה זה אם בלוק של מידע נמחק אז רק הוא נמחק ולא כל מה שאחריו ברשימה . חסרונות : הטבלה יכולה להיות גדולה מאוד ואנחנו שומרים אותה בזיכרון . גודל הטבלה הוא כגודל הבלוקים בדיסק .לדוגמא אם גודל הדיסק הוא אז יהיו בטבלה 32GBוגודל כל בלוק הוא 4KB כניסות .אם מניחים שגודל כל מצביע הוא 4 בתים אז מקבלים שהגודל של ה FATהוא .32MB מכיוון שהטבלה מאוד חשובה ולא רוצים שנאבד שם אף מידע שומרים את הטבלה פעמיים למקרה שיהיה איבוד מידע באחת הטבלאות . RAID יש 2 בעיות בעבודה עם דיסקים :דיסקים יכולים ליפול ובכך אנו מאבדים מידע) דיסקים מתקלקלים לעיתים קרובות( .בנוסף הגישה לדיסק איטית וכתוצאה מכך אנחנו מקבלים ביצועים לא טובים . החלק הראשון של הפיתרון הוא לחלק את המידע שלנו על כמה דיסקים ,מה שנקרא ,striping ואז אפשר לקרוא את המידע במקביל ולשפר את הביצועים אך הסיכוי שדיסק אחד מתוכם ייפול הוא יותר גדול .בנוסף כדי להתמודד עם הנפילות נצטרך להוסיף גם מידע מיותר ממנו לשחזר את המידע שנפל במקרה של נפילה . RAID0 בשיטה זאת אין בכלל יתירות .מחלקים את כל המידע בין כמה דיסקים .במידה ודיסק מסוים נופל לא ניתן לשחזר את המידע .בשיטה זאת רק השגנו ביצועים יותר טובים כי ניתן לקרוא במקביל משני הדיסקים . RAID1 בשיטה זאת אנו שומרים את כל המידע פעמיים) או יותר( .במידה שיש נפילה של דיסק ניתן לשחזר את הכל מהדיסק הנוסף.בנוסף 74 ניתן לקרוא בקצב כפול .קצב הכתיבה הוא זהה לכתיבה לדיסק אחד .קצב הקריאה הוא יותר מהיר מקריאה בדיסק אחד .החיסרון של השיטה הזאת הוא שחצי מהקיבולת שלנו הוא מיותר . כיום משתמשים בשיטה זאת בענן של שיתוף מידע .משכפלים את המידע במערכות אכסון נפוצות 3 פעמים כדי שנוכל להתמודד עם עד 2 נפילות בו זמנית .לאחר זמן מסוים דיסק יכול לזייף להתחיל להחזיר ערכים לא טובים )זבל( ולכן צריך את הדיסק השלישי שנדע מהו באמת הערך ששמרנו בדיסק . נבחין כי במערכות ענן בד"כ אין ממש זוג של דיסקים שמהווים העתק אחד של השני ,אלא הגיבוי של דיסק מסוים מפוזר בין מסבר דיסקים . RAID4 משתמשים בדיסק שמכיל את הביטים של ה .parityאם יש לנו N דיסקים אז ב N1הדיסקים הראשונים אנו שומרים את המידע ובדיסק ה Nאנחנו שומרים xor של הבלוקים בכל הדיסקים של המידע) ה xorהוא ברמת הביטים( .במקרה של נפילה של דיסק כלשהו ניתן לשחזר ע"י דיסק ה parityאת המידע שנפל . כדי לקרוא ניגשים רק לדיסק של המידע שממנו רוצים לקרוא) כמו ב , (RAID0כלומר מקבלים קצב קריאה פי .N כדי לכתוב ניגשים לדיסק המידע וגם לדיסק של ה ,parityואפשר לכתוב רק לדיסק אחד בכל זמן כי אי אפשר לכתוב לדיסק ה parityבמקביל בשני מקומות . במקרה שדיסק אחד נפל ,מה עושים? קודם צריך לגלות מי זה הדיסק שנפל ואז נדע לשחזר את המידע ע"י .xor במידה ולא יודעים איזה דיסק נפל אז אמנם נוכל לגלות שנפל דיסק כלשהו מחוסר העקביות של ה ,parityאבל לא נוכל לדעת איזהו מהדיסקים נפל . מבחינת ניצול זיכרון הוא יותר טוב מ RAID1כי שומרים פה רק דיסק אחד של יתירות עבור המון דיסקים של מידע . RAID5 שיטה זאת דומה לשיטה הקודמת רק שכאן מפזרים את המידע המיותר (parity) בין כל הדיסקים ולא שומרים אותו על דיסק אחד . שיטה זאת יותר טובה מהשיטה הקודמת כי עכשיו אין לנו דיסק שניגשים אליו הכי הרבה וככה לא שוחקים את הדיסק) בשיטה הקודמת היינו ניגשים המון לדיסק של ה .(parityבנוסף כשביצענו 2 כתיבות בשיטה הקודמת היינו חייבים שאחת תתבצע לפני השנייה כי שתיהן כתבו לדיסק עם המידע המיותר ,כעת ייתכן שאת המידע הנוסף נצטרך לכתוב לשני דיסקים שונים ולכן נוכל לבצע את הכתיבה במקביל במקרים מסוימים ולכן מקבלים ביצועים יותר טובים) הכתיבה המקבילה היא רק במקרים שהכתיבות בלי תלויות( . 75 במידה ודיסק אחד נפל אפשר לשחזר ,אם יותר מדיסק אחד נפל אי אפשר לשחזר ולכן אם דיסק אחד נופל צריך לשחזר אותו מהר לפני שייפול עוד דיסק אחר . RAID6 כמו בשיטה הקודמת רק שכעת מוסיפים עוד רמה של יתירות .במקרה זה ישנו יתרון שניתן לתמוך ב 2 נפילות בו זמנית אך יש יותר דיסקים שהם עבור מידע מיותר .כאן יש לנו 2 פונקציות שונות לחישוב parityאחת xor ואחת פונקציה אחרת שאפשר לשחזר איתה . אם יש כמה דיסקים שמזייפים אז אפשר רק לגלות שיש בעיה ואי אפשר לשחזר . 76 VFS - Virtual File System – VFSמערכת קבצים מדומה היא שכבת תוכנה בגרעין ,המספקת ממשק אחיד לכל מערכות הקבצים הספציפיות . מאפשרת להרכיב מערכות קבצים מסוגים שונים ומספקת ממשק אחיד לגישה לקבצים הנמצאים במערכות הקבצים השונות) גן מערכות קבצים שלא נמצאות אצלנו במחשב( . כל מערכת קבצים יושבת על חומרה מסוימת וממומשת בצורה אחרת VFS ,יוצרת גישה אחידה לכל הפעולות השונות עבור כל מערכות הקבצים השונות . מתרגמת את קריאות הכלליות לקריאות ספציפיות המתאימות למערכת קבצים בה נמצא הקובץ ,ובכך בעצם מאפשרת שימוש פשוט יותר בקריאות מערכת כי לא צריך להתייחס למערכת קבצים ספציפית . מערכת קבצים ספציפית קוראת את הנתונים מה cache buffer ומשם דרך API של ההתקן חומרה המסוים . המשתמשים יכולים להתייחס לעץ התיקיות והקבצים כישות אחת גם במקרה שבעצם הוא מחבר מספר מערכות קבצים שונות . כל השירותים של מערכת ההפעלה שצריכים לגשת לקבצים משתמשים ב) VFSבערך 100 שירותים( . Linux VFS objects ● ● ● ● ● ● superblock objectמכיל מידע כללי על מערכת קבצים מסוימת .יש לו גם עותק בדיסק . ○ לכל מערכת קבצים קיים אובייקט מיוחד המכיל מידע על המערכת הנ"ל) מידע על ההתקן ,סוג מערכת הקבצים וכו'( .superblock – ○ ה superblockכולל מבנה המכיל פונקציות טיפול המוגדרות עבור מערכת הקבצים הנתונה . – inode objectמכיל מידע על קובץ מסוים מתמלא ע"י נתונים במערכת קבצים .יש לו גם עותק בדיסק . ○ לכל קובץ במערכת קבצים רגילה יש inode על הדיסק המכיל מידע על הקובץ . ○ מכיל הרשאות ,בעלות ,גודל ,חותמות זמן ,וכו' .בנוסף הוא כולל מצביעים לפונקציות טיפול בקובץ . ○ מספר ה inodeמזהה בצורה חדמשמעית את הקובץ במערכת הקבצים . – dentry objectמכיל מידע לגבי כניסה במדריך . ○ מכיל מצביע ל inodeשל הקובץ או המדריך אליו הכניסה מתייחסת . ○ אם ה dentryהוא של כניסה של תתמדריך ,אז הוא מצביע לרשימה של הבנים של אותו תתמדריך . ○ מטמון של dentries מאפשר לצמצם את הזמן הדרוש ע"י שמירה של dentries בזיכרון . – file objectמכיל מידע על קובץ שנפתח ע"י תהליך כלשהו . ○ מכיל מצביע ל dentryהמקושר לקובץ ,דגלים שהוגדרו בפתיחת הקובץ) קריאה , כתיבה ,(...,מחוון הקובץ ,וכו' .כמו כן מכיל גם מבנה המצביע לפונקציות הטיפול בקובץ . file system typeנותן מידע איך לקרוא את ה .superblock vfsmountזהו מבנה שקשור ל .superblockהוא נותן מידע על נקודות חיבור של מערכות קבצים . לכל נקודת הרכבה ,חיפוש קובץ דורש גם את ה dentryוגם את ה .vfsmount * כל האובייקטים האלה זהים לכל מערכת קבצים .מכל מערכת קבצים שמוסיפים למחשב צריך לקחת את המידע הנדרש כדי לאתחל את האובייקטים הנ"ל של .VFS 77 הקשר בין כל האובייקטים ב) VFSבזיכרון( : וזהו ,שיהיה לנו בהצלחה במבחן (: 78