שורה אחת של File.ReadAllText, או קריאה אחת ל-ReadFile. מה קורה בתוך Windows בין הרגע שבו קוראים לפונקציה לרגע שבו היא חוזרת?
ברגע שמכירים את הזרימה הזאת, אפשר לחשוב על IRP_MJ_READ ו-FASTIO_READ שמופיעים ב-Process Monitor, על קובץ שלא משוחרר למרות שנקרא ל-CloseHandle, ועל התנהגות שמשתנה רק על כונן רשת או במחשב שעליו מותקן אנטי-וירוס — הכול בתוך אותו מנגנון.
הסדרה “מעמקי ה-I/O של Windows” נכנסת לתכנון ה-kernel שספר כמו Windows Internals (שיצא לאור ביפנית בשם Inside Windows) מכסה, ומסבירה לא רק איך להשתמש ב-API אלא למה הוא מתנהג כפי שהוא מתנהג.1 המאמר הראשון הזה מקבע את היסוד לכל השאר — פתרון השמות, הדמויות, וזרימת הבקשה והשלמתה — עם דיאגרמות. הוא לא נכתב כדי שתוכלו לכתוב driver; הוא נכתב כדי שמפתחי אפליקציות יבינו לאן באמת הולך ה-API שהם קראו לו.
ידע מוקדם: אפשר לקרוא את המאמר אם השתמשתם ב-FileStream ב-C#, או קראתם ל-CreateFile / ReadFile ב-C/C++. אין צורך בניסיון בפיתוח drivers, והקוד בצד ה-kernel מוצג כתרשים מושגי. כמדריך גס, הקצו כ-25 דקות לקריאה רצופה עם הדיאגרמות, או 2–3 דקות אם אתם רוצים רק את השורה התחתונה של פרק אחד.
המבנה של הסדרה
| חלק | נושא |
|---|---|
| חלק 1 (המאמר הזה) | התמונה הכוללת של מערכת ה-I/O — כל קריאה וכתיבה הופכת ל-IRP |
| חלק 2 | I/O סינכרוני ואסינכרוני — מה OVERLAPPED באמת אומר |
| חלק 3 | I/O completion ports (IOCP) ו-thread pool של .NET — המרתף שמתחת ל-async/await |
| חלק 4 | ה-Cache Manager — מתי ה-WriteFile שלכם באמת מגיע לדיסק? |
| חלק 5 | בתוך NTFS — להבין את מערכת הקבצים דרך ה-MFT |
| חלק 6 | Filter drivers ומיניפילטרים — למה Procmon וסורקי אנטי-וירוס יכולים ליירט I/O |
1. קודם השורה התחתונה
יש שלוש נקודות לתפוס לפני כל דבר אחר.
- ה-I/O של Windows קובע את היעד לפי שם ומוסר את הבקשה כחבילה. מה ש-
CreateFileפותח אינו בהכרח קובץ על הדיסק, וגםC:הוא symbolic link לשם NT device. רוב הבקשות שנשלחות ל-device driver זורמות ב-device stack כ-IRP (I/O Request Packet). יש גם קיצור דרך בשם Fast I/O שלא בונה IRP (פרקים 2 ו-4, וסעיף 5.2).2345 - הפרידו בראש בין “מה שמטפל בבקשה”, “היעד” ו”המצב של פתיחה אחת”. ה-driver object מחזיק טבלה של פונקציות טיפול, ה-device object הוא היעד של בקשה, וה-file object מחזיק את המצב של פתיחה מסוימת. ה-
HANDLEשהאפליקציה מחזיקה הוא הפניה ל-file object (פרק 3).6789 - הוצאת בקשה והשלמתה, סגירת handle ושחרור הפניה — כולם שלבים נפרדים. ה-driver משלב השלמה של IRP, העברה הלאה והחזקה כ-pending. I/O סינכרוני הוא הבטחה שהקריאה לא חוזרת לפני ההשלמה; הוא לא מנגנון I/O אחר. גם cleanup, שקורה כשהה-handle האחרון נסגר, צריך להבדיל מ-close, שקורה כשההפניות נעלמו (פרקים 4 עד 6).310111213
איך לקרוא לפי מטרה
| מה רוצים לדעת | איפה לקרוא |
|---|---|
| התמונה הכוללת של מה שקורה אחרי קריאה ל-API | פרק 1 → פרק 2 (פתרון שמות) → פרק 5 (סבב אחד של ReadFile) |
| ההבדל בין driver object, device object ו-file object לבין ה-IRP | טבלת ההתאמה בפרק 3 → פרק 4 |
| למה קורים handle leaks ו”סגרתי אבל זה עדיין בשימוש” | סעיף 3.3 → פרק 6 |
| איך לאמת את זה על המחשב שלכם | פרק 7. לצפות במרחב השמות עם WinObj וב-I/O עם Process Monitor |
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 40, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. מה באמת עומד מאחורי “הכול נראה כמו קובץ”
2.1. לאן הולך השם שמוסרים ל-CreateFile?
נקודת הכניסה לפתיחת קובץ או device היא שם. נתיב מקומי כמו C:\project\report.csv, נתיב UNC כמו \\server\share\data.csv, וציון device כמו \\.\COM3 — כולם יכולים לעבור לאותו CreateFile.14
מה שתומך בעקביות הזאת הוא מרחב השמות שמנהל ה-Object Manager של ה-kernel. devices עם שם, events, sections של זיכרון משותף ועוד — כולם מטופלים במרחב שמות בצורת עץ. WinObj של Sysinternals מאפשר לבחון את מרחב השמות הזה ישירות.15
flowchart TB
ROOT["\ (שורש מרחב השמות)"]
DEV["\Device<br/>(device objects שה-drivers יוצרים)"]
GLB["\GLOBAL??<br/>(שם שבו חיים השמות הגלובליים הנראים מ-Win32)"]
BNO["\BaseNamedObjects<br/>(named mutexes וכדומה)"]
ROOT --> DEV
ROOT --> GLB
ROOT --> BNO
DEV --> D1["HarddiskVolume3"]
DEV --> D2["Serial0"]
DEV --> D3["Mup (network redirector)"]
GLB --> L1["C: → \Device\HarddiskVolume3"]
GLB --> L2["COM1 → \Device\Serial0"]
GLB --> L3["PhysicalDrive0 → \Device\Harddisk0\DR0"]
איור 1: מרחב השמות של ה-Object Manager (קטע). מה שנמצא תחת \GLOBAL?? הם symbolic links; האובייקטים האמיתיים נמצאים תחת \Device
באיור 1, הסתכלו על שני הצדדים האלה בנפרד.
| צד השם | דוגמה | תפקיד |
|---|---|---|
| שמות שאפליקציות Win32 משתמשות בהן | אותיות כונן, שמות COM ports | נקודת הכניסה מהאפליקציה |
| שמות שה-kernel משתמש בהם | שמות NT device כמו \Device\HarddiskVolume3 |
שמות שמצביעים על device object |
מה שמחבר בין השניים הוא symbolic link. השם שהאפליקציה רואה והשם של האובייקט האמיתי בצד ה-kernel אינם אותו דבר.
2.2. אות כונן היא symbolic link
כדי ש-device יהיה גלוי לאפליקציות Win32, ה-driver משתמש ב-IoCreateSymbolicLink כדי ליצור קישור מ-MS-DOS device name כמו \DosDevices\COM1 לשם NT device.4 C: עובד באותה צורה: הוא קישור ל-device של volume כמו \Device\HarddiskVolume3.
פתרון השם עבור CreateFile("C:\project\report.csv") מתקדם כך.
flowchart TB
A["השם שהאפליקציה העבירה<br/>C:\project\report.csv"]
B["שכבת Win32 ממירה אותו לצורת NT<br/>\??\C:\project\report.csv"]
C["ה-Object Manager מחפש במרחב השמות<br/>ומגלה ש-\??\C: הוא symbolic link"]
D["הולכים אחרי הקישור ומחליפים<br/>\Device\HarddiskVolume3\project\report.csv"]
E["\Device\HarddiskVolume3 מגיע<br/>ל-device object של ה-volume"]
F["את שארית \project\report.csv ה-I/O Manager<br/>פותר על ידי הוצאת IRP_MJ_CREATE<br/>ומשאיר את הפתרון ל-file system driver (NTFS)"]
A --> B
B --> C
C --> D
D --> E
E --> F
איור 2: פתרון השם ב-CreateFile. המחצית הראשונה היא העבודה של ה-Object Manager; אחרי שמגיעים ל-device, המחצית השנייה שייכת ל-file system
להפריד בין השם שמוביל ל-device לבין השם בתוך מערכת הקבצים
במחצית הראשונה של איור 2, נתיב ה-Win32 מומר לצורת NT, וכך הולכים אחרי הקישור ומגיעים ל-device object של ה-volume. את פתרון מה שנשאר, \project\report.csv, משאירים ל-file system driver (NTFS), שאליו ה-I/O Manager מוציא IRP_MJ_CREATE.
ברגע שיודעים איפה הגבול הזה, גם הציונים הבאים נקראים באותה צורה.
\\.\הוא ציון למרחב השמות של ה-devices של Win32.\\.\PhysicalDrive0ו-\\.\COM10מציינים ישירות את הקישור לשם ה-device, בלי לעבור דרך אות כונן.514CONו-NULהם שמות MS-DOS device שמורים. גם כשהם כתובים בתוך נתיב הם יכולים להיפתר לצד ה-device, ולכן אי אפשר להשתמש בהם כשמות קבצים רגילים.5 ההשלכות המעשיות מכוסות ב”MAX_PATH והמלכודות של נתיבים ושמות קבצים ב-Windows”.- בנתיב UNC ה-device שאליו השם נפתר משתנה.
\\server\shareנפתר ל-device של ה-network redirector (\Device\Mup), ומשם לקוח ה-SMB נושא את הבקשה מעל הרשת. למה ההתנהגות שונה בין נתיבים מקומיים לנתיבי UNC אפשר להסיק מההבדל הזה ביעד הפתרון (ראו “מלכודות של כונני רשת ונתיבי UNC”).
\?? ו-\GLOBAL?? הם לא אותו דבר
\?? אינו סתם שם נוסף לספרייה שקיימת. הוא נקודת הכניסה של סדר חיפוש: קודם לחפש ב-DOS device map המקומי של ה-logon session, ואם אין שם כלום — לחפש ב-\GLOBAL??. מה שאיור 1 מצייר הוא רק הצד הגלובלי, \GLOBAL??.
אותיות כונן שנוצרות עם net use או subst נכנסות לצד המקומי. זו הסיבה לכך שהכוננים שאפשר לראות שונים בין logon sessions גם באותו מחשב, ולכך שלפעמים service לא רואה את כונני הרשת של המשתמש.
לסיכום עד כאן: אפשר לטפל בקבצים וב-devices דרך אותו API מפני שהשם מוביל ל-device object, והבקשות שבאות אחר כך יכולות להימסר בפורמט משותף. עכשיו נסדר את האובייקטים שמייצגים את היעד הזה ואת המצב של פתיחה אחת.
3. הדמויות הן שלושה אובייקטים
ראשית נעמיד זה מול זה את מה שרואים בקוד Win32 / .NET ביום-יום ואת המקבילה בצד ה-kernel. בכל פעם שהמינוח בהמשך מתבלבל, חזרו לטבלה הזאת.
| מה רואים בצד Win32 / .NET | המקבילה בצד ה-kernel | במשפט אחד |
|---|---|---|
HANDLE / SafeFileHandle |
רשומה ב-handle table (הפניה ל-file object) | מספר כרטיס שמצביע על “פתיחה אחת” (3.3) |
מצב הפתיחה ש-FileStream מחזיק |
ה-file object | המכל של share mode, flags והמיקום הנוכחי (3.3) |
אות הכונן C: או \\.\COM3 |
ה-device object (התוצאה של פתרון השם) | היעד של הבקשה (פרקים 2 ו-3.2) |
| “ה-NTFS driver”, “ה-disk driver” | ה-driver object | טבלה של פונקציות טיפול, אחת לכל סוג בקשה (3.1) |
קריאה אחת ל-ReadFile / stream.Read |
בדרך כלל IRP אחד | חבילת הבקשה שמובלת ליעד (פרק 4). קריאה או כתיבה סינכרונית של קובץ שנמצא ב-cache מטופלת בקיצור דרך בשם Fast I/O, שלא בונה IRP (5.2). אלה השורות שמתחילות ב-FASTIO_ ב-Procmon |
FileOptions / flags של CreateFile |
מאפיינים שנרשמים על ה-file object | הם קובעים read-ahead והתנהגות אסינכרונית (טבלת ההתאמה בפרק 7) |
הערך של GetLastError / חריגה ב-.NET |
NTSTATUS (כמו STATUS_PENDING) |
סטטוס ההשלמה. הוא מתורגם לקוד שגיאה של Win32 בדרך למעלה |
הטבלה הזאת נועדה לעזור להבין את ההתאמה. היא לא אומרת שתמיד זורם IRP אחד ויחיד מ-ReadFile עד הדיסק. המסלול שלא בונה IRP מתואר בסעיף 5.2, והגבול שבו נשלח IRP תחתון נפרד — בסעיף 4.2.
3.1. ה-driver object — טבלה של פונקציות טיפול
כשה-driver נטען, ה-I/O Manager יוצר driver object (DRIVER_OBJECT) שמייצג את ה-driver הזה.6
מה שמפתחי אפליקציות רוצים לדעת עליו הוא המערך MajorFunction. זו טבלה שממפה “סוג בקשה” ל”פונקציית טיפול”, וסוג הבקשה נקרא major function code. הבולטים הם IRP_MJ_CREATE (פתיחה), IRP_MJ_READ (קריאה), IRP_MJ_WRITE (כתיבה), IRP_MJ_CLEANUP ו-IRP_MJ_CLOSE.16
ביטוי של הקשרים בלבד ב-C# נראה כך. זו לא דוגמת מימוש אלא תרשים מושגי להבנת המבנה של C בתוך ה-kernel.
// תרשים מושגי. במציאות זהו struct של C בתוך ה-kernel
class DriverObject
{
// IRP_MJ_XXX הוא האינדקס. 28 סוגים בסך הכול
public DispatchRoutine[] MajorFunction = new DispatchRoutine[28];
}
// ב-ntfs.sys, MajorFunction[IRP_MJ_READ] מחזיק את "מטפל הקריאה של NTFS"
3.2. ה-device object — היעד של בקשה
ה-driver יוצר device object (DEVICE_OBJECT) לכל device שהוא מטפל בו. זהו היעד של בקשת I/O.7
אבל זה לא בהכרח אחד-לאחד עם device פיזי. ישות לוגית כמו volume (HarddiskVolume3), וגם device ש-filter driver יוצר כדי שיוכל להתערב — שניהם מיוצגים על ידי האובייקט הזה.
ה-device object מצביע על ה-driver object שיצר אותו. ולכן היחס הוא שברגע שהיעד נקבע, גם הטבלה של הפונקציות שיעבדו את הבקשה נקבעת.
3.3. ה-file object — המצב של “פתיחה אחת”
בכל פעם ש-CreateFile מצליח, ה-kernel יוצר file object אחד. מה שהוא מייצג אינו הקובץ שעל הדיסק עצמו אלא המצב של פתיחה מסוימת של קובץ או device.8
אם פותחים את אותו קובץ פעמיים, מקבלים שני file objects. ה-file pointer הנוכחי של handle סינכרוני, וה-share mode וה-flags שניתנו בזמן הפתיחה, שייכים כל אחד למצב הפתיחה שלו. ה-HANDLE שהאפליקציה מקבלת הוא הפניה ל-file object הזה, שמגיעים אליה דרך ה-handle table של התהליך.9
flowchart LR
subgraph P["תהליך (user mode)"]
H1["HANDLE 0x1A4"]
H2["HANDLE 0x1B8"]
end
subgraph K["מרחב ה-kernel"]
FO1["file object 1<br/>report.csv נפתח לקריאה<br/>offset נוכחי 4096"]
FO2["file object 2<br/>report.csv נפתח להוספה בסוף<br/>offset נוכחי 65536"]
DO["device object<br/>מקביל ל-HarddiskVolume3"]
DR["driver object NTFS<br/>MajorFunction = טבלת פונקציות הטיפול"]
end
H1 --> FO1
H2 --> FO2
FO1 --> DO
FO2 --> DO
DO --> DR
איור 3: היחס בין שלושת האובייקטים. handle מצביע על file object דרך ה-handle table, ה-file object מוביל ל-device, וה-device מוביל ל-driver
“לפתוח את אותו קובץ פעמיים” ו”לשכפל handle” הם דברים שונים
| פעולה | file object | file pointer |
|---|---|---|
| פתיחה נפרדת של אותו קובץ | נוצר אחד לכל פתיחה | הם בלתי תלויים |
שכפול handle עם DuplicateHandle |
הם מפנים לאותו אובייקט | הוא משותף |
גם ב-handles שמצביעים על אותו קובץ, המצב שמשותף ביניהם תלוי בשאלה אם הקובץ נפתח מחדש או שה-handle שוכפל.
גם handle leaks והפרות שיתוף נובעים ממצב הפתיחה
כשה-file handle דולף, ה-file object והמשאבים שמעבר לו נשארים בהפניה. מה שסופרים בחקירה הוא רשומות ב-handle table. המידע ש-Process Explorer ו-handle.exe מציגים תואם לזה (ראו “Process Explorer / Handle / VMMap בפרקטיקה” ואת תיאור החקירה “חקירת קריסה בהרצה ארוכה במצלמה תעשייתית - מהדורת handle leak”).
הפרת שיתוף (sharing violation) נקבעת בהשוואה בין ה-share mode של מצב פתיחה קיים לבין הבקשה של CreateFile חדש. הצד המעשי של בקרת הבלעדיות מכוסה ב”יסודות בקרת הבלעדיות בשילוב קבצים”.
4. ה-IRP — בקשת I/O הופכת לחבילה
4.1. למה לארוז את זה כחבילה?
ה-I/O Manager אורז בקשות כמו פתיחה, קריאה וכתיבה ל-IRP (I/O Request Packet) ומוסר אותו ל-driver. רוב הבקשות ל-device driver מקבלות את הצורה הזאת.3
הסיבה לשימוש בחבילה היא לאפשר להפריד בין הוצאת הבקשה לבין השלמתה. devices כמו דיסקים לא רצים במהירות של ה-CPU. אם אפשר להחזיק את הבקשה כחבילה עצמאית, אפשר להשלים אותה ברגע מאוחר יותר, אחרי שהוצאה.2
גם בתוך IRP יש הפרדה בין מידע על הבקשה כולה לבין הוראות לכל driver.17
| אזור | מה הוא מחזיק |
|---|---|
| Header | מידע על הבקשה כולה |
| I/O stack locations | סוג הבקשה והפרמטרים לכל driver שהבקשה עוברת דרכו |
ה-I/O stack locations מסודרים לפי מספר ה-drivers שהבקשה צפויה לעבור דרכם. כל driver קורא את האזור שלו ומבין איזו פעולה מתבקשת ממנו.
4.2. לרדת ב-device stack
אוסף של device objects מונחים זה על זה נקרא device stack.18 בקריאה של קובץ בדיסק מקומי, הבקשה עוברת בערך במסלול הבא.
flowchart TB
IOM["ה-I/O Manager בונה את ה-IRP<br/>(IRP_MJ_READ + stack locations)"]
subgraph FSSTACK["ה-stack בצד מערכת הקבצים"]
FLT["file system filters<br/>(אנטי-וירוס, הצפנה, Procmon וכדומה)"]
NTFS["NTFS<br/>מתרגם offset בתוך הקובץ למיקום על ה-volume"]
end
subgraph STSTACK["ה-stack בצד האחסון"]
VOL["ניהול volume ומחיצות<br/>(volmgr וכדומה)"]
DISK["disk class driver<br/>(disk.sys)"]
PORT["storage port ומיניפורט<br/>(storport וכדומה)"]
end
HW[("התקן דיסק")]
IOM --> FLT
FLT --> NTFS
NTFS --> VOL
VOL --> DISK
DISK --> PORT
PORT --> HW
איור 4: הדרך שבקשת קריאה עוברת. צד מערכת הקבצים וצד האחסון הם שני device stacks נפרדים, ו-NTFS מוציא IRP תחתון חדש שמופנה לצד האחסון כדי לבצע את העבודה
filters הם נקודת הרחבה שמערכת ההפעלה מספקת
אנטי-וירוס יכול לבדוק I/O של קבצים מפני שהוא משתמש במנגנון שמערכת ההפעלה מספקת רשמית כדי להתערב באמצע בקשה.19 גם Process Monitor רושם I/O מהמיקום הזה.
בחקירה של “גישה לקבצים איטית רק במחשב הזה”, ה-filters שיושבים על המסלול הזה הם המועמדים הראשונים לבדיקה. חלק 6 מכסה זאת בפירוט.
IRP אחד לא עובר עד הסוף אל הדיסק
האפליקציה מבקשת “8KB מ-offset 4096 של הקובץ הזה”. NTFS ממפה את זה ל-clusters על ה-volume, וה-stack של האחסון ממפה את זה ל-sectors על הדיסק. שכבה עליונה יכולה להוציא את הבקשה שלה בלי לדעת את הפרטים של השכבה שמתחתיה.
מה שחשוב כאן הוא שצד מערכת הקבצים וצד האחסון הם stacks נפרדים. כדי לעבד IRP שמופנה לקובץ, NTFS יוצר ומוציא IRP תחתון חדש שמופנה ל-volume. אותו IRP אחד ויחיד לא נמסר ישר מהאפליקציה לדיסק.
בקובץ מפוצל (fragmented), קריאה אחת יכולה אפילו להתפצל לכמה IRPs תחתונים. את הגרנולריות ואת תוחלת החיים של בקשה צריך לחשוב בנפרד לכל שכבה.
4.3. לכל driver יש שלוש אפשרויות
מה ש-driver עושה עם IRP שהוא מקבל אפשר בדרך כלל למיין לשלוש אפשרויות.310
| פעולה | ההתנהגות העיקרית | דוגמה |
|---|---|---|
| להשלים בעצמו | להשלים עם IoCompleteRequest |
לספק את הבקשה מ-cache מקומי |
| להעביר ל-driver שמתחתיו | לשלוח ל-device הבא עם IoCallDriver |
filter בודק ומעביר הלאה |
| להחזיק כ-pending | להחזיר STATUS_PENDING ולהשלים מאוחר יותר |
להמתין לתגובת החומרה |
flowchart TB
RECV["driver מקבל IRP"]
Q{"איך לטפל בבקשה הזאת"}
DONE["(1) להשלים בעצמו<br/>לקרוא ל-IoCompleteRequest<br/>למשל: לענות מיד עם נתונים מה-cache"]
PASS["(2) להעביר ל-device שמתחתיו<br/>לקרוא ל-IoCallDriver<br/>למשל: filter בודק ומעביר הלאה"]
PEND["(3) להחזיק כ-pending<br/>להחזיר STATUS_PENDING ולהכניס את ה-IRP ל-queue<br/>למשל: המתנה לתגובת החומרה"]
LATER["IoCompleteRequest מאוחר יותר,<br/>בעקבות interrupt וכדומה"]
UP["ההשלמה עולה חזרה במעלה ה-stack בסדר הפוך<br/>(נקראת ה-completion routine של כל שכבה)"]
RECV --> Q
Q --> DONE
Q --> PASS
Q --> PEND
PASS -->|"השכבה שמתחת השלימה במקום"| UP
PASS -->|"השכבה שמתחת החזיקה כ-pending<br/>(pending מתפשט לקורא)"| LATER
PEND --> LATER
DONE --> UP
LATER --> UP
איור 5: שלוש האפשרויות של driver שמקבל IRP. השלוש אינן בלעדיות; הדרך הרגילה ביותר היא “הועבר למטה ואז הוחזק כ-pending איפשהו בהמשך”, וכולן נגמרות בהשלמה דרך IoCompleteRequest
“העברה הלאה” ו”pending” חופפים באותה בקשה
השלוש אינן אפשרויות בלעדיות. המקרה האופייני הוא ש-driver עליון מעביר את הבקשה למטה, ואיפשהו בהמשך היא נעשית pending. במקרה כזה STATUS_PENDING עובר חזרה דרך ה-drivers שביניהם אל הקורא.
כששכבה תחתונה משלימה את הבקשה מאוחר יותר, כל שכבה שרשמה completion routine בזמן שהעבירה את הבקשה נקראת חזרה בסדר הפוך. את הפעולה של הורדת בקשה למטה ואת עיבוד ההשלמה שמחזיר את התוצאה למעלה יש לקרוא כשני דברים נפרדים.
השילוב הזה הוא הבסיס למנגנונים שמכוסים בהמשך הסדרה. אם ה-cache יכול להשלים בקשה מיד, זה מהיר יותר (חלק 4), ואפשר להעמיס filters בדרך (חלק 6). ומכיוון שאפשר להפריד בין pending להשלמה, I/O אסינכרוני מאפשר ל-thread לעשות עבודה אחרת בזמן שממתינים ל-device איטי (חלקים 2 ו-3).
5. לעקוב אחרי סבב אחד של ReadFile
כאן נעקוב אחרי ReadFile במקרה שהוא מחטיא את ה-cache וצריך ללכת לדיסק. באיור, הסתכלו קודם ברגל היציאה שמוציאה את הבקשה, ואחר כך ברגל החזרה אחרי שהנתונים נקראו.
sequenceDiagram
participant App as thread של האפליקציה
participant IOM as I/O Manager
participant FS as filters + NTFS
participant ST as storage stack
participant HW as התקן דיסק
App->>IOM: ReadFile → NtReadFile (system call)
Note over IOM: לפתור את ה-file object מה-handle<br/>ולבנות את ה-IRP (IRP_MJ_READ)
IOM->>FS: IoCallDriver (לראש ה-stack)
FS->>ST: לתרגם את המיקום ל-volume ולהוציא<br/>IRP תחתון שמופנה ל-storage stack
ST->>HW: להוציא את פקודת הקריאה
ST-->>FS: STATUS_PENDING (ה-IRP התחתון ב-pending)
FS-->>IOM: גם ה-IRP המקורי חוזר כ-pending<br/>(כאן נגמרת רגל היציאה)
Note over App: I/O סינכרוני ישן כאן בהמתנה להשלמה<br/>I/O אסינכרוני מחזיר שליטה ויכול לעשות עבודה אחרת
HW-->>ST: interrupt, "הנתונים נקראו"
Note over ST: מ-interrupt service routine (ISR)<br/>עיבוד ההשלמה ממשיך ב-DPC
ST->>FS: להשלים את ה-IRP התחתון (IoCompleteRequest)<br/>ה-completion routine בצד NTFS מקבל אותו
Note over FS: אחרי שכל ה-IRPs התחתונים<br/>הנדרשים הושלמו
FS->>IOM: להשלים את ה-IRP המקורי (IRP_MJ_READ)
Note over IOM: להריץ את ה-completion routine של כל שכבה בסדר הפוך<br/>ולקבע את התוצאה עם APC ל-thread המבקש
IOM->>App: הסטטוס ומספר הבתים סופיים (סימון event וכדומה)
איור 6: סבב אחד של ReadFile שהחטיא את ה-cache. השלמת ה-IRP התחתון והשלמת ה-IRP המקורי הם שני שלבים נפרדים, וגם רגל היציאה ורגל החזרה מתקדמות כאירועים נפרדים
5.1. רגל היציאה ורגל החזרה הן אירועים נפרדים
קו הגבול באיור 6 הוא STATUS_PENDING. אחרי שה-storage driver מוציא פקודה לחומרה, הוא מחזיק את הבקשה כ-pending וחוזר. שם נגמרת רגל היציאה, ועיבוד ההשלמה של רגל החזרה מתקדם מאוחר יותר, בעקבות interrupt וכדומה.10
| שלב | מה קורה |
|---|---|
| הוצאה | למצוא את היעד מה-handle ולשלוח את ה-IRP המקורי ואת ה-IRPs התחתונים הנדרשים |
| pending | כל עוד החומרה לא סיימה, להחזיק את הבקשה כלא-מושלמת |
| השלמה תחתונה | אחרי שהקריאה נגמרת, ה-IRP התחתון בצד האחסון מושלם |
| השלמת הבקשה המקורית | כשכל ה-IRPs התחתונים הנדרשים הושלמו, גם ה-IRP המקורי מושלם, והסטטוס ומספר הבתים נעשים סופיים |
ההבדל בין סינכרוני לאסינכרוני הוא מתי הקורא חוזר
אין בתוך ה-kernel מנגנון נפרד שמיועד ל-I/O סינכרוני. I/O סינכרוני הוא ההבטחה ש”הקריאה לא חוזרת לפני ההשלמה”. ההמתנה להשלמה שמתוארת כאן מתרחשת כשבקשה נעשית pending; בקשה שאפשר להשלים במקום יכולה להחזיר את התוצאה שלה בלי להמתין להשלמה מאוחרת.11
ב-Win32 בוחרים בין טיפול סינכרוני לאסינכרוני עם FILE_FLAG_OVERLAPPED בזמן פתיחת ה-handle. עם זאת, שימו לב שה-structure של OVERLAPPED עצמו נדרש פעם אחת לכל פעולה שנמצאת בדרך. החזיקו את “ההגדרה על ה-handle” ואת “המצב של פעולה בודדת” כשני רעיונות נפרדים.
ברגע שרואים את ההבדל הזה, אפשר להעביר לחלק 2 את “למה אסינכרוני או לא נקבע כשפותחים את ה-handle” ואת “מה המשמעות של משהו שאמור להיות אסינכרוני ולהסתיים במקום”. הסיבה ש-async/await של .NET לא צריך להחזיק thread רק כדי להמתין ל-I/O טמונה גם היא בהפרדה הזאת בין הוצאה להשלמה (ראו “טבלת החלטות מעשית ל-async/await ב-C#”).
5.2. יש גם יוצאים מן הכלל — קיצור הדרך שלא בונה IRP
לא כל I/O הופך ל-IRP. כשקוראים או כותבים באופן סינכרוני נתוני קובץ שנמצאים ב-cache, יש קיצור דרך בשם Fast I/O שמעתיק ישירות אל ה-cache וממנו בלי לבנות IRP.
כשמפעילים advanced output ב-Procmon, השורות בעמודת Operation שמתחילות ב-FASTIO_ תואמות למסלול הזה. התנאים שבהם אי אפשר להשתמש בקיצור הדרך, והיחס שלו ל-Cache Manager, מכוסים בחלק 4.
לכן בחקירה מתחשבים בגם “המסלול הבסיסי שמשתמש ב-IRP” וגם “קיצור הדרך שלא בונה IRP”. לא מסיקים ש-IRP נוצר בוודאות רק מפני שנקרא ל-ReadFile.
6. מאחורי CloseHandle — cleanup ו-close הם דברים שונים
גם סגירה של מה שפתחנו צריכה להתחלק לשלבים. מה ש-CloseHandle עושה הוא להסיר רשומה אחת מה-handle table של התהליך.
ל-file object יש handle count, שהוא מספר ה-handles, ו-reference count, שעוקב אחרי הפניות בתוך ה-kernel. הרגע שבו ה-handles נעלמים והרגע שבו כל הפניה לאובייקט נעלמת אינם בהכרח אותו רגע.
sequenceDiagram
participant App as אפליקציה
participant OB as Object Manager
participant IOM as I/O Manager
participant FS as מערכת הקבצים
App->>OB: CloseHandle(h)
Note over OB: להסיר את הרשומה מה-handle table<br/>ולהקטין את ה-handle count
alt זה היה ה-handle האחרון
IOM->>FS: IRP_MJ_CLEANUP
Note over FS: לבטל I/O שלא הסתיים ולשחרר locks<br/>עבור ה-file object הזה
end
Note over OB: אבל אם נשארו הפניות בתוך ה-kernel, כמו<br/>I/O שלא הסתיים או section (memory mapping),<br/>ה-file object עוד חי
alt גם ה-reference count הגיע לאפס
IOM->>FS: IRP_MJ_CLOSE
Note over FS: הסדר האחרון של ה-file object מושלם<br/>רק עכשיו הוא באמת סגור
end
איור 7: שני השלבים — cleanup (ה-handle האחרון נסגר) ו-close (גם כל ההפניות נעלמו)
6.1. להבדיל בין ה-handle האחרון להפניה האחרונה
| הודעה | מה קרה | מה עוד יכול להישאר |
|---|---|---|
IRP_MJ_CLEANUP |
ה-handle האחרון על ה-file object הזה נסגר | הפניות מ-I/O שלא הסתיים וכדומה |
IRP_MJ_CLOSE |
ה-reference count של ה-file object הגיע לאפס | ה-file object נמצא בשלב של שחרור |
התיעוד של IRP_MJ_CLEANUP קובע במפורש שאם נשארו בקשות I/O שלא הסתיימו, ייתכן שה-file object עוד לא משוחרר.12 IRP_MJ_CLOSE נשלח אחר כך, אבל לא בהכרח מיד אחרי ה-cleanup.13
6.2. בחקירת “בשימוש”, להסתכל גם מעבר ל-handle
ל-memory mapping יש תוחלת חיים משלו, נפרדת מה-file handle. כל עוד ה-section הממופה מחזיק הפניה ל-file object, סגירת ה-handle המקורי לא משחררת אותו. בדקו שגם ה-view שוחרר (unmapped) וגם ה-section נסגר (ראו “מלכודות של זיכרון משותף ופרקטיקות מומלצות”).
לא למצוא handle לא אומר שהחקירה נגמרה. הפניה בתוך ה-kernel, כמו image ממופה, יכולה להחזיק את הקובץ. זו הסיבה שהחיפוש של Process Explorer מכסה לא רק handles אלא גם DLLs (קבצים ממופים).
גם ב-.NET, להיסגר בסופו של דבר ולהיסגר ברגע שבו צריך את זה סגור הם שני דברים שונים. אם מסתמכים על SafeFileHandle או על finalizer שיסגרו מתישהו, handle ששכחתם לסגור מאחר את ה-cleanup ומאריך הפרות שיתוף ו-locks מוחזקים.
7. לראות את זה במו עיניכם
אפשר לצפות במרחב השמות ובזרימת ה-I/O במחשב Windows שבו יש לכם הרשאות מנהל. התחילו בקובץ מקומי ובטוח ועקבו אחרי פעולות הפתיחה, הקריאה, הכתיבה והסגירה.
7.1. להכין את מה שצריך לתצפית
| מה צריך | הכנה ואזהרות |
|---|---|
| הרשאות מנהל | Process Monitor טוען driver של kernel, ולכן מריצים אותו כמנהל. גם ב-WinObj חלק מהאובייקטים בלתי נראים אם אינכם מנהל |
| כלי Sysinternals | השתמשו ב-WinObj וב-Process Monitor, שמופצים בחינם על ידי Microsoft. אפשר להשיג אותם יחד ב-Sysinternals Suite. אפשר לחלץ את הארכיון ולהריץ את ה-exe; אין צורך ב-installer |
| פעולה לתצפית | שמירה של קובץ אחד ב-Notepad, או העתקה של קובץ קטן אחד, מספיקה. אל תשתמשו בתיקייה עסקית משותפת או במכונת production; נסו על הדיסק המקומי שלפניכם |
7.2. להשתמש ב-WinObj כדי לראות את הקישור מהשם לאובייקט האמיתי
פתחו את הספרייה GLOBAL?? ב-WinObj ותוכלו לאמת ש-C: הוא symbolic link ל-\Device\HarddiskVolumeN. אחר כך הסתכלו תחת \Device ותראו את השמות האמיתיים של ה-device objects שה-drivers יצרו. זה הנוהל לבדוק את השמות והקישורים מאיור 1 מול מחשב אמיתי.15
7.3. להשתמש ב-Procmon כדי להבדיל בין IRPs ל-Fast I/O
הפעילו Filter > Enable Advanced Output ב-Process Monitor ועמודת Operation תשתנה מתצוגות כמו ReadFile לאוצר המילים של צד ה-kernel: IRP_MJ_READ ו-FASTIO_READ.
בהעתקת קובץ, חפשו את הפעולות שמתוארות במאמר הזה: IRP_MJ_CREATE → FASTIO_READ / IRP_MJ_READ → IRP_MJ_WRITE → IRP_MJ_CLEANUP → IRP_MJ_CLOSE. עקבו אחריהן כשמסלול הקריאה והשלבים מ-cleanup ל-close בראש, וה-trace מפסיק להיות סתם רשימה של שמות פעולות.
השימוש המעשי ב-Procmon מרוכז ב”מדריך מעשי ל-Process Monitor (ProcMon)”.
7.4. למפות אפשרויות של .NET ל-flags של Win32
ב-Windows, ה-FileStream של C# פותח בפנים handle עם CreateFileW ומחזיק אותו כ-SafeFileHandle. ה-FileOptions שמועברים ל-constructor ממופים ל-flags של Win32 כך.
.NET (FileOptions) |
Win32 (flag של CreateFile) |
משמעות (החלק הרלוונטי) |
|---|---|---|
Asynchronous |
FILE_FLAG_OVERLAPPED |
לפתוח את ה-handle ל-I/O אסינכרוני (חלקים 2 ו-3) |
WriteThrough |
FILE_FLAG_WRITE_THROUGH |
לא לתת ל-cache לעכב את הכתיבה (חלק 4) |
SequentialScan |
FILE_FLAG_SEQUENTIAL_SCAN |
רמז ל-read-ahead (חלק 4) |
RandomAccess |
FILE_FLAG_RANDOM_ACCESS |
רמז לדיכוי read-ahead (חלק 4) |
DeleteOnClose |
FILE_FLAG_DELETE_ON_CLOSE |
למחוק כשהה-handle האחרון נסגר (יישום של המנגנון בפרק 6) |
ב-.NET 6 ואילך אפשר גם להשתמש ב-File.OpenHandle ובמחלקה RandomAccess כדי לעבוד בצורה של “handle ועוד I/O עם offset מפורש”, בלי FileStream באמצע. הבנת הצד הימני של הטבלה הזאת מאפשרת לבחור את האפשרויות בצד השמאלי לפי ההתנהגות שרוצים.
8. סיכום
את ה-I/O של Windows אפשר לסדר לפי הסדר הזה: פתרון השם → מצב הפתיחה → הוצאת הבקשה → ההשלמה → שחרור ההפניה.
- לפתור את השם ולקבוע את היעד.
C:הוא symbolic link,\\.\הוא ציון למרחב השמות של ה-devices של Win32, ו-UNC נפתר ל-redirector. היסוד שמאפשר לטפל בקבצים וב-devices דרך אותו API הוא העקביות הזאת של פתרון השמות.45 - להפריד בין שלושת האובייקטים. ה-driver הוא טבלה של פונקציות טיפול, ה-device הוא היעד, וה-file object הוא המצב של פתיחה מסוימת.
HANDLEמפנה ל-file object הזה.6789 - IRP נושא את הבקשה, ואפשר להתקדם בהשלמה ברגע אחר. רוב הבקשות נשלחות ל-device stack כ-IRPs, וכל driver משלב השלמה, העברה הלאה והחזקה כ-pending. תוחלת החיים של IRP תחתון ושל ה-IRP המקורי נפרדות, ועם Fast I/O יש גם מסלול שלא בונה IRP.231810
- לחשוב על I/O סינכרוני ואסינכרוני דרך היחס בין הוצאה להשלמה. I/O סינכרוני הוא ההבטחה לא לחזור לפני ההשלמה. בבקשה שנעשתה pending, ההוצאה וההשלמה שמתקדמת מאוחר יותר, בעקבות interrupt וכדומה, הם אירועים נפרדים.1110
- להחזיר handle אינו אותו דבר כמו לשחרר את האובייקט. cleanup הוא השלב שבו ה-handle האחרון נעלם, ו-close הוא השלב שבו ההפניה האחרונה נעלמה. ההבחנה הזאת עוזרת בחקירת “סגרתי אבל זה עדיין בשימוש”.1213
המשך בחלק 2: “I/O סינכרוני ואסינכרוני — מה OVERLAPPED באמת אומר”. הוא מכסה את FILE_FLAG_OVERLAPPED, שהוא הדרך שבה אפליקציה משתמשת בהפרדה הזאת בין הוצאה להשלמה, את ארבע הדרכים לקבל הודעות השלמה, ביטול, ואת התנאים שבהם “משהו שאמור להיות אסינכרוני חוזר סינכרונית”.
מאמרים קשורים
- מדריך מעשי ל-Process Monitor (ProcMon) — לאתר ב-10 דקות “ההגדרה לא נקראת” ו-“ACCESS DENIED”
- Process Explorer / Handle / VMMap בפרקטיקה — לרדוף אחרי hangs, leaks ו”הקובץ בשימוש” מתוך המצב ברגע זה
- חקירת קריסה בהרצה ארוכה במצלמה תעשייתית - מהדורת handle leak
- יסודות בקרת הבלעדיות בשילוב קבצים - best practices ל-file locking ו-atomic claim
- מלכודות של זיכרון משותף ופרקטיקות מומלצות
- טבלת החלטות מעשית ל-async/await ב-C# - Task.Run ו-ConfigureAwait
- מלכודות של כונני רשת ונתיבי UNC — לעבוד עם שרתי קבצים (תיקיות משותפות) באפליקציות עסקיות
- MAX_PATH והמלכודות של נתיבים ושמות קבצים ב-Windows — מגבלת 260 התווים, שמות שמורים, נקודות בסוף, ורישיות
תחומי ייעוץ קשורים
KomuraSoft LLC עוסקת בתכנון ובחקירת תקלות סביב I/O של קבצים באפליקציות Windows עסקיות, כולל handle leaks, “הקובץ בשימוש”, והאטות I/O בסביבות מסוימות.
- פיתוח אפליקציות Windows
- חקירת תקלות וניתוח שורש הבעיה
- שימוש חוזר בנכסים קיימים ותמיכה במיגרציה
- יצירת קשר
מקורות
-
Microsoft Learn, Windows Internals - Sysinternals. העמוד שמציג את הספר Windows Internals (שיצא לאור ביפנית בשם Inside Windows). זהו הספר התקני על המבנה הפנימי של Windows, כולל ארכיטקטורת ה-kernel ומערכת ה-I/O, ונקודת פתיחה ללימוד מעמיק יותר של הנושאים שהסדרה הזאת מכסה. ↩
-
Microsoft Learn, I/O manager. על כך ש-I/O Manager במצב kernel של Windows מנהל את התקשורת בין האפליקציות לבין הממשקים שמספקים device drivers; שמכיוון ש-devices לא רצים במהירויות שמתאימות למערכת ההפעלה, התקשורת בין מערכת ההפעלה ל-drivers נעשית בעיקר דרך IRPs (I/O request packets); וש-IRP מועבר ממערכת ההפעלה ל-driver ומ-driver ל-driver בצורה שמזכירה packet ברשת או הודעה של Windows. ↩ ↩2 ↩3
-
Microsoft Learn, I/O request packets. על כך שרוב הבקשות שנשלחות ל-device driver נארזות ל-IRP; שרכיבי מערכת ההפעלה וה-drivers שולחים IRP ל-driver עם IoCallDriver (שמקבל מצביע ל-device object ומצביע ל-IRP); ש-IRP מעובד בדרך כלל על ידי כמה drivers שמונחים כ-device stack ונשלח ראשית ל-device object שבראש ה-stack; ושכל driver יכול לבחור אם לעבד ולהשלים את ה-IRP או להעביר אותו ל-driver שמתחתיו. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Introduction to MS-DOS device names. על כך ש-MS-DOS device name הוא symbolic link לשם device בסגנון NT; שאפליקציות Windows במצב user ניגשות ל-devices בשמות MS-DOS device (אותיות כונן ושמות COM ports) בעוד ש-drivers וה-kernel משתמשים בשמות בסגנון NT; וש-driver יוצר symbolic link מ-\DosDevices\שם אל device עם IoCreateSymbolicLink. ↩ ↩2 ↩3
-
Microsoft Learn, Naming files, paths, and namespaces. על כך ש-CON, PRN, AUX, NUL, COM1 עד COM9 ו-LPT1 עד LPT9 שמורים כשמות קבצים; שבמרחב השמות של Win32 יש “file namespace” ו-“device namespace”, כשהקידומת “\\.\” פירושה גישה למרחב השמות של ה-devices של Win32 (למשל \\.\PhysicalDrive0); ועל הכללים לפתרון השמות האלה. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Introduction to driver objects. על כך שה-I/O Manager יוצר מבנה DRIVER_OBJECT כשנטען driver; שה-driver object מחזיק את נקודות הכניסה לשגרות הסטנדרטיות של ה-driver (כולל מערך MajorFunction, שהוא dispatch table); ושהמהנדס של ה-I/O Manager משתמש בטבלה הזאת כדי לקרוא לפונקציית הטיפול המתאימה לבקשה. ↩ ↩2 ↩3
-
Microsoft Learn, Introduction to device objects. על כך שמבנה DEVICE_OBJECT מייצג device לוגי, וירטואלי או פיזי ונעשה היעד (target) של בקשת I/O; ש-driver יוצר device object עם IoCreateDevice; ושה-device object מקושר ל-driver (ה-driver object) שיצר אותו. ↩ ↩2 ↩3
-
Microsoft Learn, Using files in a driver. על כך שב-kernel ה-file object מייצג מופע של קובץ (או device) שנפתח; ושבכל פתיחה של קובץ נוצר file object שמחזיק את ההקשר של אותה פתיחה, כמו ה-byte offset הנוכחי. ↩ ↩2 ↩3
-
Microsoft Learn, File handles. על כך שה-file handle ש-CreateFile מחזיר הוא ספציפי לתהליך ומקושר ל-file object שנפתח; שפתיחה של אותו קובץ כמה פעמים נותנת בכל פעם handle נפרד (ומצב פתיחה נפרד); ושכשה-handle כבר לא נדרש יש לסגור אותו עם CloseHandle. ↩ ↩2 ↩3
-
Microsoft Learn, Completing IRPs. על כך שהקריאה ל-IoCompleteRequest היא שמשלימה פעולת I/O; שה-IoCompletion routines שנרשמו על ידי ה-drivers שמעל בסדר הפוך נקראים בתורם בהשלמה; ועל הזרימה שבה השלמת בקשה מתרחשת במועד שונה מהוצאתה, והסטטוס נמסר לבסוף למבקש. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Synchronous and asynchronous I/O. על כך שב-I/O סינכרוני הפונקציה לא חוזרת עד שה-I/O מושלם וה-thread מומתין, בעוד שב-I/O אסינכרוני (overlapped) הפונקציה שמוציאה את הבקשה חוזרת מיד וה-thread יכול להמשיך בעבודה אחרת; שעבור I/O אסינכרוני יש לפתוח את ה-handle עם FILE_FLAG_OVERLAPPED; ושיש כמה דרכים לקבל הודעה על ההשלמה. ↩ ↩2 ↩3
-
Microsoft Learn, IRP_MJ_CLEANUP. על כך שקבלת הבקשה הזאת מעידה שה-handle האחרון ל-file object שמשויך ל-device object שאליו היא מכוונת נסגר; שעם זאת ייתכן שה-file object עוד לא משוחרר בגלל בקשות I/O שלא הושלמו; ושהבקשה הזאת נשלחת בהקשר של התהליך שסגר את ה-handle. ↩ ↩2 ↩3
-
Microsoft Learn, IRP_MJ_CLOSE. על כך שקבלת הבקשה הזאת מעידה שה-reference count של ה-file object הגיע לאפס וה-file object עומד להשתחרר; ושהבקשה נשלחת אחרי בקשת ה-cleanup, אבל לא בהכרח מיד אחריה, כי היא ממתינה להשלמה של I/O שלא הסתיים. ↩ ↩2 ↩3
-
Microsoft Learn, CreateFileW function. על כך ש-CreateFile יכול לפתוח ולהחזיר handle לא רק לקבצים אלא גם ל-devices כמו דיסקים פיזיים, volumes, קונסולה, יציאות תקשורת (COM ports) ו-pipes; שמשתמשים בשמות בצורת “\\.\” כשפותחים device; ועל המשמעות של ה-flags השונים, החל מ-FILE_FLAG_OVERLAPPED. ↩ ↩2
-
Microsoft Learn, WinObj - Sysinternals. על כך ש-WinObj הוא כלי שמציג את מרחב השמות של NT Object Manager ומאפשר לעיין באובייקטים שבו, כולל device objects ו-symbolic links. ↩ ↩2
-
Microsoft Learn, IRP major function codes. רשימה של major function codes של IRP (IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_CLEANUP, IRP_MJ_CLOSE, IRP_MJ_DEVICE_CONTROL, IRP_MJ_PNP וכדומה), מה כל בקשה אומרת, ואילו drivers אמורים לטפל בה. ↩
-
Microsoft Learn, I/O stack locations. על כך שה-I/O Manager מספק I/O stack location בתוך IRP לכל driver בשרשרת של drivers בשכבות; שכל stack location מחזיק את ה-major וה-minor function codes ואת הפרמטרים של אותה בקשה; ושכל driver משיג את ה-stack location שלו עם IoGetCurrentIrpStackLocation כדי לדעת מה מתבקש ממנו. ↩
-
Microsoft Learn, Device nodes and device stacks. על כך ש-device objects מונחים זה על זה ויוצרים device stack; ש-IRP נשלח ראשית ל-device object שבראש ה-stack ואחר כך מעובד או מועבר למטה בכל שכבה; ושגם ה-device objects של filter drivers קיימים כשהם מוכנסים לתוך ה-stack. ↩ ↩2
-
Microsoft Learn, Filter Manager Concepts. על כך שה-filter manager הוא driver במצב kernel שמגיע עם Windows; שמיניפילטר drivers יכולים להתערב בבקשות I/O למערכת הקבצים עם callbacks של pre ו-post; ושהמיקום שבו כל מיניפילטר מתערב (ה-altitude שלו) קובע את סדרו בתוך ה-I/O stack. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מעמקי ה-I/O של Windows (חלק 2) — I/O סינכרוני ואסינכרוני: מה OVERLAPPED באמת אומר
חלק 2 בסדרה מסביר בדיאגרמות I/O סינכרוני ואסינכרוני (Overlapped I/O) ב-Windows: המשמעות של FILE_FLAG_OVERLAPPED, ארבע דרכי ההודעה על הסיו...
מעמקי ה-I/O ב-Windows (פרק 4) — Cache Manager: מתי WriteFile באמת מגיע לדיסק
פרק 4 בסדרה שמסבירה בתרשימים את Cache Manager של Windows. המאמר עובר על מטמון שממומש כמיפוי קובץ, read-ahead ו-lazy writer, הבחירה בין Fl...
מעמקי ה-I/O של Windows (חלק 3) — I/O completion ports (IOCP) ו-thread pool של .NET: המרתף שמתחת ל-async/await
חלק 3 בסדרה עם איורים על I/O completion ports (IOCP): איחוד ה-queue עם בקרת מספר ה-threads, ערך ה-concurrency, שחרור ה-threads ב-LIFO והה...
שיטות עבודה מומלצות ל-multithreading בפועל: מהדורת .NET — מה להחליט לפני שמוסיפים threads
כללי תכנון שמונעים מקוד multithreaded ב-.NET/C# לקרוס או להיתקע מדי פעם: להשתמש ב-Task במקום ליצור threads בעצמכם, לצמצם shared mutable s...
למה arguments נשברים — כללי command-line arguments ב-Windows
Windows מעביר ל-CreateProcess מחרוזת אחת שהמקבל מפצל. מכסה את כללי CommandLineToArgvW, CRT ו-.NET, ArgumentList, ובניה ב-C++.
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה IRP?
- IRP (I/O Request Packet) הוא החבילה שבה ה-I/O Manager של ה-kernel של Windows אורז את בקשות הקריאה, הכתיבה ובקשות אחרות של האפליקציה ומוסר אותן ל-device driver. התיעוד של Microsoft לפיתוח drivers מסביר שרוב הבקשות שנשלחות ל-device driver נארזות כ-IRP. כל IRP נושא major function code שמציין את סוג הבקשה (create, read, write, cleanup וכן הלאה) ו-I/O stack location לכל driver שהוא עובר דרכו, והוא מעובד תוך כדי שהוא מועבר מלמעלה למטה ב-device stack. כל driver בוחר אם להשלים את ה-IRP בעצמו, להעביר אותו ל-driver שמתחתיו, או להחזיק אותו כ-pending ולהשלים אותו מאוחר יותר. מפתחי אפליקציות לא נוגעים ב-IRP ישירות, אבל ציונים כמו IRP_MJ_READ שמופיעים בעמודת Operation של Process Monitor הם בדיוק המנגנון הזה.
- למה Windows יכול לפתוח קובץ, serial port ומדפסת באותה קריאה ל-CreateFile?
- כי כל שם שמועבר ל-CreateFile עובר את אותו מסלול: בסופו של דבר הוא נפתר ל-device object במרחב השמות של ה-Object Manager, ואז ה-I/O Manager יוצר file object שמקושר ל-device הזה ומחזיר handle. אות כונן כמו C: היא בעצם symbolic link לשם NT device כמו \Device\HarddiskVolume3, וציון כמו \\.\COM1 נפתר באותו אופן ל-device object של ה-serial port. לאיזה device שהשם ייפתר, כל בקשה שלאחר מכן נארזת לאותו פורמט IRP ונמסרת ל-driver, ולכן אפשר לפתוח ולקרוא או לכתוב גם קבצים וגם devices דרך אותו API. נתיבי UNC משתמשים באותו מנגנון ופשוט נפתרים ל-device של ה-network redirector. התכנון הזה של מרחב שמות ועוד חבילה הוא המקור האמיתי לעקביות של ה-I/O ב-Windows.
- למה לפעמים קובץ לא משוחרר מיד למרות שנקרא ל-CloseHandle?
- כי מה ש-CloseHandle עושה הוא להחזיר handle אחד, לא לסגור את הקובץ. ה-file object בתוך ה-kernel נושא שני מונים: handle count, שהוא מספר ה-handles, ו-reference count, שהוא מספר ההפניות שמחזיקים רכיבים ב-kernel. IRP_MJ_CLEANUP נשלח ל-file system ברגע שה-handle האחרון נסגר, אבל כל עוד נשארות הפניות בתוך ה-kernel — למשל I/O שלא הסתיים או section של memory-mapped file — ה-file object עצמו נשאר חי, ו-IRP_MJ_CLOSE נשלח רק כשמונה ההפניות מגיע לאפס. תופעות רבות, כמו קובץ שאי אפשר למחוק אחרי שנעשה בו memory mapping, או קובץ שמדווח כ"בשימוש" אחרי שהאפליקציה נסגרה, מוסברות במנגנון הדו-שלבי הזה.
- מה התועלת בידע על IRPs ו-device stacks למפתח אפליקציות?
- גם אם לא תכתבו IRP בעצמכם, זה משתלם גם בחקירה וגם בתכנון. ראשית, עמודת Operation של Process Monitor (כשמפעילים advanced output) מציגה בדיוק את אוצר המילים של IRP, כמו IRP_MJ_CREATE ו-IRP_MJ_READ, ולכן הכרת המונחים בשכבה הזאת מאפשרת לקרוא את הלוג באמת. שנית, ברגע שיודעים ש-filter drivers כמו אנטי-וירוס יושבים על כל מסלול שה-I/O של הקבצים עובר בו, יש מאיפה להתחיל בחקירת בעיה כמו גישה איטית לקבצים בסביבה מסוימת בלבד. וכשמבינים שה-I/O של Windows בנוי כך שאפשר להפריד בין הוצאת הבקשה להשלמתה, ושה-I/O הסינכרוני אינו אלא הבטחה שהקריאה לא חוזרת לפני ההשלמה, אפשר להשתמש ב-I/O אסינכרוני, ב-I/O completion ports וב-async/await של .NET מתוך הבנה איך הם עובדים.