מסננים וחיי קריאות חוזרות¶
נניח שאנחנו רוצים לרשום ביומן כל פתיחת קובץ. הפעולה נשמעת פשוטה: מקבלים callback וכותבים שורה לקובץ. אבל הכתיבה של היומן יכולה לעבור בעצמה דרך המסנן. עכשיו קיבלנו callback מתוך העבודה שהcallback הקודם התחיל.
הכניסה למסלול של מערכת ההפעלה מוסיפה יותר מפרמטרים חדשים. היא מחברת אותנו לתזמון, לבעלות ולנעילות של קוד שלא כתבנו. לפני שמחליטים לשנות החלטה במסלול כזה, כדאי להצליח קודם לצפות בו בלי לשנות את ההתנהגות שלו.
מה צריך להיות מוכן לפני הרישום¶
המסנן נרשם כאשר כל state שהcallbacks צריכים כבר קיים. בFilter Manager אפשר לקבל callbacks של instance גם לפני שFltStartFiltering חזרה. לכן הסדר ״נרשום, ואז נאתחל את התור״ עלול להיות מאוחר מדי. בזמן teardown צריך לעצור קבלת עבודה, לטפל בעבודה שכבר ממתינה ולשחרר contexts לפי מחזור החיים שלהם. הסדר מפורט בתיעוד טעינה ופריקה של minifilter.
נחלק את חיי המסנן לשלושה מצבים: מקבל, מתרוקן וסגור. המעבר למתרוקן מונע רכישה חדשה של context, אבל אינו מוחק את מי שכבר רכש אותו. המעבר לסגור מותר רק אחרי שכל המשתמשים הקיימים יצאו.
בונים את הרעיון קודם במצב משתמש¶
המודל הבא הוא קוד user mode לצורך ניסוי. הוא אינו primitive לקרנל ואסור להעתיק אליו std::mutex מתוך ההנחה שמותר לחסום בכל callback. המטרה היא לראות את המרוץ בלי לסכן את הVM.
#include <cassert>
#include <condition_variable>
#include <cstddef>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <utility>
class Registration : public std::enable_shared_from_this<Registration> {
std::mutex mutex;
std::condition_variable empty;
bool accepting = true;
std::size_t active = 0;
void leave() noexcept {
std::lock_guard lock(mutex);
assert(active != 0);
if (--active == 0) empty.notify_all();
}
public:
class Lease {
std::shared_ptr<Registration> owner;
friend class Registration;
explicit Lease(std::shared_ptr<Registration> p) : owner(std::move(p)) {}
public:
Lease(const Lease&) = delete;
Lease& operator=(const Lease&) = delete;
Lease(Lease&& other) noexcept : owner(std::move(other.owner)) {}
~Lease() { if (owner) owner->leave(); }
};
std::optional<Lease> enter() {
auto self = shared_from_this();
std::lock_guard lock(mutex);
if (!accepting) return std::nullopt;
++active;
return Lease(std::move(self));
}
void stop_and_wait() {
std::unique_lock lock(mutex);
accepting = false;
empty.wait(lock, [&] { return active == 0; });
}
};
int main() {
auto registration = std::make_shared<Registration>();
auto lease = registration->enter();
assert(lease);
std::jthread remover([registration] { registration->stop_and_wait(); });
lease.reset();
remover.join();
assert(!registration->enter());
}
הבדיקה אינה תלויה במי מהthreadים התחיל ראשון. אם הפריקה הגיעה לפני שחרור הlease היא תמתין. אם הlease כבר שוחרר, התנאי יאפשר לה להמשיך. מה שחשוב הוא ששינוי accepting ובדיקת active נעשים תחת אותה נעילה.
הcontext עצמו נשאר חי בזכות הshared pointer. זהו חלק מהפתרון, כי מונה active אינו עוזר אם ניסינו להגדיל אותו דרך pointer שכבר שוחרר. במערכת אמיתית צריך להוכיח גם מי מחזיק את רישום הcallback בדרך אל enter.
למה פריקה מתוך callback יכולה להיתקע¶
הcallback מחזיק lease, ואז קורא לstop_and_wait. הפונקציה ממתינה לactive שיגיע לאפס, אבל הlease ישתחרר רק אחרי שהcallback יחזור. זהו מעגל המתנה בלי שני mutexים בכלל.
הפתרון הוא לפצל בקשת עצירה מהמתנה לסיום. הcallback רשאי לסמן בקשה או לתזמן עבודה מתאימה, וההמתנה מתבצעת בהקשר שאינו אחד המשתמשים שהסיום שלהם נדרש. בקרנל מוסיפים גם את מגבלות הIRQL והאפשרות לגשת לזיכרון pageable.
מה שונה בין מסנן קבצים למסנן רשת¶
המשפחות של Filter Manager ושל WFP מציעות נקודות הרחבה שונות. בWFP יש layers, arbitration וcallouts, וההחלטה יכולה להשתתף במדיניות של תעבורה. יש להבחין בין רישום callback בזמן ריצה לבין אובייקטי המדיניות שמפנים אליו. גם ההתנהגות כשיש filter שמפנה לcallout שטרם נרשם תלויה בסוג הפעולה, כפי שמוגדר בחוזה רישום callout.
הכלל שלנו הוא לכתוב ליד כל callback את context הביצוע, המשאבים שמותר לשאול, המשאבים שהועברו אליו והפעולה שמסיימת את האחריות. לא מעתיקים הנחה מpreoperation של קובץ אל classify של packet רק מפני שבשניהם קיבלנו pointer.
הפריקה של minifilter משתמשת בFltUnregisterFilter ומפעילה את מסלולי teardown המתאימים. היא אינה מתירה לשכוח worker פרטי ששמר pointer מחוץ לחוזה. את העבודה הפרטית שלנו צריך להפסיק ולהשלים לפי הבעלות שלה. אפשר לקרוא את גבול הפעולה בחוזה FltUnregisterFilter.
המעבדה שנבחר¶
המסלול הראשי משתמש בminifilter של תצפית בלבד בתוך VM. מתחילים מדוגמה רשמית בגרסה נעולה, מוציאים metadata מוגבל לתור ומעבירים אותו לuser mode. לא מבצעים כתיבה סינכרונית לקובץ היומן מתוך כל callback.
השלב הבא הוא עומס ופריקה חוזרת בזמן פעילות. מחפשים אירוע שנשמט, תור שהתמלא וcontext ששרד אחרי הinstance. שלושת המקרים דורשים טיפול שונה. אובדן רשומת תצפית אינו סיבה להחזיק מסלול I/O בלי גבול, ובוודאי שאינו הוכחה שהפעולה המקורית לא קרתה.