לדלג לתוכן

הקשר פסיקה ובחירת סנכרון

אותה פונקציה בהקשר אחר יכולה להיות אסורה

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

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

שכבות של עבודה דחויה

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

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

מקור אירועים נשלט

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

נשמור את הקוד בשם deferred_probe.c ונבנה עם קובץ שמכיל obj-m += deferred_probe.o, בדיוק מול עץ הבנייה של הקרנל הניסיוני.

#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/workqueue.h>
#include <linux/delay.h>
#include <linux/atomic.h>

static struct hrtimer source_timer;
static struct work_struct work;
static struct workqueue_struct *queue;
static atomic_t produced = ATOMIC_INIT(0);
static atomic_t pending = ATOMIC_INIT(0);
static atomic_t consumed = ATOMIC_INIT(0);
static ktime_t interval;

static void consume(struct work_struct *item)
{
    int count;
    while ((count = atomic_xchg(&pending, 0)) != 0) {
        msleep(20);
        atomic_add(count, &consumed);
    }
}

static enum hrtimer_restart produce(struct hrtimer *timer)
{
    int total = atomic_inc_return(&produced);
    atomic_inc(&pending);
    queue_work(queue, &work);
    if (total >= 50)
        return HRTIMER_NORESTART;
    hrtimer_forward_now(timer, interval);
    return HRTIMER_RESTART;
}

static int __init deferred_probe_init(void)
{
    queue = alloc_workqueue("amit_deferred", WQ_UNBOUND, 1);
    if (!queue)
        return -ENOMEM;
    INIT_WORK(&work, consume);
    interval = ms_to_ktime(10);
    hrtimer_init(&source_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    source_timer.function = produce;
    hrtimer_start(&source_timer, interval, HRTIMER_MODE_REL);
    pr_info("deferred_probe started\n");
    return 0;
}

static void __exit deferred_probe_exit(void)
{
    hrtimer_cancel(&source_timer);
    flush_work(&work);
    pr_info("deferred_probe produced=%d consumed=%d pending=%d\n",
            atomic_read(&produced), atomic_read(&consumed), atomic_read(&pending));
    destroy_workqueue(queue);
}

module_init(deferred_probe_init);
module_exit(deferred_probe_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Amit deferred execution lab");

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

בקשת עבודה אינה אירוע פרטי

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

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

סדר הסיום הוא חלק מהאלגוריתם

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

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

נעילה מסתובבת מול נעילה ממתינה

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

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

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

תכנון רשומה במקום הקצאה בזמן אירוע

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

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

בדיקת הקשר ובדיקת התנהגות

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

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