לדלג לתוכן

ביטול וסיום בקשות

נניח שבקשת IOCTL ממתינה בתור של driver. בדיוק כאשר timer מוציא אותה לעיבוד, הלקוח סוגר את ה־handle. שני מסלולים יכולים להגיע למסקנה שהם צריכים לסיים את הבקשה: מסלול העבודה ומסלול הביטול. אם שניהם קוראים completion, השגיאה אינה רק תשובה כפולה למשתמש; היא יכולה להפוך לשימוש באובייקט שכבר הוחזר למערכת.

נפריד בין מי שמחזיק את הבקשה לבין מי שביקש לבטל אותה. בקרנל, כמו ב־user mode, בקשת ביטול אינה מחיקה מיידית. כל מסלול צריך לדעת אם הוא עדיין רשאי לגעת בבקשה, מי משחרר משאבים תלויים ואיך מוכיחים שלא נשאר callback שיגיע אליה בהמשך.

מודל חיים קטן לפני callback אמיתי

התוכנית הבאה היא מודל C++ שניתן להריץ ב־user mode. היא אינה driver ואינה מדמה את כל KMDF; היא בודקת את ההכרעה בין שני בעלי סמכות לסיום:

#include <cassert>

enum class State { queued, owned, cancel_claimed, completing, done };

struct RequestModel {
    State state = State::queued;
    unsigned completions = 0;

    void retrieve() {
        assert(state == State::queued);
        state = State::owned;
    }
    bool claim_cancel() {
        if (state != State::owned) return false;
        state = State::cancel_claimed;
        return true;
    }
    bool claim_completion() {
        if (state != State::owned) return false;
        state = State::completing;
        return true;
    }
    void finish() {
        assert(state == State::cancel_claimed || state == State::completing);
        state = State::done;
        ++completions;
        assert(completions == 1);
    }
};

int main() {
    RequestModel cancel_first;
    cancel_first.retrieve();
    assert(cancel_first.claim_cancel());
    assert(!cancel_first.claim_completion());
    cancel_first.finish();

    RequestModel completion_first;
    completion_first.retrieve();
    assert(completion_first.claim_completion());
    assert(!completion_first.claim_cancel());
    completion_first.finish();
}

המעברים כאן סדרתיים בכוונה. אם משתמשים במצב כזה בקוד מקבילי אמיתי, ההכרעה צריכה להיעשות תחת מנגנון סנכרון מתאים. כתיבת enum רגיל משני callbacks אינה מספקת אטומיות. המודל עוזר לזהות את ההחלטה שדורשת הגנה, אבל אינו מממש אותה עבורנו.

בקשה שנמצאת בתור framework

כאשר בקשה נמצאת בתור KMDF, ה־framework מחזיק אותה. הוא יכול להסיר אותה עקב ביטול לפני שה־driver קיבל אותה. אם נרשם EvtIoCanceledOnQueue, ה־callback מקבל את הבקשה שבוטלה בתור וצריך להשלים את הטיפול לפי החוזה. בלי צורך בניקוי פרטי, אפשר להשאיר ל־framework את המסלול הרגיל.

אחרי WdfIoQueueRetrieveNextRequest מוצלח, הבעלות עוברת ל־driver. מרגע זה אין להניח שהבקשה עדיין מנוהלת כמו פריט בתור. יש לעבד אותה, להעביר בעלות באופן חוקי או להשלים אותה. בפרויקט נבחר עיבוד קצר ומוגבל מיד לאחר ההוצאה, כך שלא נחזיק בקשה פעילה לאורך זמן נוסף.

העברה באמצעות WdfRequestForwardToIoQueue מעבירה שוב בעלות ל־framework רק אם הצליחה. אחרי הצלחה אסור לקרוא שדות בבקשה כאילו היא עדיין שלנו. במקרה של כשל, ה־driver עדיין צריך לטפל בה, בדרך כלל באמצעות completion עם הסטטוס המתאים.

בקשה פעילה שניתנת לביטול

כאשר driver מחזיק בקשה לאורך זמן, הוא יכול לרשום EvtRequestCancel באמצעות WdfRequestMarkCancelableEx. יש לבדוק את ערך ההחזרה. אם הביטול כבר קרה והפונקציה חוזרת בכשל, ה־callback לא בהכרח ייקרא כדי לנקות בשבילנו; המסלול שרשם צריך לבצע את הניקוי הנדרש.

לפני completion רגיל של בקשה שסומנה כניתנת לביטול, צריך להסיר את אפשרות הביטול באמצעות WdfRequestUnmarkCancelable. החזרה STATUS_CANCELLED משאירה את סמכות הסיום למסלול הביטול. אין להשלים בכל זאת מתוך הנחה שה־callback עוד לא הגיע.

הקטע הבא הוא ליבה לשילוב בדרייבר שבו callbacks של העבודה והביטול מסונכרנים אוטומטית באותו scope. המשתנה Current נמצא ב־context הלא־מדופדף של אותו scope, ואין עבודה חיצונית נוספת שמחזיקה את הבקשה:

WDFREQUEST request = context->Current;
if (request != nullptr) {
    const NTSTATUS status = WdfRequestUnmarkCancelable(request);
    if (status != STATUS_CANCELLED) {
        context->Current = nullptr;
        WdfRequestComplete(request, STATUS_SUCCESS);
    }
}

החוזה מחייב שהבקשה הייתה בבעלותנו וסומנה קודם כניתנת לביטול. סטטוס שמצביע על הפרת תנאי הקדם הוא פגם בדרייבר שצריך לאבחן, ולא אירוע עסקי רגיל שמנסים שוב. ה־callback של הביטול מאפס את Current לפני completion, כדי שאף מסלול מסונכרן מאוחר לא ישתמש ב־handle שהושלם.

סיום I/O אינו סיום כל העבודה הנלווית

אם timer, DPC או work item מחזיקים pointer אל context של בקשה, completion לבדו אינו מבטל את העבודה הנלווית. צריך לעצור אותה או להעביר את הנתונים שהיא צריכה לאחסון בעל חיים נפרדים. גם WdfObjectReference אינו היתר להשתמש בבקשה אחרי שהחוזה שלה הסתיים; reference לאובייקט והזכות לבצע פעולה מסוימת אינם אותו דבר.

סגירת handle של משתמש גורמת למסלולי cleanup ו־close, אך הם אינם זהים. מנגנון cleanup קשור לסיום השימוש דרך handles של file object, בעוד close יכול להגיע מאוחר יותר כאשר הפניות פנימיות מסתיימות. דרייבר שמחזיק בקשות לפי לקוח צריך לשייך אותן ל־file object ולא להסתמך על PID בלבד.

ניסוי מבודד

ב־VM של הפרויקט נשלח IOCTL חופף, נבקש CancelIoEx בחלק מן ההרצות ונסגור את ה־handle בחלק אחר. נשמור מזהה בקשה, נקודת הוצאה מן התור ותוצאת completion. המטרה היא להראות מסיים יחיד בכל תזמון, לא לכפות שכל ביטול יחזיר תמיד STATUS_CANCELLED.

בדיקות Driver Verifier ו־WDF diagnostics יבדקו חלק מהפרות החוזה. הן אינן תחליף לטבלת הבעלות. אין להפעיל את ניסוי הדרייבר על מחשב העבודה, ואין בפרק טענה שהקוד כבר נבנה או רץ ב־WDK בסביבה הנוכחית.

קריאה להעמקה

התיעוד של רישום ביטול, הסרת אפשרות ביטול והעברה לתור מגדיר את נקודות מעבר הבעלות.