הרחבת הליבה באמצעות eBPF

Extended Berkeley Packet Filter‏ (eBPF) היא מכונה וירטואלית בליבה שמריצה תוכניות eBPF שסופקו על ידי משתמשים כדי להרחיב את הפונקציונליות של הליבה. אפשר לקשר את התוכניות האלה למכשירי מעקב או לאירועים בליבה, ולהשתמש בהן כדי לאסוף נתונים סטטיסטיים שימושיים של הליבה, לעקוב אחריה ולנתח את הבאגים שלה. התוכנה נטענת בליבה באמצעות ה-syscall bpf(2) והמשתמשים מספקים אותה כ-blob בינארי של הוראות למכונת eBPF. מערכת ה-build של Android תומכת בתרגום של תוכניות C ל-eBPF באמצעות תחביר פשוט של קובץ build שמתואר במסמך הזה.

מידע נוסף על הארכיטקטורה והרכיבים הפנימיים של eBPF זמין בדף של Brendan Gregg בנושא eBPF.

ב-Android יש טוען eBPF וספרייה שטוענים תוכניות eBPF בזמן האתחול.

Android BPF loader

במהלך האתחול של Android, כל תוכניות ה-eBPF שנמצאות ב-/system/etc/bpf/ נטענות. התוכנות האלה הן אובייקטים בינאריים שנוצרו על ידי מערכת ה-build של Android מתוכנות C, ומלוות בקובצי Android.bp בעץ המקור של Android. מערכת ה-build שומרת את האובייקטים שנוצרו ב-/system/etc/bpf, והם הופכים לחלק מתמונת המערכת.

הפורמט של תוכנית C ב-eBPF ל-Android

תוכנית C ב-eBPF חייבת להיות בפורמט הבא:

#include <bpf_helpers.h>

/* Define one or more maps in the maps section, for example
 * define a map of type array int -> uint32_t, with 10 entries
 */
DEFINE_BPF_MAP(name_of_my_map, ARRAY, int, uint32_t, 10);

/* this also defines type-safe accessors:
 *   value * bpf_name_of_my_map_lookup_elem(&key);
 *   int bpf_name_of_my_map_update_elem(&key, &value, flags);
 *   int bpf_name_of_my_map_delete_elem(&key);
 * as such it is heavily suggested to use lowercase *_map names.
 * Also note that due to compiler deficiencies you cannot use a type
 * of 'struct foo' but must instead use just 'foo'.  As such structs
 * must not be defined as 'struct foo {}' and must instead be
 * 'typedef struct {} foo'.
 */

DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
   <body-of-code
    ... read or write to MY_MAPNAME
    ... do other things
   >
}

LICENSE("GPL"); // or other license

איפה:

  • name_of_my_map הוא השם של משתנה המפה. השם הזה מאפשר למטען ה-BPF לדעת איזה סוג מפה ליצור ואילו פרמטרים להשתמש בהם. הגדרת המבנה הזו מסופקת על ידי הכותרת bpf_helpers.h שכלולה.
  • PROGTYPE/PROGNAME מייצג את סוג התוכנית ואת שם התוכנית. סוג התוכנית יכול להיות כל אחד מאלה המפורטים בטבלה הבאה. אם סוג של תוכנית לא מופיע ברשימה, אין כלל קפדני למתן שמות לתוכנית. השם צריך להיות ידוע רק לתהליך שמצרף את התוכנית.

  • PROGFUNC היא פונקציה שמוקמת בקטע מסוים בקובץ שנוצר אחרי הידור.

kprobe ה-hook PROGFUNC מצורף להוראה בליבה באמצעות תשתית kprobe. PROGNAME חייב להיות השם של פונקציית הליבה (kernel) שאחריה kprobed. מידע נוסף על kprobes זמין במסמכי התיעוד של הליבה של kprobe.
נקודת מעקב צריך לחבר את PROGFUNC לנקודת מעקב. PROGNAME חייב להיות בפורמט SUBSYSTEM/EVENT. לדוגמה, קטע של נקודת מעקב (tracepoint) להצמדת פונקציות לאירועי מעבר הקשר של מתזמן יהיה SEC("tracepoint/sched/sched_switch"), כאשר sched הוא השם של מערכת המשנה למעקב ו-sched_switch הוא השם של אירוע המעקב. למידע נוסף על נקודות מעקב, עיינו במסמכי התיעוד בליבה של אירועי מעקב.
skfilter התוכנית פועלת כמסנן שקע של רשתות.
schedcls התוכנית פועלת כסיווג של תעבורת הרשת.
cgroupskb, ‏ cgroupsock התוכנית פועלת בכל פעם שתהליכים ב-CGroup יוצרים שקע AF_INET או AF_INET6.

סוגי קבצים נוספים מפורטים בקוד המקור של ה-Loader.

לדוגמה, תוכנית myschedtp.c הבאה מוסיפה מידע על מזהה המשימה (PID) האחרון שפעל ב-CPU מסוים. התוכנית הזו משיגה את המטרה שלה על ידי יצירת מפה והגדרת פונקציית tp_sched_switch שאפשר לצרף לאירוע המעקב sched:sched_switch. מידע נוסף זמין במאמר צירוף תוכנות לנקודות מעקב.

#include <linux/bpf.h>
#include <stdbool.h>
#include <stdint.h>
#include <bpf_helpers.h>

DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024);

struct switch_args {
    unsigned long long ignore;
    char prev_comm[16];
    int prev_pid;
    int prev_prio;
    long long prev_state;
    char next_comm[16];
    int next_pid;
    int next_prio;
};

DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_SYSTEM, tp_sched_switch)
(struct switch_args *args) {
    int key;
    uint32_t val;

    key = bpf_get_smp_processor_id();
    val = args->next_pid;

    bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY);
    return 1; // return 1 to avoid blocking simpleperf from receiving events
}

LICENSE("GPL");

המאקרו LICENSE משמש לאימות שהתוכנית תואמת לרישיון הליבה כשהתוכנית משתמשת בפונקציות העזר של BPF שסופקו על ידי הליבה. מציינים את שם הרישיון של התוכנית כמחרוזת, למשל LICENSE("GPL") או LICENSE("Apache 2.0").

הפורמט של הקובץ Android.bp

כדי שמערכת ה-build של Android תיצור תוכנית .c של eBPF, צריך ליצור רשומה בקובץ Android.bp של הפרויקט. לדוגמה, כדי לבנות תוכנת eBPF C בשם bpf_test.c, יוצרים את הערך הבא בקובץ Android.bp של הפרויקט:

bpf {
    name: "bpf_test.o",
    srcs: ["bpf_test.c"],
    cflags: [
        "-Wall",
        "-Werror",
    ],
}

הרשומה הזו מקמפלת את תוכנית ה-C שמתקבלת מהאובייקט /system/etc/bpf/bpf_test.o. בזמן האתחול, מערכת Android טוענת באופן אוטומטי את התוכנית bpf_test.o לליבת המערכת.

קבצים שזמינים ב-sysfs

במהלך ההפעלה, מערכת Android טוענת באופן אוטומטי את כל אובייקטי eBPF מ-/system/etc/bpf/, יוצרת את המפות הנחוצות לתוכנה ומצמידים את התוכנית שנטענה עם המפות שלה למערכת הקבצים BPF. לאחר מכן אפשר להשתמש בקבצים האלה לצורך אינטראקציה נוספת עם תוכנית ה-eBPF או לקריאת מפות. בקטע הזה מתוארים המוסכמים לבחירת השמות של הקבצים האלה והמיקומים שלהם ב-sysfs.

הקבצים הבאים נוצרים ומתויגים כמקבצים מוצמדים:

  • לכל תוכנית שנטענת, בהנחה ש-PROGNAME הוא שם התוכנית ו-FILENAME הוא שם קובץ ה-C של eBPF, מערך האתחול של Android יוצר ומצמיד כל תוכנית אל /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    לדוגמה, בדוגמה הקודמת של נקודת המעקב sched_switch ב-myschedtp.c, נוצר קובץ תוכנית שמקובע ל-/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch.

  • לכל המפות שנוצרות, בהנחה ש-MAPNAME הוא שם המפה ו-FILENAME הוא שם קובץ ה-C של eBPF, מערך האפליקציות של Android יוצר ומצמיד כל מפה אל /sys/fs/bpf/map_FILENAME_MAPNAME.

    לדוגמה, בדוגמה הקודמת של נקודת המעקב sched_switch ב-myschedtp.c, נוצר קובץ מפה ומוצמד ל-/sys/fs/bpf/map_myschedtp_cpu_pid_map.

  • הפונקציה bpf_obj_get() בספריית Android BPF מחזירה מתאר קובץ מהקובץ /sys/fs/bpf המוצמד. אפשר להשתמש במתאר הקובץ הזה לפעולות נוספות, כמו קריאת מפות או צירוף תוכנה לנקודת מעקב.

ספריית BPF ל-Android

ספריית BPF של Android נקראת libbpf_android.so והיא חלק מתמונת המערכת. הספרייה הזו מספקת למשתמש יכולות eBPF ברמה נמוכה שנדרשות ליצירה ולקריאה של מפות, ליצירה של בדיקות, נקודות מעקב ומאגרי ביצועים.

צירוף תוכניות לנקודות מעקב

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

  1. קוראים ל-bpf_obj_get() כדי לקבל את התוכנית fd מהמיקום של הקובץ המוצמד. מידע נוסף זמין במאמר קבצים שזמינים ב-sysfs.
  2. קוראים ל-bpf_attach_tracepoint() בספריית ה-BPF, ומעבירים לה את התוכנית fd ואת שם נקודת המעקב.

בקטע הקוד הבא מוסבר איך לצרף את נקודת המעקב sched_switch שהוגדרה בקובץ המקור הקודם myschedtp.c (בדיקת השגיאות לא מוצגת):

  char *tp_prog_path = "/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch";
  char *tp_map_path = "/sys/fs/bpf/map_myschedtp_cpu_pid";

  // Attach tracepoint and wait for 4 seconds
  int mProgFd = bpf_obj_get(tp_prog_path);
  int mMapFd = bpf_obj_get(tp_map_path);
  int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch");
  sleep(4);

  // Read the map to find the last PID that ran on CPU 0
  android::bpf::BpfMap<int, int> myMap(mMapFd);
  printf("last PID running on CPU %d is %d\n", 0, myMap.readValue(0));

הקראה מהמפות

מפות BPF תומכות במבנים או בסוגים מורכבים שרירותיים של מפתחות וערכים. ספריית Android BPF כוללת את הכיתה android::BpfMap שמשתמשת בתבניות C++‎ כדי ליצור מופע של BpfMap על סמך סוג המפתח והערך של המפה הרלוונטית. דוגמת הקוד הקודמת מדגימה את השימוש ב-BpfMap עם מפתח וערך בתור מספרים שלמים. המספרים השלמים יכולים להיות גם מבנים שרירותיים.

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

מידע נוסף על BpfMap זמין במקורות Android.

פתרון בעיות בניפוי באגים

במהלך תהליך האתחול, מתועדות ביומן כמה הודעות שקשורות לטעינה של BPF. אם תהליך הטעינה נכשל מסיבה כלשהי, תופיע הודעת יומן מפורטת ב-Logcat. סינון היומנים של logcat לפי bpf מדפיס את כל ההודעות ואת כל השגיאות המפורטות במהלך זמן הטעינה, כמו שגיאות של מאמת eBPF.

דוגמאות ל-eBPF ב-Android

בתוכניות הבאות ב-AOSP יש דוגמאות נוספות לשימוש ב-eBPF:

  • תוכנית eBPF Cnetd משמשת את הדימון (daemon) ב-Android למטרות שונות, כמו סינון שקעים ואיסוף נתונים סטטיסטיים. כדי לראות איך משתמשים בתוכנית הזו, תוכלו לעיין במקורות של eBPF traffic monitor.

  • התוכנית ב-C של eBPFtime_in_state מחשבת את משך הזמן שבו אפליקציית Android נמצאת בתדרים שונים של מעבד, ומשמשת לחישוב צריכת האנרגיה.

  • ב-Android 12, התוכנית C של eBPFgpu_mem עוקבת אחרי השימוש הכולל בזיכרון ה-GPU לכל תהליך ולמערכת כולה. התוכנית הזו משמשת ליצירת פרופיל זיכרון ב-GPU.