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

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

מידע נוסף על המבנה הפנימי והארכיטקטורה של eBPF זמין בדף 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

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

#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 ‫Hooks PROGFUNC onto at a kernel instruction using the kprobe infrastructure. ‫PROGNAME חייב להיות השם של פונקציית הליבה שנבדקת באמצעות kprobe. מידע נוסף על kprobes מופיע במסמכי העזרה של ליבת kprobe.
נקודת מעקב ‫Hooks PROGFUNC אל נקודת מעקב. ‫PROGNAME חייב להיות בפורמט SUBSYSTEM/EVENT. לדוגמה, קטע של נקודת מעקב לצירוף פונקציות לאירועי החלפת הקשר של המתזמן יהיה SEC("tracepoint/sched/sched_switch"), כאשר sched הוא השם של מערכת המשנה של המעקב ו-sched_switch הוא השם של אירוע המעקב. מידע נוסף על נקודות מעקב זמין במסמכי התיעוד בנושא אירועי מעקב של ליבת המערכת.
skfilter פונקציות תוכנית כמסנן שקע ברשת.
schedcls פונקציות תוכנה ככלי לסיווג תנועת גולשים ברשת.
cgroupskb, cgroupsock התוכנית פועלת בכל פעם שתהליכים ב-CGroup יוצרים שקע AF_INET או AF_INET6.

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

לדוגמה, התוכנית myschedtp.c הבאה מוסיפה מידע על מזהה התהליך (PID) של המשימה האחרונה שהופעלה במעבד מסוים. התוכנית הזו משיגה את המטרה שלה על ידי יצירת מפה והגדרת 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");

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

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

כדי שמערכת ה-build של Android תיצור תוכנית eBPF .c, צריך ליצור רשומה בקובץ 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 או כדי לקרוא מפות. בקטע הזה מתואמות המוסכמות שמשמשות למתן שמות לקבצים האלה ולמיקומים שלהם ב-sysfs.

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

  • לכל תוכנית שנטענה, בהנחה ש-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 הוא שם קובץ ה-eBPF C, טוען 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 המוצמד. אפשר להשתמש בתיאור הקובץ הזה לפעולות נוספות, כמו קריאת מפות או צירוף תוכנית לנקודת מעקב.

ספריית Android BPF

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

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

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

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

בדוגמה הבאה של קוד אפשר לראות איך לצרף את sched_switch tracepoint שמוגדר בקובץ המקור 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:

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

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

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