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

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

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

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

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

در طول بوت اندروید، تمام برنامه‌های eBPF که در مسیر /system/etc/bpf/ قرار دارند، بارگذاری می‌شوند. این برنامه‌ها اشیاء دودویی هستند که توسط سیستم ساخت اندروید از برنامه‌های C ساخته شده‌اند و با فایل‌های Android.bp در درخت منبع اندروید همراه هستند. سیستم ساخت، اشیاء تولید شده را در مسیر /system/etc/bpf ذخیره می‌کند و آن اشیاء بخشی از تصویر سیستم می‌شوند.

قالب یک برنامه 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 نام متغیر map شماست. این نام، نوع map مورد نظر و پارامترهای آن را به بارگذار BPF اطلاع می‌دهد. این تعریف ساختار توسط هدر bpf_helpers.h ارائه می‌شود.
  • PROGTYPE/PROGNAME نشان دهنده نوع برنامه و نام برنامه است. نوع برنامه می‌تواند هر یک از موارد ذکر شده در جدول زیر باشد. وقتی نوع برنامه‌ای ذکر نشده باشد، هیچ قرارداد نامگذاری دقیقی برای برنامه وجود ندارد؛ فقط کافی است نام آن برای فرآیندی که برنامه را پیوست می‌کند، شناخته شده باشد.

  • PROGFUNC تابعی است که پس از کامپایل، در بخشی از فایل حاصل قرار می‌گیرد.

کپروب PROGFUNC با استفاده از زیرساخت kprobe به یک دستورالعمل هسته متصل می‌کند. PROGNAME باید نام تابع هسته مورد نظر برای kprobe باشد. برای اطلاعات بیشتر در مورد kprobeها به مستندات هسته kprobe مراجعه کنید.
نقطه ردیابی PROGFUNC به یک نقطه ردیابی قلاب می‌کند. PROGNAME باید با فرمت SUBSYSTEM/EVENT باشد. برای مثال، بخش نقطه ردیابی برای اتصال توابع به رویدادهای سوئیچ زمینه زمان‌بندی به SEC("tracepoint/sched/sched_switch") خواهد بود، که در آن sched نام زیرسیستم ردیابی و sched_switch نام رویداد ردیابی است. برای اطلاعات بیشتر در مورد نقاط ردیابی، مستندات هسته رویدادهای ردیابی را بررسی کنید.
اسکفیلتر این برنامه به عنوان یک فیلتر سوکت شبکه عمل می‌کند.
زمانبندی‌ها این برنامه به عنوان یک طبقه‌بندی‌کننده ترافیک شبکه عمل می‌کند.
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

برای اینکه سیستم ساخت اندروید بتواند یک برنامه 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 باشد، لودر اندروید هر برنامه را ایجاد و در /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 باشد، لودر اندروید هر نقشه را ایجاد و به /sys/fs/bpf/map_FILENAME_MAPNAME پین می‌کند.

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

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

کتابخانه BPF اندروید

کتابخانه 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 از ساختارها یا انواع کلید و مقدار پیچیده دلخواه پشتیبانی می‌کنند. کتابخانه BPF اندروید شامل یک کلاس android::BpfMap است که از قالب‌های C++ برای نمونه‌سازی BpfMap بر اساس نوع کلید و مقدار برای نقشه مورد نظر استفاده می‌کند. نمونه کد قبلی استفاده از یک BpfMap با کلید و مقدار به عنوان اعداد صحیح را نشان می‌دهد. اعداد صحیح همچنین می‌توانند ساختارهای دلخواه باشند.

بنابراین کلاس BpfMap قالب‌بندی شده به شما امکان می‌دهد یک شیء BpfMap سفارشی مناسب برای نقشه خاص تعریف کنید. سپس می‌توان با استفاده از توابع تولید شده سفارشی، که از نوع داده آگاه هستند و در نتیجه کد تمیزتری ایجاد می‌کنند، به نقشه دسترسی پیدا کرد.

برای اطلاعات بیشتر در مورد BpfMap ، به منابع اندروید مراجعه کنید.

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

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

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

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

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

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

  • در اندروید ۱۲، برنامه‌ی gpu_mem eBPF C میزان کل استفاده از حافظه‌ی پردازنده‌ی گرافیکی (GPU) را برای هر فرآیند و برای کل سیستم ردیابی می‌کند. این برنامه برای پروفایل‌بندی حافظه‌ی پردازنده‌ی گرافیکی (GPU memory profiling) استفاده می‌شود.