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

Extended Berkeley Packet Filter (eBPF) یک ماشین مجازی درون هسته ای است که برنامه های eBPF ارائه شده توسط کاربر را برای گسترش عملکرد هسته اجرا می کند. این برنامه‌ها را می‌توان به پروب‌ها یا رویدادهای هسته متصل کرد و برای جمع‌آوری آمار مفید هسته، نظارت و اشکال‌زدایی استفاده کرد. یک برنامه با استفاده از bpf(2) syscall در هسته بارگذاری می شود و توسط کاربر به عنوان یک حباب باینری از دستورالعمل های ماشین eBPF ارائه می شود. سیستم ساخت آندروید از کامپایل برنامه های C در eBPF با استفاده از نحو فایل ساخت ساده که در این سند توضیح داده شده است، پشتیبانی می کند.

اطلاعات بیشتر در مورد ساختارهای داخلی و معماری eBPF را می توانید در صفحه eBPF برندان گرگ بیابید.

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

لودر BPF اندروید

در طول بوت اندروید، تمام برنامه های eBPF واقع در /system/etc/bpf/ بارگذاری می شوند. این برنامه ها اشیای باینری هستند که توسط سیستم ساخت اندروید از برنامه های C ساخته شده اند و با فایل های Android.bp در درخت منبع اندروید همراه هستند. سیستم ساخت اشیاء تولید شده را در /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 will also define 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 باید نام تابع هسته ای باشد که kprob می شود. برای اطلاعات بیشتر در مورد kprobe به مستندات هسته kprobe مراجعه کنید.
نقطه ردیابی PROGFUNC روی نقطه ردیابی قلاب می کند. PROGNAME باید از قالب SUBSYSTEM/EVENT باشد. به عنوان مثال، یک بخش نقطه ردیابی برای پیوست کردن توابع به رویدادهای سوئیچ زمینه زمانبندی SEC("tracepoint/sched/sched_switch") خواهد بود، که در آن sched نام زیرسیستم ردیابی است و sched_switch نام رویداد ردیابی است. برای اطلاعات بیشتر در مورد نقاط ردیابی ، اسناد هسته رویدادهای ردیابی را بررسی کنید.
فیلتر شکن برنامه به عنوان فیلتر سوکت شبکه عمل می کند.
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");

هنگامی که برنامه از توابع کمکی BPF ارائه شده توسط کرنل استفاده می کند، از ماکرو LICENSE برای تأیید سازگاری برنامه با مجوز هسته استفاده می شود. نام مجوز برنامه خود را به صورت رشته ای، مانند LICENSE("GPL") یا LICENSE("Apache 2.0") مشخص کنید.

فرمت فایل Android.bp

برای اینکه سیستم ساخت اندروید بتواند یک برنامه 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 می‌شود. در هنگام بوت، سیستم اندروید به طور خودکار برنامه bpf_test.o را در هسته بارگذاری می کند.

فایل های موجود در sysfs

در هنگام بوت، سیستم اندروید به طور خودکار تمام اشیاء eBPF را از /system/etc/bpf/ بارگیری می کند، نقشه های مورد نیاز برنامه را ایجاد می کند و برنامه بارگذاری شده را با نقشه هایش به سیستم فایل BPF پین می کند. سپس می توان از این فایل ها برای تعامل بیشتر با برنامه eBPF یا خواندن نقشه ها استفاده کرد. این بخش قراردادهای مورد استفاده برای نامگذاری این فایل ها و مکان آنها در sysfs را شرح می دهد.

فایل های زیر ایجاد و پین می شوند:

  • برای هر برنامه‌ای که بارگذاری شده است، با فرض اینکه PROGNAME نام برنامه و FILENAME نام فایل eBPF C باشد، بارگذار Android هر برنامه را در /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME ایجاد کرده و پین می‌کند.

    به عنوان مثال، برای مثال قبلی sched_switch tracepoint در myschedtp.c ، یک فایل برنامه ایجاد شده و به /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch پین می‌شود.

  • برای هر نقشه ایجاد شده، با فرض اینکه MAPNAME نام نقشه و FILENAME نام فایل eBPF C باشد، بارگذار Android هر نقشه را ایجاد کرده و به /sys/fs/bpf/map_FILENAME_MAPNAME پین می‌کند.

    به عنوان مثال، برای مثال قبلی sched_switch tracepoint در myschedtp.c ، یک فایل نقشه ایجاد شده و به /sys/fs/bpf/map_myschedtp_cpu_pid_map پین می‌شود.

  • bpf_obj_get() در کتابخانه Android BPF یک توصیفگر فایل را از فایل /sys/fs/bpf پین شده برمی گرداند. این توصیفگر فایل می تواند برای عملیات های بعدی مانند خواندن نقشه ها یا پیوست کردن یک برنامه به یک نقطه ردیابی استفاده شود.

کتابخانه Android BPF

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

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

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

  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 به منابع اندروید مراجعه کنید.

مشکلات اشکال زدایی

در طول زمان بوت، چندین پیام مربوط به بارگذاری BPF ثبت می شود. اگر فرآیند بارگذاری به هر دلیلی با شکست مواجه شود، یک پیام گزارش دقیق در logcat ارائه می شود. فیلتر کردن گزارش‌های logcat بر اساس "bpf" همه پیام‌ها و هرگونه خطای دقیق را در طول زمان بارگذاری چاپ می‌کند، مانند خطاهای تأییدکننده eBPF.

نمونه هایی از eBPF در اندروید

برنامه های زیر در AOSP نمونه های بیشتری از استفاده از eBPF ارائه می دهند:

  • برنامه netd eBPF C توسط شبح شبکه (netd) در اندروید برای اهداف مختلفی مانند فیلتر سوکت و جمع آوری آمار استفاده می شود. برای مشاهده نحوه استفاده از این برنامه، منابع نظارت بر ترافیک eBPF را بررسی کنید.

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

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