Extended Berkeley Packet Filter (eBPF) היא מכונה וירטואלית בתוך הליבה שמריצה תוכניות eBPF שסופקו על ידי המשתמש כדי להרחיב את הפונקציונליות של הליבה. אפשר לחבר את התוכנות האלה לבדיקות או לאירועים בליבה (kernel) ולהשתמש בהן כדי לאסוף נתונים סטטיסטיים שימושיים על הליבה, לעקוב אחריה ולנפות בה באגים. תוכנית נטענת לליבה באמצעות קריאת המערכת bpf(2) והמשתמש מספק אותה כ-blob בינארי של הוראות מכונה של eBPF. מערכת ה-build של Android תומכת בהידור של תוכנות C ל-eBPF באמצעות תחביר פשוט של קובץ build שמתואר במסמך הזה.
מידע נוסף על הארכיטקטורה והרכיבים הפנימיים של eBPF זמין בדף eBPF של Brendan Gregg.
Android כולל טוען eBPF וספרייה שטוענים תוכניות eBPF בזמן האתחול.
Android BPF loader
במהלך ההפעלה של Android, כל תוכניות ה-eBPF שנמצאות ב-/system/etc/bpf/ נטענות. התוכנות האלה הן אובייקטים בינאריים שנוצרו על ידי מערכת ה-build של Android מתוכנות C, והן מלוות בקובצי Android.bp בעץ המקור של Android. מערכת ה-build שומרת את האובייקטים שנוצרו ב-/system/etc/bpf, והאובייקטים האלה הופכים לחלק מקובץ האימג' של המערכת.
פורמט של תוכנית Android eBPF C
תוכנית 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 | מתחבר אל PROGFUNC בהוראת ליבה (kernel) באמצעות תשתית kprobe. 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) של המשימה האחרונה שרצה במעבד (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 תיצור תוכנית eBPF .c, צריך ליצור רשומה בקובץ 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שמוצמד. אפשר להשתמש בתיאור הקובץ הזה לפעולות נוספות, כמו קריאת מפות או צירוף תוכנית לנקודת מעקב.
ספריית Android BPF
ספריית ה-BPF של Android נקראת libbpf_android.so והיא חלק מקובץ האימג' של המערכת. הספרייה הזו מספקת למשתמש יכולות eBPF ברמה נמוכה שנדרשות ליצירה ולקריאה של מפות, ליצירה של בדיקות, נקודות מעקב ומאגרי perf.
צירוף תוכניות לנקודות מעקב (tracepoints)
תוכניות Tracepoint נטענות באופן אוטומטי בזמן ההפעלה. אחרי הטעינה, צריך להפעיל את תוכנית נקודת המעקב באמצעות השלבים הבאים:
- קוראים לפונקציה
bpf_obj_get()כדי לקבל את התוכניתfdמהמיקום של הקובץ המוצמד. מידע נוסף זמין במאמר קבצים שזמינים ב-sysfs. - קוראים לפונקציה
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 C של
netdמשמשת את הדימון (daemon) של הרשת (netd) ב-Android למטרות שונות, כמו סינון שקעים ואיסוף נתונים סטטיסטיים. כדי לראות איך משתמשים בתוכנית הזו, אפשר לעיין במקורות של כלי המעקב אחר תנועת נתונים ב-eBPF.time_in_stateתוכנית eBPF C מחשבת את משך הזמן שאפליקציית Android מבלה בתדרים שונים של CPU, ומשתמשת בנתונים האלה כדי לחשב את צריכת החשמל.ב-Android 12,
gpu_memתוכנית eBPF C עוקבת אחרי השימוש הכולל בזיכרון ה-GPU בכל תהליך ובמערכת כולה. התוכנה הזו משמשת ליצירת פרופילים של זיכרון GPU.
במאמר איסוף נתוני טלמטריה של הליבה באמצעות eBPF מוסבר איך משתמשים ב-eBPF כדי לאסוף היסטוגרמות של ניהול זיכרון הליבה וזמן האחזור של החשמל עם תקורה נמוכה ב-statsd.