בדיקה ותיקון של רכיב קרנל¶
אזהרה אינה הסבר לתקלה¶
נניח שהמודול שלנו עובד מאה פעמים וגורם לאזהרה בפעם המאה ואחת. מחיקת השורה שמדפיסה את האזהרה אינה תיקון, וגם הוספת השהיה אינה הוכחה. אנחנו צריכים להגדיר איזה אינווריאנט נשבר, לזהות אילו מעברים אפשרו זאת ולבנות בדיקה שמבדילה בין המימוש השגוי למימוש המתוקן.
נתחיל בתקלה קטנה בתור מעגלי. הקוד רץ בתוך הקרנל, אך מרבית הלוגיקה שלו אינה דורשת התקן אמיתי. הפרדה בין לוגיקה לבין תזמון והעתקה למרחב משתמש מאפשרת לבדוק יותר מצבים בפחות רעש. לאחר שהלוגיקה נכונה נחזור לנעילות, לחיי המשאבים ולכשלי הקצאה.
תור שנראה נכון עד שהאינדקס חוזר להתחלה¶
נוסיף לעץ גרסת הייחוס קובץ lib/amit_ring_test.c. התור מחזיק ארבעה מספרים ומבחין בין אינדקס הראש לבין מספר האיברים. אנחנו משתמשים במונה מפורש ולכן כל ארבעת התאים זמינים. אין כאן ניסיון להבחין בין מלא וריק באמצעות השוואת שני אינדקסים בלבד.
#include <kunit/test.h>
#include <linux/errno.h>
#include <linux/module.h>
#define CAPACITY 4
struct ring {
unsigned int head;
unsigned int count;
int data[CAPACITY];
};
static int push(struct ring *r, int value)
{
unsigned int tail;
if (r->count == CAPACITY)
return -ENOSPC;
tail = (r->head + r->count) % CAPACITY;
r->data[tail] = value;
r->count++;
return 0;
}
static int pop(struct ring *r, int *value)
{
if (!r->count)
return -EAGAIN;
*value = r->data[r->head];
r->head = (r->head + 1) % CAPACITY;
r->count--;
return 0;
}
static void wrap_test(struct kunit *test)
{
struct ring r = {};
int value;
unsigned int round, i;
KUNIT_EXPECT_EQ(test, pop(&r, &value), -EAGAIN);
for (round = 0; round < 10; round++) {
for (i = 0; i < CAPACITY; i++)
KUNIT_ASSERT_EQ(test, push(&r, round * 10 + i), 0);
KUNIT_EXPECT_EQ(test, push(&r, 999), -ENOSPC);
for (i = 0; i < CAPACITY; i++) {
KUNIT_ASSERT_EQ(test, pop(&r, &value), 0);
KUNIT_EXPECT_EQ(test, value, (int)(round * 10 + i));
}
KUNIT_EXPECT_EQ(test, r.count, 0U);
}
}
static struct kunit_case cases[] = {
KUNIT_CASE(wrap_test),
{}
};
static struct kunit_suite suite = {
.name = "amit-ring",
.test_cases = cases,
};
kunit_test_suite(suite);
MODULE_LICENSE("GPL");
נוסיף ל־lib/Makefile את השורה הבאה ול־lib/Kconfig.debug את ההגדרה שאחריה. נשמור את השינוי בענף המעבדה כדי שנוכל להסירו בלי לאבד את תצורת הקרנל.
ניצור קובץ amit.kunitconfig ונריץ מתוך עץ המקור. הבנייה משתמשת בספריית פלט נפרדת ובהדמיית מרחב משתמש של הקרנל, בהתאם לתמיכה של סביבת הבנייה.
python3 tools/testing/kunit/kunit.py run \
--kunitconfig=amit.kunitconfig --build_dir=.kunit-amit 'amit-ring*'
התוצאה הצפויה היא בדיקה שעוברת, אך אין לנו עדיין הוכחה שהבדיקה מסוגלת לגלות תקלה. נשנה זמנית את קידום הראש ל־r->head++ ונריץ שוב. הבדיקה אמורה להיכשל, ובתצורה מתאימה ייתכן גם דיווח על גישה מחוץ לתחום. נחזיר את פעולת המודולו ונשמור את הבדיקה כתנאי קבלה קבוע. נבצע שינוי פגום כזה רק בעץ ובסביבת הבדיקה המבודדים.
בדיקה של אלגוריתם אינה בדיקת סנכרון¶
הבדיקה שלנו קוראת לפונקציות באופן סדרתי. אם שני תהליכונים משתמשים באותו תור, נדרשת נעילה או הוכחת אלגוריתם מקבילית נפרדת. הוספת READ_ONCE למונה אינה הופכת סדרה של בדיקת מקום, כתיבה וקידום מונה לפעולה אטומית.
לשם בדיקת סדר נעילות נבנה תצורת אורח נוספת עם CONFIG_PROVE_LOCKING. מנגנון lockdep עוקב אחר יחסי סדר שנצפו בין מחלקות נעילה ויכול לזהות מעגל אפשרי גם אם ההרצה המסוימת לא נתקעה. הוא אינו מבטיח שנצפו כל מסלולי התוכנית. אם מתקבל דיווח, נשרטט את סדר הרכישה בכל מסלול ונתקן את הסדר או את חלוקת האחריות.
לשם בדיקת זיכרון נשתמש בתצורת KASAN נפרדת. היא מוסיפה מידע וגישה מכשירית כדי לזהות סוגים של שימוש אחרי שחרור וחריגה מתחום. עבור מירוצי נתונים נשתמש בתצורת KCSAN, שמבצעת דגימה ולכן הרצה נקייה אינה מוכיחה היעדר מירוץ. הבחירה בין הכלים נובעת מהטענה שאנחנו רוצים לבדוק, ולא מהרצון להפעיל כל אפשרות בו זמנית.
כשל הקצאה הוא מסלול רגיל בממשק¶
נחזור לפתיחה של התקן התא מהשיעור הקודם. אם kzalloc נכשל, הפתיחה חייבת להחזיר שגיאה בלי לפרסם מצביע חלקי ובלי לרשום משאב שלא יוסר. נוכל להתחיל בהזרקה מקומית וצפויה: נוסיף פרמטר מעבדה שגורם לפתיחה הראשונה להחזיר -ENOMEM לפני הפרסום. לאחר מכן נבדוק את המסלול בעזרת תשתית ההזרקה של הקרנל, למשל failslab, בתצורה שתומכת בה.
לא נפעיל הסתברות גלובלית של מאה אחוז במערכת המארחת. באורח המבודד נגדיר מספר כשלונות מוגבל וסינון למשימה הנבדקת לפי הממשק המתועד של התצורה שבנינו. חשוב לבדוק גם מי נכשל בפועל. כשל בהקצאה של תוכנת הבדיקה לפני קריאת הפתיחה אינו ראיה למסלול ניקוי בדרייבר.
נבנה טבלת רכישה: הקצאת מצב, רישום התקן, יצירת עבודה ופרסום מוכנות. עבור כל נקודת כשל נרשום אילו משאבים כבר קיימים ובאיזה סדר הם משתחררים. בדיקה מוצלחת דורשת לא רק קוד שגיאה נכון אלא גם אפשרות לטעון שוב, לפתוח שוב ולפרוק בלי אזהרה או דליפה.
קריאת נפילה והקטנת שינוי¶
כאשר מתקבל oops, נשמור את ההודעה המלאה, גרסת הקרנל, התצורה, קובץ vmlinux עם סמלים, המודולים ותסריט השחזור. נזהה את סוג החריגה, הכתובת, המשימה והמסגרת הראשונה ששייכת לקוד שלנו. כתובת אפס עשויה להתאים למצביע חסר, אך אינה לבדה הסבר מדוע הגיע לשם.
נשתמש ב־scripts/decode_stacktrace.sh או בכלי הסמלים המתאימים לעץ כדי לקשר כתובות למקור המדויק. נבדוק אם הבעיה התחילה אחרי שינוי מסוים בעזרת git bisect, כאשר בכל צעד נבנה באותה תצורה ונריץ את אותו מבחן. מצב שלא נבנה יסומן כדילוג ולא כהצלחה. תקלה הסתברותית דורשת מדיניות חזרות מתועדת, אחרת חיפוש בינארי עלול לבחור שינוי שגוי.
התיקון הסופי צריך לכלול שינוי קטן שמחזיר אינווריאנט ובדיקה שנכשלה לפניו. נוסיף גם נימוק חיים: מי רשאי לגשת לערך, מה מפסיק גישות חדשות, איזו המתנה מסיימת גישות קיימות ומתי מתבצע השחרור. לבסוף נבנה מול גרסה נוספת שנבחרה במפורש ונבדוק שינויי ממשק. הצלחה בגרסה אחת אינה התחייבות לממשק פנימי יציב בין גרסאות.
להרחבה ניעזר בתיעוד בדיקות היחידה, בתיעוד בדיקת הזיכרון, בתיעוד זיהוי המירוצים ובתיעוד הזרקת הכשלים.