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

Extended Berkeley Packet Filter‏ (eBPF) היא מכונה וירטואלית בליבה שמריצה תוכניות eBPF שסופקו על ידי משתמשים כדי להרחיב את הפונקציונליות של הליבה. התוכניות האלה יכול להיות מחובר לגששים או לאירועים בליבה (kernel) ולהשתמש בו כדי לאסוף נתונים סטטיסטיים של ליבה (kernel), מעקב וניפוי באגים. תוכנית היא נטען לליבה (kernel) באמצעות ה-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 מחבר את PROGFUNC להוראה ליבה (kernel) באמצעות בתשתית kprobe. PROGNAME חייב להיות שם הליבה שמקודדת ב-Kprobed. מידע נוסף על kprobes זמין במסמכי התיעוד של הליבה של kprobe.
tracepoint צריך לחבר את PROGFUNC לנקודת מעקב. PROGNAME חייב להיות בפורמט SUBSYSTEM/EVENT. לדוגמה, קטע של נקודת מעקב לצירוף פונקציות כדי לקבוע אירועי החלפת הקשר במתזמן, SEC("tracepoint/sched/sched_switch"), כאשר sched השם של מערכת המשנה למעקב, ו-sched_switch הוא השם של אירוע המעקב. בדיקת הליבה (kernel) של אירועי מעקב תיעודלמידע נוסף על נקודות מעקב.
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 של הפרויקט. לדוגמה, כדי ליצור תוכנית C ב-eBPF בשם 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 או קריאת מפות. הקטע הזה מתאר את המוסכמות למתן שמות לקבצים האלה ואת המיקום שלהם sysf.

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

  • לגבי כל תוכנית שנטענה, בהנחה ש-PROGNAME הוא שם התוכנית FILENAME הוא השם של קובץ eBPF C, טוען ה-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

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

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

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

  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:

  • netd eBPF C תוכנית משמש את הדימון (daemon) ב-Android למטרות שונות, סינון שקעים ואיסוף נתונים סטטיסטיים. כדי לראות את אופן השימוש בתוכנית הזו: בדיקת תנועת eBPF לעקוב אחרי מקורות.

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

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