-
פעולות קלט/פלט של תהליך ב-Linux מבוצעות דרך "קבצים":
- קבצים רגילים לאחסון מידע הנמצאים בדיסק.
- התקני חומרה המיוצגים כקבצים אך נמצאים בזיכרון.
-
כל תהליך הניגש לקובץ, שומר את הקשר בינו לבין הקובץ כ-
intבשם FD.- למשל, זה המספר המוחזר מקריאת המערכת
open.
- למשל, זה המספר המוחזר מקריאת המערכת
- המספר
FDהוא אינדקס בטבלת מתארי הקבצים,FDT.- הערה: לכל תהליך
FDTמשלו! תהליך יכול לגשת אליה מהשדהfiles->fdבטבלהPCB. - כל כניסה ב-
FDTמצביעה על אובייקט של קובץ פתוח (file object), שמכיל שדות כמוseek pointer, מצביע למיקום הנוכחי בקובץ.
- הערה: לכל תהליך
הערה: כל הקבצים הפתוחים של כל התהליכים במערכת נשמרים בטבלה המנוהלת ע"י הגרעין ונקראת GFDT (global file descriptor table).
ערכי ה-FD הבאים מקושרים להתקנים הבאים כברירת מחדל:
- 0 - ערוץ הקלט הסטנדרטי (STDIN), בדרך-כלל מקושר למקלדת.
- 1 - ערוץ הפלט הסטנדרטי (STDOUT), בדרך-כלל מקושר למסך.
- 2 - ערוץ השגיאות הסטנדרטי (STDERR), בדרך-כלל גם הוא מקושר למסך.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flags);
int open(const char *path, int flags, mode_t mode);- פעולה: פותחת קובץ/התקן לפי
pathעם תכונות הגישה ב-flagsוהרשאות יצירה לפיmode. - הגדרה מלאה:
int open(const char *path, int flags, ...).modeנדרש רק כאשרflagsכוללO_CREAT, ובשאר המקרים הוא מתעלם ממנו.
- ערך מוחזר: בהצלחה מוחזר
FDחדש. מוקצה האינדקס הפנוי הנמוך ביותר ב-FDT. בכישלון מוחזר-1ונקבעerrno.
פרמטרים:
-
path- מסלול לקובץ/התקן לפתיחה.- לדוגמה:
"file1"(יחסי) או"/usr/hw/file1"(אבסולוטי).
- לדוגמה:
-
flags- חייב לכלול אחת מהאפשרויות הבאות:O_RDONLY- קריאה בלבד.O_WRONLY- כתיבה בלבד.O_RDWR- קריאה וכתיבה.- ניתן להוסיף דגלים אופציונליים בעזרת
|:O_CREAT- ליצור קובץ אם אינו קיים.O_APPEND- כתיבה בסוף הקובץ הקיים.
-
mode- הרשאות קובץ חדש (רלוונטי רק כש-O_CREATקיים). -
הרשאות: ההרשאות לקובץ חדש הן
(mode & ~umask).
#include <unistd.h>
int close(int fd);- פעולה: סוגרת את ה-
fdומנתקת אותו מהקובץ/התקן. לאחר הסגירה לא ניתן להשתמש ב-fd. - ערך מוחזר: בהצלחה
0, בכישלון-1ונקבעerrno. - פרמטרים:
fd- ה-FDהמיועד לסגירה.
- הערה: ה-
file objectמשתחרר רק כאשר מונה ההפניות שלו מגיע ל-0 (ייתכנו תהליכים/חוטים נוספים שמצביעים אליו).
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);- פעולה: קוראת עד
countבתים מהקובץ המקושר ל-fdלתוךbuf. - מחוון קובץ: ה-
seek pointerמתקדם לפי מספר הבתים שנקראו בפועל. - חסימה: ייתכן חסימה עד שיהיו נתונים זמינים (למשל בדיסק או ב-pipe).
- ערך מוחזר: מספר הבתים שנקראו;
0עבור EOF אוcount == 0; בכישלון-1ונקבעerrno. - הערות:
- ייתכן שייקראו פחות מ-
countבתים (short read). - משתמשים ב-
ssize_tכדי לאפשר ערך שלילי בכישלון.
- ייתכן שייקראו פחות מ-
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);- פעולה: כותבת עד
countבתים מ-bufלקובץ המקושר ל-fd. - מחוון קובץ: ה-
seek pointerמתקדם לפי מספר הבתים שנכתבו בפועל. - חסימה: ייתכן חסימה (למשל בעת גישה לדיסק).
- ערך מוחזר: מספר הבתים שנכתבו;
0אםcount == 0; בכישלון-1ונקבעerrno. - הערה: ייתכן שייכתבו פחות מ-
countבתים (short write).
openמוסיפה כניסה חדשה במקום הפנוי הראשון ב-FDTשל התהליך.- הכניסה מצביעה על אובייקט מטיפוס
struct file.
struct file {
atomic_t f_count;
loff_t f_pos;
mode_t f_mode;
struct file_operations *f_op;
...
};f_count- מונה הפניות לאובייקט (למשל אב ובן אחריfork, אוdupבאותו תהליך). משמש לשחרור האובייקט כאשר המונה מתאפס.f_pos- מחוון הקובץ (seek pointer) למיקום הקריאה/כתיבה הנוכחי.f_mode- הרשאות הקובץ; נבדק לפניread/write.f_op- מצביע ל-file_operationsשמכיל את המימושים שלopen,read,write,lseekועוד.
- חוטים של אותו תהליך משתפים את אותה
FDT. - פתיחה/סגירה של קובץ ע"י חוט אחד משפיעה על כל החוטים.
- שימוש ב-
FDמשותף מחייב תיאום כדי למנוע שיבוש. - בלינוקס קיימים מנגנוני נעילה שונים:
advisory,mandatory,leases, וכן נעילה בעזרתflockאוfcntl.
forkמעתיקה את ה-FDTלתהליך הבן, שינויים ב-FDTלאחר מכן לא משותפים.- ה-
file objectsעצמם משותפים לאב ולבן, ולכן מחוון הקובץ (f_pos) משותף. - דוגמה: אם האב קורא 10 בתים ואז הבן קורא 3, הבן יקרא את 3 הבתים שאחרי ה-10 של האב.
- כל
openיוצרfile objectחדש, גם אם זה אותו קובץ.
execvודומותיה אינן משנות את ה-FDT, ולכן קבצים פתוחים נשארים פתוחים.- שימושי ל-
input/output redirection, לדוגמה:
date > file.txt- שחרור זיכרון של
file objectמתבצע רק לאחר שכל המפנים אליו נסגרו. - לכל
file objectיש מונהf_countשמופחת בכלclose. - כאשר
f_countמגיע ל-0, האובייקט משתחרר.
ערוץ תקשורת חד-כיווני בסדר FIFO. ה-pipes מאפשרים העברת מידע וסנכרון בין תהליכים.
- מערכת ההפעלה מממשת
pipesבאמצעות "קבצים" בזיכרון, ללא שימוש בדיסק (אינם מופיעים במערכת הקבצים). - קיים
FDלכתיבה, ו-FDלקריאה, אשר עובדים עם קריאות המערכת המוכרות:read/write. - המידע נשמר ב-
buffersפנימיים של מערכת ההפעלה.
#include <unistd.h>
int pipe(int filedes[2]);- פעולה: יוצרת
pipeצינור חדש עם שני קצוות (קריאה וכתיבה). - ערך מוחזר:
0בהצלחה,-1בכישלון. - פרמטרים:
filedes- מערך בגודל 2 תאים:filedes[0]יאחסן את ה-FDלקריאה.filedes[1]יאחסן את ה-FDלכתיבה.
- הערות: הכניסות ב-
FDTהן הראשונות הפנויות.
- ה-
pipeפרטי לתהליך ואינו נגיש לתהליכים אחרים (אלא באמצעות מנגנון fork). - שיתוף מתבצע בעזרת קשרי משפחה: אב יוצר
pipe(תופס שתי כניסות ב-FDT), ואז קורא ל-fork. ה-FDTמשתכפל, וכעת לאב ולבן יש גישה לשני צידי הצינור. - מכיוון שה-
pipeחד-כיווני, האחד צריך לקרוא והשני לכתוב (או להפך). לכן, חשוב שכל תהליך יסגור את הקצה שאינו בשימוש אצלו. - משאבי ה-
pipeמפונים אוטומטית כשהוא נסגר ע"י כולם (מספר המפנים יורד ל-0).
- מתבצעות בעזרת הקריאות
readו-write, כשה-pipeמתנהג כתור FIFO. המצביעים (של הכתיבה והקריאה) מקודמים אוטומטית מתוך הגרעין. - תיאום זמנים: בעת גישה מקבילית, הגרעין משתמש במנעולים פנימיים כדי למנוע race conditions.
התנהגות מערכת ההפעלה בקריאה מ-pipe:
- מצליח לקרוא את הכמות המבוקשת אם יש מספיק נתונים.
- קורא פחות מהכמות (short read) אם נותרו פחות נתונים זמינים כרגע.
- קורא
0(כלומר EOF) אם לא קיימים יותר נתונים בתור וגם כל קצוות הכתיבה (write FDs) נסגרו. - חוסם אם אין נתונים זמינים אבל יש לפחות קצה כתיבה אחד שעדיין פתוח.
התנהגות מערכת ההפעלה בכתיבה ל-pipe:
- כותב מיד אם יש מקום פנוי בחוצץ ה-
pipe(מוגבל לרוב ל-64KB). - חוסם וממתין לפינוי מקום אם חוצץ ה-
pipeמלא. - טיפול בכישלון משמעותי: אם כל קצוות הקריאה (read FDs) נסגרו, תהליך הכותב ל-
pipeיקבל אוטומטית סיגנלSIGPIPEממערכת ההפעלה (שכברירת מחדל הורג את התהליך).
דוגמת קוד - למה חייבים לסגור קצוות מיותרים?
int main {
int fd[2];
int grade;
pipe(fd);
if (fork == 0) { // teacher (child)
close(fd[0]);
do {
grade = get_random_between(0, 100);
write(fd[1], &grade, sizeof(int));
} while (grade != 0);
close(fd[1]);
} else { // student (parent)
close(fd[1]);
while (read(fd[0], &grade, sizeof(int)) > 0) {
if (grade == 100) break;
}
printf("Grade = %d\n", grade);
close(fd[0]);
}
return 0;
}- תרחיש 1: מה יקרה אם המורה לא יסגור את ה-
fd[0]שלו? המורה ימשיך להגריל ציונים, גם אחרי שהתלמיד מצא ציון 100, סיים את הביצוע, וסגר את קצה הקריאה שלו במערכת. כיוון שהמורה השאיר את קצה הקריאה אצלו פתוח, הוא לא יקבלSIGPIPEשישבור אותו. במקרה הרע תור הצינור יתמלא והמורה ייתקע לנצח. - תרחיש 2: מה יקרה אם התלמיד לא יסגור את ה-
fd[1]שלו? אחרי שהמורה יגריל 0 ויסגור את ה-fd[1]העצמי שלו, התלמיד יצפה לעוד נתונים כי ערוץ הכתיבה עדיין נחשב "פתוח" בראיה הכוללת (בגלל שה-fd[1]אצל התלמיד עצמו לא נסגר!). ה-readלעולם לא יחזיר EOF והתלמיד ייתקע לנצח על פעולה חוסמת.
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);- פעולה (duplicate): משכפלת
FDומייצרתFDחדש אשר מצביע בדיוק אל אותוfile objectכמו ה-FDהמקורי. כעת שניהם יחלקו את מחוון הקובץ (seek pointer), הרשאות ועוד. - ערך מוחזר:
FDהחדש. בכישלון מוחזר-1. - פרמטרים:
oldfd: תא ה-FDהמיועד להעתקה (חייב להיות פתוח קודם).newfd: (בלעדי ל-dup2) -FDהספציפי שאנחנו רוצים לדרוס ולמקם בדיוק עליו את השכפול.dup2תסגור אתnewfd(אם היה פתוח) כפעולה ראשונה לפני ההעתקה. בשימוש ב-dupרגיל, הגרעין ימצא בעצמו ויתפוס את ה-FDהפנוי הנמוך ביותר בטבלה.
FIFO הוא pipe בעל מזהה (שם) במערכת הקבצים.
קיומו מאפשר תקשורת גם בין תהליכים ללא קשרי משפחה.
- נגיש לכל תהליך (בהתאם להרשאות), ומופיע כקובץ מיוחד (למשל
/home/user/myfifo). - אינו שומר נתונים על גבי הדיסק. המידע נמצא בזיכרון, כשם שמתרחש ב-
anonymous pipe. - שימוש אופייני: תקשורת שרת-לקוח. לקוחות שונים באותה מכונה יכולים לפנות לשרת דרך שם קבוע מראש.
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);- פעולה: יוצרת ערוץ תקשורת מסוג
FIFOבנתיב המבוקש. - ערך מוחזר:
0בהצלחה,-1בכישלון. - פרמטרים:
pathname- שם ה-FIFO(והנתיב אליו במערכת הקבצים).mode- הרשאות הגישה.
- שלא כמו
pipeשיש לו שני קצוות בעליFDעצמאי (קריאה/כתיבה), ל-FIFOניגשים באמצעות קריאתopen, כקובץ לכל דבר. - ניתן לבצע אליו קריאה וכתיבה (באמצעות
read/write). הוא יכול לתמוך בתקשורת דו-כיוונית אך מבוסס עלFDאחד אחרי הפתיחה. - חסימות בפתיחה:
- תהליך הפותח לקריאה בלבד (
O_RDONLY) ייחסם עד שתהליך אחר יפתח אותו לכתיבה (ולהפך). - פתיחת ה-
FIFOלקריאה-וכתיבה (O_RDWR) אינה חוסמת.
- תהליך הפותח לקריאה בלבד (
- חוקי ה-
readוה-writeעליו פועלים בדיוק כמו ב-pipeרגיל.
מודולים הם תוספות קוד המרחיבות את פונקציונליות הגרעין הניתנות לטעינה ולפריקה באופן דינמי בזמן ריצה.
- מייתרים את הצורך בקימפול מחדש של הגרעין או אתחול המחשב בכל הוספת תכונה חדשה.
- שימוש מוביל: מנהלי התקנים (דרייברים) לחומרה. למשל:
- התקני תווים (Character Devices): מקלדת, עכבר, מדפסת ועוד.
- התקני בלוקים (Block Devices): דיסק קשיח, Disk-on-Key ועוד.
תהליך האתחול פותר את ה"פרדוקס": כיצד טוענים מערכת הפעלה מהדיסק, אם הקוד שניגש לדיסק נמצא בתוך מערכת ההפעלה עצמה? התהליך מבוצע בצורה הדרגתית על ידי שרשרת טעינה:
הגישה לחומרה מיד עם הדלקת המחשב.
- ה-
BIOSצרוב בלוח האם ורץ אוטומטית. הוא מזהה ומוודא כל חומרה בסיסית. - סורק התקנים ומחפש התקן אתחול חוקי (Bootable Device).
- ברגע שנמצא התקן מתאים, הוא מעתיק את הסקטור הראשון המסוים שלו לזיכרון הראשי.
- הסקטור הראשון (512 בתים) על חומרת האחסון.
- מכיל קוד אסמבלי בסיסי שתפקידו היחיד הוא לאתר ולהפעיל את מנהל האתחול המלא (הגדול לרוב מ-512 בתים ולכן ממוקם בנפרד בדיסק).
- מציג תפריט לבחירת מערכת ההפעלה (או גרסת הגרעין) לטעינה. יכולת קריטית לחזרה לגרעין יציב (vanilla) אם התרחשה קריסה בגרעין מותאם אישית (custom).
- GRUB טוען לזיכרון הראשי את תמונת הגרעין הדחוסה (לרוב קרויה
bzImageאוvmlinuz) ולצידה "מיני-מערכת קבצים" בשםinitramfs(אוinitrd).
- הגרעין מתחיל לרוץ ופורס את עצמו. הוא אינו קורא ישירות ליחידות הדיסק בשלב זה.
- מערכת הקבצים הראשונית (
initramfs): מספקת לגרעין סביבה המכילה אך ורק את המודולים (הדרייברים) הנחוצים לגישה למערכת הקבצים המלאה (כמו דרייברים לבקר הדיסק ולרשת). - הגרעין מריץ סקריפט
/initמתוך ה-initramfs. הוא טוען את המודולים הנדרשים, ומעגן (mount) את מערכת הקבצים האמיתית במקום מערכת הקבצים הראשונית. - לסיום, תהליך העבודה מועבר למערכת ההפעלה המלאה באמצעות קריאה לתהליך
/sbin/init(במערכות מודרניות, לרוב סמליל ל-systemd).
הערה: בזמן האתחול נזרקות הודעות מערכת רבות למסך באופן מהיר מאד. ניתן לקרוא ולחקור אותן לאחר שהמערכת עולה ע"י שימוש בפקודה
dmesg.
מודול הוא ספרייה משותפת הניתנת לטעינה ופריקה דינמית בזמן ריצה, ופועלת במצב גרעין (CPL=0).
- פקודות ניהול בסיסיות (דורשות הרשאות
root):insmod- טעינת מודול.rmmod- פריקת מודול.lsmod- הצגת רשימת המודולים הפעילים.
הרחבת פונקציונליות הגרעין ללא הגבלה:
- הוספת מנהלי התקנים (Drivers) כדוגמת התקני תווים (מקלדת), התקני בלוקים (דיסק) והתקני רשת.
- הוספת תמיכה במערכות קבצים חדשות.
- הוספה או שינוי של קריאות מערכת קימות.
- דינמיות: הוספת יכולות מבלי לקמפל מחדש את הגרעין וללא צורך באתחול (reboot).
- חיסכון בזיכרון: נטען רק מה שצריך, וניתן לפריקה כשאינו בשימוש.
- יעילות פיתוח: מפותח ומקומפל בנפרד, מה שמקטין את קוד הגרעין ומקצר זמני קומפילציה.
- המודול רץ במצב גרעין ולכן נגיש לכל מבני הנתונים בו. כל באג (כגון חוסר איתחול, buffer overrun) עלול להפיל את המערכת.
- בשל כך, לינוקס מגבילה טעינת מודולים למשתמשי
rootבלבד. - חשוב להימנע מחורי אבטחה במודול למשל ע"י הקפדה על אתחול משתנים, בדיקת תקינות קלט משתמש כדי למנוע buffer overrun וכו'.
#include <linux/module.h> // חובה בכל מודול
#include <linux/kernel.h> // מאפשר שימוש ב-printk
int init_module(void) {
printk("Hello World!\n");
return 0;
}
void cleanup_module(void) {
printk("Goodbye cruel world!\n");
}הערה חשובה (printk מול printf): קוד גרעין אינו יכול להשתמש בקריאות גישה לספריות מעטפת ה-User Mode (כגון libc), כיוון שתהליך הגרעין מוקם עוד לפני שספריות אלו זמינות. לשם כך, משתמשים בפונקציות שמספק הגרעין בעצמו, למשל printk.
יש ליצור קובץ 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- חובת תאימות: גרסת לינוקס שעליה נבנה המודול חייבת להיות זהה לחלוטין לגרסה שבה הוא ירוץ. חוסר תאימות עלול לגרום לקריסות ושגיאות בזמן ריצה.
make
sudo insmod ./hello.ko # פקודה זו מפעילה וטוענת את המודול (מחייבת root)
lsmod # מוודא שהמודול נטען ומציג רשימת מודולים פעילים
sudo rmmod hello # פורק את המודול מהזיכרון (מחייב root)- הרשאות: פקודות הטעינה והפריקה בסביבת האמת דורשות הרשאות
root. modprobeלעומתinsmod: בעודinsmodטוען מודול ספציפי בצורה "טיפשה", הפקודהmodprobeחכמה יותר - היא מבצעת חיפוש בתיקיית/lib/modules/וטוענת אוטומטית את כל תלויותיו של מסלול המודול אם הוא דורש מודולים מקדימים.
מודולים יכולים לקבל משתנים מהמשתמש בזמן הטעינה (בדומה לערכי argv).
ההגדרות מתבצעות באזור הגלובלי של הקוד (מחוץ לפונקציה) באמצעות המאקרו module_param. יש להגדיר לקריאות ערך ברירת מחדל לאתחול.
#include <linux/moduleparam.h>
int iValue = 0; // ערך ברירת המחדל אם לא הוזן
char *sValue;
// חתימה: module_param(variable, type, permissions);
module_param(iValue, int, S_IRUGO);
module_param(sValue, charp, S_IRUGO);
// מערך:
int iArray[4];
int len;
module_param_array(iArray, int, &len, S_IRUGO);- סוגים נפוצים נתמכים:
byte,short,int,charp(מחרוזת) ו-bool. - טיפ להעברת פרמטר "אופציונלי": הגדרת מערך בגודל 0-1.
- עזר - הוספת מידע למשתמש מתאפשרת בעזרת מאקרו נוסף:
MODULE_PARM_DESC. כך כלי ניהול משתמש כמוmodinfoיחשפו בקלות מה עושה כל פרמטר.
צורת ההעברה בטעינה ממערכת ההפעלה:
sudo insmod ./params.ko iValue=3 sValue="hello"כאשר מודול טוען ספריות ליבה מתאימות באמצעות #include, נחשפת אליו הגישה המלאה למבני הנתונים של גרעין המערכת:
#include <linux/sched.h>
int init_module(void) {
printk("The process is \"%s\" (pid %d)\n", current->comm, current->pid);
return 0;
}- התהליך שמריץ את הטעינה: בדוגמה מעלה שדה ה-
commמצביע לשם התוכנית של התהליך הנוכחי (current). התהליך הטוען את פקודת הביצוע אל תוך הליבה הואinsmodבעצמו, ולכן הפלט שיתקבל:The process is "insmod" (pid 21676).
- התקנים (devices): מייצגים חומרה או רכיב וירטואלי, ומופיעים עקרונית כקבצים מיוחדים (לרוב בתיקיה
/dev).- המשתמש מתקשר מול ההתקן בקריאות VFS רגילות:
open,read,write. - לדוגמה: כתיבה לקובץ
/dev/lp0שולחת מידע למדפסת.
- המשתמש מתקשר מול ההתקן בקריאות VFS רגילות:
- דרייבר (driver): שכבת תוכנה החוצצת בין האפליקציות להתקן:
- רץ תחת הרשאות הגרעין.
- ממפה את קריאות הקלט/פלט לפעולות ספציפיות על החומרה או הרכיב.
- הפרדת מכניזם ומדיניות: הדרייבר עצמו מממש מכניזם (הגישה הטכנית, "מה אפשר לעשות" - איך לשלוט בבקר דיסקט). מערכת ההפעלה או יישומי המשתמש עוטפים במדיניות ("מי יכול או מתי ניגשים"). רצוי לשמור את המדיניות באזור המשתמש (User Mode).
- התקני תווים (character devices):
- ניגשים אליהם כרצף בתים, בדרך כלל ניתן לגשת להתקן תווים רק באופן סדרתי (ולא אקראי).
- אין יכולות התמצאות מהירה במיקום בתוך ההתקן.
- דוגמאות: מקלדת, מסך, עכבר מחשב.
- התקני בלוקים (block devices):
- ניגשים למידע שלהם רק בכפולות של בלוק (למשל 512 בתים).
- הגישה אקראית ומאפשרת שיטוט באמצעות פוינטרים.
- לינוקס מוסיפה שכבה נוספת (page cache) ומאפשרת לקרוא מהתקני בלוקים גם בתים בודדים.
- דוגמאות: הארד-דיסק, דיסק און קי.
כל התקן תווים מאופיין בצמד מספרים:
- מספר ראשי (Major number): מזהה את הדרייבר האחראי על ההתקן - משמש את
openלאיתור הדרייבר הנכון בטבלת הגרעין. - מספר משני (Minor number): מזהה את ההתקן הספציפי המקושר לאותו דרייבר (יכולים להיות מספר התקנים, למשל מספר עכברי מחשב, המנוהלים ע"י אותו דרייבר).
שימוש ב-ls -l /dev יציג התקני תווים עם האות 'c':
crw-rw-rw- 1 root root 1, 3 Aug 31 10:33 null
crw------- 1 root root 10, 1 May 12 10:33 psaux
crw------- 1 root tty 4, 1 May 12 10:33 tty1
crw-rw-rw- 1 root root 1, 5 Aug 31 10:33 zero- ההתקנים
nullו-zeroמנוהלים על ידי אותו דרייבר (Major 1), אך מובדלים במספר המשני (3 לעומת 5).
לינוקס מספקת התקני תווים וירטואליים שאינם מחוברים לחומרה פיזית (בעלי Major מספר 1):
| קובץ התקן | read |
write |
|---|---|---|
/dev/null |
מחזירה מיד EOF (end of file) |
מצליחה ולא עושה דבר (המידע שנשלח נזרק) |
/dev/zero |
מחזירה רצף אפסים באורך המבוקש | מצליחה ולא עושה דבר (המידע שנשלח נזרק) |
/dev/full |
מחזירה רצף אפסים באורך המבוקש | מחזירה מיד `ENOSPC (no space left on device) |
/dev/random |
מחזירה רצף בתים אקראי שנוצר בזמן הריצה מתוך "רעש" | כותבת לרצף הבתים האקראי |
/dev/urandom |
מחזירה רצף בתים אקראי שנוצר בזמן הריצה מתוך "רעש" | כותבת לרצף הבתים האקראי |
/dev/arandom |
מחזירה רצף בתים אקראי שנוצר בזמן הריצה מתוך "רעש" | כותבת לרצף הבתים האקראי |
בדרך כלל מערכת ההפעלה מייצרת אוטומטית התקנים עבורנו כשהחומרה מזוהה (באמצעות תהליך מיוחד בשם udev). אולם, ניתן ליצור התקן ידנית עם התוכנית mknod (שממומשת באמצעות קריאת המערכת mknod):
mknod <NAME> <TYPE> <MAJOR> <MINOR>- פרמטרים:
NAME: מסלול הקובץ ונקודת העיגון.TYPE: סוג -c(תווים) אוb(בלוקים).MAJOR: המספר הראשי.MINOR: המספר המשני.
דוגמה:
sudo mknod /dev/myDev c 254 0-
הפקודה תיצור קובץ בשם
/dev/myDev, המייצג התקן תווים, המנוהל ע"י הדרייבר הרשום עם מספר ראשי 254. המספר המשני של ההתקן הוא 0. -
התקנים חדשים נוצרים כברירת מחדל עם הרשאות כתיבה ליוצר ההתקן (לרוב root) והרשאות קריאה לשאר המשתמשים.
- במידת הצורך ניתן לשנות את הרשאות אלה לאחר יצירת ההתקן.
-
הסרת ההתקן באופן דומה למחיקת קובץ רגיל:
sudo rm /dev/myDev # מחיקהקריאת open מקצה כניסה חדשה בטבלת מתארי הקבצים (FDT), המצביעה בסופו של דבר לאובייקט מסוג struct file:
struct file {
loff_t f_pos; // מצביע למיקום הקריאה/הכתיבה הנוכחי.
void *private_data; // שדה (אתחול ל-NULL) בו הדרייבר יכול לשמור מידע פנימי שלו.
struct file_operations *f_op; // אובייקט הדרייבר עצמו.
// ... רכיבים אחרים ...
};השדה f_op במסגרת אובייקט הקובץ מגדיר מה בפועל מתבצע כאשר המשתמש קורא לקריאות מערכת כמו read ו-write מול ההתקן.
- מדובר במבנה נתונים המכיל מערך של מצביעים לפונקציות (Function Pointers) המממשות את אותן יכולות.
- ניתן להתייחס לכך כ-"ממשק מונחה עצמים": הקובץ/ההתקן הוא האובייקט, ומערך
file_operationsהוא אוסף המתודות שלו. - מצביע
NULLבאחת מהפונקציות הללו מראה לרוב שאין תמיכה בפעולה (או שישנה פעולת ברירת מחדל). את הפונקציות הללו מממש הדרייבר עצמו במסגרת קוד הגרעין.
הערה: חתימות הפונקציות הפנימיות בדרייבר שונות מהחתימות החיצוניות של קריאות המערכת שהן חשופות למשתמש הכללי.
open: אם מוגדרת כ-NULL, קריאתopenעל ההתקן תמיד תצליח ללא פעולת המשך מותאמת.
int (*open)(struct inode *, struct file *);release: הפונקציה המופעלת בשחרור ההתקן (מופעלת רק כאשר מונה ההפניות, כגון מירושותfork, מתאפס וכל עותקי ההתקן נסגרו).
int (*release)(struct inode *, struct file *);flush: מופעלת בכל סגירת העתק כדי לנקות חוצצים ולדחוף מידע. כשהיאNULL, המערכת תתעלם מהשלב.
int (*flush)(struct file *);read/write: מצביעים לפונקציות לקריאת/כתיבת בתים בהתקן.- במידה ומאותחלים ל-
NULL, קריאות המערכת המתאימותread/writeיחזירו-EINVAL.
- במידה ומאותחלים ל-
ssize_t (*read)(struct file *, char *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char *, size_t, loff_t *);llseek: משנה את מחוון המיקום בקובץ ומשפיעה על הגישות הבאות (רלוונטי להתקני בלוקים שניתנים להזזה).
loff_t (*llseek)(struct file *, loff_t, int);ioctl: משמש להעברת פקודות בקרה ייחודיות בהתקנים.NULLיוביל שוב לשגיאת-EINVAL.
int (*ioctl)(struct inode *, struct file *, unsigned int cmd_id, unsigned long arg);בעוד שפעולות כגון read ו-write מציעות ממשק קלאסי העוטף טקסט, חלק מההתקנים דורשים אופרציות מותאמות אישית שאין להן מענה בפעולות הללו.
לשם כך נועדה קריאת המערכת (input/output control) ioctl:
#include <sys/ioctl.h>
int ioctl(int fd, int cmd, ...);- פרמטרים:
fd: מתאר קובץ להתקן פעיל, שהתקבל כתוצאה מ-open.cmd: קוד פקודה/מספר סילי שהדרייבר בגרעין יזהה ומחכה לו (מועבר "AS IS" למערכת המופעלת).- פרמטר אופציונלי (
...): ניתן לצרף מצביע למבנה מידע, מחרוזת, או סתם ערך. כיוון שהלינקר (והקומפיילר) אינו יכול לדעת מהי הדרישה בהתקן העצמאי, החובה על הפונקציה המממשת במודול הדרייבר היא לפענח ולוודא את חוקיות הערך במדויק. לרוב מתייחסים אליו כ-char *argp. - ערך חזרה: תלוי בכותב הדרייבר, כאשר נהוג שאם יש כישלון הקריאה תחזיר ערך שלילי כמו
-1.
השימוש בדרייברים מבוסס על גישה מונחית עצמים בה הדרייבר רושם את אוסף הפעולות שלו כהתנהגויות עבור התקן. המודול נטען במרחב הזיכרון של הגרעין ומגשר בין השכבות.
גרעין המערכת מנהל מערך גלובלי בשם chrdevs השומר את כל הדרייברים הרשומים בה.
- האינדקס של המערך הוא למעשה המספר הראשי (Major) של הדרייבר.
- כל תא במערך מכיל את שם הדרייבר ומצביע למבנה ה-
file_operationsשלו.
פונקציית גרעין (ולא קריאת מערכת!) המופעלת על ידי המודול (לרוב בפונקציית הטעינה init_module) כדי לרשום את עצמו במערכת כדרייבר פעיל:
#include <linux/fs.h>
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);פעולות:
- רושמת את הטיפול במערך ה-
chrdevs. - מקצה לו את המספר הראשי
major. - מוסיפה לו רישום בקובץ הניהול בסביבת מערכת הקבצים
/proc/devices.
ערך חזרה: במקרה של הצלחה יוחזר 0 או חיובי. אחרת -1 השלילי. (שימו לב: שם הפונקציה מטעה, כיוון שהיא מדברת על התקן - chrdev אך רושמת את הדרייבר של ההתקנים הללו).
לינוקס תומכת בעד 512 מספרים ראשיים. העברה של המספר הקבוע דורשת זהירות מחשש להתנגשויות חוקיות.
- הקצאה סטטית: היזם מזין מספר קבוע בקוד. סכנה של תפיסת המספר על ידי התקן פופולרי קיים (פירוט שמור ב-
Documentation/devices.txt). - הקצאה דינמית (מומלצת): העברת הפרמטר
0כערך ה-major. הליבה תחפש מספר פנוי מתחת ל-512 ותחזיר אותו כערך החזרה ב-register_chrdev.
ניתן לצפות ברשימת הדרייברים הרשומים והmajor שלהם על ידי cat /proc/devices.
בשחרור המודול (למשל על ידי rmmod), יש לפנות את הרשומות שהדרייבר תפס על מנת למנוע קריסות (kernel panic) מוגדרות:
int unregister_chrdev(unsigned int major, const char *name);- מסירה את הרישום מ-
chrdevsומשחררת את ה-Major. פעולה זו אינה מוחקת פיזית התקני/dev/שנוצרו על יתר הדרייברים. - בעיה אפשרית: אם המודול מוסר בלי קריאה לפונקציה, כיוון שקרתה שגיאה למשל, ייתכן שתהליך אחר ינסה לרשום מודול עם אותו Major בגלל שזה נראה כאילו ההעדפה נשמרה.
נסכם איך sys_open פועלת מאחורי הקלעים ברגע שתהליך מריץ על קובץ fd = open("/dev/mydev", O_RDONLY):
-
איתור פיזי: מערכת ההפעלה ניגשת למערכת הקבצים ומאתרת עבור ההתקן המבוקש את רשומת המידע שלו (הנקראת
inode). הרשומה נושאת את שדה ה-i_rdevהמכיל את המספר הראשי ($M$ ) והמשני ($m$ ). -
בדיקת רישום: המערכת פונה לתא
$M$ במערךchrdevs. במידה ושם מצטיירNULL, משמע הדרייבר חסר, המערכת תיזרק החוצה עם שגיאתENODEV. -
הקצאה ב-
file_operationsוקשר: מוקצה אובייקטstruct fileחדש (filp), ומערכת מתאימה אליו כניסה חדשה בטבלת ה-FDT של התהליך. בשלב זה, מוצמד לפוינטר הפנימיfilp->f_opהמצביע שלchrdevs[M]->f_op. -
קריאה מותאמת: הליבה בודקת האם קיימת בנייה למתודת ה-
openבמערך שירשה (filp->f_op->open). אם קיימת, היא מזומנת ומועברים אליה ה-inodeוה-filpכפרמטרים. - הדרייבר דורס או בונה נתונים לפי רצונו, ולבסוף הפונקציה משיבה לתהליך המקור את ה-
FD- האינדקס החדש שצלח ב-FDT.
int (*open)(struct inode *inodep, struct file *filp);הפונקציה של הדרייבר מקבלת לידיה את מבנה ה-inode. במקרה שישנם כמה התקנים דומים המנוהלים תחת אותו דרייבר (אותו Major), הוא יכול להיעזר במאקרו מובנים בגרעין לשליפת המספר על מנת להחליט כיצד לשרת את הקריאה במדויק:
- שליפת המספר ראשי:
MAJOR(inodep->i_rdev) - שליפת המספר המשני:
MINOR(inodep->i_rdev)
בעוד שתהליך (Process) הוא מופע ריצה מבודד של תוכנית, חוט (Thread) הוא יחידת ביצוע קלה יותר שיכולה לרוץ במקביל בתוך אותו תהליך ולחלוק איתו משאבים.
ההבדל המרכזי בין ריבוי תהליכים (Multiprocessing) לריבוי חוטים (Multithreading) הוא רמת השיתוף:
- תהליכים: לא חולקים כלום. לכל תהליך יש מרחב כתובות משלו, והם מבודדים לחלוטין ע"י מערכת ההפעלה.
- חוטים: חולקים (כמעט) הכל. חוטים באותו תהליך משתפים את מרחב הכתובות בזיכרון.
| משאב | ייחודי לתהליך? | ייחודי לחוט? |
|---|---|---|
| אוגרים (Registers) ומונה פקודות (PC) | כן | כן |
| מחסנית ביצוע (Execution Stack) | כן | כן |
| מרחב כתובות בזיכרון (Code, Data, Heap) | כן | לא (משותף) |
| קבצים פתוחים (FDT) ומיקום בקובץ | כן | לא (משותף) |
| הרשאות משתמש ותיקיית עבודה | כן | לא (משותף) |
ב-Linux, ברמת הגרעין, אין באמת הבדל בין "תהליך" ל-"חוט". שניהם מיוצגים ע"י אותו מבנה נתונים - task_struct, ונקראים "משימות".
- קריאות המערכת המוכרות לנו,
forkליצירת תהליך ו-pthread_createליצירת חוט, הן למעשה רק פונקציות מעטפת (Wrappers) לאותה קריאת מערכת:clone. - ההבדל ביניהן הוא בדגלים שמועברים ל-
clone. לדוגמה, אם מעבירים את הדגלCLONE_VM, האב והבן ישתפו את אותו מרחב זיכרון (התנהגות של חוט). אם לא, ייווצר מרחב כתובות נפרד (התנהגות של תהליך).
- OpenMP: אוסף של הנחיות לקומפיילר שמאפשרות לכתוב קוד סדרתי שהקומפיילר יהפוך למקבילי באופן אוטומטי.
- POSIX Threads (pthreads): ספריית C סטנדרטית למערכות UNIX (דרך
<pthread.h>). מאפשרת שליטה ידנית ביצירה וסנכרון של חוטים. - תמיכת שפה: שפות כמו Java ו-C++ מציעות תמיכה מובנית בחוטים כחלק מזמן הריצה שלהן.
אופן התקשורת נגזר ישירות מרמת השיתוף:
- תקשורת בין חוטים: מתבצעת באופן טבעי דרך זיכרון משותף.
- יתרון: יעיל מאוד, אין צורך בהעתקת נתונים ("zero copy").
- חיסרון: דורש סנכרון זהיר ומורכב כדי למנוע race conditions.
- תקשורת בין תהליכים: מתבצעת לרוב דרך העברת הודעות כדוגמת
pipes.- יתרון: קל יותר להבנה, לתכנון ולמניעת שגיאות סנכרון.
- חיסרון: פחות יעיל, לרוב דורש העתקת נתונים פנימית של הליבה ושימוש אינטנסיבי בקריאות מערכת (
read/write).
הזמן שמערכת ההפעלה מבזבזת על מעבר ממשימה אחת לאחרת מורכב משני גורמים:
- עלות ישירה (direct overhead): הזמן שלוקח לגרעין לשמור את המצב (אוגרים) של המשימה הקודמת ולטעון את המצב של המשימה החדשה.
- עלות עקיפה (indirect overhead): הזמן שלוקח לחומרה "להתחמם" מחדש עבור המשימה החדשה - בעיקר אכלוס מחדש של זיכרון המטמון (Cache warm-up). העלות העקיפה משמעותית בהרבה ויכולה להיות גדולה בשני סדרי גודל מהעלות הישירה.
מעבר בין חוטים לעומת תהליכים: כאשר מתבצע Context Switch בין חוטים של אותו תהליך, הם לעיתים קרובות עובדים על אותם נתונים. לכן, הזיכרון במטמון עשוי עדיין להיות רלוונטי עבורם, מה שמקטין דרמטית את העלות העקיפה.
אם נגדיר זמן החלפת הקשר כ-$C$, והמשימה רצה למשך הזמן
הסיבה שהעלות העקיפה כה משמעותית נובעת מפער הביצועים העצום בין המעבד לזיכרון. המעבד מהיר בהרבה מזיכרון הראשי (DRAM) שאיטי ממנו פי 100 ויותר. הפתרון לכך הוא זיכרון המטמון (cache).
הקאש עובד בזכות עיקרון המקומיות:
- מקומיות זמנית (temporal locality): אם התוכנית ניגשה לכתובת מסוימת בזיכרון, סביר שהיא תיגש אליה שוב בקרוב.
- מקומיות מרחבית (spatial locality): אם התוכנית ניגשה לכתובת מסוימת, סביר שהיא תיגש בקרוב לכתובות סמוכות אליה.
כאשר מתבצעת החלפת הקשר לתהליך אחר, המידע הרלוונטי שלו בדרך כלל לא נמצא במטמון ויש להביאו מה-DRAM האיטי, מה שגורם להאטה משמעותית בתחילת הריצה.