6.1 הקדמה לקרנל הרצאה
הקדמה¶
בפרקים הקודמים למדנו איך לינוקס נראה מצד היוזר מוד - syscall-ים, תהליכים, fork, סיגנלים, mmap, תהליכונים, ועוד.
עכשיו הגיע הזמן לצלול פנימה, אל הצד השני של המשוואה - הקרנל עצמו.
בפרק הזה נלמד מה זה קרנל, איך קוד המקור של הקרנל של לינוקס מאורגן, איך הקרנל עולה כשאנחנו מדליקים את המחשב, ואיך אפשר להרחיב את הקרנל בזמן ריצה עם מודולים.
מה זה קרנל - kernel?¶
הקרנל הוא התוכנה המרכזית שרצה על המחשב. היא רצה ב-Ring 0 (קרנל מוד), ויש לה גישה מלאה לחומרה, לזכרון, ולכל משאב במערכת.
כזכור מפרק 2, ב-Protected Mode הגדרנו הפרדה בין קוד קרנלי (Ring 0) לקוד יוזר מודי (Ring 3). הקרנל הוא בדיוק הקוד שרץ ב-Ring 0.
התפקידים המרכזיים של הקרנל:
- ניהול חומרה - מדבר עם הדיסקים, כרטיסי הרשת, המסך, המקלדת, ושאר ההתקנים
- ניהול זכרון - מנהל את הpaging (כזכור מפרק 2.5), מקצה זכרון פיזי ווירטואלי לתהליכים
- ניהול תהליכים - יוצר תהליכים, מבצע context switch ביניהם (כזכור מפרק 2.4), ומחליט מי רץ
- ממשק למשתמש - חושף את כל היכולות האלה ליוזר מוד דרך syscall-ים (כזכור מפרק 5.1)
אפשר להגיד שהקרנל הוא "התוכנה שמנהלת את כל שאר התוכנות". כל תוכנה יוזר מודית שרצה על המחשב - תלויה בקרנל כדי לקבל זכרון, לפתוח קבצים, לתקשר ברשת, ולעשות כל פעולה שדורשת גישה לחומרה.
קרנל מונוליטי מול מיקרוקרנל - monolithic vs microkernel¶
יש שתי גישות עיקריות לבניית קרנל:
קרנל מונוליטי - monolithic kernel¶
בגישה הזו, כל הקוד של הקרנל - דרייברים, מערכת קבצים, ניהול זכרון, רשת - הכל רץ באותו מרחב כתובות, באותו Ring 0. זה מה שלינוקס עושה.
היתרון: ביצועים גבוהים. כשהקרנל צריך לקרוא לדרייבר או למערכת הקבצים, זו פשוט קריאת פונקציה רגילה - אין צורך במעבר הרשאות או context switch.
החיסרון: באג בדרייבר אחד יכול להפיל את כל המערכת. אם דרייבר של כרטיס רשת כותב לכתובת לא חוקית, כל הקרנל קורס.
מיקרוקרנל - microkernel¶
בגישה הזו, רק הליבה הקטנה ביותר רצה ב-Ring 0 (context switch, IPC בסיסי, ניהול הרשאות). כל השאר - דרייברים, מערכת קבצים, רשת - רץ ביוזר מוד כתהליכים נפרדים.
דוגמאות: Minix, QNX, GNU Hurd.
היתרון: אם דרייבר קורס, אפשר פשוט להפעיל אותו מחדש בלי שכל המערכת נופלת.
החיסרון: כל תקשורת בין הרכיבים דורשת מעבר הרשאות (מיוזר מוד לקרנל ובחזרה), מה שמאט משמעותית.
לינוקס - מונוליטי אבל מודולרי¶
לינוקס הוא קרנל מונוליטי, אבל הוא מודולרי - ניתן לטעון ולהסיר קוד מהקרנל בזמן ריצה באמצעות מודולים של קרנל (kernel modules). ניתן לחשוב על זה כעל DLL-ים של הקרנל. למשל, דרייבר של כרטיס רשת חדש יכול להיטען בלי לאתחל מחדש את המחשב. נדבר על זה בהמשך ההרצאה.
עץ קוד המקור של לינוקס - source tree¶
הקרנל של לינוקס הוא פרויקט קוד פתוח ענק - מיליוני שורות קוד. אבל הוא מאורגן בצורה מאוד ברורה. הנה התיקיות המרכזיות בעץ קוד המקור:
| תיקיה | מה יש בה |
|---|---|
arch/ |
קוד ספציפי לארכיטקטורה - x86, ARM, RISC-V וכו'. כאן נמצא הקוד של כניסת ה-syscall, ה-boot, ניהול הpaging הספציפי לx86, וכו' |
kernel/ |
הליבה של הקרנל - הscheduler, סיגנלים, fork, ניהול תהליכים |
mm/ |
ניהול זכרון - page allocator, slab allocator, mmap, page fault handler |
fs/ |
מערכות קבצים - ext4, proc, tmpfs, ומעל הכל שכבת ה-VFS (ממשק אחיד לכל מערכות הקבצים) |
drivers/ |
דרייברים - החלק הכי גדול בקרנל! דרייברים לכל סוגי החומרה |
net/ |
מחסנית הרשת - TCP/IP, UDP, sockets, ניתוב |
include/ |
קבצי header - הגדרות של מבני נתונים, מאקרואים, ממשקים |
init/ |
קוד אתחול - כאן נמצאת הפונקציה start_kernel() שממנה הכל מתחיל |
כשנלמד בפרקים הבאים על syscall-ים מבפנים או על הscheduler, נראה קוד אמיתי מתוך התיקיות האלה.
תהליך האתחול - boot process¶
בואו נבין מה קורה מרגע שמדליקים את המחשב ועד שהקרנל רץ. זה חיבור ישיר למה שלמדנו בפרק 2 על Protected Mode ו-Paging.
שלב 1: הBIOS או UEFI¶
כשהמחשב נדלק, המעבד מתחיל לבצע קוד מכתובת קבועה ב-ROM. הקוד הזה הוא ה-BIOS (או UEFI במחשבים מודרניים). הוא מבצע בדיקות חומרה בסיסיות (POST), מזהה דיסקים, ומחפש bootloader.
שלב 2: הbootloader (בדרך כלל GRUB)¶
ה-BIOS טוען את ה-bootloader מהדיסק. בלינוקס זה בדרך כלל GRUB. ה-bootloader מציג תפריט (אם יש כמה מערכות הפעלה), ואז טוען את image דחוס של הקרנל לזכרון.
שלב 3: הקרנל עולה¶
הקרנל מפרק את עצמו מה-image הדחוס, עובר ל-Protected Mode (אם עדיין ב-Real Mode), מפעיל את ה-Paging, ומתחיל לרוץ מהפונקציה start_kernel() שנמצאת בקובץ init/main.c.
הפונקציה start_kernel() היא הפונקציה המרכזית שמאתחלת את כל תתי המערכות של הקרנל:
// init/main.c (מפושט)
asmlinkage void __init start_kernel(void)
{
// אתחול הלוגים של הקרנל
setup_log_buf();
// אתחול ה-scheduler
sched_init();
// אתחול ניהול הזכרון
mm_init();
// אתחול ה-VFS (מערכת קבצים וירטואלית)
vfs_caches_init();
// אתחול הפסיקות
init_IRQ();
// אתחול הטיימר
time_init();
// ... עוד המון אתחולים ...
// בסוף - מפעיל את תהליך ה-init
rest_init(); // יוצר את תהליך PID 1
}
שלב 4: תהליך הinit (תהליך 1 - PID 1)¶
אחרי שהקרנל סיים לאתחל את כל תתי המערכות, הוא מעלה (mount) את מערכת הקבצים הראשית (root filesystem), ואז מריץ את התוכנית /sbin/init (או systemd בהפצות מודרניות) כתהליך יוזר מודי ראשון - עם PID 1.
מכאן ואילך, הכל רץ כמו שלמדנו בפרק 5 - תהליכים שיוצרים תהליכים אחרים עם fork ו-execve, שירותים שעולים, ובסוף אנחנו מקבלים terminal שאנחנו יכולים לעבוד בו.
זוכרים שדיברנו על עץ התהליכים בפרק 5.2? תהליך ה-init הוא השורש של העץ הזה.
מרחב קרנל ומרחב יוזר - kernel space vs user space¶
בפרק 2.5 למדנו על Paging - כל תהליך רואה מרחב כתובות וירטואלי משלו. אבל מה בדיוק נמצא במרחב הזה?
בארכיטקטורת 32 ביט¶
במערכת 32 ביט יש 4GB של מרחב כתובות וירטואלי. לינוקס מחלק אותו כך:
- הגיגה העליונה (0xC0000000 עד 0xFFFFFFFF) - שייכת לקרנל
- 3 הגיגות התחתונות (0x00000000 עד 0xBFFFFFFF) - שייכות ליוזר מוד
בארכיטקטורת 64 ביט¶
ב-64 ביט המרחב הוא הרבה יותר גדול, והחלוקה שונה - החצי העליון של המרחב הוא של הקרנל, והחצי התחתון הוא של היוזר מוד. בפועל רק חלק קטן מהמרחב מנוצל.
הנקודה החשובה¶
הדפים של הקרנל ממופים בטבלאות הpaging של כל תהליך. כלומר, לא משנה איזה תהליך רץ, הכתובות העליונות תמיד מצביעות לאותו קוד ונתונים של הקרנל. ההבדל הוא שהדפים האלה מסומנים ברמת הרשאות Ring 0 - אז קוד יוזר מודי לא יכול לגשת אליהם. אם תהליך יוזר מודי מנסה לקרוא מכתובת בקרנל, המעבד יייצר page fault ומערכת ההפעלה תהרוג את התהליך עם SIGSEGV.
זו הסיבה שכשמתבצע syscall ועוברים מ-Ring 3 ל-Ring 0, אין צורך להחליף את טבלאות הpaging (את ה-CR3). הקוד של הקרנל כבר ממופה - המעבד פשוט מקבל הרשאה לגשת אליו כי עכשיו אנחנו ב-Ring 0.
מודולים של קרנל - kernel modules¶
אמרנו שלינוקס הוא קרנל מונוליטי אבל מודולרי. מה זה אומר בפועל?
מודול של קרנל (kernel module) הוא קטע קוד שאפשר לטעון לתוך הקרנל בזמן ריצה, בלי לאתחל מחדש את המחשב ובלי לקמפל את כל הקרנל מחדש. ברגע שמודול נטען, הוא רץ ב-Ring 0 כחלק מהקרנל.
דוגמאות לשימושים:
- דרייברים לחומרה (כרטיס רשת, כרטיס מסך, USB)
- מערכות קבצים (NTFS, exFAT)
- פרוטוקולי רשת
- מודולי אבטחה
פקודות לניהול מודולים¶
lsmod- מציגה את כל המודולים הטעונים כרגעinsmod module.ko- טוענת מודול לקרנלrmmod module- מסירה מודול מהקרנלmodprobe module- טוענת מודול כולל כל התלויות שלו (יותר חכם מ-insmod)modinfo module- מציגה מידע על מודול
כתיבת מודול בסיסי - "שלום עולם"¶
בואו נכתוב את המודול הכי פשוט שיש - מודול שמדפיס הודעה כשהוא נטען ועוד הודעה כשהוא מוסר:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
static int __init hello_init(void)
{
printk(KERN_INFO "Hello from kernel module!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye from kernel module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
בואו נפרק את הקוד:
- #include <linux/init.h> - מכיל את המאקרואים __init ו-__exit
- #include <linux/module.h> - מכיל את המאקרואים module_init ו-module_exit
- #include <linux/kernel.h> - מכיל את printk ואת מאקרואי רמות הלוג כמו KERN_INFO
- MODULE_LICENSE("GPL") - מגדיר את הרישיון של המודול. הקרנל דורש את זה, ומודולים ללא רישיון GPL מקבלים פחות גישה לAPI פנימי
- __init - סימון שהפונקציה רלוונטית רק בזמן טעינה, ואפשר לשחרר את הזכרון שלה אחרי
- __exit - סימון שהפונקציה רלוונטית רק בזמן הסרה
- printk - הגרסה הקרנלית של printf. לא ניתן להשתמש ב-printf רגיל בקרנל! כי printf הוא פונקציה של libc, ו-libc היא ספרייה יוזר מודית. הפונקציה printk כותבת ללוג הקרנלי שאפשר לקרוא עם dmesg
- module_init(hello_init) - מגדיר איזו פונקציה תיקרא כשטוענים את המודול
- module_exit(hello_exit) - מגדיר איזו פונקציה תיקרא כשמסירים את המודול
קימפול המודול¶
כדי לקמפל מודול של קרנל, צריך Makefile מיוחד שמשתמש במערכת הבנייה של הקרנל:
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
מה שקורה כאן: ה-Makefile מפנה את הקימפול למערכת הבנייה של הקרנל המותקן (/lib/modules/...), שיודעת לקמפל את הקובץ בפורמט הנכון ועם כל הדגלים הנכונים. התוצאה היא קובץ hello.ko (ko = kernel object).
טעינה והסרה¶
# טוענים את המודול
sudo insmod hello.ko
# רואים את ההודעה בלוג הקרנל
dmesg | tail -1
# Hello from kernel module!
# מסירים את המודול
sudo rmmod hello
# רואים את הודעת הפרידה
dmesg | tail -1
# Goodbye from kernel module!
הלוג של הקרנל - dmesg¶
הקרנל לא יכול להדפיס למסך כמו תוכנית יוזר מודית רגילה (אין לו stdout). במקום זאת, כל ההודעות של הקרנל נכתבות לbuffer פנימי שנקרא ring buffer.
הפקודה dmesg מאפשרת לקרוא את ההודעות האלה:
# כל ההודעות של הקרנל
dmesg
# רק 10 ההודעות האחרונות
dmesg | tail -10
# מעקב בזמן אמת (כמו tail -f)
dmesg -w
ההודעות של הקרנל כוללות מידע על אתחול החומרה, טעינת דרייברים, שגיאות, ועוד. אם יש בעיות חומרה או שגיאות קרנליות, dmesg הוא המקום הראשון לחפש בו.
ממשקים לקרנל - proc ו-sys¶
בפרק 5.9 למדנו על מערכת הקבצים /proc - מערכת קבצים וירטואלית שהקרנל חושף דרכה מידע על תהליכים ועל המערכת.
יש מערכת קבצים וירטואלית נוספת שכדאי להכיר: /sys (sysfs). בעוד ש-/proc מכיל בעיקר מידע על תהליכים ומידע כללי על המערכת, /sys מכיל מידע על חומרה ודרייברים בצורה מאורגנת ומסודרת.
דוגמאות:
# מידע על המעבדים
ls /sys/devices/system/cpu/
# מידע על בלוקי דיסק
ls /sys/block/
# בהירות מסך (בלפטופ)
cat /sys/class/backlight/*/brightness
גם /proc וגם /sys הם לא קבצים אמיתיים על הדיסק - הם נוצרים דינמית על ידי הקרנל כשקוראים אותם. זה ממשק נוח שמאפשר ליוזר מוד לקרוא מידע מהקרנל ולפעמים גם לכתוב אליו (למשל לשנות הגדרות).
פניקת קרנל - kernel panic¶
כשהקרנל נתקל בשגיאה שהוא לא יכול להתאושש ממנה, הוא מבצע kernel panic - עוצר הכל ומציג הודעת שגיאה. זה המקבילה הקרנלית של segfault ביוזר מוד, אבל הרבה יותר גרוע - כי אם הקרנל קורס, כל המערכת קורסת.
דוגמאות למצבים שגורמים ל-kernel panic:
- באג בדרייבר שגורם לכתיבה לכתובת לא חוקית בזכרון הקרנל
- הקרנל לא מצליח למצוא את מערכת הקבצים הראשית (root filesystem) בזמן boot
- שחיתות (corruption) במבני נתונים פנימיים קריטיים של הקרנל
כשיש kernel panic, המערכת נתקעת ובדרך כלל צריך לאתחל ידנית. הודעת הpanic נשמרת בלוג ואפשר לראות אותה אחרי האתחול עם dmesg (אם המערכת הצליחה לשמור אותה).
סיכום¶
בהרצאה הזו הנחנו את הבסיס להבנת הקרנל של לינוקס:
- הקרנל הוא התוכנה שרצה ב-Ring 0 ומנהלת את כל החומרה, הזכרון והתהליכים
- לינוקס הוא קרנל מונוליטי מודולרי - כל הקוד ב-Ring 0, אבל אפשר לטעון ולהסיר מודולים
- הקרנל עולה דרך שרשרת של BIOS -> bootloader -> start_kernel -> init
- מרחב הכתובות מחולק בין קרנל ליוזר מוד, כשהקרנל ממופה בכל טבלת paging
- מודולים של קרנל מרחיבים את הקרנל בזמן ריצה
- הקרנל מתקשר דרך printk ו-dmesg, ודרך מערכות הקבצים הוירטואליות proc ו-sys
בפרקים הבאים ניכנס לעומק - נראה מה קורה בתוך הקרנל כשקוראים ל-syscall, ואיך הscheduler מחליט מי רץ.