گسترش هسته با eBPF

«فیلتر بسته برکلی گسترش‌یافته» (eBPF) یک ماشین مجازی درون هسته‌ای است که برنامه‌های eBPF ارائه‌شده توسط کاربر را برای گسترش عملکرد هسته اجرا می‌کند. این برنامه‌ها می‌توانند به کاوشگرها یا رویدادهای هسته متصل شوند و برای جمع‌آوری آمار هسته مفید، نظارت، و اشکال‌زدایی استفاده شوند. برنامه بااستفاده از فراخوان سیستم bpf(2) در هسته بار می‌شود و کاربر آن را به‌صورت یک حباب دودویی از دستورالعمل‌های ماشین eBPF ارائه می‌دهد. سیستم ساخت Android از کامپایل کردن برنامه‌های C به eBPF بااستفاده از نحو ساده فایل ساخت که در این سند شرح داده شده است پشتیبانی می‌کند.

اطلاعات بیشتر درباره معماری و اجزای داخلی eBPF را می‌توانید در صفحه eBPF برندان گرگ پیدا کنید.

‫Android شامل بارکننده و کتابخانه eBPF است که برنامه‌های eBPF را در زمان راه‌اندازی بار می‌کند.

بارکننده BPF در Android

درطول راه‌اندازی Android، همه برنامه‌های eBPF واقع در /system/etc/bpf/ بارگیری می‌شوند. این برنامه‌ها اشیای دودویی هستند که توسط سیستم ساخت Android از برنامه‌های C ساخته شده‌اند و با فایل‌های Android.bp در درخت منبع Android همراه هستند. سیستم ساخت، اشیای تولیدشده را در /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 بااستفاده از زیرساخت kprobe، به دستورالعمل هسته در PROGFUNC متصل می‌شود. ‫PROGNAME باید نام تابع هسته ای باشد که درحال کاوش با kprobe است. برای کسب اطلاعات بیشتر درباره kprobes، به اسناد هسته kprobe مراجعه کنید.
نقطه ردیابی PROGFUNC را به نقطه ردیابی متصل می‌کند. ‫PROGNAME باید در قالب SUBSYSTEM/EVENT باشد. برای مثال، بخش نقطه ردیابی برای پیوست کردن توابع به رویدادهای تغییر بافت زمان‌بندی‌کننده به‌صورت SEC("tracepoint/sched/sched_switch") خواهد بود، که در آن sched نام زیرسیستم ردیابی و sched_switch نام رویداد ردیابی است. برای اطلاعات بیشتر درباره نقاط ردیابی، اسناد هسته رویدادهای ردیابی را بررسی کنید.
skfilter برنامه به‌عنوان فیلتر سوکت شبکه‌سازی عمل می‌کند.
schedcls برنامه به‌عنوان طبقه‌بندی‌کننده ترافیک شبکه عمل می‌کند.
cgroupskb, cgroupsock برنامه هرگاه فرایندهای موجود در CGroup یک سوکت AF_INET یا AF_INET6 ایجاد کنند اجرا می‌شود.

انواع دیگر را می‌توانید در کد منبع Loader پیدا کنید.

برای مثال، برنامه myschedtp.c زیر اطلاعاتی درباره آخرین شناسه فرایند وظیفه که در واحد پردازش مرکزی خاصی اجرا شده است اضافه می‌کند. این برنامه با ایجاد نقشه و تعریف تابع 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

برای اینکه سیستم ساخت 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 برمی‌گرداند. از این توصیف‌گر فایل می‌توان برای عملیات‌های بیشتر، مثل خواندن نقشه‌ها یا پیوست کردن برنامه به نقطه ردیابی، استفاده کرد.

کتابخانه BPF در Android

کتابخانه Android BPF با نام libbpf_android.so بخشی از تصویر سیستم است. این کتابخانه قابلیت‌های سطح پایین eBPF موردنیاز برای ایجاد و خواندن نقشه‌ها، ایجاد کاوشگرها، نقاط ردیابی، و بافرهای perf را دراختیار کاربر قرار می‌دهد.

پیوست کردن برنامه‌ها به نقاط ردیابی

برنامه‌های نقطه ردیابی به‌طور خودکار در زمان راه‌اندازی بار می‌شوند. پس‌از بار کردن، برنامه نقطه ردیابی باید بااستفاده از این مراحل فعال شود:

  1. برای دریافت برنامه fd از مکان فایل سنجاق‌شده، با bpf_obj_get() تماس بگیرید. برای اطلاعات بیشتر، به فایل‌های دردسترس در 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 برنامه C ‏eBPF توسط کارگزار شبکه (netd) در Android برای اهداف مختلفی مانند فیلتر کردن سوکت و جمع‌آوری آمار استفاده می‌شود. برای دیدن نحوه استفاده از این برنامه، منابع مانیتور ترافیک eBPF را بررسی کنید.

  • time_in_state برنامه eBPF C مدت زمانی را که برنامه Android در بسامدهای مختلف CPU صرف می‌کند محاسبه می‌کند، که از آن برای محاسبه توان استفاده می‌شود.

  • در Android 12، gpu_mem برنامه eBPF C کل استفاده از حافظه GPU را برای هر فرایند و برای کل سیستم ردیابی می‌کند. این برنامه برای نمایه‌سازی حافظه GPU استفاده می‌شود.

برای کسب اطلاعات بیشتر درباره استفاده از eBPF برای جمع‌آوری هیستوگرام‌های مدیریت حافظه هسته و تأخیر توان با سربار کم برای statsd، به جمع‌آوری دورسنجی هسته با eBPF مراجعه کنید.