← Linux Kernel Programming for Embedded Systems Course

#2 (2025-05-22)

טוען נגן…

תמלול

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

אז אני מקווה שכולכם הצלחתם להקים את הסביבה.

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

ניכנס כבר ל-process fundamentals, איך הקרנל משתמש ב-process descriptor וב-task structure. נעבור קצת על process states ו-lifecycle, נדבר על הסוגים השונים של הסקדולרים שיש לנו ועל ה-scheduler classes.

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

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

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

## Understanding Kernel Architecture

טוב, אז בעצם מה היא הארכיטקטורה של הקרנל ואיך היא נראית?

אז יש לנו בעצם core component שזה ה-Linux kernel, ומעבר לזה יש לנו interface שממומש בכמה דרכים, זה לא רק דרך אחת, ממש בכמה דרכים, בין החומרה לבין ה-user space applications.

כמו שרואים את זה בכל מערכות embedded, שיש לנו איזושהי הפרדה ברמת ה-CPU בין privileged mode ל-unprivileged mode. אז בעצם privileged mode יכול לגשת גם בחומרה, ו-unprivileged mode זה mode שלא יכול לגשת לחומרה. וההפרדה ב-Linux kernel היא הפרדה שהיא לוגית. זאת אומרת, כדי להגיע לחומרה אנחנו ממש צריכים לקפוץ ל-privileged mode. אבל יותר על זה בשיעור הבא שנדבר על system calls. בכללי, מה שאנחנו מדברים עכשיו, אז זה בעצם איזשהו intro ל-system calls. בשיעור הבא אנחנו נעשה את זה וגם ניצור אחד.

לקרנל יש לו גם resource manager ל-CPU, ל-memory ול-I/O devices.

ל-CPU בעצם יש לנו איזושהי קומפוננטה שעושה לנו את הניהול של ה-CPU, אותו דבר ל-memory space, ו-I/O devices זה בעצם ה-devices שאנחנו עושים איתם input ו-output.

הארכיטקטורה עצמה, כמו שאמרנו בשיעור הקודם, היא מונוליתית, זאת אומרת שיש לנו core אחד עיקרי עם loadable modules. כל מיני מודולים שאנחנו יכולים לטעון אל הארכיטקטורה, לטעון אל ה-core העיקרי, בעצם אל ה-process העיקרי, ולהשתמש בפונקציונליות שלו.

אז יש לנו שני אזורים מובהקים: יש לנו את ה-kernel space ואת ה-user space.

ה-kernel space עצמו משתמש ב-execution שהיא מאוד privileged, זאת אומרת שיש לה גישה אל ה-hardware, אין בעצם אזור שאני לא יכול לגשת אליו מהקרנל, ממה שהקוד שרץ על הקרנל, ואני לא יכול לעשות preempt ל-non-real-time kernels. מה זה preempt ומה זה real-time kernel, זה בשיעור הזה, אז אל דאגה. אבל preemption זה בעצם עצירה בשביל החלפה של ה-context ל-context עם priority גבוה יותר. למה אנחנו צריכים את זה? במערכות real-time, כדי לעבור מאיזושהי פעולה של הקרנל לפעולה שבה אני רוצה לעשות בצורה אפליקטיבית בזמן אמת, real-time.

וייש לנו את ה-user space, שזה האזור האפליקטיבי שממנו אנחנו כותבים את הלוגיקה שלנו.

ל-user space יש restricted execution, ברמת ה-assembly הוא לא יכול לעשות את כל פקודות ה-assembly שיכול לעשות הקרנל. איך אנחנו מגבילים את זה? ב-bare metal אנחנו נוגעים בזה יותר, פה פחות, יש לנו mode של ה-CPU, של כמעט כל CPU, mode של privileged לעומת unprivileged. אז user space יהיה unprivileged.

אין לנו גישה ישירה אל ה-hardware, אנחנו צריכים לבקש...

- "אור, אפשר שאלה קטנה? בזמן שאתה מקמפל, איך הקומפיילר יודע בכלל אם זה user או לא user, או כאילו privileged? אז קצת, הגישה קצת לא מובנת לי. הרי פקודות assembly זה סך הכל הקומפיילר מעביר מקובץ object file לפקודות assembly."

- אז שאלה טובה. אני לא יודע להגיד בזה, אבל אנסה שנייה לחשוב איך הקומפיילר... בגדול...

- "בוא, תשאיר את זה לאחר כך. תמשיך עם השיעור, רק תזכיר אותי."

- אני מנסה לחשוב כי כן הגענו בזה ישירות. אז כן, אני יודע את התשובה. הקומפיילר לא יודע. בוא נגיד את זה ככה. הקומפיילר לא יודע. ברגע שאני מנסה לעשות איזושהי פקודת assembly שהיא לא חוקית ב-unprivileged mode, אני פשוט אעוף. ה-CPU שלי יזהה איזשהו hardfault, ואני אקפוץ לתוך ה-hardfault handler. אינטראפט של hardfault.

- "אז איך מלכתחילה כאילו איך אני כשאני בונה תוכנה, אני מקמפל, איך אני... מה, אני אמור להגיד לקומפיילר אל תשתמש בחלק מהפקודות?"

- אז לרוב...

- "או שיש פה איזה משהו חסר פה בדעה?"

- אני אגיד, לרוב אם אנחנו נעשה פעולות כאלה, אנחנו נעשה אותן באסמבלי. נגיד פעולות של... אתן דוגמה, כל מיני מניפולציות על ה-core registers, נכון? יש לנו את ה-registers של ה-CPU, ה-registers... אז כל מיני פעולות על ה-core registers, ברגע שאני אגש אליהן ואעשה אותן, אז אני אעשה אותן רק בצורה של assembly, ככה שהקומפיילר לא יודע, זה עליי, המפתח.

- "זאת אומרת, אם אני עכשיו מפתח תוכנה אפליקטיבית משלי, ואני משתמש בחלק מפקודות assembly, נגיד אני מכניס קטע קוד assembly, ונגיד הוא משתמש בחלק מה-registers שאפשר לגשת אליהם רק ב-privileged mode, זאת אומרת התוכנה תתרסק. לזה הכוונה?"

- נכון. ושיש עוד מאוד איזה חלק, כאשר אני בונה modules, אני בונה אותם מול הקרנל עצמו. כשאני בונה loadable module, אנחנו נגיע לזה בהמשך, אני בונה אותם מול הקרנל עצמו ואז הקומפיילר כן מודע לזה שאני ארוץ ב-privileged mode.

- "אוקיי. אז אני מניח שאין לי מושג. אז זה, כמו שאמרת, עוד מעט."

- זו שאלה מצוינת.

אז user space, אמרנו, אין לנו גישה ישירה אל ה-hardware, והגישה וה-API שאנחנו עובדים מולו הוא בעצם API מאוד limited, שהקרנל מספק ל-user, בעצם איזשהו service. אני לא יודע אם יצא לכם קצת לקרוא בטוויטר או לקרוא את לינוס, אחד מהדברים שהוא דוגל בו זה לא לשבור את ה-user space. ואם יצא לכם לקרוא קצת ת'רדים, אז הוא עושה את זה בצורה מאוד אגרסיבית למפתחים שמנסים לשבור את ה-user space.

דבר נוסף על ה-user space, אז הוא preemptive, זאת אומרת שה-schedulers שרצים על ה-processes שלו, הם כן עוצרים אותו, זאת אומרת שיש לנו priority בין ה-processes.

## CPU Protection Rings

אז ה-CPU protection rings, יש לנו בעצם כמה טבעות של protection.

הטבעת הראשונה, Ring 0, זה בעצם highest privilege level, יש לנו גישה לכל ה-CPU instructions, גישה אל החומרה, וזה בדרך כלל שמור ל-kernel code. ואנחנו עושים את זה כאשר אנחנו צריכים לעשות execution של critical operations, לדוגמה אינטראפטים - לא נגענו בהם, אל דאגה אנחנו ניגע - וגם scheduling של המערכת. זאת אומרת, העברה של ה-CPU בין כמה processes. זה נעשה מתוך אינטראפט בגדול.

Ring 1 ו-Ring 2 זה בעצם איזשהו intermediate privilege level, אנחנו כן יכולים להשתמש ב-device drivers בשביל ה-services של מערכת ההפעלה. אנחנו פחות משתמשים בזה במערכות Linux, אבל זה עדיין נמצא שם. חלק מה-supervisor code, בעצם מה שאנחנו משתמשים בשביל ה-system calls, נמצא שם.

אל תדאגו, אנחנו נדבר על זה בשבוע הבא, אבל אני אתן לכם איזה טיזר, של ה-supervisor call יש לו פקודת assembly מיוחדת שנקראת SVC, וה-supervisor עצמו, הוא לא רק שהוא קורא לפקודה, אלא גם מעביר מספר שהוא מומר לאיזושהי טבלה ששכחתי את השם שלה עכשיו, והטבלה הזאת היא זאת שנאספת על ידי הקרנל ומבצעת את ה-execution ברמת החומרה. זה על Ring 1 ו-2.

ו-Ring 3 כבר אמרנו, הטבעת עם ה-priority הנמוך ביותר, בלי כל ה-instructions שאנחנו משתמשים, בלי גישה ישירה ל-I/O, user application והלוגיקה שרצה שם, ואנחנו חייבים להשתמש ב-system calls כדי לקבל את ה-kernel services.

## Transitioning Between Rings

אז איך אנחנו עוברים בין ה-rings?

הדרך הקלה ביותר והמוכרת ביותר זה system calls, בעצם המתודה העיקרית כדי לבקש services מהקרנל. דוגמאות ל-system calls: read, write, socket, מה שאנחנו מכירים מ-Linux.

אין להתבלבל בין כל מיני בקשות שאנחנו בעצם... שבעצם הם איזשהם wrappers של glibc ל-system call. אתן דוגמה: כאשר אנחנו עובדים עם fopen, מישהו יצא לו לעבוד עם fopen פה? File open. כשאנחנו עובדים עם fopen, בעצם אנחנו עובדים עם איזשהו buffered I/O, נגענו בזה טיפה בקורסים אחרים אבל זה לא קריטי, בעצם איזשהו buffered I/O, שה-buffered I/O הזה הוא בעצם איזשהו wrapper שעושה לנו buffering, וזה נעשה מתוך ה-glibc, ובסוף זה קורא ל-open, או ל-read, או ל-write. אז fopen, fread ו-fwrite זה בעצם glibc עושה לנו wrapper ל-system call. סבבה?

אז system calls בעצם זה איזשהו entry point שאנחנו מקבלים מהקרנל, אנחנו יכולים לבקש בקשה, וכאשר מתבצעים כמה וכמה פעולות, אמרנו, אנחנו עוברים דרך Ring 1, מגיעים לקרנל, לוקחים את הבקשה, מבצעים אותה. זה בעצם נעשה על ידי special instructions, דוגמה: syscalls ו-SVC שזה supervisor calls, נדבר על זה בשיעור הבא.

צור נוספת של transition שנעשית, דרך נוספת שבה נעשה transition, זה על ידי hardware interrupts. איזה hardware interrupts יש לנו? יש לנו כמה סוגים של hardware interrupts, יש לנו אינטראפטים של המערכת עצמה, לדוגמה אינטראפטים, exceptions, זה אותו דבר... זה לא בדיוק אותו דבר, exceptions זה detection של החומרה...

של ה-CPU לאירועים שקרו מתוך ה-CPU, ו-interrupts זה בעצם detection של אירועים שקרו מחוץ ל-CPU, בעצם מה שנקרא manufacturer configurable interrupts או בגדול אינטראפטים. אז עוד צורה שבה נעשה transition בין ה-rings זה על ידי hardware interrupt. בעצם ברגע שיש לנו interrupt, ה-execution שלנו קופץ למקום ספציפי בקוד בצורה אסינכרונית, ומתבצע ה-handler של ה-interrupt. אז נעשה switch מה-user space או מאישהו אזור, בגדול זה הרוב ה-user space, ל-kernel space ב-kernel mode, ואנחנו עושים handling לאירוע.

עוד משנה קטנה על החלק הזה: הרבה פעמים כאשר אנחנו עובדים במערכות של high bandwidth, אנחנו נרצה לצמצם את כמות ה-interrupts. למה? כי בעצם, אנחנו לא מדברים על איך זה קורה ברמת ה-stack, אבל בעצם זה הרבה מאוד פעולות שאנחנו עושים כדי לקפוץ מ-execution רגיל ברמת האלגוריתמיקה או ברמת הלוגיקה ל-execution שהוא אסינכרוני ברמת הפעולה. אז לדוגמה, עיבוד פקודות, אנחנו נרצה לעשות כמה שפחות interrupts לעומת איך שכרגע עובד ה-stack ב-Linux.

Software interrupts זה בעצם בקשות של ה-user space ל-interrupts. סיגנלים, אם אנחנו מכירים סיגנלים, סיגנלים זה בעצם איזושהי בקשה מהמערכת הפעלה לעבוד בצורה אסינכרונית. איך זה נעשה? זה נעשה באמצעות software interrupt. Exceptions, לדוגמה exceptions של ה-processes, null reference, בעצם גישה ל-pointer שהוא לא מוגדר, ו-system calls זה גם בקשה של software.

ויש לנו את ה-protection mechanisms. אז יש לנו את ה-Memory Management Unit (MMU). ה-Memory Management Unit זה היחידה ששומרת לנו על מרחב הכתובות הווירטואלי, והיא בעצם נעה כל הזמן בין ה-user space, בעצם אמרתי user space ו-user mode כי זה ה-space שהיא מקצה ל-user, לבין ה-kernel space שזה עבודה פיזית מול הזיכרון.

עוד protection mechanism שיש לנו: יש לנו את ה-privileged instructions שדיברנו עליהם, IO permissions של סליחה, IO permissions של כל מיני devices בתוך המערכת. לא כל device יכול לכתוב לכל מקום. זה עוד protection mechanism לעבודה בין ה-rings, זאת אומרת שאני לא יכול להשתמש ב-IO שמוגדר על ה-user space ל-kernel, אוקיי? לא יכול לכתוב לכתובות שהן פיזיות. ו-system call validation, זה כבר נעשה מתוך האימפלמנטציה/אפשר לחשוב על זה כעל פקפוק כזה של פעולות שקורות מ-request של system call עד ל-service routine.

## Key Architectural Components

אז key architectural components של ה-Linux kernel:

יש את ה-Core Kernel, מה שעושה לנו את ה-process scheduling, memory management וה-IPC.

יש לנו את ה-Loadable Kernel Modules, בעצם ה-kernel modules שהם נטענים לתוך המערכת והם לא חלק ישיר מה-kernel. אגב, ה-kernel עצמו כולו כתוב ב-C, אבל loadable kernel modules אפשר לכתוב בשתי שפות. אתם יודעים באיזה שפות?

— Rust.

נכון, C ו-Rust. אז אם מישהו מעניין C ו-Rust, אז יש הרצאה ממש מעניינת של Rust שעושה שחר, אני לא יודע אם הוא הצטרף היום, אבל הוא עושה ביום ראשון.

אז בעצם loadable kernel modules זה מודולים שאפשר לטעון לתוך ה-kernel ולהשתמש בהם. שימו לב, עוד איזה side note לגבי Rust: אין כל כך אדפטציה של הקהילה ל-Rust, וזה יוצר לנו binary files מאוד גדולים. אני לא יודע אם זה יישאר לנצח או שזה יקטן באיזושהי צורה.

עוד חלק זה system calls. שוב, interface בין ה-user space ל-kernel space.

VFS – Virtual File System: בעצם זה איזשהו interface ברמת ה-interface כמו שאנחנו מכירים interface מ-object oriented, שזה בעצם הכול מיוצג על ידי file system. שזה בעצם unified interface ל-file system. זאת אומרת שכל דבר הוא file, אז גם IO devices וגם קבצים, גם IPC, אז הכול בעצם זה ה-VFS.

ו-Device Models, שזה hardware abstractions ו-management ל-device driver.

## Linux Kernel Subsystems Overview

אז בעצם מה יש לנו בתוך ה-kernel subsystems, ה-major subsystems שיש לנו?

יש לנו את ה-process management, אנחנו נראה אותו תחת `sched`, אוקיי? אז יש לנו את ה-kernel, יש לנו את `sched`. במצגת הבאה, אם אתם רוצים לעקוב תוך כדי.

Process management הוא אחראי על task creation, scheduling ועל termination. יצירה של טאסקים ושל פרוססים, עוד שנייה נראה איך אנחנו יוצרים אותם. ה-scheduling זה בעצם ה-scheduler שיבחר לכל פרוסס. ו-termination בעצם זה ההשמדה של הפרוססים עד לרמת ה-zombies וכולי.

Memory Management: בעצם יש לנו את ה-virtual memory, זה ה-MMU שמבצע לנו, paging של הזיכרון לסקטורים, ו-swapping של paging שזה עוד feature.

חלק נוסף זה ה-VFS, בעצם ה-file abstraction וה-file caching. ו-file caching זה לא אותו דבר כמו caching של ה-buffer IO שאנחנו עושים דרך `fopen`, דרך `glibc`.

עוד subsystem שיש לנו, יש לנו את ה-network stack, בעצם protocol implementation ו-socket API שאנחנו יכולים להשתמש בו.

IPC – Inter-Process Communication: message queues, shared memory, socket גם יכול להיכנס פה, ו-synchronization methods, אוקיי? זה גם נכנס לכאן. חלק מהם, כן.

Device Drivers: בעצם hardware abstraction ו-control ל-devices.

ו-Security Framework: access control שאנחנו משתמשים בו, ו-privileges שאנחנו מכירים מ-`ls -l`, כל ה-RWX.

## Kernel Code Organization

אז ה-kernel code הוא עצמו... אתם יכולים גם להפסיק אותי כאן, רגע, אני כבר אגדיל את זה.

ה-kernel code, אנחנו נראה אותו תחת `linux`... תחת `linux/kernel`, ואז יש לנו את כל הדברים העיקריים.

אז יש לנו את ה-`arch`, זה architecture specific code. שם אנחנו נראה בעצם דברים שהם באסמבלר ספציפי לארכיטקטורה.

`block` – block devices ו-block drivers שאנחנו משתמשים.

`crypto` – שזה כל מיני שימושים של חומרה. בואו נסביר את זה יותר בהרחבה כדי שזה יהיה יותר ברור. אז היום ל-CPUs יש רכיבים, בכוונתי ל-CPUs, יש רכיבים שהם עושים לנו פעולות קריפטו. מה הכוונה פעולות קריפטו? זה פעולות קריפטוגרפיות שעוזרות לנו לעשות הצפנה. אז כדי לגשת לפעולה הקריפטוגרפית אנחנו גם צריכים תמיכה מהחומרה, עוד אם אתם רוצים אני אראה לכם איך יודעים אם יש תמיכה, וגם צריכים את ה-API, וה-API נמצא בתיקייה הזאתי. ואם כבר אמרתי, אז אני אראה את זה.

אז רוצים להסתכל `cat /proc/cpuinfo`. זה מראה לנו את כל ה-CPUs. אז חלק מה-features האלה שאנחנו רואים שנקראים `flags`, זה פעולות קריפטוגרפיות, או אפשרויות לעבוד עם פעולות קריפטוגרפיות. AES, אני לא רואה את זה כאן, אבל AES ו... אני לא זוכר לגבי ה-SSE, עוד כל מיני חלקים. ממש מעניין, רק צריך לדעת להשתמש, זה הכול. טוב, יכול להיות שפשוט לי אין AES במחשב הזה, אבל יש עוד כל מיני.

— מה היית צריך לכתוב? `cat` מקף...

`cat /proc/cpuinfo`. או שיכול להיות שזה פשוט לא על ה-CPU הזה, על CPU 0. הפתיע אותי אם אין לי את זה...

— יש את AES, יש את SHA למיניהם.

אה, נכון. יש את AES, יש SHA למיניהם, וזה כן מופיע פה. בדרך כלל זה אותו דבר. אני חושב שגם SSE והמשפחה, ועוד כאלה שאולי אני לא מכיר/לא קולט אותם.

מעבר לזה, יש לנו את ה-`documentation`, בעצם זה התיקייה של הדוקומנטציה עצמה. אצלי אני לא רואה אותה, אבל זה משתנה בין קרנלים.

`drivers` – אז השכבה של ה-drivers. אולי אני פשוט לא מסתכל במקום הנכון, שנייה. `drivers`, `documentation`, זה שורות מתחת ל-`linux`. יש לנו מתחת ל-`kernel` חלק מהדברים וחלק מתחת ל-`linux`.

אז `arch`, אמרנו architecture specific.

`block` – זה ה...

`drivers`, `crypto` – זה כל ה-drivers והתמיכה בקריפטו functionalities.

`documentation` נמצאים כאן.

`drivers` – זה ה-device drivers שאנחנו יכולים להשתמש. אז יש לנו Bluetooth, יש לנו char drivers, GPIO כמובן, GNSS לדעתי זה GPS, זה בעצם הפרוטוקול של GPS. I2C עם שלושה קווים. וכולי וכולי. קיצור, drivers.

לאחר מכן יש לנו את ה-`fs`, אימפלמנטציה של ה-file system עצמו. זה תמיכה בכל מיני סוגים של file system שאנחנו משתמשים. אנחנו נראה פה ext4, FAT, ווטאבר, JFS.

`include` – שזה ה-header files שאיתם אנחנו יכולים להתממשק.

`init` – שזה ה-initialization code. במקרה שלנו, אנחנו שינינו בו איזושהי חתיכה, נכון?

`ipc` – רגע שנייה, קפצתי פה. `ipc` עצמו זה ה-Inter-Process Communication שיש לנו. אז יש לנו פה message queues, יש לנו shared memories, ויש לנו עוד utils ו-system calls. זה מתוך ה-IPC.

יש לנו את ה-`kernel`, שזה ה-core kernel code. שבו יש לנו את ה-IRQs, `futex` שזה ה-kernel mutex, BPF שזה Berkeley Packet Filter שאפשר להשתמש בהם, שזה feature יחסית חדש. טוב, מקרנל 5 משהו, אבל מתקדמים מהר מאוד בשנים האחרונות.

`lib` – שזה ה-library שאנחנו יכולים להשתמש בהן: `libmath`, `libcrypto` וכולי.

`mm` – שזה ה-memory management.

`net` – network code.

ו-`security` – security framework. בסדר?

## Process Fundamentals

אז בואו עכשיו נקפוץ לתוך ה-Linux `sched`, שיש לנו אותו בשני מקומות, אבל אנחנו רוצים ספציפית את זה שנמצא בתוך ה-`linux`, אמרנו `include/linux/sched.h`, שזה קיצור של scheduler. אוקיי, מתוכו אנחנו יכולים ממש למצוא את ה-process descriptor, אוקיי?

בעצם ה-`struct` נמצא ב-`linux/include/linux/sched.h`.

פה יש לנו בעצם את ה-`struct` של ה-tasks, שבתוכו יש לנו את כל הקומפוננטות שאחנו צריכים בשביל לקבל אינפורמציה על ה-process עצמו. יש הרבה מאוד members, אם תסתכלו על הקוד הזה זה 700 שורות, 700 שורות עם הרבה דברים. והדברים העיקריים שמניינים אותנו זה ה-PID שזה ה-process ID עצמו, ה-process state שנדבר עליו, ה-scheduling information, file system information זה ה-VFS, memory management information גם כן, signal handlers שאנחנו כבר יכולים להשתמש בהם בשביל signal handlers שאפשר לרשת אותם כאילו עם מימוש סטנדרטי, ו-IPC שאנחנו נשתמש בו.

לקחתי את הדברים העיקריים, כי אנחנו לא רוצים את הכל. אז יש לנו דבר ראשון את ה-`state`, שזה ה-processor state, נדבר עליו עוד רגע. את ה-`stack`, בעצם לכל process יש stack משלו. ה-stacks של ה-processes ב-Linux הם דינמיים, אלא אם כן הגדרנו אותם בקונפיגורציה להיות סטטיים. מה הכוונה דינמיים או סטטיים? אם הם סטטיים אז הם בגודל קבוע, ואני יכול להגדיל את התוכנית שלי עד כדי הגודל הסטטי שנבחר. אם הם דינמיים אז בעצם אני צריך לעשות אלוקציה מה-heap.

אחר כך יש לנו priority (`prio`), עכשיו priority זה משתנה בין הסוגים של ה-schedulers. Memory descriptor, ה-`sched_class` שאנחנו עוד שנייה דווקא כן נדבר עליו.

אני משתמש ב-Text Marker, אם זה עוזר לכם, לי זה מאוד עוזר. Text Marker ב-extensions, אני משתמש בו הרבה. ואפשר לעשות לו גם קיצור.

אוקיי, יש לנו עוד איזשהו חלק, ה-Red-Black Node שנדבר עליו עוד רגע. וה-CPU mask, זה לדעת באיזה CPU יכול להשתמש אם אנחנו מקצים tasks עם affinity.

ואת ה-`files` struct שזה בעצם ה-VFS. ה-VFS ככה אנחנו משתמשים בו כדי לעשות את האבסטרקציה של ה-process לתוך file.

מה שאותנו מעניין כרגע זה להכיר קצת את ה-`struct` הזה, לראות מה יש לנו פה, באיזה CPU אנחנו עובדים ואנחנו יכולים לקרוא ממנו, מה ה-stack שלו – הנה זה ה-pointer אל ה-stack. שימו לב שה-kernel כתוב עם tabs ולא עם רווחים, למי שזה קריטי לו.

ובתוך ה-`sched_class` עצמו אנחנו כן ניכנס אליו, הוא נמצא בתוך `kernel/sched/sched.h`. ופה נחכה עוד רגע, כי כאן אנחנו נדבר על ה-`sched_class`.

אז זה בעצם ה-`struct` שאנחנו עובדים מולו, זה ה-`task_struct`. ה-kernel עצמו מנהל כל task, כל process עם ה-`struct` הזה, ובעצם יש לו איזשהו linked list או איזושהי היררכיה, בגדול זה עם Red-Black Tree, איזושהי היררכיה של ניהול `struct`-ים.

## Process States and Lifecycle

לפני זה אני רק רוצה שנדבר על ה-scheduler classes.

ה-scheduler classes עצמו, יש לנו כמה states לכל scheduler class שאנחנו משתמשים בו בתוך ה-process שלנו:

- `TASK_RUNNING` – זה אומר שה-process כרגע הוא או רץ או מוכן לרוץ. יש אימפלמנטציות ש-running אומר שרץ כרגע, אבל במקרה הזה זה או רץ או מוכן לרוץ.

- `TASK_INTERRUPTIBLE` – זאת אומרת ש-process ישן אבל אפשר להעיר אותו על ידי signal.

- `TASK_UNINTERRUPTIBLE` – זאת אומרת שה-process ישן ואי אפשר להעיר אותו עם סיגנלים.

- `TASK_STOPPED` – זאת אומרת שה-process execution נעצר, או על ידי `SIGSTOP` או איזשהו graceful shutdown.

- `TASK_TRACED` – שברגע שנדבר על debugging של ה-kernel אנחנו נשתמש גם בזה.

- `EXIT_ZOMBIE` ו-`EXIT_DEAD`.

מה ההבדל בין `EXIT_ZOMBIE` ל-`EXIT_DEAD`?

`EXIT_DEAD` זה הכי פשוט, בעצם אנחנו כבר לא משתמשים בו והוא מוסר מה-scheduler שלנו.

ו-`EXIT_ZOMBIE` אומר שה-process הוא terminated, אבל האבא שלו צריך לאסוף את ה-exit status. בעצם כאשר ה-process מת, צריך לחכות עד שהאבא שלו, ה-parent process, יאסוף את הסיבה שהוא מת ואת המידע שלו אם הוא צריך להחזיר מידע כלשהו לתוך האבא, כמו ה-status יציאה שלו, לדוגמה.

אז זה ה-process state וה-lifecycle שלו.

## Process Creation

איך אנחנו בעצם יוצרים process מהשכבה של האפליקציה?

אנחנו משתמשים ב-`fork`. `fork` יוצר לנו copy של ה-calling process, הוא יצור new `task_struct` ומעתיק את ה-resources של האבא.

מי שמכיר יודע שזה לא לגמרי נכון. מתי אנחנו בעצם מעתיקים את ה-resources של ה-process האבא? כאשר יש שינוי. אז זה נקרא COW – Copy-On-Write. זה אומר שברגע שאנחנו כותבים לתוך אחד מה-resources, אז בא ה-kernel ומעתיק לנו את כל מרחב הכתובות ואת כל ה-resources ל-`struct` חדש. אז אנחנו כן יוצרים `struct` חדש, בעצם הוא מצביע על ה-`struct` הישן, וברגע שאנחנו כותבים לתוך הזיכרון של ה-process הילד, מערכת ההפעלה באה ומעתיקה לנו את כל המידע של ה-`struct` ובעצם משכפלת אותו.

אז מה מחזיר לנו `fork`? הוא מחזיר לנו שלוש אפשרויות:

ברגע שאנחנו עושים `fork`, אנחנו בעצם מפצלים את עצמנו לשני processes ואז אנחנו רצים פעמיים. אם הוא מחזיר את ה-PID של ה-child לתוך ההורה, זאת אומרת ה-process ID של ה-task שיצרנו חוזר לתוך ה-parent process. יש גם קריאה כזאת שנקראת `getppid`, שזה מ-child אל האבא, `getppid` זה Parent Process ID. אם אנחנו חוזרים עם return code של 0, אז אנחנו בעצם ה-child process, ואנחנו יכולים להשתמש ב-system calls אחרים כדי לקחת את ה-PID שלנו ואת ה-PID של ה-parent שלנו. ואם לא הצלחנו, אז ערך שלילי (negative) עם שגיאה.

זה process או task creation עם `fork`.

חוץ מזה יש לנו עוד משפחה של `exec`-ים, אבל נדבר עליהם כקבוצה אחת, כי זה לא באמת משנה. אז `exec` הוא מחליף לנו את ה-process הנוכחי עם תוכנית חדשה. זאת אומרת stack חדש לגמרי, אנחנו כן שומרים את ה-PID הנוכחי ואת ה-resources שהוקצו לנו, אבל אנחנו לא מקבלים הקצאה חדשה, אנחנו פשוט מחליפים לגמרי את התוכנית ואנחנו טוענים executable חדש. זו צורה נוספת ליצור process.

ויש לנו `exit`, שזה בעצם להרוג את ה-process, לשחרר את ה-resources ולהחזיר את המידע לאב. ה-process עצמו בעצם נחשב כ-zombie עד שה-parent קורא ל-`wait` ומקבל את המידע על ה-process.

## Process Scheduling

אז אילו סוגים של schedulers יש לנו, וזה ה-scheduler design עצמו:

ה-Linux scheduler architecture מורכב מכמה schedulers שרצים עם היררכיה. כל process יכול להיות שייך רק ל-scheduler אחד. וה-scheduler classes בעצם משתמשים בהם בעדיפות (priority):

1. `Deadline` (הכי גבוה) – אנחנו מתחילים להבין למה ה-deadline הכי חשוב, כי אם אנחנו רוצים שמשהו יקרה בעוד $X$ זמן על ה-epsilon, אז אנחנו חייבים להשתמש ב-deadline. זאת אומרת שחייבים לעמוד ב-deadline, אז הוא עם ה-priority הגבוה ביותר.

2. Real-Time – יש לנו שני סוגים של schedulers שהם real-time.

3. `CFS` (Completely Fair Scheduler) – שזה הסטנדרטי, שהוא completely fair.

4. `Idle` – שזה ה-scheduler עם ה-priority הנמוך ביותר.

שאלות על מה שדיברנו עד עכשיו?

אני צריך הפסקה קצרה, אז בואו ניקח את ההפסקה שלנו טיפה יותר מוקדם היום, ונחזור ב-8:18, ונמשיך עם ה-CFS ונבין קצת את המבנה של ה-CFS עצמו.

אז אתן עוד איזה כמה דקות, כי ההמשך של זה... אוקיי, השם שלי היה רשום לא נכון.

אנחנו מתחילים, אני רק פספסתי איזהשהו חלק אחד.

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

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

אז, מבחינת ה-SSH, פשוט ה-SSH key שיצרתי, שה-authorized_keys שיצרתי, יצרתי לא במקום הנכון, אז זה בדיוק כמו ההסבר ששלחתי לכם אותו גם. אז `~/.ssh/authorized_keys`.

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

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

אז בעצם אנחנו... אז אני לא הצלחתי לקבוע את התגים ספציפית לזה, אז שימו לב שהתגים שאתם עובדים איתם זה אותם התגים שאני הגדרתי, או שתצטרכו לעשות עוד עבודה. אז ל-Linux עצמו, התג שאנחנו משתמשים בו הוא 5.15, בסדר? זה ה-git tag עצמו, ול-BusyBox זה 1.37.0.

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

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

## Process Scheduling

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

אז יש לנו את ה-Deadline, שזה ה-scheduler עם ה-priority הגבוה ביותר. למה הוא עם ה-priority הגבוה ביותר? כי אנחנו לא נרצה לפספס deadline בקטגוריה.

אחר כך יש לנו Real-Time. אז Real-Time בתוך Linux זה פחות Real-Time ברמת ה-בזמן אמת, ברגע זה, בזה הרגע, אלא יותר Real-Time ברמת ה-average של bandwidth כמה שיותר מהר מצד אחד לצד שני, זה ברמה הזאתי.

CFS, אם אנחנו רוצים להשתמש ב-Completely Fair Scheduling של המערכת, ו-Idle, עוד שנייה נדבר עליהם.

## CFS (Completely Fair Scheduler)

אז מה זה CFS? Completely Fair Scheduler. זה scheduler שבעצם מצפה לתת זמן שהוא fair בין כל ה-processes. יש לו כן עבודה עם process priority של nice values. יש לנו בתוך ה-sched_class, בתוך ה-task_struct, יש לנו את המשתנה של ה-priority. יש לנו כמה סוגים של priorities, יש לנו nice ויש לנו עוד שני סוגים. אז ה-sched_class עצמו, ה-Completely Fair Scheduler הוא משתמש בתצורה של nice values, שזה מ-‎-19 עד +20, שכמה שיותר נמוך, המספר היותר נמוך זה priority גבוה יותר.

הוא משתמש באלגוריתם ממש נחמד כדי להבטיח סקלביליות, והוא משתמש ב-Red-Black trees. אם אתם זוכרים Red-Black trees ממבני נתונים, אז זה בעצם המבנה שה-kernel משתמש בו כדי לקבוע בעצם מה ה-task הבא שירוץ. אם אתם לא זוכרים מה זה Red-Black tree זה הכל בסדר, יש לנו שיעור על כל המבני נתונים שאפשר להשתמש בהם בתוך ה-kernel.

בעצם לכל process מחושב לו ה-vruntime, שזה ה-virtual runtime שהוא מריץ, וה-process עם ה-vruntime הקטן ביותר הוא זה שירוץ הבא. ככה שאנחנו כל פעם נתפוס את ה-process הבא שצריך לרוץ, בזמן הקטן ביותר.

ה-CFS נותן לנו איזהשהו balance בין throughput לבין latency. בעצם אמרנו שיש לנו את ה-Real-Time כדי... את ה-Real-Time scheduler בשביל throughput, ויש לנו את ה-Deadline בשביל ה-latency הנמוך ביותר. אז כמה שיותר throughput נשתמש ב-scheduler של Real-Time, וכמה שפחות latency נשתמש ב-Deadline.

## CFS: How It Works

אז איך הוא עובד? בעצם אנחנו מחשבים את ה-virtual runtime של כל process. אנחנו בעצם שומרים על ה-CPU time לפי משקל על ידי priority. אנחנו עושים time slicing של ה-scheduler כדי לחשב את מספר ה-runnable processes וה-nice values של ה-priority שאנחנו נותנים.

ה-CFS הוא בעצם קובע את ה-Sleeper Fairness, בעצם הוא קובע כמה זמן כל process הולך לישון, ככה שאנחנו לא נצטרך לפצות... סליחה, נכנס הכלב... כדי שלא נצטרך לפצות על הזמן של ההתעוררות מבין ה-scheduler, ואנחנו עושים איזהשהו group scheduling, בעצם אנחנו עושים חלוקה בין processes של אותו user group על ידי cgroups. פחות נגע בזה, זה בסדר.

אז אם אנחנו ניכנס לתוך ה-kernel/sched/fair.c, בתוך ה-kernel/sched/fair.c עצמו, אנחנו נראה את ה-vruntime calculation. vruntime calculation שהמערכת עושה, אז בעצם זה ה-task שעושה את ה-update_curr_fair. זה הפונקציה שעושה את ה-update_curr, בעצם היא לוקחת את ה-sched_entity מתוך ה-scheduler מה שרץ כרגע, מחשבת כמה זמן עבר, ואחר כך מחשבת כמה זמן עבר מתחילת הריצה של ה-process שרץ כרגע.

אחר כך הן קובעות את ה-זמן ריצה של ה-process שרץ כרגע להיות now, ואנחנו מחשבים באמצעות calc_delta_fair את ה-זמן הבא שאנחנו... את ה-זמן ריצה של ה-process הנוכחי, שבגדול יהיה ה-process הבא כאשר אנחנו ניתן לו את ה-CPU.

אז בסוף אנחנו עושים את ה-load לפי ה-load weight, וזה ה-nice value הבסיסי, ככה שאנחנו מחשבים באמצעות את ה-זמן האמצעי של nice priority כדי שיהיה אפשר לעשות מהר יותר או פחות מהר. מפה אנחנו מחשבים את ה-calculated delta. מה ה-delta בין הזמנים לפי המשקל, ואנחנו קובעים את... ואנחנו קובעים, למרות שפה אני לא רואה את זה, את הזמן שה-CPU הולך להקצות ל-process הנוכחי.

קצת קשוח, אז בואו שנייה נחזור. יש לנו את ה-struct של cfs_rq, שזה ה-queue של ה-CFS. בעצם זה בסופו של דבר ייכנס לתוך ה-struct שלנו של ה-scheduler. יש לו את ה-load weight, כמה אנחנו בעצם צריכים לרוץ, את ה-scheduler בעצם באיזה type של scheduler זה, ואחר כך יש לנו את ה-current_entity, next, last ו-skip, שזה בעצם איזהשהו linked list.

בכל שלב אנחנו בעצם קוראים ל-pick_next_entity. בעצם pick_next_entity הוא האחד שהוא קובע מה ה-entity הבא שירוץ. אז הוא לוקח את ה-entity הנוכחי. אם ה-entity הוא ה-entity הראשון אז הוא לוקח אותו מה-Red-Black tree, אחרת הוא לוקח את ה-next_entity עצמו. ה-next_entity זה rb_first_cached, זה ה-API של Linux ל-Red-Black tree, ובסופו של דבר הוא מחזיר לנו את ה-node של ה-Red-Black entry. בעצם מה שעשיתי פה זה לקחתי את הפונקציה הזאת והמרתי אותה למה שקורה בסוף מ-כאילו אחרי ה-API.

אז איך זה עובד, אמרנו? אנחנו מחשבים את ה-runtime של כל task לפי ה-priority, וככה אנחנו קובעים כמה ה-CPU הולך להריץ. אנחנו מחלקים את זה ל-time slice של ה-CPU. יש לנו את מספר ה-runnable processes שאנחנו רצים איתם עכשיו, ולכל process יש את ה-nice value שלו. Sleeper Fairness באמצעות המבנה נתונים שאנחנו משתמשים בו, Red-Black tree, אנחנו בעצם עושים קומפנסציה על החישוב, ככה שאנחנו בעצם מחשבים את זה תמיד ב-O(log n). זה הזמן קבלה של ה-node הבא. ה-scheduling שלנו נעשה על ידי ה-user group או ה-cgroup, לא נגע בזה, זה בסדר. ה-vruntime זה ה-runtime שנשמר לכל process. כל process runtime בעצם הוא מנורמל על ידי המשקל שלו בתוך ה-scheduler. ויש לנו את ה-priority, higher priority שווה slower...

runtime accumulation, אז זה הערך עצמו. Lower runtime יותר, lower runtime - higher chance of being scheduled next. זאת אומרת שאם אני רוצה ירוץ לזמן יותר קצר, אז יש לי יותר סיכוי לקבל את ה-execution.

## CFS: The Red-Black Tree in Action

איך בעצם נעשה שימוש ב-Red-Black Tree? אז בעצם ה-tree property, אז יש לנו אחד מה-tree properties זה self-balancing binary tree. בעצם Red-Black Tree זה איזשהו עץ בינארי שבאמצעות הצביעה של ה-nodes אנחנו עושים לו balancing.

ה-sort key שאנחנו ממיינים עליו זה ה-vruntime, וה-balancing בעצם, העבודה של ה-balance, נעשית ב-O(log n).

ה-task הבא תמיד יהיה ה-task השמאלי שאנחנו זה, וככה אנחנו בוחרים את ה-task הבא. אנחנו משתמשים ב-pick_next_entity שזה הפונקציה הזאתי. אנחנו אומרים לו: "תביא לי את ה-task השמאלי", זה ה-rb_next. ואם אין לנו task שמאלי, אנחנו חוזרים ואין לנו מה לעשות עם זה. אחרת, אנחנו פשוט מביאים את ה-entry.

ולשמאלי אנחנו מביאים את ה-entry הבא שלו, אוקיי?

מאוד יעיל, אנחנו משתמשים ב-O(1) כדי להביא את ה- סליחה, אמרתי קודם O(log n), סליחה זה O(1), כדי להביא את ה-entry השמאלי הבא שנמצא לנו ב-cache. ואם אין לנו tasks לרוץ, tasks שאנחנו מריצים, אנחנו מחזירים NULL, ואז אנחנו בעצם נקפוץ ל-idle CPU, אוקיי?

## CFS: Tree Operations

כאשר אנחנו רוצים להוסיף task, אנחנו משתמשים ב-enqueue_entity, שהוא בעצם [נקפוץ שנייה אליו] enqueue_entity, מה שהוא עושה הוא בעצם מצהיר לנו על היצירה של ה-task, על העדכון של העץ, בעצם הסרה של מה שאנחנו לא צריכים בתוך העץ, ועדכון של ה-vruntime.

עוד דברים שאנחנו עושים בתוך הריצה עצמה, אז יש לנו יצירה, יש לנו את ה-running, ואז יש לנו preemption/sleep.

Preemption זה אם הגיע הזמן לעצור את ה-task שהוא לא ויתר על ה-CPU בצורה התנדבותית (לא יודע אם זה עובד בעברית, בצורה וולנטרית... לא מצליח להגיד את המילה הזאת, בצורה התנדבותית).

Sleep זה בעצם שיהיה ויתור על ה-CPU כדי לתת ל-processes אחרים לרוץ.

Termination - אז הסרה מהעץ לצמיתות, אוקיי? או עד יצירה.

## Understanding Scheduler Classes

איך בעצם נראה ה-scheduler classes שאנחנו משתמשים? אז בעצם הארכיטקטורה היא בצורה של interface. כל scheduler class יש לו את ה-interface שלו, אנחנו יכולים להשתמש בכמה scheduler classes שירוצו במקביל, אוקיי? זאת אומרת שלכל task descriptor יש לו scheduler class אחד, ורצים במקביל אם ה-tasks רצים במקביל. וה-classes הם עצמם מחולקים לפי ההיררכיה של ה-schedulers שראינו מקודם.

אז היינו כבר פה. אז אנחנו נרצה לקפוץ ל-sched_class.

ה-class עצמו, אז בעצם יש לו את ה-enqueue_task שממנו הוא מוציא את ה-task הבא, יש לנו את ה-dequeue_task שבו הוא מחולק, את ה-yield_task שזה כאשר אנחנו מוותרים על ה-CPU בצורה וולנטרית, וה-yield_task זה הבא בתור.

יש לנו עוד את ה-task struct הבא שממנו אנחנו לוקחים את ה-task הבא, אוקיי? ו-task הקודם, ה-task הבא שאנחנו יכולים לשים אותם.

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

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

## Scheduling Classes and Policies

אז מהם ה-policies של schedulers בכללי? אז יש לנו כמה policies שאנחנו משתמשים בהם, וכל אחד מהם הוא מתאים ספציפית ל-scheduler שלו.

אז SCHED_NORMAL זה ה-default, זה ה-CFS שלנו. אז כאשר אנחנו נקבע SCHED_NORMAL, בעצם זה ה-default, זה שימוש ב-CFS.

SCHED_BATCH - זה האחד שאמרתי לכם שאין לו class... אין לו priority ספציפי. בעצם מה שעושים עם BATCH זה כאשר אנחנו מקמפלים, עושים פעולות של high bandwidth או פעולות של high processing, כמו עיבוד תמונה, כמו קומפילציה וכולי, אז אנחנו לא צריכים לדאוג ל-preemption, אנחנו פשוט משתמשים ב-scheduler batch. הוא, מה שהוא עושה, כל process שנפתח ב-SCHED_BATCH אז הוא בלי preemption, הוא פשוט רץ עד שהוא מסיים עם כמה שיותר CPU. זה SCHED_BATCH.

SCHED_IDLE - אנחנו משתמשים בו כאשר אנחנו רוצים להריץ פעולות עם priority נמוך, לדוגמה file indexing, כל מיני פעולות של איך זה נקרא, security וכולי.

ויש לנו אחר כך עוד שני schedulers שאנחנו... scheduler classes שמשתמשים בהם, שאחד מהם זה FIFO ו-Round-Robin.

אז FIFO בעצם הוא עושה לנו First-In First-Out, שזה בעצם אומר שהפעולות שנכנסות ראשונות (סליחה, ה-priority זה הראשונים) קודם כל הם רצים ואחר כך ה-priority האחרונים, בעצם ה-processes עם ה-priority הנמוך יותר.

Round-Robin - אז הוא גם עושה לנו Round-Robin, אבל הוא עושה לנו Round-Robin עם priority. זאת אומרת שהוא יריץ, אם יש לנו... איך אני אסביר את זה? אנחנו רצים ב-Round-Robin. עכשיו בוא נדמיין כמה Round-Robin-ים, נניח יש לנו processes עם priority גבוה ו-processes עם priority נמוך. כל עוד ה-processes עם ה-priority הגבוה לא יסיימו... לא ישחררו בצורה אלטרואיסטית את ה-CPU, אז ה-processes עם ה-priority הנמוך לא יקבלו אותו, וה-Round-Robin ייעשה ביניהם.

מה שכן, feature שיש לנו בתוך ה-Round-Robin זה time slicing. זאת אומרת שאם אנחנו רוצים למנוע הרעבה (מה שנקרא בעברית starvation) של processes עם priority נמוך, אז פעם בכמה סליחה, פעם בכמה החלפות בין אותו process או בין כמה processes שרצים ברצף, אנחנו נשחרר ונעשה בעצם מה שנקרא throttling ל-low-priority processes.

process נוסף זה ה-SCHED_DEADLINE, שזה deadline-based scheduling. זאת אומרת אנחנו נותנים ל-process עצמו, אנחנו נותנים לו איזשהו deadline שהוא צריך שהוא יכול לעמוד בו. זאת אומרת עוד X זמן אני אצטרך ככה וככה CPU, ויש עוד איזה פרמטר, עוד שנייה נגע בזה. וזה scheduler שהוא התפקיד שלו זה לגרום לכל process לעמוד ב-deadline ספציפי. אוקיי? זה בעצם אפשר לחשוב עליו כעל project manager. זה ה-process הזה.

## Linux Preemption and Context Switching

מה זה בעצם context switching? context switching זה ההחלפה של ה-execution בין task אחד ל-task אחר.

איך זה נעשה? זה נעשה על ידי הקריאה שנקראת context_switch בתוך ה-kernel sched.c file, אוקיי? בתוך ה-C file יש לנו את הפונקציה שנקראת context_switch שהיא עושה לנו את ההחלפה בין ה-processes, אוקיי? process אחד יוצא, ה-execution שלו מפסיק, process אחר מתחיל.

מתי זה נקרא? זה נקרא כאשר process נבחר לרוץ על ידי הקריאה שנקראת schedule.

אז בואו פשוט אני אמצא את זה שנייה.

אז זה ה-context_switch עצמו, בעצם הוא זה שמגדיר... הוא זה וכאשר אנחנו עושים context switch, זה הקוד שאנחנו נריץ. בעצם אנחנו נכין את ה-tasks ל-context switch עצמו, אנחנו נעשה את ה-context switch הספציפי של הארכיטקטורה. זאת אומרת שאם אנחנו בארכיטקטורה של ARM, אז יש לנו את ה-core registers שאותם אנחנו נדחוף לתוך ה-stack, ואם אנחנו בארכיטקטורה של x86, אז יש את ה-core registers של ה-x86 וכולי וכולי.

סבבה. זה ה-זה עצמו. אחר כך יש לנו את ה-lazy TLB שזה ה- שכחתי את השם שלו, זה בעצם ה-task משהו משהו... task table או בעצם ה-entry הבא שאנחנו ניקח את זה כשאנחנו עוברים ל-kernel, ובעצם זה האזור שממנו אנחנו עושים את ה-context switch.

כאשר אנחנו בעצם... מתי אנחנו נקרא לזה? מתוך הקריאה שנקראת schedule. אז בואו שנייה נגיע לכאן.

וזה schedule. schedule עצמו, אנחנו יכולים ממש לקרוא כאן, זה בעצם ה-main scheduler function שזה הפונקציה של ה-scheduler עצמו, והיא בעצם תחליט לנו מתי אנחנו קוראים ל-context_switch. אז היא בעצם בודקת לנו אם יש לנו IRQ שצריך לרוץ, אם אנחנו צריכים לעשות איזשהו mode... אם אנחנו רוצים לעשות איזשהו context switch שאנחנו רוצים להוסיף, אחר כך אנחנו עושים lock ל-request של ה-context switch, ובסופו של דבר אנחנו מגיעים ממש ל- יש לנו קצת מעבר פה, אנחנו בעצם מחשבים כמה זמן אנחנו רוצים להוציא, ואנחנו עושים את ה-context_switch עצמו.

מדי פעם דברים קופצים. זה ה-context_switch עצמו, שזה הקריאה. זה בעצם ה-prev execution, וזה ה-next. אוקיי? שה-prev הוא גם ה-current.

ככה זה נראה בתוך ה-kernel, בתוך ה-scheduler. אנחנו לרוב לא נוגעים, אלא אם כן אנחנו צריכים לעשות איזשהם patches ספציפיים, אבל זה חשוב להכיר.

אז אם אנחנו נרצה בכללי לעבור על scheduler אחר, אז אנחנו פשוט נכיר את ה-entities שלו, בעצם נכיר את ה-interface שלו. אז יש לנו את ה-interface של... שוב, את ה-interface של ה-sched_class.

שזה בעצם אנחנו רוצים להכיר. אז לדוגמה, ל-fair, אז ה-enqueue_task זה ה-task שאנחנו בעצם מגדירים לו כאן, אז לכל אחד מהם יש את ה-definition של ה-class עצמו. אז ה-enqueue_task_fair זה של ה-CFS בעצם. אם אנחנו נרצה ללכת לאחד אחר, אז פשוט נחפש את ה-define הזה, וה-define הזה ייתן לנו לעבוד עם כל אחד מהם אחר. אז יש לנו את ה-realtime, יש לנו את ה-deadline, ויש לנו את ה-fair. אנחנו לא ניכנס לכולם עכשיו, זה יקח קצת הרבה, וגם הנושא הוא לא הכי פשוט.

אז זה בעצם ה-interface שאיתו אנחנו עובדים, אוקיי? ולכל אחד מהם יש את הפונקציה שלו, ה-enqueue_class, dequeue_class... בעצם enqueue מעביר אותנו ל-task הבא, dequeue זה להוציא את ה-task הבא מ-queue, והבחירה של כל אחד, הבחירה של ה-tasks הבאים, בחירה של tasks הקודמים, ה-task_systick, task_fork אם אנחנו עושים task חדש, ושינוי של priority.

שזה בעצם איזה שהוא API, איזה שהוא interface בגדול.

## Context Switching Fundamentals

סבבה. דיברנו אז על מה זה context switch. אחר כך יש לנו memory context switch. זה בעצם ההחלפה של ה-virtual memory לתוך ה-RAM, בעצם פריקה ובעצם שחרור של ה-RAM, שחרור של RAM של process נוכחי ולקיחה של process חדש, אוקיי? אם אנחנו עושים את זה.

אחר כך יש לנו CPU switch. בעצם ב-CPU אנחנו שומרים את ה-process core registers, ראינו שזה תלוי ארכיטקטורה פה. אנחנו מחליפים את ה-stack information שאנחנו רוצים, ואנחנו בעצם עושים architecture-specific state של הריצה. אוקיי, אז ראינו שרוב הריצה פה היא ריצה ספציפית לארכיטקטורה.

## The Need for Preemption

מה בעצם, למה אנחנו צריכים, מה זה בעצם הצורך לעשות preemption? יש לנו שני דברים: יש לנו או בצורה voluntary, שזה בעצם מרצון לשחרר את ה-CPU עם הקריאה של yield, כאשר אנחנו רצים עד שה-process עצמו משחרר את ה-CPU בצורה אלטרואיסטית כזאתי.

ויש לנו preemptive scheduling, שבה ה-OS יכול להכריח process לשחרר את ה-CPU.

אז מתי אנחנו בעצם עושים את ה-schedule? מתי אנחנו בעצם עושים את ההחלפה? כאשר יש לנו אינדיקציה מה-scheduler ש-process חדש צריך לעשות invoked, כאשר אנחנו עובדים עם process flag ספציפי של ה-structure. ומי יודע מה זה נקרא? נקרא על ידי scheduler_tick, שזה בעצם מה שאומר לנו שה-slice הנוכחי של ה-scheduler הוא expired.

אז ראינו שוב מתוך ה-interface הזה, אז יש לנו את ה-scheduler_tick ובעצם... בואו ננסה לראות אם אני אצליח למצוא את זה פה.

יש לנו את ה-`task_tick_fair`, שהוא בעצם מה שקובע לנו את ה-ticks של ה-slicing של כל task. בעצם כמה זמן כל task יכול לרוץ, זה ה-slicing.

יש לנו אחר כך `try_to_wake_up`, כאשר higher priority process מנסה להתעורר ולראות אם הוא יכול לקחת את ה-CPU. ויש לנו עוד איזה חלק שאנחנו יכולים לעשות אותו... את ה-schedule, כאשר אנחנו באיזה שהוא safe point של ה-execution. זאת אומרת שאין לנו interrupt-ים שרצים כרגע במערכת.

## User Preemption

מה זה בעצם user preemption? יש לנו שני סוגים של preemption: יש לנו user preemption ויש לנו kernel preemption.

User preemption זה כאשר task מה-user space... כאשר task של ה-user space הוא חוזר מה-kernel אל ה-user space, אוקיי? זאת אומרת שה-kernel, שבו נמצא ה-scheduler, הוא הגיע להחלטה שבעצם ה-user צריך את ה-CPU יותר דחוף ממנו. למה שאנחנו נרצה את זה? דוגמה אחת, יש לנו את ה-deadline, נכון? ה-deadline של ה-SCHED_DEADLINE. דוגמה נוספת זה כאשר אנחנו בעצם חוזרים מ-syscall.

אז כאשר ה-kernel מסיים להריץ system call, אז הוא בעצם בודק שהוא לא צריך לעשות עוד שום דבר ב-kernel, ואז הוא עושה את ה-preemption ל-user, בעצם מחזיר את המידע אל ה-user.

כאשר ה-kernel מסיים להריץ interrupt, ה-user ביקש מה-kernel... סליחה, kernel ביצע interrupt, הוא בודק שהוא לא צריך לבצע עוד שום דבר ואז הוא חוזר אל ה-user. אז זה ה-user preemption.

## Kernel Preemption

מה זה kernel preemption? Kernel preemption זה בעצם איזה שהוא מנגנון שהוא משתמש ב-locking. אם אין לנו שום lock שתופס לנו את ה-kernel עצמו, אז אנחנו בעצם עושים preemption ל-kernel task.

מתי kernel preemption קורה? כאשר interrupt handler מסתיים ואנחנו חוזרים אל ה-kernel space, כאשר ה-kernel code הופך להיות preemptible again, כאשר אפשר לשחרר את ה-kernel, וכאשר task בצורה explicitly קוראת ל-schedule, זאת אומרת היא משחררת את ה-execution.

אם task ספציפי של ה-kernel תופס אותו, בעצם הוא blocked, אז ברגע שאני אשחרר אותו, אז השחרור עצמו יקרא ל-schedule, מה שיעביר לי את ה-preemption... יעשה לי preemption ל-kernel task.

## Real-Time Scheduler

Scheduler נוסף, וזה כבר האינפו הספציפי של ה-scheduler, זה ה-Real-Time Scheduler. אנחנו משתמשים בו בשביל time-critical processes. יש לו שני סוגים של scheduler classes שמשתמשים: זה SCHED_FIFO ו-SCHED_RR.

SCHED_FIFO בעצם מייצר לנו איזה שהוא queue של task-ים, ו-SCHED_RR בעצם מנסה לעשות לנו איזה שהוא fairness time slice לפי priority, אוקיי?

SCHED_FIFO אז זה ה-First-In-First-Out, זה בעצם ה-scheduler שהראשון שמגיע הוא זה שאנחנו משתמשים בו. אנחנו לא עושים slicing, אנחנו רצים עד שה-process היחידי משחרר את ה-CPU או שהוא בעצם ב-blocking state. מה זאת אומרת ב-blocking state? זאת אומרת שלקחו לו איזה שהוא resource והוא לא מצליח לקחת אותו.

מתי אפשר לעשות לו preemption? רק כאשר higher priority process מבקש את ה-CPU. מהם ה-priority ranges? אז אמרנו, יש לנו את ה-nice, שה-nice רץ מ-19- עד 20, שזה של ה-CFS. פה יש לנו מ-1 עד 99. ככל שהמספר גבוה יותר, ה-priority הוא גבוה יותר, שזה בדרך כלל הפוך אבל פה זה ככה.

כאשר אנחנו משתמשים בזה, בעצם יש לנו הרבה מאוד process-ים עם אותו priority ואנחנו רוצים שהם ירוצו ביחד.

Scheduler Round-Robin זה בעצם ה-Round-Robin processes, שהם בעצם נקבעים עם time slice קבוע. כל X זמן, כל tick, אנחנו עושים schedule ב-Round-Robin fashion, בעיגול.

אנחנו עדיין רצים בצורה שהיא preemptible, כאשר higher priority real-time process יכול לקחת לנו את ה-CPU.

## Deadline Scheduler (SCHED_DEADLINE)

Scheduler נוסף, שזה scheduler ממש נחמד, Deadline Scheduler. בעצם זה scheduler אני חושב שהוא האחרון שהתווסף אל ה-kernel. וה-scheduler הזה עושה לנו scheduling ל-process עם explicit time requirements.

הוא משתמש באלגוריתם שנקרא EDF, Earliest Deadline First, זאת אומרת שכל פעם הוא משרשר את ה-deadline-ים של ה-process-ים שלו לתוך ה-scheduler, וכל פעם שצריך אז הוא מעיר בזמן המדויק את כל אחד מה-process-ים.

ב-process parameters יש לו כמה פרמטרים ספציפיים: יש לו runtime, כמה CPU time הוא יצטרך כל זמן מסוים; מה ה-deadline, זאת אומרת מתי צריכה להסתיים הריצה של ה-scheduler הזה; ו-period, שזה הזמן החוזר של ה-task הזה.

לדוגמה, כל 3 שניות אני רוצה לרוץ עד 5 שניות, ואני אצטרך 2 שניות לרוץ. אז זה בעצם הקביעה. הקביעה עצמה נעשית באמצעות system call של ה-SCHED_DEADLINE עצמו.

ההבטחות זה בעצם שה-task-ים עם ה-deadline הקרוב ביותר, הוא יקבל את ה-CPU ראשון. ויש לו גם admission control, שזה מבטיח שהמערכת יכולה לעמוד ב-requirements של ה-task. זאת אומרת שהזמן שהוא runtime לחלק ל-period חייב להיות קטן מ-1, אוקיי?

דבר נוסף שיש לו בעצם זה bandwidth isolation, שככה כמה process-ים הם לא יכולים לקחת את ה-bandwidth אחד של השני, אז יש איזה שהיא חלוקה של bandwidth בין ה-process-ים עצמם, אם הגענו לאותו deadline ביחד.

## Idle Scheduler

יש לנו את ה-scheduler שהוא עם ה-priority הנמוך ביותר, שזה ה-Idle Scheduler, שזה בעצם special-purpose scheduler אשר אנחנו נותנים לו את ה-execution כאשר שום process לא רוצה את ה-CPU.

זאת אומרת, כולם היו מאוד נחמדים ונהיו נדבנים גדולים והם רוצים לשחרר את ה-CPU, ואין לנו שום task, אז אין דבר כזה שאין task, אז בעצם אנחנו לוקחים את ה-idle task.

וב-idle task, בשביל זה יש לנו idle task אחד. כמו במערכות קטנות, יש לנו הרבה idle task-ים, אז בשביל זה אנחנו צריכים Idle Scheduler. והרבה פעולות של system maintenance נעשות על ידי ה-Idle Scheduler.

אז שוב, priority נמוך ביותר, המערכת לא צריכה יותר לעשות שום execution, הכל ב-sleep. אנחנו משתמשים ב-Idle Scheduler, ו-Idle Scheduler הוא בשביל משימות של maintenance. לדוגמה, לעשות flushing לזיכרון, לעשות בדיקה של vulnerabilities, או whatever, כאילו יש כל מיני דברים שה-CPU עושה, שהמערכת הפעלה עושה. אז אמרנו, אחד מהם זה flushing לבאפרים, ועוד כל מיני בדיקות, keep alive-ים למיניהם אם אנחנו צריכים, בעצם לשלוח keep alive בכל מיני חלקים, ועוד כל מיני.

אז מה הסיבה? אנחנו בעצם רצים את זה כאשר אין שום task שהוא רוצה לרוץ עם priority יותר גבוה מאיתנו. אנחנו בעצם עושים CPU power management כאשר אנחנו ב-idle periods. זאת אומרת שאם אנחנו מריצים איזה שהיא חומרה, אנחנו גם יכולים לתת ל-CPU לישון. זאת אומרת, לישון עד ל-interrupt הבא, אוקיי? זה נותן לנו איזה שהוא פילטר כזה שה-CPU יהיה מוכן כאשר הוא צריך לרוץ.

וה-implementation, אז כל CPU יש לו את ה-idle task משלו. הארכיטקטורה בעצם קובעת מה הם ה-idle functions שאנחנו משתמשים בהם. אנחנו יכולים להשתמש ב-CPU halt כדי לשמור power אם יש לנו את התמיכה הזאת בארכיטקטורה עצמה, ואנחנו יכולים גם ליצור idle task מתוך ה-kernel, ה-idle task של ה-kernel, ליצור sleep state שהוא בעצם יהיה עם duration שבו אנחנו נוכל לצרוך כמה שפחות אנרגיה מה-CPU.

איזה power management integration אפשר להשתמש? אז יש לנו CPU frequency scaling, CPU idle power states שזה בעצם C-states שזה בתוך ה-power management, ויש לנו idle time predictors עם power saving שזה בעצם כמו ה-branch predictor שיש לנו ב-CPU, אז זה רק שהוא בעצם מנחש עוד כמה זמן ה-CPU יצטרך לחזור לעבוד.

## SCHED_BATCH

Scheduler שהוא פחות שימושי, רק לפעולות ממש ספציפיות, אז קצת אמרנו עליו, אז יש לנו את SCHED_BATCH. הוא בעצם אמור לעשות הרבה מאוד עיבוד והוא מיועד ל-throughput-oriented workload.

הוא עובד כמו SCHED_NORMAL, אבל ה-CPU utilization שלו הוא הרבה יותר גבוה, וזה גם ה-emphasis שלו, זה הדגש שלו.

אנחנו פחות אכפת לנו מ-latency, אנחנו כמעט ולא עושים preemption, ואנחנו רק מנסים לדחוף כמה שיותר throughput, אוקיי?

דוגמאות לכאלה: אז בעצם יש לנו rendering, compilation ו-scientific computing שאנחנו עושים.

אז ה-task scheduler class הזה, אנחנו פחות יכולים ניגש אליו, הוא גם פחות אינטראקטיבי, וכל מה שהוא עושה זה עכשיו הוא עושה את המשימה שלו ומשחרר אותו.

## Priority Management

Priority Management. איזה priority management יש לנו? יש לנו כמה סוגים של priority, ראינו אותם בכל מיני task-ים, בעיקר ב-task-ים של real-time ו-CFS.

אז יש לנו static priority, מ-0 עד 99 ל-real-time processes. אנחנו קובעים אותם על ידי system call וזה לא משתנה על ידי ה-scheduler.

בנוסף, יש לנו dynamic priorities. בעצם ערכים מ-100 עד 139, שזה ממופה ל-20- עד 19+ של ערכי nice. והפרוססור עצמו הוא זה שבעצם משפיע על מה שיקבע. ה-scheduler יכול...

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

Nice values - ככל שה-Nice value יותר גבוה, ה-priority שלו הוא הכי נמוך. 19 זה הכי נמוך ו-20- זה הכי גבוה. ה-default הוא 0 (או 20 כפי שראינו ב-scheduler של ה-CFS), וזה ה-range שלו, מ-20- עד 19. רק root privileges יכולים לקבוע negative Nice values.

## Building and Configuring the Kernel

איך אנחנו בונים ומקנפגים את ה-kernel? עשינו את זה ככה בחטף, בואו נדבר קצת על מה שעשינו בשיעור הקודם.

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

יש לנו את `make menuconfig`, שזה בעצם איזשהו terminal-based menu configuration. נקרא לו בצורה הזאת: `make menuconfig`.

אפשר לעבוד בנוסף עם `defconfig`. `defconfig` זו ה-revision שלנו, שמשתמשת ב-default configuration לארכיטקטורה ספציפית ולהגדרות ספציפיות.

`make defconfig` ו-`make menuconfig` אלה בעצם שתי פלטפורמות או שני סוגים של שיטות לעבוד עם קונפיגורציות.

יש לנו גם `make xconfig`, שזה Qt-based graphical configuration, אם אנחנו רוצים לקנפג בצורה גרפית כזאת. ויש לנו `make gconfig`, שזו עוד צורה גרפית לקונפיגורציה.

ובסוף יש לנו `make alldefconfig`, שהוא נותן את כל האופציות וקובע את ה-default values.

## .config לעומת defconfig

מה ההבדל בין `.config` ל-`defconfig`? בואו שנייה נפתח את ה-Linux. ראינו שיש לנו פה את הקובץ שנקרא `.config`, ומתוך `arch/x86/configs` יש לנו את ה-`defconfig` שבו השתמשנו. שימו לב שזה נראה אותו דבר, אך לא בדיוק.

`.config` הוא קובץ שמג'ונרט על ידי הקונפיגורציה של המערכת שעשינו. הוא בעצם מכיל את כל ההחלטות שלנו וכמה אלפי אופציות, כולל platform-specific configuration.

`defconfig` הוא בעצם קובץ שהוא שמירה של קונפיגורציות ספציפיות לארכיטקטורה. אנחנו נמצא אותו ב-`arch/<arch>/configs`. עבור x86_64, יש לנו פה ל-ARM, ל-RISC-V, ל-PowerPC וכולי.

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

## שמירת קונפיגורציה עם savedefconfig

איך שומרים קונפיגורציות? קונפיגורציה היא סט של הגדרות. `make savedefconfig` יוצר לנו minimal configuration file שאנחנו יכולים להשתמש בו בלי default options.

הצורה לשמור היא `make savedefconfig`, ואחר כך אנחנו מעתיקים אותו מה-build directory לאיפה שנרצה ואפשר לשמור אותו שם.

מתי אנחנו משתמשים בו? כאשר אנחנו יוצרים קונפיגורציה חדשה, כאשר אנחנו רוצים לשתף קונפיגורציות בין חברים, וכאשר רוצים version control במהלך הפיתוח.

## תהליך ה-Build

מה ה-build process?

יש לנו את `make`, שהוא יוצר לנו את ה-kernel image. הוא יוצר לנו את ה-`vmlinux` (uncompressed), וגם לפי הארכיטקטורה את ה-compressed images. עבור x86 הוא יוצר לנו `bzImage`, עבור ARM הוא יוצר לנו `zImage` או `uImage` (כאשר `uImage` הוא uncompressed image).

אחר כך אנחנו בונים את כל ה-kernel modules, שזאת הבנייה של ה-kernel objects (`.ko`) שאנחנו משתמשים בהם.

`make clean` מסיר לנו את כל ה-generated files.

`make mrproper` עושה לנו complete clean, הוא גם מוחק לנו את ה-backup files וכל מה שנמצא במערכת.

`make distclean` מוחק את כל מה שיש, גם patch files וגם backup files, ומחזיר אותנו לתצורה שעשינו ב-clone או ב-checkout.

## בניית רכיבים ספציפיים ב-Kernel

יש לנו את האפשרות לבנות חלקים ספציפיים בתוך ה-kernel. כולנו יודעים שבנייה של kernel זאת פעולה ממושכת, אז אם אנחנו רוצים לבנות חלקים ספציפיים אנחנו יכולים לעשות את זה.

אם אנחנו רוצים לבנות למשל רק את ה-scheduler, נריץ `make kernel/sched/`.

אם אנחנו רוצים לבנות קובץ ספציפי, אנחנו יכולים ממש לבחור את האובייקט הספציפי שאנחנו רוצים לבנות: `make kernel/fork.o` יבנה לנו את `fork.o` מההתחלה.

ובנייה של מודול ספציפי נעשית באותו דבר עם ה-driver וה-path אל ה-driver עצמו, למשל ה-e1000 של Intel.

## targets מועילים ב-Make לפיתוח

כלים טובים כאשר אנחנו רוצים לפתח על ה-kernel:

- `make help` עוזר לנו לקבל את המידע והדברים שאנחנו צריכים.

- `make tags` מייצר לנו תגיות יותר נוחות כדי שיהיה נוח לעבוד עם code navigation ולעבור בין קוד בצורה יותר נוחה.

- `make kernelrelease` כאשר אנחנו רוצים לקבל את ה-release version של ה-kernel.

- `make kernelversion` כאשר אנחנו רוצים לשמור את הגרסה של ה-kernel.

- `make scripts` עבור סקריפטים שאנחנו רוצים להשתמש בהם.

- `make headers_install` קובע לנו לאן להתקין את כל ה-headers של ה-kernel עצמו.

- `make -j$(nproc) sub-dir=kernel/sched/` בונה רק את ה-scheduler בשימוש בכל ליבות ה-CPU.

## מעבדה 1: ניהול תהליכים ורכיבי Kernel

בואו נדבר עכשיו על המעבדה שהכנתי.

המעבדה עוסקת בחקירה של ה-process management ושל ה-kernel subsystems.

המטרה שלנו היא להבין את ה-process management subsystem, לחקור את המבנה של `task_struct` והיחסים, לראות איך נוצרים processes וה-termination שלהם, לחקור את ה-scheduling behavior ולראות אינטראקציה בין ה-kernel לבין ה-`/proc` file system.

רכיבי המעבדה:

נבנה את ה-kernel עם debug options, ניצור כמה kernel modules שיעזרו לנו עם הגישה ל-task information, נחקור את ה-process hierarchy וה-relationships, ונעשה חקירה של כמה scheduling policies.

בתוך תיקיית המעבדה יש לנו מסמכים: מסמך `instructions` שמסביר ספציפית איך מוסיפים את המעבדות ל-initramfs שאנחנו משתמשים בו, ומדריך להרצת QEMU. במסמך ה-`README` של המעבדה מפורטות המשימות והתרגילים שעליכם לבצע.

## סקירת המעבדה והגדרת סביבת העבודה

מה אנחנו הולכים לעשות בתוך המעבדה? לחקור את ה-Linux process management subsystem, בעצם לדבר על ה-task_struct context ואימפלמנטציה של modules ש... אתם כרגע עדיין לא יודעים איך ליצור modules, אבל כשנגיע לזה, נסביר קצת מה קורה כאן. ובעצם אנחנו נחקור כמה test policies שונים.

יש לנו כמה קבצים בתוך המעבדה. יש לנו את ה-process_info.c, שזה kernel module שיציג לנו את כל ה-running processes. task_monitor, שזה עוד kernel module שהוא יעשה monitor ספציפי ל-process, בעצם ל-details של ה-process. עוד user space program שקובעת את ה-scheduler, ככה שאם אתם מרגישים שאין לכם את המידע או את הידע שצריך כדי להבין איך אנחנו קובעים את ה-process scheduler, אז אתם יכולים לקחת את זה מתוך sched_test ו-Makefile שמבונה את הכול.

ה-kernel modules הם בנויים מול 5.15. אם בחרתם לעבוד עם גרסה אחרת, אתם צריכים לעשות את ההתאמה בעצמכם.

## בניית רכיבי המעבדה

איך אנחנו בעצם בונים את המעבדה הזאת? אני אראה לכם, אני כבר אפתח רק את ה-VS Code עצמו.

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

הפעם נקבע את ה-KERNEL_SRC בשביל ה-Makefile. אז export KERNEL_SRC, המיקום עצמו. שימו לב שאם אנחנו לא קובעים את זה, אז יש הגדרה ספציפית כבר בתוך ה-Makefile. אם עשיתם clone ל-kernel ל-repo שלנו למקום אחר, לא לתוך course/kernel_programming/linux, אז תשנו את זה.

נעשה export כזה. אחר כך אנחנו נעשה unset ל-CROSS_COMPILE, כי אנחנו לא רוצים להשתמש בפרמטר הזה, אתם לא רוצים לרשת את הפרמטר הזה. unset CROSS_COMPILE. ובסוף make clean ו-make כדי שבעצם נבנה את הכול ביחד.

ואז אנחנו נראה שנבנו לנו הרבה דברים. מה יש לנו בנוי פה? יש לנו את ה-process_info kernel object, יש לנו את ה-process_info.o שממנו הפכנו את זה ל-kernel object, task_monitor kernel object, ו-sched_test.

ב-Makefile תעשו uncomment ל-PROGRAMS ול-sched_test.

## הרצת המעבדה ב-QEMU

אחרי שבנינו את ה-kernel modules ואת ה-sched_test, אנחנו בעצם נרצה ליצור תיקייה כדי ליצור את ה-initramfs ספציפי למעבדה שלנו. אז mkdir /tmp/initramfs_new/lab1. תבחרו שם, ככה שיראו שזה יותר נוח.

אנחנו מעתיקים את ה-kernel modules ואת ה-sched_test לתוך ה-initramfs החדש שאנחנו הולכים לעשות, lab1. את זה אנחנו גם כן מכניסים לבפנים.

קופצים לתוך זה עצמו, עושים export ל-base line שלנו, שזה ה-base line שלי (שוב, אם אצלכם זה שונה, תעשו שונה), ואנחנו עושים extract ל-initramfs שלנו.

אנחנו צריכים להוסיף dynamic linker כדי שנוכל להריץ תוכניות C על ה-QEMU שלנו, כי אין שמה linker. אז פשוט נעתיק את ה-dynamic linker מה-lib64 שלנו במערכת. ואותו דבר גם עם ה-glibc עצמו, שזה ה-glibc שאנחנו משתמשים בו.

- תלמיד: "אור, יש לי כמה שאלות לפני שאתה ממשיך, על כל התהליך הזה. קודם כול, מה זה kernel object?"

- מרצה: "Kernel object זה בעצם ה-loadable object שה-kernel יכול לטעון ולהשתמש בהם כ-resources, כעוד פונקציונליות למערכת. אז אמרנו שה-kernel עצמו הוא מונוליתי, והוא יכול לטעון אליו כל מיני מודולים. יש לנו שיעור ספציפי על זה, אבל יש לנו שני סוגים של מודולים: יש לנו כאלה שעולים עם ה-kernel, סוגים של אובייקטים שעולים עם ה-kernel, וכאלה שאנחנו יכולים לטעון אותם אל ה-kernel כשאנחנו רוצים להשתמש בפונקציונליות שלהם.

אני יודע שזה עוד לא ברור, אבל אנחנו צריכים את הפונקציונליות שלהם מתוך ה-kernel. נכסה את הקוד ונעשה את זה גם לפני שנריץ. אבל לפני זה, רק נסגור את הפינה עם ה-QEMU, נראה שעובד לכולם ושזה עובד, ואז אני אחזור לאובייקטים עצמם ואסביר מה אנחנו מריצים."

- תלמיד: "אוקיי, אז אני אשאל אותך אחרי זה."

- מרצה: "בסדר."

שוב יוצרים את ה-initramfs, cpio.gz, שאנחנו נרצה להשתמש בו כדי להעלות את ה-QEMU. ומפה זה פשוט הפקודה של ה-QEMU עצמו.

## ניתוח קוד ה-Kernel Modules

אמרנו שיש לנו שלושה kernel modules, אלה שלושה loadable modules שאנחנו יכולים לטעון לתוך ה-kernel. יש לנו את process_info.

לכל kernel module יש לו entry point ו-exit point. אנחנו קובעים אותם באמצעות המאקרואים האלה. זאת אומרת שכאשר שטוענים אותם לתוך ה-kernel, אז בטעינה אנחנו נריץ את הדבר הזה, ובהוצאה אנחנו נריץ את הדבר הזה.

מה בעצם עושה לנו process_info, ה-kernel עצמו? ברגע שאנחנו טוענים את ה-process עצמו לתוך ה-kernel, הוא יוצר לנו process חדש, שזה ה-proc_fops.

ה-proc_fops בעצם יש לו ארבעה operations שהוא עושה: open, read, llseek ו-release. ה-proc_open בעצם עושה לנו single_open, שהוא עושה בעצם אלוקציה של kernel account והוא נותן לנו את הגישה ל-file descriptor עצמו. יש לנו את ה-read, את ה-llseek, ואת ה-single_release. אלה בעצם ה-interface שאנחנו עובדים איתו.

מה אנחנו בעצם עושים כאן? אנחנו יוצרים איזשהו process שהוא משתמש ב-API הזה. יש לו open, read, llseek ו-release. ה-process עצמו, מה שהוא עושה, הוא לוקח לכל process את הפרמטרים האלה. בעצם אנחנו עושים process_print_info_to_dmesg. dmesg זה ה-logger שאליו מודפסות כל ההודעות של ה-kernel. אנחנו משתמשים ב-pr_info כדי להדפיס info, אבל אנחנו בעצם עושים הדפסה של כל אחד.

לכל process, ב-for_each_process, אנחנו לוקחים את כל ה-processes שיש לנו ולמה אנחנו מדפיסים את ה-task PID, את ה-parent PID, את ה-state name, ואת ה-policy. ככה אנחנו נחקור את מה שאנחנו עושים על ה-kernel.

יצרנו את האובייקט הזה של ה-proc_ops, ואז אנחנו בעצם מריצים את כל המידע על כל ה-processes. ב-get_process_state_name אנחנו מדפיסים את ה-state (אם הוא RUNNING נדפיס RUNNING), וב-get_policy_name זה ה-scheduler policy הנוכחי שבו אנחנו משתמשים.

זה בעצם ה-kernel module. הפונקציונליות שהוא נותן לנו זה את האפשרות להדפיס את כל המידע על כל ה-processes שלנו במערכת מתוך ה-kernel. הוא גם מייצר לנו proc...

`/proc/process_info` לקבל את כל המידע הזה גם כן. אז בואו פשוט נעשה את זה. אז איך אנחנו טוענים קובצי ko, kernel object, לתוך ה-kernel? אנחנו עושים את זה עם insmod. יש לנו insmod של הקובץ עצמו. אז איפה נמצא הקובץ? בתוך lab1.

יש לנו פה את כל מה שהעתקנו, בעצם יש לנו את שני ה-kernel objects. יש לנו את ה-sched_test, ויש לנו עוד איזשהו wrapper של ה-sched_test שא אנחנו לא חייבים אותו. השתמשתי בו רק כדי לבדוק איזשהו משהו.

אנחנו נרצה לטעון את ה-process_info. וברגע שטנענו אותו, אתם רואים שהוא הדפיס לנו את כל המידע על כל ה-processes. אז יש לנו את ה-process הראשון. ה-process הראשון זה ה-kernel init. רואים שהוא SCHED_NORMAL, יש לנו אחד שהוא מסוג FIFO ועוד כל מיני כאלה שאנחנו לא יודעים מה הם עושים.

אז חלק מהמעבדה זה להריץ processes באמצעות ה-sched_test, להריץ processes מסוג ספציפי. אז sched_test עם SCHED_OTHER בעצם יריץ לנו את ה-process הזה. אם אנחנו לא יודעים איך להריץ, אז יש לנו גם את המידע עצמו.

אז מה ה-usage? sched_test, השם של ה-scheduler, וה-priority שאנחנו רוצים לתת לו. שימו לב שה-priority צריך לעמוד ב-priority range של כל אחד מהם.

סבבה? אז זה לכולם. ואחד ספציפי, אם אנחנו רוצים לקחת את ה-task monitor הספציפי, אז שוב insmod ל-task_monitor.ko עם PID ספציפי. אז זה אנחנו נעשה ל-init thread של ה-kernel, שזה PID 1.

ונקבל את כל המידע: מה ה-state שלו, process group, מה ה-session ID, אם יש לו parent, מה ה-child process שלו. שימו לב לגבי PID 1: כל child process שמת לו ההורה, אז הוא אוטומטית עושה reparenting ל-PID 1, זה תכונה שאתם מכירים. מה ה-scheduling policy, מה ה-priority, ה-nice value שהוא משתמש, וה-memory information שלו.

זה ה-kernel object השני שעושה לנו task monitor ל-task ספציפי.

אז מה שאתם צריכים לעשות זה ליצור processes עם sched_test ולעשות להם monitoring באחת משתי הצורות. שימו לב שגם בתוך `/proc/process_info` יש את ההדפסה של כל ה-tasks עצמם.

אז זו המעבדה. רשום לכם פה את כל ההוראות. אתם צריכים לבדוק את כל ה-states של כל ה-processes, להשתמש ב-task_monitor, לעשות process hierarchy, להסתכל על PID 1, וכל זה.

זה גם ה-assignments. אם יש לכם שאלות, זה הזמן.

## שאלות ותשובות

- אם תוכל לשים את ה-git בקבוצה?

- זה בקבוצה... אה, פה או ב...

- ב-WhatsApp, אני לא יודע איפה עוד יש.

- זה ב-WhatsApp.

- סבבה.

- שאלה. הסיומת ko, kernel object, זה כביכול כמו shared object, פשוט רק של ה-kernel?

- בדיוק.

- הבנתי, אוקיי.

- יש לנו שיעור שלם על זה, ואנחנו גם ניצור אחד עם כל מיני מתודות של synchronization. זה בהמשך. אנחנו עושים את זה בעוד שני שיעורים.

- הבנתי.

- עוד שאלות בקשר למעבדה. בעצם יצרת איזשהם קבצים, נגיד ה-sched_test וה-process_info. השתמשת ב-API של Linux בשביל ליצור את כל ה-process ואז לקחת מידע על ה-processes האלה?

- אני בעצם יוצר process עם priority, וקובע את ה-scheduler policy.

- כן, אבל זה API-ים של Linux, כן? זה משהו שהוא system calls.

- system calls, בעזרת system calls. `sched_getscheduler` מחזיר לי, `sched_setscheduler` אומר שאני נותן לו את ה-policy עצמו לפי ה-policy ID, שזה נמצא לנו בתוך user include linux, מגיע ממהערכת ההפעלה, זה לא מתוך ה-kernel.

- מערכת ההפעלה, זה לא ה-kernel, אני מבין.

- מגיע מהמערכת ההפעלה עצמה של המחשב שאתה עובד עליו, שאתה מקמפל עליו. אם אתה רוצה לראות, יש בתוך ה-header עצמו. בגדול עושים `sched.h`. זה לא `linux/sched.h`.

- הבנתי. טוב, אני כבר אעשה reverse engineering על הקובץ שלך להבין מה קורה שם.

- כן, אתה יכול גם לשאול.

- בגדול אני שואל: מה שיצרת קבצים, אחד שאתה יוצר task, משתמש ב-API של מערכת ההפעלה בשביל ליצור process עם priority או כל דבר אחר שם, ויצרת עוד קובץ שנקרא task_monitor או process_info, שאנחנו בעצם צריכים...

- `process_info` ו-`task_monitor`, אלה שניים. זה פשוט מראה לנו את המידע על כולם, וזה מראה לנו על task ספציפי לפי ה-PID שלו.

- ובאותו רעיון, יצרת קובץ C, השתמשת ב-API-ים של Linux. שוב, גם את ה-Makefile הוספת שם בשביל ליצור את כל הדברים האלה.

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

- כן כן, בפרטים הקטנים צריך להבין. מלמעלה גם ננסה להבין.

- שוב, אתם לא צריכים עדיין להבין איך ליצור kernel modules, כי עוד לא נגענו בזה. בהמשך כשניצור, אתם כבר תבינו. סבבה, יש פה assignment של יצירה, לא חייב. מי שרוצה, בכיף.

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

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

- תודה. אז נפגש בעוד שבוע.

- תודה רבה, ביי.