סנכרון ועבודה נדחית¶
אירוע חומרה מגיע בזמן שהדרייבר מעדכן תור. נרצה לשמור על המבנה בלי לחסום במקום שאסור לחסום בו ובלי להחזיק מעבד זמן ממושך ב־IRQL גבוה. הבחירה בין spin lock לבין primitive שמאפשר המתנה מתחילה מהקשר הביצוע, ולא מהעדפה כללית ל־API שנראה מהיר יותר.
ה־spin lock מתאים לקטע קצר שמגן על נתונים resident. הרוכש יכול להסתובב בזמן שמעבד אחר מחזיק אותו, ולכן אסור לבצע בתוכו I/O חוסם, להמתין לאירוע או לקרוא לקוד שמחכה לאותה נעילה. העלאת IRQL שמלווה חלק מפעולות הנעילה מגינה מפני הפרעות מתאימות במעבד המקומי, אבל התיאום בין מעבדים מגיע מן הנעילה עצמה.
נממש תור קבוע של אירועים קטנים. זהו helper מלא ל־dispatch או DPC שרצים לכל היותר ב־DISPATCH_LEVEL, בתוך context שמוקצה ב־nonpaged memory. הוא אינו מיועד ל־ISR שרץ ב־DIRQL.
#include <ntddk.h>
#define LAB_CAPACITY 16
typedef struct _LAB_QUEUE {
KSPIN_LOCK Lock;
ULONG Head;
ULONG Count;
ULONGLONG Values[LAB_CAPACITY];
} LAB_QUEUE;
VOID LabQueueInitialize(_Out_ LAB_QUEUE* Queue)
{
RtlZeroMemory(Queue, sizeof(*Queue));
KeInitializeSpinLock(&Queue->Lock);
}
_IRQL_requires_max_(DISPATCH_LEVEL)
BOOLEAN LabQueuePush(_Inout_ LAB_QUEUE* Queue, ULONGLONG Value)
{
KIRQL oldIrql;
BOOLEAN accepted = FALSE;
KeAcquireSpinLock(&Queue->Lock, &oldIrql);
if (Queue->Count != LAB_CAPACITY) {
ULONG tail = (Queue->Head + Queue->Count) % LAB_CAPACITY;
Queue->Values[tail] = Value;
Queue->Count++;
accepted = TRUE;
}
KeReleaseSpinLock(&Queue->Lock, oldIrql);
return accepted;
}
_IRQL_requires_max_(DISPATCH_LEVEL)
BOOLEAN LabQueuePop(_Inout_ LAB_QUEUE* Queue, _Out_ ULONGLONG* Value)
{
KIRQL oldIrql;
BOOLEAN available = FALSE;
KeAcquireSpinLock(&Queue->Lock, &oldIrql);
if (Queue->Count != 0) {
*Value = Queue->Values[Queue->Head];
Queue->Head = (Queue->Head + 1) % LAB_CAPACITY;
Queue->Count--;
available = TRUE;
}
KeReleaseSpinLock(&Queue->Lock, oldIrql);
return available;
}
ה־push המלא מחזיר FALSE בלי להקצות ובלי לחכות. על הקורא להגדיר מדיניות איבוד או backpressure שמתאימה למקור האירוע. ה־pop כותב את Value רק בהצלחה, ולכן אסור להשתמש בפלט אם הוא חזר FALSE. כל השדות של מצב התור משתנים תחת אותה נעילה, וה־oldIrql המקומי מאפשר לשחזר את רמת הכניסה המתאימה לכל קריאה.
ה־ISR צריך לבצע את העבודה ההכרחית מול ההתקן, לאשר או ללכוד את המצב ולתזמן עיבוד מאוחר יותר לפי מודל הדרייבר. ה־DPC מבצע חלק נוסף ב־DISPATCH_LEVEL, וה־work item מאפשר עבודה שמתאימה ל־PASSIVE_LEVEL. המעבר בין השלבים אינו מאריך חיי pointers באופן אוטומטי. הנתון שמועבר חייב להיות מועתק או מוחזק ב־context שנשאר תקף עד שהשלב הבא מסיים.
כאשר ISR ו־DPC משתפים מידע נשתמש במנגנון interrupt synchronization שמתאים ל־interrupt object, ולא נקרא ל־KeAcquireSpinLock הרגיל מן ISR רק מפני שהוא נקרא "spin lock". ה־IRQL הנוכחי וה־synchronize IRQL הם חלק מן החוזה. גם executive mutex או event דורשים בדיקת תנאי המתנה; אין לבחור primitive חוסם בתוך DPC.
ב־teardown נתחיל בעצירת מקור העבודה, נבטל timers לפי החוזה ונמתין לשגרות שכבר בתהליך. ביטול timer אינו בהכרח הוכחה ש־DPC שכבר נשלף מן התור הפסיק לרוץ. מחיקת context לפני quiescence יכולה לגרום לקריאה לתוך קוד או זיכרון שכבר נפרקו. בהמשך KMDF ייתן מנגנוני parent/child וסנכרון, אבל גם שם צריך להבין איזה callback יכול להיות פעיל.
נבחן lost wake-up: מפיק מוסיף אירוע בדיוק כשהצרכן רואה תור ריק ומתכוון לסיים. אם הדגל "worker scheduled" והתור אינם מנוהלים תחת אותו invariant, האירוע יכול להישאר בלי מי שיעבד אותו. נבנה מכונת מצבים שבה ההחלטה לצאת או לתזמן עבודה חדשה מסונכרנת עם ההכנסה. התיעוד על spin locks ועל system worker threads הוא נקודת התחלה להוכחת התזמון והסגירה.