Mở rộng hạt nhân với eBPF

Bộ lọc gói Berkeley mở rộng (eBPF) là một máy ảo trong kernel chạy các chương trình eBPF do người dùng cung cấp để mở rộng chức năng kernel. Các chương trình này có thể được nối với các đầu dò hoặc sự kiện trong kernel và được sử dụng để thu thập số liệu thống kê, giám sát và gỡ lỗi hữu ích của kernel. Một chương trình được tải vào kernel bằng cách sử dụng lệnh gọi tòa nhà bpf(2) và được người dùng cung cấp dưới dạng blob nhị phân của các lệnh máy eBPF. Hệ thống xây dựng Android có hỗ trợ biên dịch chương trình C thành eBPF bằng cú pháp tệp xây dựng đơn giản được mô tả trong tài liệu này.

Thông tin thêm về cấu trúc và nội bộ của eBPF có thể được tìm thấy tại trang eBPF của Brendan Gregg .

Android bao gồm trình tải eBPF và thư viện tải các chương trình eBPF khi khởi động.

Trình tải BPF của Android

Trong quá trình khởi động Android, tất cả các chương trình eBPF có tại /system/etc/bpf/ đều được tải. Các chương trình này là các đối tượng nhị phân được hệ thống xây dựng Android xây dựng từ các chương trình C và đi kèm với các tệp Android.bp trong cây nguồn Android. Hệ thống xây dựng lưu trữ các đối tượng được tạo tại /system/etc/bpf và những đối tượng đó trở thành một phần của hình ảnh hệ thống.

Định dạng của chương trình Android eBPF C

Chương trình eBPF C phải có định dạng sau:

#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

Ở đâu:

  • name_of_my_map là tên biến bản đồ của bạn. Tên này thông báo cho trình tải BPF về loại bản đồ cần tạo và với những tham số nào. Định nghĩa cấu trúc này được cung cấp bởi tiêu đề bpf_helpers.h đi kèm.
  • PROGTYPE/PROGNAME đại diện cho loại chương trình và tên chương trình. Loại chương trình có thể là bất kỳ loại nào được liệt kê trong bảng sau. Khi một loại chương trình không được liệt kê thì không có quy ước đặt tên nghiêm ngặt nào cho chương trình đó; quy trình đính kèm chương trình chỉ cần biết tên.

  • PROGFUNC là một hàm mà khi biên dịch sẽ được đặt trong một phần của tệp kết quả.

đầu dò Kết nối PROGFUNC vào lệnh kernel bằng cơ sở hạ tầng kprobe. PROGNAME phải là tên của hàm kernel đang được kiểm tra. Tham khảo tài liệu kernel kprobe để biết thêm thông tin về kprobe.
dấu vết Nối PROGFUNC vào một điểm theo dõi. PROGNAME phải có định dạng SUBSYSTEM/EVENT . Ví dụ: phần tracepoint để gắn các hàm vào các sự kiện chuyển ngữ cảnh của bộ lập lịch sẽ là SEC("tracepoint/sched/sched_switch") , trong đó sched là tên của hệ thống con theo dõi và sched_switch là tên của sự kiện theo dõi. Kiểm tra tài liệu hạt nhân sự kiện theo dõi để biết thêm thông tin về điểm theo dõi.
skfilter Chương trình hoạt động như một bộ lọc ổ cắm mạng.
lịch trình Chương trình hoạt động như một bộ phân loại lưu lượng truy cập mạng.
cgroupskb, cgroupsock Chương trình chạy bất cứ khi nào các tiến trình trong CGroup tạo ổ cắm AF_INET hoặc AF_INET6.

Các loại bổ sung có thể được tìm thấy trong mã nguồn Trình tải .

Ví dụ: chương trình myschedtp.c sau đây bổ sung thêm thông tin về tác vụ PID mới nhất đã chạy trên một CPU cụ thể. Chương trình này đạt được mục tiêu bằng cách tạo bản đồ và xác định hàm tp_sched_switch có thể được gắn vào sự kiện theo dõi sched:sched_switch . Để biết thêm thông tin, hãy xem Đính kèm chương trình vào điểm theo dõi .

#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");

Macro LICENSE được sử dụng để xác minh xem chương trình có tương thích với giấy phép của kernel hay không khi chương trình sử dụng các chức năng trợ giúp BPF do kernel cung cấp. Chỉ định tên giấy phép chương trình của bạn ở dạng chuỗi, chẳng hạn như LICENSE("GPL") hoặc LICENSE("Apache 2.0") .

Định dạng của tệp Android.bp

Để hệ thống xây dựng Android xây dựng chương trình eBPF .c , bạn phải tạo một mục trong tệp Android.bp của dự án. Ví dụ: để xây dựng chương trình eBPF C có tên bpf_test.c , hãy tạo mục nhập sau trong tệp Android.bp của dự án của bạn:

bpf {
    name: "bpf_test.o",
    srcs: ["bpf_test.c"],
    cflags: [
        "-Wall",
        "-Werror",
    ],
}

Mục này biên dịch chương trình C dẫn đến đối tượng /system/etc/bpf/bpf_test.o . Khi khởi động, hệ thống Android sẽ tự động tải chương trình bpf_test.o vào kernel.

Các tập tin có sẵn trong sysfs

Trong quá trình khởi động, hệ thống Android sẽ tự động tải tất cả các đối tượng eBPF từ /system/etc/bpf/ , tạo các bản đồ mà chương trình cần và ghim chương trình đã tải cùng với các bản đồ của nó vào hệ thống tệp BPF. Sau đó, những tệp này có thể được sử dụng để tương tác thêm với chương trình eBPF hoặc đọc bản đồ. Phần này mô tả các quy ước được sử dụng để đặt tên cho các tệp này và vị trí của chúng trong sysfs.

Các tệp sau đây được tạo và ghim:

  • Đối với bất kỳ chương trình nào được tải, giả sử PROGNAME là tên của chương trình và FILENAME là tên của tệp eBPF C, trình tải Android sẽ tạo và ghim từng chương trình tại /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME .

    Ví dụ: đối với ví dụ về điểm theo dõi sched_switch trước đó trong myschedtp.c , một tệp chương trình sẽ được tạo và ghim vào /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch .

  • Đối với mọi bản đồ được tạo, giả sử MAPNAME là tên bản đồ và FILENAME là tên của tệp eBPF C, trình tải Android sẽ tạo và ghim từng bản đồ vào /sys/fs/bpf/map_FILENAME_MAPNAME .

    Ví dụ: đối với ví dụ về điểm theo dõi sched_switch trước đó trong myschedtp.c , một tệp bản đồ sẽ được tạo và ghim vào /sys/fs/bpf/map_myschedtp_cpu_pid_map .

  • bpf_obj_get() trong thư viện BPF của Android trả về bộ mô tả tệp từ tệp /sys/fs/bpf được ghim. Bộ mô tả tệp này có thể được sử dụng cho các hoạt động tiếp theo, chẳng hạn như đọc bản đồ hoặc đính kèm chương trình vào điểm theo dõi.

Thư viện BPF của Android

Thư viện BPF của Android có tên libbpf_android.so và là một phần của hình ảnh hệ thống. Thư viện này cung cấp cho người dùng chức năng eBPF cấp thấp cần thiết để tạo và đọc bản đồ, tạo đầu dò, điểm theo dõi và bộ đệm hoàn hảo.

Gắn chương trình vào tracepoint

Các chương trình Tracepoint được tải tự động khi khởi động. Sau khi tải, chương trình tracepoint phải được kích hoạt bằng các bước sau:

  1. Gọi bpf_obj_get() để lấy chương trình fd từ vị trí của tệp được ghim. Để biết thêm thông tin, hãy tham khảo Tệp có sẵn trong sysfs .
  2. Gọi bpf_attach_tracepoint() trong thư viện BPF, chuyển cho nó chương trình fd và tên tracepoint.

Mẫu mã sau đây cho biết cách đính kèm điểm theo dõi sched_switch được xác định trong tệp nguồn myschedtp.c trước đó (việc kiểm tra lỗi không được hiển thị):

  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));

Đọc từ bản đồ

Bản đồ BPF hỗ trợ các cấu trúc hoặc loại khóa và giá trị phức tạp tùy ý. Thư viện BPF của Android bao gồm một lớp android::BpfMap sử dụng các mẫu C++ để khởi tạo BpfMap dựa trên loại khóa và giá trị cho bản đồ được đề cập. Mẫu mã trước đó minh họa cách sử dụng BpfMap với khóa và giá trị dưới dạng số nguyên. Các số nguyên cũng có thể là cấu trúc tùy ý.

Do đó, lớp BpfMap được tạo khuôn mẫu giúp dễ dàng xác định đối tượng BpfMap tùy chỉnh phù hợp với bản đồ cụ thể. Sau đó, bản đồ có thể được truy cập bằng cách sử dụng các hàm được tạo tùy chỉnh, nhận biết loại, dẫn đến mã sạch hơn.

Để biết thêm thông tin về BpfMap , hãy tham khảo các nguồn Android .

Sự cố gỡ lỗi

Trong thời gian khởi động, một số thông báo liên quan đến tải BPF sẽ được ghi lại. Nếu quá trình tải không thành công vì bất kỳ lý do gì, thông báo tường trình chi tiết sẽ được cung cấp trong logcat. Lọc nhật ký logcat theo "bpf" sẽ in tất cả thông báo và mọi lỗi chi tiết trong thời gian tải, chẳng hạn như lỗi xác minh eBPF.

Ví dụ về eBPF trong Android

Các chương trình sau trong AOSP cung cấp thêm các ví dụ về cách sử dụng eBPF:

  • Chương trình netd eBPF C được daemon mạng (netd) trong Android sử dụng cho nhiều mục đích khác nhau như lọc ổ cắm và thu thập số liệu thống kê. Để xem chương trình này được sử dụng như thế nào, hãy kiểm tra các nguồn giám sát lưu lượng eBPF .

  • Chương trình time_in_state eBPF C tính toán lượng thời gian mà ứng dụng Android sử dụng ở các tần số CPU khác nhau, dùng để tính toán công suất.

  • Trong Android 12, chương trình gpu_mem eBPF C theo dõi tổng mức sử dụng bộ nhớ GPU cho từng quy trình và cho toàn bộ hệ thống. Chương trình này được sử dụng để lập hồ sơ bộ nhớ GPU.