6.8 דרייברים פתרון
תרגיל 1 - סיור ב/dev/¶
1-2. זיהוי סוגי התקנים:
- התקני תו (האות c בתחילת שורת הpermissions):
- crw-rw-rw- 1 root root 1, 3 ... /dev/null
- crw-rw-rw- 1 root root 1, 5 ... /dev/zero
- crw-rw-rw- 1 root root 1, 8 ... /dev/random
- התקני בלוק (האות b בתחילה):
- brw-rw---- 1 root disk 8, 0 ... /dev/sda
- brw-rw---- 1 root disk 8, 1 ... /dev/sda1
- brw-rw---- 1 root disk 8, 2 ... /dev/sda2
המספרים שמופיעים במקום גודל הקובץ (למשל 1, 3) הם המספר הראשי (major) והמספר המשני (minor).
/dev/null - major 1, minor 3
/dev/zero - major 1, minor 5
שניהם חולקים major 1 כי שניהם שייכים לאותו דרייבר (mem). הדרייבר משתמש בminor number כדי לדעת איזה התקן ספציפי לטפל בו.
- ניתוח של
/dev/sda*: - כולם חולקים major 8 (הדרייבר sd - SCSI/SATA disk)
/dev/sda- minor 0 (הדיסק כולו)/dev/sda1- minor 1 (מחיצה ראשונה)/dev/sda2- minor 2 (מחיצה שנייה)- הminor number מזהה את המחיצה. 0 = הדיסק כולו, 1-15 = מחיצות
תרגיל 2 - הבנת קוד הדרייבר¶
- כתיבה של יותר מ-1024 בתים:
בפונקציהdev_write:
השורה הזו קוטמת את האורך ל-1024 (BUF_SIZE). כלומר, רק 1024 הבתים הראשונים יישמרו, והשאר יאבדו. הפונקציה תחזיר 1024 (לא את האורך המקורי), כך שהתוכנה ביוזר תדע שלא כל המידע נכתב.
הערה: בפועל, הsyscall write עשוי לחזור עם ערך קטן מהמבוקש, והתוכנה אמורה לטפל בזה ולנסות שוב. אבל הדרייבר הזה פשטני - הוא תמיד דורס את כל הבאפר, כך שכתיבה חוזרת תדרוס את מה שנכתב קודם.
- הפוינטר off (offset):
הoffset מייצג את המיקום הנוכחי בקובץ. כשcat קורא מההתקן, הוא קורא בלולאה - כל קריאה ממשיכה מאיפה שהקודמת הפסיקה. - קריאה ראשונה: off=0, קוראת buffer_len בתים, off מתעדכן ל-buffer_len
- קריאה שנייה: off=buffer_len, הפונקציה רואה ש-off >= buffer_len ומחזירה 0
- כשread מחזיר 0, cat יודע שהגיע לסוף ועוצר
בלי off, כל קריאה היתה מתחילה מההתחלה, ו-cat היה נתקע בלולאה אינסופית.
- למה copy_to_user ולא memcpy:
שלוש סיבות: - אבטחה: copy_to_user מוודאת שהכתובת באמת שייכת ליוזר ולא לקרנל. memcpy היתה מאפשרת לתוכנה זדונית לגרום לקרנל לכתוב לזיכרון שלו עצמו.
- טיפול ב-page faults: הדף של היוזר אולי לא ממופה כרגע (swapped out). copy_to_user יודעת לטפל בpage fault - memcpy בהקשר של קרנל עלולה לגרום לkernel panic.
-
ערך החזרה: copy_to_user מחזירה את מספר הבתים שלא הועתקו. אם ההעתקה נכשלת באמצע, אפשר לדעת כמה הצלחנו.
-
הפרמטר הראשון של register_chrdev:
כשמעבירים 0, הקרנל מקצה אוטומטית major number פנוי. זו הדרך המומלצת כי אנחנו לא צריכים לדאוג לקונפליקטים עם דרייברים אחרים. אם היינו מעבירים 200, היינו דורשים ספציפית major 200. אם דרייבר אחר כבר תפס את 200, register_chrdev היתה נכשלת ומחזירה שגיאה. -
open vs release:
openנקרא בכל פעם שמישהו עושה syscallopen()על ההתקןreleaseנקרא רק כשהfile descriptor האחרון שמצביע על ה-file object נסגר
לדוגמה: אם תהליך פותח את ההתקן ואז עושה dup() או fork(), יש עכשיו כמה file descriptors שמצביעים לאותו file object. release ייקרא רק כשכולם נסגרו. זה חשוב כי בrelease בדרך כלל משחררים משאבים שמשותפים לכל הFDs.
תרגיל 3 - major ו-minor בפעולה¶
- ניתוח
/proc/devices: -
דרייבר mem (major 1): ההתקנים שמשתמשים בו:
/dev/null(minor 3)/dev/zero(minor 5)/dev/full(minor 7)/dev/random(minor 8)/dev/urandom(minor 9)/dev/kmem(minor 2)/dev/mem(minor 1)
-
דרייבר tty: בד"כ עשרות קבצי tty ב/dev/ -
/dev/tty0עד/dev/tty63,/dev/console,/dev/ptmxועוד. -
למה null, zero, random חולקים major?
כולם שייכים לאותו דרייבר (drivers/char/mem.cבקוד המקור של הקרנל). הדרייבר הזה מטפל ב"התקני זיכרון פסאודו" - התקנים שלא מייצגים חומרה אמיתית אלא שירותים של הקרנל.
הדרייבר בודק את הminor number בפונקציות שלו ומחליט מה לעשות:
- minor 3 (null): read מחזיר 0 (EOF), write מצליח ומשליך הכל
- minor 5 (zero): read מחזיר אפסים אינסופיים, write מצליח ומשליך
- minor 8 (random): read מחזיר בתים אקראיים
מבחינת הקרנל, זה יעיל - דרייבר אחד מטפל בכמה התקנים קשורים, במקום לרשום דרייבר נפרד לכל אחד.
תרגיל 4 - מעקב אחרי הנתיב בקרנל¶
הפקודה cat /dev/null:
- syscalls של cat:
open("/dev/null", O_RDONLY)- פתיחת ההתקן לקריאהread(fd, buf, 4096)- ניסיון לקרוא-
close(fd)- סגירה -
open בקרנל:
- הsyscall open מגיע לVFS (מפרק 6.5)
- הVFS הולך לפי הנתיב "/dev/null" ומוצא את הinode
- הinode מסומן כ-character device (דגל S_IFCHR)
-
הVFS רואה שזה קובץ התקן ולא קובץ רגיל
-
מציאת הדרייבר:
- הVFS קורא את הmajor number מהinode (major 1)
- מחפש בטבלת cdev_map את הדרייבר שרשום ל-major 1
- מוצא את דרייבר mem ואת הfile_operations שלו
- מעתיק את הfile_operations לfile object החדש
-
קורא את פונקציית open של הדרייבר
-
read של /dev/null:
הפונקציה read של null מחזירה 0 מיד. ערך החזרה 0 מread אומר EOF (סוף הקובץ). -
cat מקבל 0 מread:
כשread מחזיר 0, cat מבין שהגיע לסוף הקובץ. הוא לא מדפיס כלום (כי לא קרא כלום) וסוגר את הfile descriptor.
הפקודה echo "hello" > /dev/null:
- syscalls של echo ושל הshell:
- הshell פותח את
/dev/nullלכתיבה (O_WRONLY | O_TRUNC) - הshell עושה fork+exec ל-echo, כשstdout מנותב ל-/dev/null
-
echo קורא
write(1, "hello\n", 6) -
write בקרנל:
- הsyscall write מגיע לVFS
-
הVFS קורא את פונקציית write של הדרייבר
-
write של /dev/null:
הפונקציה מחזירה את len (מספר הבתים שביקשו לכתוב) בלי לעשות כלום עם הנתונים. היא "בולעת" את הכל. -
echo מקבל 6 מwrite:
echo חושב שהכתיבה הצליחה (כי הערך שחזר שווה למה שביקש) ומסיים.
תרגיל 5 - sysfs וudev¶
- התוכן של
/sys/class/משתנה בין מערכות. כמה classes נפוצים: net- ממשקי רשתblock- התקני בלוקinput- התקני קלט (מקלדת, עכבר)tty- טרמינלים-
sound- התקני סאונד -
ניתוח
/sys/class/net/: speed- מהירות החיבור ב-Mbps (למשל 1000 ל-Gigabit). לממשק lo (loopback) אין ערך.address- כתובת הMAC של הממשק (למשל00:11:22:33:44:55). לlo הכתובת היא00:00:00:00:00:00.-
operstate- מצב הממשק:up(פעיל),down(כבוי),unknown. ממשק lo בדרך כללunknown. -
udevadm monitor:
כשמחברים USB device, רואים רצף של אירועים: KERNELevents: הקרנל מזהה התקן חדש על אפיק USB ויוצר ייצוג ב-sysfsUDEVevents: udev מקבל את האירוע, מחיל חוקים (rules), ויוצר את קובץ ההתקן ב/dev/
כשמנתקים, רואים אירועים הפוכים - הקרנל מסיר את ההתקן מsysfs ו-udev מוחק את הקובץ מ/dev/.
- יתרון udev על mknod ידני:
- אוטומטי: udev יוצר ומוחק קבצי התקנים אוטומטית כשחומרה מתחברת/מתנתקת
- דינמי: לא צריך ליצור מראש קבצים לכל חומרה אפשרית
- חוקים: אפשר להגדיר כללים מותאמים (למשל "תמיד תן לUSB flash שם /dev/myflash")
- הרשאות: udev מגדיר הרשאות נכונות אוטומטית לפי חוקים
- עקביות: major numbers יכולים להשתנות בין גרסאות קרנל. udev מתאים את עצמו אוטומטית