לדלג לתוכן

6.1 - הקדמה לקרנל - הרצאה

הקדמה

בפרקים הקודמים למדנו איך לינוקס נראה מצד היוזר מוד - syscall-ים, processes, fork, סיגנלים, mmap, threads, ועוד.
עכשיו הגיע הזמן לצלול פנימה, אל הצד השני של המשוואה - הקרנל עצמו.

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


מה זה קרנל - kernel?

הקרנל הוא התוכנה המרכזית שרצה על המחשב. היא רצה ב-Ring 0 (קרנל מוד), ויש לה גישה מלאה לחומרה, לזכרון, ולכל משאב במערכת.
כזכור מפרק 2, ב-Protected Mode הגדרנו הפרדה בין קוד קרנלי (Ring 0) לקוד יוזר מודי (Ring 3). הקרנל הוא בדיוק הקוד שרץ ב-Ring 0.

התפקידים המרכזיים של הקרנל:
- ניהול חומרה - מדבר עם הדיסקים, כרטיסי הרשת, המסך, המקלדת, ושאר ההתקנים
- ניהול זכרון - מנהל את הpaging (כזכור מפרק 2.5), מקצה זכרון פיזי ווירטואלי לprocesses
- ניהול processes - יוצר processes, מבצע context switch ביניהם (כזכור מפרק 2.4), ומחליט מי רץ
- הinterface למשתמש - חושף את כל היכולות האלה ליוזר מוד דרך syscall-ים (כזכור מפרק 5.1)

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


קרנל מונוליטי מול מיקרוקרנל - monolithic vs microkernel

יש שתי גישות עיקריות לבניית קרנל:

קרנל מונוליטי - monolithic kernel

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

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

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

מיקרוקרנל - microkernel

בגישה הזו, רק הליבה הקטנה ביותר רצה ב-Ring 0 (context switch, IPC בסיסי, ניהול הרשאות). כל השאר - דרייברים, מערכת קבצים, רשת - רץ ביוזר מוד כprocesses נפרדים.

דוגמאות: Minix, QNX, GNU Hurd.

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

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

לינוקס - מונוליטי אבל מודולרי

לינוקס הוא קרנל מונוליטי, אבל הוא מודולרי - ניתן לטעון ולהסיר קוד מהקרנל בזמן ריצה באמצעות מודולים של קרנל (kernel modules). ניתן לחשוב על זה כעל DLL-ים של הקרנל. למשל, דרייבר של כרטיס רשת חדש יכול להיטען בלי לאתחל מחדש את המחשב. נדבר על זה בהמשך ההרצאה.


עץ קוד המקור של לינוקס - source tree

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

תיקיה מה יש בה
arch/ קוד ספציפי לארכיטקטורה - x86, ARM, RISC-V וכו'. כאן נמצא הקוד של כניסת ה-syscall, ה-boot, ניהול הpaging הספציפי לx86, וכו'
kernel/ הליבה של הקרנל - הscheduler, סיגנלים, fork, ניהול processes
mm/ ניהול זכרון - page allocator, slab allocator, mmap, page fault handler
fs/ מערכות קבצים - ext4, proc, tmpfs, ומעל הכל שכבת ה-VFS (interface אחיד לכל מערכות הקבצים)
drivers/ דרייברים - החלק הכי גדול בקרנל! דרייברים לכל סוגי החומרה
net/ מחסנית הרשת - TCP/IP, UDP, sockets, ניתוב
include/ קבצי header - הגדרות של מבני נתונים, מאקרואים, interfaces
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 (simplified)
asmlinkage void __init start_kernel(void)
{
    // Initialize the kernel logs
    setup_log_buf();

    // Initialize the scheduler
    sched_init();

    // Initialize memory management
    mm_init();

    // Initialize the VFS (virtual file system)
    vfs_caches_init();

    // Initialize interrupts
    init_IRQ();

    // Initialize the timer
    time_init();

    // ... plenty more initialization ...

    // Finally - starts the init process
    rest_init();  // creates process PID 1
}

שלב 4: הprocess הinit (process 1 - PID 1)

אחרי שהקרנל סיים לאתחל את כל תתי המערכות, הוא מעלה (mount) את מערכת הקבצים הראשית (root filesystem), ואז מריץ את התוכנית /sbin/init (או systemd בהפצות מודרניות) כprocess יוזר מודי ראשון - עם PID 1.
מכאן ואילך, הכל רץ כמו שלמדנו בפרק 5 - processes שיוצרים processes אחרים עם fork ו-execve, שירותים שעולים, ובסוף אנחנו מקבלים terminal שאנחנו יכולים לעבוד בו.

זוכרים שדיברנו על עץ הprocesses בפרק 5.2? הprocess ה-init הוא השורש של העץ הזה.


מרחב קרנל ומרחב יוזר - kernel space vs user space

בפרק 2.5 למדנו על Paging - כל process רואה מרחב כתובות וירטואלי משלו. אבל מה בדיוק נמצא במרחב הזה?

בארכיטקטורת 32 ביט

במערכת 32 ביט יש 4GB של מרחב כתובות וירטואלי. לינוקס מחלק אותו כך:
- הגיגה העליונה (0xC0000000 עד 0xFFFFFFFF) - שייכת לקרנל
- 3 הגיגות התחתונות (0x00000000 עד 0xBFFFFFFF) - שייכות ליוזר מוד

בארכיטקטורת 64 ביט

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

הנקודה החשובה

הpages של הקרנל ממופים בטבלאות הpaging של כל process. כלומר, לא משנה איזה process רץ, הכתובות העליונות תמיד מצביעות לאותו קוד ונתונים של הקרנל. ההבדל הוא שהpages האלה מסומנים ברמת הרשאות Ring 0 - אז קוד יוזר מודי לא יכול לגשת אליהם. אם process יוזר מודי מנסה לקרוא מכתובת בקרנל, המעבד יייצר page fault ומערכת ההפעלה תהרוג את הprocess עם 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).

טעינה והסרה

# load the module
sudo insmod hello.ko

# see the message in the kernel log
dmesg | tail -1
# Hello from kernel module!

# remove the module
sudo rmmod hello

# see the goodbye message
dmesg | tail -1
# Goodbye from kernel module!

הלוג של הקרנל - dmesg

הקרנל לא יכול להדפיס למסך כמו תוכנית יוזר מודית רגילה (אין לו stdout). במקום זאת, כל ההודעות של הקרנל נכתבות לbuffer פנימי שנקרא ring buffer.

הפקודה dmesg מאפשרת לקרוא את ההודעות האלה:

# all the kernel messages
dmesg

# only the last 10 messages
dmesg | tail -10

# real-time follow (like tail -f)
dmesg -w

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


הinterfaces לקרנל - proc ו-sys

בפרק 5.9 למדנו על מערכת הקבצים /proc - מערכת קבצים וירטואלית שהקרנל חושף דרכה מידע על processes ועל המערכת.

יש מערכת קבצים וירטואלית נוספת שכדאי להכיר: /sys (sysfs). בעוד ש-/proc מכיל בעיקר מידע על processes ומידע כללי על המערכת, /sys מכיל מידע על חומרה ודרייברים בצורה מאורגנת ומסודרת.

דוגמאות:

# information about the CPUs
ls /sys/devices/system/cpu/

# information about disk blocks
ls /sys/block/

# screen brightness (on a laptop)
cat /sys/class/backlight/*/brightness

גם /proc וגם /sys הם לא קבצים אמיתיים על הדיסק - הם נוצרים דינמית על ידי הקרנל כשקוראים אותם. זה interface נוח שמאפשר ליוזר מוד לקרוא מידע מהקרנל ולפעמים גם לכתוב אליו (למשל לשנות הגדרות).


פניקת קרנל - kernel panic

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

דוגמאות למצבים שגורמים ל-kernel panic:
- באג בדרייבר שגורם לכתיבה לכתובת לא חוקית בזכרון הקרנל
- הקרנל לא מצליח למצוא את מערכת הקבצים הראשית (root filesystem) בזמן boot
- קורפשן (corruption) במבני נתונים פנימיים קריטיים של הקרנל

כשיש kernel panic, המערכת נתקעת ובדרך כלל צריך לאתחל ידנית. הודעת הpanic נשמרת בלוג ואפשר לראות אותה אחרי האתחול עם dmesg (אם המערכת הצליחה לשמור אותה).


סיכום

בהרצאה הזו הנחנו את הבסיס להבנת הקרנל של לינוקס:
- הקרנל הוא התוכנה שרצה ב-Ring 0 ומנהלת את כל החומרה, הזכרון והprocesses
- לינוקס הוא קרנל מונוליטי מודולרי - כל הקוד ב-Ring 0, אבל אפשר לטעון ולהסיר מודולים
- הקרנל עולה דרך שרשרת של BIOS -> bootloader -> start_kernel -> init
- מרחב הכתובות מחולק בין קרנל ליוזר מוד, כשהקרנל ממופה בכל טבלת paging
- מודולים של קרנל מרחיבים את הקרנל בזמן ריצה
- הקרנל מתקשר דרך printk ו-dmesg, ודרך מערכות הקבצים הוירטואליות proc ו-sys

בפרקים הבאים ניכנס לעומק - נראה מה קורה בתוך הקרנל כשקוראים ל-syscall, ואיך הscheduler מחליט מי רץ.