מעמקי ה-I/O של Windows (חלק 1) — כל קריאה וכתיבה הופכת ל-IRP: התמונה הכוללת של מערכת ה-I/O

· עודכן בתאריך: · · Windows, Win32, I/O, Kernel, Device Driver, .NET, C#, חקירת תקלות

שורה אחת של 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. קודם השורה התחתונה

יש שלוש נקודות לתפוס לפני כל דבר אחר.

  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
  2. הפרידו בראש בין “מה שמטפל בבקשה”, “היעד” ו”המצב של פתיחה אחת”. ה-driver object מחזיק טבלה של פונקציות טיפול, ה-device object הוא היעד של בקשה, וה-file object מחזיק את המצב של פתיחה מסוימת. ה-HANDLE שהאפליקציה מחזיקה הוא הפניה ל-file object (פרק 3).6789
  3. הוצאת בקשה והשלמתה, סגירת 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

\ (שורש מרחב השמות)\Device(device objects שה-drivers יוצרים)\GLOBAL??(שם שבו חיים השמות הגלובליים הנראים מ-Win32)\BaseNamedObjects(named mutexes וכדומה)HarddiskVolume3Serial0Mup (network redirector)C: → \Device\HarddiskVolume3COM1 → \Device\Serial0PhysicalDrive0 → \Device\Harddisk0\DR0

איור 1: מרחב השמות של ה-Object Manager (קטע). מה שנמצא תחת \GLOBAL?? הם symbolic links; האובייקטים האמיתיים נמצאים תחת \Device

באיור 1, הסתכלו על שני הצדדים האלה בנפרד.

צד השם דוגמה תפקיד
שמות שאפליקציות Win32 משתמשות בהן אותיות כונן, שמות COM ports נקודת הכניסה מהאפליקציה
שמות שה-kernel משתמש בהם שמות NT device כמו \Device\HarddiskVolume3 שמות שמצביעים על device object

מה שמחבר בין השניים הוא symbolic link. השם שהאפליקציה רואה והשם של האובייקט האמיתי בצד ה-kernel אינם אותו דבר.

כדי ש-device יהיה גלוי לאפליקציות Win32, ה-driver משתמש ב-IoCreateSymbolicLink כדי ליצור קישור מ-MS-DOS device name כמו \DosDevices\COM1 לשם NT device.4 C: עובד באותה צורה: הוא קישור ל-device של volume כמו \Device\HarddiskVolume3.

פתרון השם עבור CreateFile("C:\project\report.csv") מתקדם כך.

השם שהאפליקציה העבירהC:\project\report.csvשכבת Win32 ממירה אותו לצורת NT\??\C:\project\report.csvה-Object Manager מחפש במרחב השמותומגלה ש-\??\C: הוא symbolic linkהולכים אחרי הקישור ומחליפים\Device\HarddiskVolume3\project\report.csv\Device\HarddiskVolume3 מגיעל-device object של ה-volumeאת שארית \project\report.csv ה-I/O Managerפותר על ידי הוצאת IRP_MJ_CREATEומשאיר את הפתרון ל-file system driver (NTFS)

איור 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, בלי לעבור דרך אות כונן.514
  • CON ו-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

מרחב ה-kernelתהליך (user mode)file object 1report.csv נפתח לקריאהoffset נוכחי 4096file object 2report.csv נפתח להוספה בסוףoffset נוכחי 65536device objectמקביל ל-HarddiskVolume3driver object NTFSMajorFunction = טבלת פונקציות הטיפולHANDLE 0x1A4HANDLE 0x1B8

איור 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 בקריאה של קובץ בדיסק מקומי, הבקשה עוברת בערך במסלול הבא.

ה-stack בצד האחסוןה-stack בצד מערכת הקבציםניהול volume ומחיצות(volmgr וכדומה)disk class driver(disk.sys)storage port ומיניפורט(storport וכדומה)file system filters(אנטי-וירוס, הצפנה, Procmon וכדומה)NTFSמתרגם offset בתוך הקובץ למיקום על ה-volumeה-I/O Manager בונה את ה-IRP(IRP_MJ_READ + stack locations)התקן דיסק

איור 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 ולהשלים מאוחר יותר להמתין לתגובת החומרה
השכבה שמתחת השלימה במקוםהשכבה שמתחת החזיקה כ-pending(pending מתפשט לקורא)driver מקבל IRPאיך לטפל בבקשה הזאת(1) להשלים בעצמולקרוא ל-IoCompleteRequestלמשל: לענות מיד עם נתונים מה-cache(2) להעביר ל-device שמתחתיולקרוא ל-IoCallDriverלמשל: filter בודק ומעביר הלאה(3) להחזיק כ-pendingלהחזיר STATUS_PENDING ולהכניס את ה-IRP ל-queueלמשל: המתנה לתגובת החומרהIoCompleteRequest מאוחר יותר,בעקבות interrupt וכדומהההשלמה עולה חזרה במעלה ה-stack בסדר הפוך(נקראת ה-completion routine של כל שכבה)

איור 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 וצריך ללכת לדיסק. באיור, הסתכלו קודם ברגל היציאה שמוציאה את הבקשה, ואחר כך ברגל החזרה אחרי שהנתונים נקראו.

התקן דיסקstorage stackfilters + NTFSI/O Managerthread של האפליקציההתקן דיסקstorage stackfilters + NTFSI/O Managerthread של האפליקציהלפתור את ה-file object מה-handleולבנות את ה-IRP (IRP_MJ_READ)I/O סינכרוני ישן כאן בהמתנה להשלמהI/O אסינכרוני מחזיר שליטה ויכול לעשות עבודה אחרתמ-interrupt service routine (ISR)עיבוד ההשלמה ממשיך ב-DPCאחרי שכל ה-IRPs התחתוניםהנדרשים הושלמולהריץ את ה-completion routine של כל שכבה בסדר הפוךולקבע את התוצאה עם APC ל-thread המבקשReadFile → NtReadFile (system call)IoCallDriver (לראש ה-stack)לתרגם את המיקום ל-volume ולהוציאIRP תחתון שמופנה ל-storage stackלהוציא את פקודת הקריאהSTATUS_PENDING (ה-IRP התחתון ב-pending)גם ה-IRP המקורי חוזר כ-pending(כאן נגמרת רגל היציאה)interrupt, "הנתונים נקראו"להשלים את ה-IRP התחתון (IoCompleteRequest)ה-completion routine בצד NTFS מקבל אותולהשלים את ה-IRP המקורי (IRP_MJ_READ)הסטטוס ומספר הבתים סופיים (סימון 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 נעלמים והרגע שבו כל הפניה לאובייקט נעלמת אינם בהכרח אותו רגע.

מערכת הקבציםI/O ManagerObject Managerאפליקציהמערכת הקבציםI/O ManagerObject Managerאפליקציהלהסיר את הרשומה מה-handle tableולהקטין את ה-handle countלבטל I/O שלא הסתיים ולשחרר locksעבור ה-file object הזהalt[זה היה ה-handle האחרון]אבל אם נשארו הפניות בתוך ה-kernel, כמוI/O שלא הסתיים או section (memory mapping),ה-file object עוד חיהסדר האחרון של ה-file object מושלםרק עכשיו הוא באמת סגורalt[גם ה-reference count הגיע לאפס]CloseHandle(h)IRP_MJ_CLEANUPIRP_MJ_CLOSE

איור 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_CREATEFASTIO_READ / IRP_MJ_READIRP_MJ_WRITEIRP_MJ_CLEANUPIRP_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, שהוא הדרך שבה אפליקציה משתמשת בהפרדה הזאת בין הוצאה להשלמה, את ארבע הדרכים לקבל הודעות השלמה, ביטול, ואת התנאים שבהם “משהו שאמור להיות אסינכרוני חוזר סינכרונית”.

מאמרים קשורים

תחומי ייעוץ קשורים

KomuraSoft LLC עוסקת בתכנון ובחקירת תקלות סביב I/O של קבצים באפליקציות Windows עסקיות, כולל handle leaks, “הקובץ בשימוש”, והאטות I/O בסביבות מסוימות.

מקורות

  1. Microsoft Learn, Windows Internals - Sysinternals. העמוד שמציג את הספר Windows Internals (שיצא לאור ביפנית בשם Inside Windows). זהו הספר התקני על המבנה הפנימי של Windows, כולל ארכיטקטורת ה-kernel ומערכת ה-I/O, ונקודת פתיחה ללימוד מעמיק יותר של הנושאים שהסדרה הזאת מכסה. 

  2. 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

  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

  4. 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

  5. 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

  6. Microsoft Learn, Introduction to driver objects. על כך שה-I/O Manager יוצר מבנה DRIVER_OBJECT כשנטען driver; שה-driver object מחזיק את נקודות הכניסה לשגרות הסטנדרטיות של ה-driver (כולל מערך MajorFunction, שהוא dispatch table); ושהמהנדס של ה-I/O Manager משתמש בטבלה הזאת כדי לקרוא לפונקציית הטיפול המתאימה לבקשה.  2 3

  7. Microsoft Learn, Introduction to device objects. על כך שמבנה DEVICE_OBJECT מייצג device לוגי, וירטואלי או פיזי ונעשה היעד (target) של בקשת I/O; ש-driver יוצר device object עם IoCreateDevice; ושה-device object מקושר ל-driver (ה-driver object) שיצר אותו.  2 3

  8. Microsoft Learn, Using files in a driver. על כך שב-kernel ה-file object מייצג מופע של קובץ (או device) שנפתח; ושבכל פתיחה של קובץ נוצר file object שמחזיק את ההקשר של אותה פתיחה, כמו ה-byte offset הנוכחי.  2 3

  9. Microsoft Learn, File handles. על כך שה-file handle ש-CreateFile מחזיר הוא ספציפי לתהליך ומקושר ל-file object שנפתח; שפתיחה של אותו קובץ כמה פעמים נותנת בכל פעם handle נפרד (ומצב פתיחה נפרד); ושכשה-handle כבר לא נדרש יש לסגור אותו עם CloseHandle.  2 3

  10. Microsoft Learn, Completing IRPs. על כך שהקריאה ל-IoCompleteRequest היא שמשלימה פעולת I/O; שה-IoCompletion routines שנרשמו על ידי ה-drivers שמעל בסדר הפוך נקראים בתורם בהשלמה; ועל הזרימה שבה השלמת בקשה מתרחשת במועד שונה מהוצאתה, והסטטוס נמסר לבסוף למבקש.  2 3 4 5

  11. Microsoft Learn, Synchronous and asynchronous I/O. על כך שב-I/O סינכרוני הפונקציה לא חוזרת עד שה-I/O מושלם וה-thread מומתין, בעוד שב-I/O אסינכרוני (overlapped) הפונקציה שמוציאה את הבקשה חוזרת מיד וה-thread יכול להמשיך בעבודה אחרת; שעבור I/O אסינכרוני יש לפתוח את ה-handle עם FILE_FLAG_OVERLAPPED; ושיש כמה דרכים לקבל הודעה על ההשלמה.  2 3

  12. Microsoft Learn, IRP_MJ_CLEANUP. על כך שקבלת הבקשה הזאת מעידה שה-handle האחרון ל-file object שמשויך ל-device object שאליו היא מכוונת נסגר; שעם זאת ייתכן שה-file object עוד לא משוחרר בגלל בקשות I/O שלא הושלמו; ושהבקשה הזאת נשלחת בהקשר של התהליך שסגר את ה-handle.  2 3

  13. Microsoft Learn, IRP_MJ_CLOSE. על כך שקבלת הבקשה הזאת מעידה שה-reference count של ה-file object הגיע לאפס וה-file object עומד להשתחרר; ושהבקשה נשלחת אחרי בקשת ה-cleanup, אבל לא בהכרח מיד אחריה, כי היא ממתינה להשלמה של I/O שלא הסתיים.  2 3

  14. Microsoft Learn, CreateFileW function. על כך ש-CreateFile יכול לפתוח ולהחזיר handle לא רק לקבצים אלא גם ל-devices כמו דיסקים פיזיים, volumes, קונסולה, יציאות תקשורת (COM ports) ו-pipes; שמשתמשים בשמות בצורת “\\.\” כשפותחים device; ועל המשמעות של ה-flags השונים, החל מ-FILE_FLAG_OVERLAPPED.  2

  15. Microsoft Learn, WinObj - Sysinternals. על כך ש-WinObj הוא כלי שמציג את מרחב השמות של NT Object Manager ומאפשר לעיין באובייקטים שבו, כולל device objects ו-symbolic links.  2

  16. 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 אמורים לטפל בה. 

  17. 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 כדי לדעת מה מתבקש ממנו. 

  18. Microsoft Learn, Device nodes and device stacks. על כך ש-device objects מונחים זה על זה ויוצרים device stack; ש-IRP נשלח ראשית ל-device object שבראש ה-stack ואחר כך מעובד או מועבר למטה בכל שכבה; ושגם ה-device objects של filter drivers קיימים כשהם מוכנסים לתוך ה-stack.  2

  19. Microsoft Learn, Filter Manager Concepts. על כך שה-filter manager הוא driver במצב kernel שמגיע עם Windows; שמיניפילטר drivers יכולים להתערב בבקשות I/O למערכת הקבצים עם callbacks של pre ו-post; ושהמיקום שבו כל מיניפילטר מתערב (ה-altitude שלו) קובע את סדרו בתוך ה-I/O stack. 

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

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה זה 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 מתוך הבנה איך הם עובדים.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג