סקירה של מערכת ההפעלה

מערכת ההפעלה היא תכונה שמנהלת את החומרה . היא מהווה את הבסיס להרצה של אפליקציות אחרות ומתווכת בין הלקוח לחומרה עצמה. מערכות הפעלה שונות מבצעות את המטרות הללו בצורות שונות תחת מיקוד תשומת הלב על אספקטים שכל מערכת הפעלה צריכה להתעסק איתם.

מה מערכת ההפעלה עושה?

מערכת מחשב יכולה להתחלק באופן מופשט לארבעה חלקים עיקריים.

  1. החומרה - CPU, I/O , זכרון.
  2. אפליקציות ותוכנות למינהן
  3. מערכת ההפעלה - שולטת בחומרה דרך ״תת תוכנה״ שנקראת kernel ומתמרנת בין כל האפליקציות שמשתמש מריץ בכל רגע.
  4. המשתמש - נקודת המבט שלו משתנה בהתאם לצורך. למשל בבית נשתמש בלפטופ בעוד שבחוץ נשתמש בסמרטפונים . אלו שתי מערכות מחשב שונות. יש גם מחשבים שאין להם בכלל פרספקטיבה של משתמש למשל רכיבי FPGA ומערכות מחשב פנימיות במכשירים ביתיים למשל.

Pasted image 20230316165427.png|400

מנקודת המבט של המערכת

מנקודת המבט של המחשב מערכת ההפעלה היא בעצם התוכנה שמעורבת עם החומרה באופן הקרוב ביותר. בקונטקסט הזה נוכל להסתכל על מערכת ההפעלה כ מחלקת משאבים . לכל מחשב יש משאבים רבים שיש לנהל ולהקצות לתוכניות שונות שהמשתמש רוצה להריץ ויש לנהל את כל המשאבים הללו כדי לייצר את ״אשליית היחידות״ כלומר שכל תוכנה חיה בעולם משלה ועם זכרון משלה , למרות שבפועל זה לא המצב.

אם כן, למערכת ההפעלה שתי תפקידים מרכזיים

  1. הקצאת משאבים ליצירת ״אשליית היחידות״
  2. control program- תוכנה שביכולתה לנהל את ההרצה של תוכניות אחרות תוך כדי מתן מענה לשגיאות במצב של שימוש לא נכון בתוכנה.
  3. חיבור של המשתמש לחומרה דרך רכיבי ה I/O .

הגדרת מערכת ההפעלה

למען האמת, אין הגדרה ברורה למערכת ההפעלה. מערכות הפעלה הן רבות ומגוונות ומשתנות בין מכשיר למכשיר. למחשבים יש פונקצינליות שונה, גדלים שונים ומטרות שונות שהובילו ליצירה של מערכות הפעלה מגוונות ושונות מאוד.
למרות זאת נוכל להסתכל על מערכת הפעלה בשתי אופנים.

הראשון -
הוא מכלול התוכנות שמייצר מערכת ההפעלה הפיץ כדי לפתור את הבעיות של אותו המכשיר עם החומרה הספציפית אליו. למשל יש שיסתכלו על office של מייקרוסופט כחלק ממערכת ההפעלה שכן, אלו תוכנות מובנות שמגיעות עם מערכת ההפעלה שהמייצר הפיץ.

השני-
יש שיגדירו מערכת הפעלה כתוכנה אחת שרצה כל הזמן כשהמחשב דולק , ונקראת kernel. זאת התוכנה שכל תכליתה היא לספק מעין API לחומרה. ביחד איתה מגיעות שתי סוגי תוכנות, תוכנות מערכת , שמערכת ההפעלה צריכה אך הן אינן קשורות לkernel בהכרח.
middleware- תוכנות ביניים שבאות עם מערכת הkernel שמאפשרות תמיכה במגוון רחב של שירותים. אפשר לראות את זה אצל מערכות ההפעלה של המובייל ios ו android שבנוסף לכך שהן מגיעות עם הkernel הן מגיעות עם תוכנות שמאפשרות תמיכה בdatabases , מולטימדיה, גרפיקה וכו׳.

באופן מסויים מערכת ההפעלה תלויה גם במערכת מחשב עליה היא נמצאת ואת אלו אפשר לחלק ל4 סוגים

Pasted image 20230316175505.png

ארגון מבנה המחשב ככלי עבור הOS

כדי להתחיל לדבר על מערכת ההפעלה , יש לדבר על ארגון מבנה המחשב שכן אחד מתפקידיה העיקריים של מערכת ההפעלה היא לדבר עם רכיבים אלו.
מערכת מחשב כללית לרוב מכילה CPU אחד או יותר ומספר רכיבים שנקראים device controllers שמחבורים אחד לשני עם ״צינורית״ שנקראת bus שמאפשר גישה בין רכיבים אלו עם זכרון משותף. device controller , כפי ששמו מציע, הוא רכיב שאחראי על סוג מסויים של מכשיר (כמו דיסק, רמקולים ומסך). בהתאם לקונטרולר ישנה האפשרות שיותר ממכשיר אחד ישוייך לאותו controller.

Pasted image 20230316171939.png

לכל קונטרולר כזה יש משהו שנקרא local buffer , שזה בעצם יחידת זכרון ואוסף של registers עם מטרות מיוחדות. תפקיד הקונטרולר הוא להזיז מידע בין הרכיב החיצוני שהוא אחראי עליו ובין הbuffer הזה. בעצם הבאפר הזה משמש מעין מטמון למידע שהרכיב צריך שמגיע ישירות מהזכרון.
ואיך מערכת ההפעלה נכנסת לתמונה?
למערכת ההפעלה יש device driver עבור כל device controller . דרייברים הם חלק ממערכת ההפעלה שמספקים בעצם ממשק עבור תוכנות ואפליקציות על רכיבי חומרה. דרייבר שכזה הוא קוד שמבין את הקונטרולר איתו הוא עובד ומבצע פעולות עליו תוך חשיפה של ממשק נוח לביצוע הפעולות האלו מקוד אחר.

ישנן הרבה שאלות שעולות מהנ״ל למשל, אנחנו כמתמשים יודעים שכל הרכיבים האלה פועלים ב״מקביל״ איך זה קורה בעצם? למעשה הcpu והcontrollers מתחרים בינהם על משאבים משותפים כמו זכרון. כדי לאפשר גישה נכונה לזכרון בלי זליגות וחריגות ישנו memory controller שמסנכרן בין כל הרכיבים.

interrupts

נחשוב על הפעולה הבאה, תוכנה מבצעת פעולת I/O .
למשל, אתם משתמשים בתוכנת הצייר של windows וביצעתם לחיצה על העכבר שמטרתה לצבוע אזור מסויים במסך באדום.
נבין מה קורה בעת לחיצה על העכבר.

  1. מידע מסויים נטען באמצעות ה device drivers לרגיסטרים המיוחדים שדיברנו עליהם שנמצאים בcontroller המטעים עבור העכבר.
  2. הקונטרולר בתמורה, בוחן את תוכן הרגיסטרים האלה כדי לקבוע איזה פעולה הוא צריך לעשות.
  3. הקונטרולר מעביר את המידע מהרגיסטרים לבאפר המקומי.
  4. ברגע שהמידע הזה הועבר , הקונטרולר מעדכן את הדרייבר עם interrupt שהוא סיים את הפעולה שהוא מבצע והדרייבר מעניק שליטה לחלקים אחרים במערכת ההפעלה ותוך כדי גם מעביר את המידע שהוא צריך באמצעות פוינטר או החזרת המידע עצמו (במקרה שלנו נעביר מידע על הפעולה שהעבר ביצע והמיקום שלו כנראה).
  5. הדרייבר מחזיר בסטטוס ביצוע עבור הפעולה שביקש לבצע , כמו flag שמסמן האם הפעולה הסתיימה , השתבשה או משהו אחר קרה.

Pasted image 20230324093228.png|450

סקירה של interrupt

החומרה יכולה להפעיל פעולה זו בכל רגע נתון על ידי שליחה אות אל הCPU. בדרך כלל זה יקרה באמצעות הsystem bus . פעולה זו היא חלק אינטגרלי ממערכת ההפעלה והתממשקות עם החומרה. כאשר ה CPU מזהה ״הפרעה״ הוא מפסיק את מה שהוא עושה ומיד עובר לבצע פעולה מ מיקום קבוע בזכרון. המיקום הזה הוא בעצם נקודת התחלה של קוד מסויים שנקרא service routine. ברגע ההפרעה הרוטינה מורצת ובסיום ה CPU ממשיך את התוכנית שהוא ביצע לפני ההפרעה.

הדוגמה הבסיסית ביותר היא כאשר אנחנו מבקשים ממערכת ההפעלה להדפיס משהו למסך בתוכנית hello world שכתבנו. בעת קריאה לפונקציית printf מתבצעת הפרעה שכזו דרך הkernel באמצעות system call , ולאחר ההפרעה הרוטינה הרלוונטית מדפיסה למסך את מה שביקשנו.

Pasted image 20230316175130.png

חשוב לציין שלכל ארכיטקטורת מחשב יש מכניקת הפרעות משלה אבל עם זאת גם הרבה מהפונקציונליות הן משותפות לכל מערכות המחשב.
ההפרעה חייבת להעביר את השליטה לרוטינה הרלוונטית. הגישה הכי ״נאיבית״ כדי לנהל את זה היא להגדיר רוטינה נוספת שבודקת מידע על ההפרעה וזאת בוחרת לקרוא לרוטינה הרלוונטית בהתאם למידע המתקבל. ממש כמו switch case על סוג ההפרעה. שיטה זאת נקראת polling ובגלל שזה יכול להיות איטי וחסום בבדיקה של כל האינטרפציות האפשריות, השיטה שעליה הולכים היא להחזיק מעין jump table של מצביעים ובאמצעותה ברגע ההפרעה ואיזה מניפולציה על המידע שלה נוכל להגיע ישירות לרוטינה הדרושה.

הטבלה הזאת ממוקמת מאוד נמוך בזכרון והיא נקראת גם interrupt vector של הכתובות שניתן להמיר מידע של אינטרפט לאינדקס בוקטור הזה כדי להגיע לאן שצריך. השיטה באופן כללי נקראת vectored interrupt system והיא בעצם אומרת שכאשר ישנה הפרעה היא עוברת כאות חשמלי שונה שמייצג מספר, המספר הזה יהיה ממופה לאינדקס בוקטור.

מימוש

מכניקת ההפרעה עובדת באופן הבא. ה CPU מכיל חוט שנקרא interrupt request line שה CPU דוגם לאחר ביצוע כל פקודה. כאשר ה CPU מזהה שקונטרולר מסויים שלח הפרעה הוא קורא את מספר ההפרעה וקופץ לרוטינה המתאימה על ידי שימוש במספר הנ״ל כאינדקס עבור הטבלה.
לפני שהרוטינה מתחילה לבצע את המימוש של ההפרעה היא שומרת את הstate שהיה לפני כיוון שהפעולות שיבוצעו עלולות לשנות את זה ולאחר מכן משחזרת את הכל למצב הקודם. בסיום סיומת היא מריצה את פקודת return_from_interrupt שמחזירה את ה CPU למצב שהוא היה לפני ההפרעה.

טרמינולוגיה

נאמר שהקונטרולר מרים הפרעה על ידי שליחת אות על הקו והCPU תופס הפרעה ומבצע עליה dispatch לרוטינה המתאימה ה interrupt handler והוא לבסוף מנקה את ההפרעה.

interrupt-driven I/O cycle

Pasted image 20230318115744.png

מנגנון ההפרעות הבסיסי שתיארנו למעלה מאפשר ל CPU להגיב לאירועים באופן אסינכרוני בהתאם לקונטרולר המתאים. אבל במערכות הפעלה מודרניות נרצה מכניקות קצת יותר מורכבות מזה

  1. נרצה את היכולת לדחות interrupt כאשר יש איזשהו חישוב קריטי.
  2. נרצה דרך יעילה לבצע dispatch ל handler המתאים עבור מכשיר כלשהו.
  3. נרצה מנגנון מרובה שלבים של הפרעות כך שמערכת ההפעלה תוכל להבדיל בין הפרעות בעדיפות גבוה והפרעות עם עדיפות נמוכה ותוכל להתמדד עם כל אחד בהתאם לרמה הזאת.

במערכת מחשב מודרניות היכולות הנ״ל מוענקות על ידי ה CPU וה interrupt controller hardware.

פתרון לבעיה 1
לרוב המעבדים יש 2 interrupt request lines . האחד הוא nonmaskable interrupt שנשמר עבור אירועים כמו חריגות שלא ניתנות לטיפול.
השני נקרא maskable והוא יכול להיות מכובה על ידי המעבד לפני ביצוע פקודות קריטיות שאסור שהפרעה תפגע בהן. זה הקו שהקונטרולרים משתמשים בו על מנת לבקש שירות.

פתרון לבעיה 2
כפי שאמרנו נרצה להחזיק vector עם כתובות כדי שלא תהיה רוטינה אחת (interrupt handle) גדולה שמחפשת את מקור ההפרעה מבין כל הרכיבים. אבל במחשבים מודרנים יש המון מכשירים ולכן גם המון handleres , יותר משהם יכולים להחזיק בוקטור הכתובות. כדי לטפל בבעיה זו משתמשים ב interrupt chaining, שבה כל אלמנט בוקטור מצביע לראש של רשימה של handleres. (מזכיר קצת שרשור של רשימה מקושרת). כאשר הפרעה עולה, מגיעים לראש הרשימה הרלוונטית וקוראים את הhandleres אחד אחרי השני עד שמוצאים את הרוטינה שיכולה לטפל בהפרעה הספציפית הזאת. זאת בעצם מהווה פשרה בין קוד אחד גדול שבודק את כל ההפרעות האפשריות לבין וקטור גדול מאוד של כתובות.

פתרון לבעיה 3
על ידי הגדרת interrupt priority levels המעבד יכול לדחות את הטיפול בהפרעות בעדיפות נמוכה מבלי להגדיר שכל ההפרעות הן maskables.

trap

בנוסף להפרעות שעוברות דרך הקו ישנן הפרעות תוכנה שנקראות trap , אלו לא בהכרח נקראות מרכיב I/O אלא משגיאה או בקשה של תוכנית כלשהי

טיפול סינכרוני ואסינכרוני בבקשה

קיימות שתי צורות לטיפול בבקשות ה- I/O – סנכרונית וא-סינכרונית.
בצורה הסינכרונית ברגע שנשלחה בקשה מחכים שהקונטרולר יסיים את העברת המידע לבאפר ושהדרייבר ישלח אינטרפט ורק לאחר ביצוע הhandler המתאים, התהליך שקרא ל I/O ממשיך בריצתו.

הצורה האינסכורנית דומה רק שבה, בעת שלב העברת המידע התהליך ממשיך בריצתו עד שהדרייבר שולח interrupt.

Pasted image 20230318133434.png
מימין התהליך באופן אסינכרוני, משמאל באופן סינכרוני

ישנה בעיה כאשר יש ריבוי מעבדים או שהטיפול הוא אסינכרוני שבה ישנן שתי בקשות I/O שמגיעות בו זמנית לאותו device controller . זה יוביל לצורך לשמור את הבקשה הבאה במקום כלשהו בזכרון כדי שיהיה אפשר לגשת לשם.
לשם כך ישנה מערכת תורים לכל מכשיר ששומרת את הבקשות ואת הסטטוס של כל מכשיר כדי לדעת מתי ניתן לשלוף בקשות מהתור.

מערכת ההפעלה מנהלת את התור הזה לכל מכשיר באמצעות Device status table.

Pasted image 20230318134507.png

מבנה וניהול הזכרון

המעבד יכול לקרוא פקודות רק מהזכרון, לפיכך, כל התוכניות חייבות להטען לזכרון כדי לרוץ. מחשבים כלליים מריצים את רוב התוכניות שלהם מהזכרון הראשי שלרוב ממומש על ידי ה DRAM.

bootstrap program

ישנן תוכניות שרצות ממקומות אחרים בזכרון. למשל, התוכנית הראשונה שרצה על המחשב בעת הפעלתו נקראת bootstrap program שזאת טוענת ומאתחלת את מערכת ההפעלה. כיוון שהזכרון RAM הוא זיכרון נדיף (זכרון שמאבד את התוכן שלו כאשר המחשב הנכבה) לא נוכל לסמוך עליו להחזיק את התוכנה הזאת. במקום זאת היא שומרת את התוכנית הזאת בזכרון שהוא read only שנקרא EEPROM או בזכרון firmware אחר שהוא זכרון אמיד. זכרון זה מחזיק בתוכו לרוב תוכניות סטטיות ומידע שלא עתידים להשתנות בגלל שלגשת אליו זה לרוב תהליך איטי יותר. לדוגמה, הiphone מחזיק בזכרון הזה פרטים על המכשיר כמו מידע חומרתי ומזהה סריאלי.

כל הזכרונות באשר הם מספקות מערך של בתים. לכל בית מוקצה כתובת משלו. אינטרקצייה עם בתים נעשת באמצעות רצף של פקודות load ו store על כתובות ספציפיות בזכרון.

load - מזיזה מידע מגודל בית או מילה מהזכרון הראשי ל register בתוך המעבד.
store - מזיזה את התוכן של רגיסטר לזכרון הראשי

בנוסף לאלו המעבד טוען באופן אוטומטי פקודות הרצה מהזכרון הראשי מכתובת ששמורה אצל ה program counter.

instruction-execution cycle

מעגל סטנדרטי שנמצא במערכת מבוססת ארכיטקטורת פון נוימן , תחילה טוענת את הפקודה מהזכרון ומאחסנת אותה ב instruction register.
הפקודה עוברת תהליך של קידוד ועלולה להוביל לכך שאופרנדים מסויימים ימשכו גם הם מהזכרון ויאוחסנו ברגיסטרים פנימיים נוספים. לאחר ביצוע הפקודה על האופרנדים התוצאה תאוחסן בחזרה בזכרון בעת הצורך.

ישנם הרבה תהליכים שמוסתרים מיחידת הזכרון (למשל זכרון וירטואלי) אבל התוכנית עובדת עם כתובות וכל מה שהיחידת אחסון צריכה להכיר זה מיהם הכתובות שהתוכנית צריכה, אפילו לא רלוונטי עבורה לדעת האם הכתובות מייצגת מידע או פקודה.
אידיאלית היינו רוצים שכל המידע של והפקודות של התוכניות שלנו יהיו בזכרון הראשי תמיד. המצב הזה אינו אפשרי משתי סיבות עיקריות

  1. הזכרון הראשי קטן מדי כדי להכיל את המידע של כל התוכניות בתוכו
  2. הזכרון הראשי הוא נדיף

מסיבות אלה רוב מערכות המחשב מספקות זכרון משני כהרחבה של הזכרון הראשי. זכרון זה נקרא אמיד והוא גדול יותר ומכיל את המידע באופן שלא נמחק גם לאחר סגירת המחשב.

Pasted image 20230318154108.png

תפקידי מערכת ההפעלה בניהול זכרון

I/O Structure

חלק נכבד מאוד מהקוד של מערכת ההפעלה מוקדל לניהול רכיבי הI/O גם בגלל החשיבות הרבה שלה לאמינות וביצועים של המערכת וגם בגלל הטבע של המכשירים מעצם היותם הדרך של משתמש לדבר עם המערכת.
כזכור , מערכות מחשב כלליות מכילות מספר מכשירים שמחליפים מידע בינהם עם bus משותף . המודל הסטנדרטי שהראנו בתמונה למעלה הוא טוב לתזוזה של חתיכות מידע קטנות אבל יכול להיות overhead מאוד גבוה עבור בלוקים גדולים של מידע למשל בכל הקשור לI/O של זכרונות שאינם נדיפים.
כדי לפתור את הבעיה הזאת משתמשים ב DMA. לאחר שכל הבאפרים , פוינטרים ומונים עבור רכיבי ה I/O של המחשב מוכנים, כל device controller יכול להעביר ישירות בלוקים של מידע מהבאפר של המכשיר אל הזכרון הראשי, בלי להשתמש במעבד כתחנת מעבר. עבור כל בלוק כזה יש צורך בהפעלת interrupt בודד כדי להודיע לדרייבר שהפעולה הסתיימה. באופן הזה המעבד יהיה פנוי לביצוע פעולות נוספות בזמן העברת המידע.

Pasted image 20230318160742.png