6.5 מערכת הקבצים הוירטואלית הרצאה
הקדמה¶
לינוקס תומך בעשרות סוגים של מערכות קבצים: ext4, xfs, btrfs, ntfs, fat32, nfs, ועוד. בנוסף, יש מערכות קבצים וירטואליות כמו procfs (שלמדנו עליה ב-5.9), sysfs, tmpfs, ו-devtmpfs. כל אחת מהן עובדת אחרת מבפנים, ובכל זאת - מנקודת מבט של המשתמש, כולן נראות אותו דבר. אתם עושים open, read, write, close - ולא אכפת לכם אם הקובץ יושב על ext4 או על nfs.
איך זה עובד? התשובה היא VFS - ה-Virtual File System. זו שכבת הפשטה (abstraction layer) בתוך הקרנל שמספקת ממשק אחיד לכל מערכות הקבצים. היא מה שהופך את הפילוסופיה של "הכל הוא קובץ" ממשפט נחמד למציאות טכנית.
ארבעת האובייקטים המרכזיים של VFS¶
ה-VFS מגדיר ארבעה מבנים (structs) מרכזיים. ביחד, הם מייצגים את כל מה שקשור לקבצים ומערכות קבצים:
superblock - גוש-על¶
ה-struct שנקרא super_block מייצג מערכת קבצים מותקנת (mounted filesystem). כשאתם עושים mount /dev/sda1 /mnt, הקרנל יוצר struct super_block חדש.
struct super_block {
struct list_head s_list; // רשימת כל ה-superblocks
dev_t s_dev; // מזהה ההתקן
unsigned long s_blocksize; // גודל בלוק (למשל 4096)
struct file_system_type *s_type; // סוג מערכת הקבצים (ext4, xfs...)
const struct super_operations *s_op; // פעולות על ה-superblock
struct dentry *s_root; // ה-dentry של תיקיית השורש
// ...
};
ה-superblock מכיל מידע גלובלי על מערכת הקבצים: גודל בלוק, סוג מערכת הקבצים, מגבלות, ומצביעים לפעולות (כמו סנכרון לדיסק, יצירת inode חדש, וכו').
inode - צומת מידע¶
ה-struct שנקרא inode מייצג קובץ או תיקייה על הדיסק. לכל קובץ במערכת יש inode אחד. שימו לב לנקודה קריטית: ה-inode לא מכיל את שם הקובץ! הוא מכיל את כל שאר המטא-דאטה.
struct inode {
umode_t i_mode; // סוג הקובץ + הרשאות (rwxrwxrwx)
kuid_t i_uid; // בעלים (user id)
kgid_t i_gid; // קבוצה (group id)
unsigned long i_ino; // מספר ה-inode (ייחודי בתוך מערכת הקבצים)
loff_t i_size; // גודל הקובץ בבתים
struct timespec64 i_atime; // זמן גישה אחרון
struct timespec64 i_mtime; // זמן שינוי אחרון
struct timespec64 i_ctime; // זמן שינוי מטא-דאטה אחרון
const struct inode_operations *i_op; // פעולות על inode
const struct file_operations *i_fop; // פעולות על קבצים (ברירת מחדל)
struct super_block *i_sb; // מצביע ל-superblock
struct address_space *i_mapping; // מיפוי לpage cache
atomic_t i_count; // מונה הפניות
// ...
};
כמה נקודות חשובות:
- i_mode - מכיל גם את סוג הקובץ (רגיל, תיקייה, symlink, pipe, socket, device) וגם את ההרשאות.
- i_ino - מספר ה-inode. ייחודי בתוך מערכת קבצים אחת.
- i_mapping - קישור ל-page cache. כשקוראים את תוכן הקובץ, הדפים נשמרים כאן.
- i_op ו-i_fop - מצביעים לטבלאות פונקציות שמממשות את הפעולות על הinode. כל מערכת קבצים מספקת מימוש משלה.
dentry - רשומת תיקייה¶
ה-struct שנקרא dentry (קיצור של directory entry) הוא הקשר בין שם לinode. הוא מייצג רכיב בודד בנתיב - למשל, בנתיב /home/user/file.txt יש ארבעה dentries: אחד ל-/, אחד ל-home, אחד ל-user, ואחד ל-file.txt.
struct dentry {
struct dentry *d_parent; // מצביע לdentry של תיקיית האב
struct qstr d_name; // שם הקובץ/תיקייה
struct inode *d_inode; // מצביע ל-inode
const struct dentry_operations *d_op; // פעולות על dentry
struct super_block *d_sb; // מצביע ל-superblock
struct list_head d_child; // רשימת אחים (dentries באותה תיקייה)
struct list_head d_subdirs; // רשימת ילדים (dentries בתוך תיקייה זו)
// ...
};
למה צריך הפרדה בין dentry לinode? בגלל hard links! שני dentries שונים (שמות שונים) יכולים להצביע על אותו inode. כשיוצרים hard link, נוצר dentry חדש שמצביע על inode קיים. מבחינת ה-inode, הוא לא יודע ולא אכפת לו כמה שמות יש לו.
מטמון ה-dentry - dcache¶
הקרנל שומר dentries במטמון (cache) שנקרא dcache. זה אחד המטמונים הכי חשובים בקרנל. כשהקרנל צריך לפתור נתיב, הוא בודק קודם ב-dcache. אם ה-dentry כבר שם - אין צורך לקרוא מהדיסק.
file - קובץ פתוח¶
ה-struct שנקרא file מייצג קובץ פתוח. הוא נוצר כשתהליך קורא ל-open() ונמחק כש-close() נקרא (או כשהתהליך מת). שימו לב - inode מייצג קובץ על הדיסק. file מייצג קובץ פתוח ע"י תהליך מסוים.
struct file {
struct path f_path; // הנתיב (כולל dentry ו-mount)
const struct file_operations *f_op; // פעולות על הקובץ
atomic_long_t f_count; // מונה הפניות
unsigned int f_flags; // דגלים (O_RDONLY, O_WRONLY, O_NONBLOCK...)
fmode_t f_mode; // מצב (קריאה/כתיבה)
loff_t f_pos; // מיקום נוכחי בקובץ
struct fown_struct f_owner; // בעלים (לסיגנלים)
void *private_data; // מידע פרטי של הdriver
// ...
};
השדה הכי חשוב כאן הוא f_op - מצביע ל-file_operations. בואו נעמיק בזה.
file_operations - הלב של VFS¶
הstruct שנקרא file_operations הוא המפתח להבנת VFS. כל מערכת קבצים, כל driver, כל pseudo-filesystem ממלא struct כזה עם מצביעים לפונקציות שלו:
struct file_operations {
struct module *owner;
loff_t (*llseek)(struct file *, loff_t, int);
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
int (*mmap)(struct file *, struct vm_area_struct *);
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
unsigned int (*poll)(struct file *, struct poll_table_struct *);
ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);
ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
// ... ועוד הרבה
};
כשאתם קוראים ל-syscall read(), הנה מה שקורה:
- הקרנל מקבל את ה-file descriptor.
- הוא מוצא את ה-struct file המתאים (דרך טבלת ה-file descriptors של התהליך, שלמדנו עליה ב-5.5).
- הוא קורא ל-
file->f_op->read(). - הפונקציה שרצה היא הread של מערכת הקבצים הספציפית - ext4, procfs, או מה שלא יהיה.
זה בעצם פולימורפיזם בC - אותו ממשק, מימושים שונים. כשקוראים read() על קובץ ב-ext4, רץ הread של ext4. כשקוראים read() על /proc/cpuinfo, רץ הread של procfs שמייצר את התוכן דינמית. מנקודת מבט של התהליך, אין הבדל.
דוגמה קונקרטית - ככה procfs מגדיר file_operations עבור /proc/meminfo:
static const struct file_operations meminfo_proc_fops = {
.open = meminfo_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
כשאתם עושים cat /proc/meminfo, הread שרץ הוא seq_read, שקורא ל-meminfo_proc_show שמייצר את התוכן דינמית ממצב הזכרון של הקרנל.
פתרון נתיבים - pathname resolution¶
כשתהליך קורא ל-open("/home/user/file.txt", O_RDONLY), הקרנל צריך לפתור את הנתיב ולמצוא את ה-inode של הקובץ. התהליך הזה נקרא pathname resolution, וזה אחד הדברים שVFS עושה הכי הרבה.
הנה הזרימה:
-
מתחילים ב-dentry של השורש (
/). כל מערכת קבצים מותקנת (mounted) יש לה dentry שורש. -
מחפשים "home" בdcache - אם נמצא, מעולה. אם לא, הקרנל קורא את תיקיית השורש מהדיסק ומחפש entry בשם "home". אם נמצא, יוצרים dentry ו-inode חדשים ומכניסים אותם לdcache.
-
מחפשים "user" ב-"home" - אותו תהליך. בודקים dcache, אם לא נמצא - קוראים מהדיסק.
-
מחפשים "file.txt" ב-"user" - אותו תהליך.
-
מצאנו את ה-inode - עכשיו אפשר ליצור struct file ולהחזיר file descriptor.
בכל שלב, הקרנל בודק גם הרשאות - האם לתהליך יש הרשאת execute (שפירושה "חיפוש" עבור תיקיות) על כל תיקייה בנתיב.
ה-dcache הופך את התהליך הזה ליעיל מאוד. אחרי שנתיב נפתר פעם אחת, כל הrכיבים שלו נמצאים ב-dcache ופתרון חוזר הוא כמעט מיידי.
מטמון הדפים - page cache¶
כשקוראים תוכן מקובץ, הוא לא נקרא ישירות מהדיסק אל הbuffer של התהליך. הנתונים עוברים דרך ה-page cache - מטמון ברמת דפים ש-VFS מנהל.
איך זה עובד¶
- תהליך קורא ל-
read()על קובץ. - הקרנל בודק אם הדף המבוקש כבר ב-page cache.
- אם כן - הנתונים מועתקים מה-cache לbuffer של התהליך. לא צריך לגשת לדיסק.
- אם לא - הקרנל קורא את הדף מהדיסק, שומר אותו ב-page cache, ואז מעתיק לתהליך.
כתיבה עובדת באופן דומה: הנתונים נכתבים ל-page cache, והדף מסומן כ-dirty. בהמשך, הדף נכתב חזרה לדיסק (על ידי pdflush/writeback threads).
למה לינוקס "משתמש בהרבה RAM"¶
אם הרצתם free או top ושמתם לב שכמעט כל הRAM תפוס - זה בגלל ה-page cache. לינוקס משתמש בכל הRAM הפנוי למטמון קבצים. זה לא בזבוז - זה אופטימיזציה. ברגע שתהליך צריך זכרון, הקרנל זורק דפים מה-page cache כדי לפנות מקום.
אפשר לראות את מצב ה-page cache:
התקנת מערכות קבצים - mount¶
מערכת קבצים לא פשוט "קיימת" - צריך להתקין (mount) אותה לנקודה בעץ התיקיות. הפקודה mount יוצרת קשר בין מערכת קבצים למיקום בעץ התיקיות.
כשהקרנל פותר נתיב ומגיע לנקודת mount, הוא "קופץ" ממערכת קבצים אחת לאחרת. למשל, אם /proc מותקן כ-procfs, אז כשפותרים את הנתיב /proc/cpuinfo, הקרנל מגיע ל-/proc, מזהה שזו נקודת mount, ועובר ל-superblock של procfs.
מרחבי שמות של mount - mount namespaces¶
בלינוקס מודרני, כל תהליך (או קבוצת תהליכים) יכול לראות עץ mount שונה. זה נקרא mount namespace. זה הבסיס לcontainers - כל container רואה מערכת קבצים שונה, עם מערכות קבצים שונות מותקנות בנקודות שונות.
סוגי מערכות קבצים חשובים¶
בלינוקס יש הרבה מערכות קבצים. הנה החשובות ביותר:
ext4¶
מערכת הקבצים הסטנדרטית בלינוקס. זו מערכת קבצים "אמיתית" שכותבת נתונים לדיסק. היא תומכת ב-journaling (כתיבת log של פעולות לפני ביצוען, כדי לאפשר recovery אחרי קריסה), extents (ייצוג יעיל של בלוקים רציפים), וגדלים של עד 1 exabyte.
procfs - מערכת הקבצים proc¶
למדנו על procfs בפרק 5.9. היא מותקנת ב-/proc ומספקת מידע על תהליכים ועל הקרנל. הקבצים בה לא קיימים באמת על הדיסק - הם נוצרים דינמית ע"י הקרנל. כשקוראים ל-read() על /proc/meminfo, הפונקציה read שרשומה ב-file_operations מייצרת את התוכן מתוך מבני נתונים פנימיים של הקרנל.
sysfs - מערכת הקבצים sys¶
מותקנת ב-/sys. דומה ל-procfs, אבל מאורגנת סביב חומרה ודרייברים. כאן תמצאו מידע על התקנים, באסים, דרייברים, ומחלקות (classes) של התקנים.
tmpfs¶
מערכת קבצים שחיה בRAM (ובswap). משמשת עבור /tmp, /dev/shm, ולפעמים /run. מאוד מהירה כי אין גישה לדיסק, אבל הנתונים נמחקים באתחול מחדש.
devtmpfs¶
מותקנת ב-/dev. מכילה את קבצי ההתקנים (device files) - character devices ו-block devices. כשאתם ניגשים ל-/dev/sda או /dev/null, אתם ניגשים לקבצי התקן שה-VFS מנתב אותם לדרייבר המתאים דרך file_operations ייעודיים.
לראות את כל מערכות הקבצים¶
אפשר לראות אילו סוגי מערכות קבצים הקרנל תומך בהם:
ואפשר לראות מה מותקן כרגע:
סיכום¶
ה-VFS הוא שכבת ההפשטה שמאחדת את כל מערכות הקבצים בלינוקס תחת ממשק אחד. ראינו את ארבעת המבנים המרכזיים:
- superblock - מייצג מערכת קבצים מותקנת.
- inode - מייצג קובץ על הדיסק (מטא-דאטה, בלי שם!).
- dentry - מקשר שם לinode, נשמר בdcache לחיפוש מהיר.
- file - מייצג קובץ פתוח, מכיל מיקום נוכחי ומצביע ל-file_operations.
ה-file_operations הוא הלב של VFS - טבלת פונקציות שכל מערכת קבצים ממלאת בפונקציות שלה. זה מה שמאפשר ל-read() לעבוד אותו דבר על ext4, על procfs, ועל pipe.
למדנו גם על פתרון נתיבים, page cache, mount, ועל סוגי מערכות הקבצים השונים בלינוקס. כל הידע הזה מתחבר למה שלמדנו ב-5.5 (מתארי קבצים), ב-5.9 (procfs), וב-6.4 (ניהול זכרון - כי ה-page cache הוא חלק מניהול הזכרון).