ขยายเคอร์เนลด้วย eBPF

Extended Berkeley Packet Filter (eBPF) คือเครื่องเสมือนในเคอร์เนลที่เรียกใช้โปรแกรม eBPF ที่ผู้ใช้จัดหาให้เพื่อขยายฟังก์ชันการทำงานของเคอร์เนล โดยโปรแกรมเหล่านี้สามารถเชื่อมต่อกับโพรบหรือเหตุการณ์ในเคอร์เนล และใช้เพื่อรวบรวมสถิติเคอร์เนลที่เป็นประโยชน์ ตรวจสอบ และแก้ไขข้อบกพร่อง ระบบจะโหลดโปรแกรมลงในเคอร์เนลโดยใช้ syscall bpf(2) และผู้ใช้จะจัดหาโปรแกรมในรูปแบบไบนารีบล็อบของคำสั่งเครื่อง eBPF ระบบบิลด์ของ Android รองรับการคอมไพล์โปรแกรม C เป็น eBPF โดยใช้ไวยากรณ์ไฟล์บิลด์อย่างง่ายที่อธิบายไว้ในเอกสารนี้

ดูข้อมูลเพิ่มเติมเกี่ยวกับภายในและสถาปัตยกรรมของ eBPF ได้ที่ Brendan Gregg's eBPF page

Android มีตัวโหลดและไลบรารี eBPF ที่โหลดโปรแกรม eBPF ในเวลาบูต

ตัวโหลด BPF ของ Android

ระหว่างการบูต Android ระบบจะโหลดโปรแกรม eBPF ทั้งหมดที่อยู่ใน /system/etc/bpf/ โปรแกรมเหล่านี้เป็นออบเจ็กต์ไบนารีที่ระบบบิลด์ของ Android สร้างขึ้นจากโปรแกรม C และมาพร้อมกับไฟล์ Android.bp ในซอร์สทรีของ Android ระบบบิลด์จะจัดเก็บออบเจ็กต์ที่สร้างขึ้นไว้ที่ /system/etc/bpf และออบเจ็กต์เหล่านั้นจะกลายเป็นส่วนหนึ่งของอิมเมจระบบ

รูปแบบของโปรแกรม eBPF C ของ Android

โปรแกรม 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 กับคำสั่งเคอร์เนลโดยใช้โครงสร้างพื้นฐาน kprobe PROGNAME ต้องเป็นชื่อของฟังก์ชันเคอร์เนลที่กำลัง kprobe ดูข้อมูลเพิ่มเติมเกี่ยวกับ kprobe ได้ในเอกสารประกอบเคอร์เนล kprobe
tracepoint เชื่อมต่อ PROGFUNC กับ tracepoint PROGNAME ต้องมี รูปแบบ SUBSYSTEM/EVENT เช่น ส่วน tracepoint สำหรับการแนบฟังก์ชันกับเหตุการณ์การสลับบริบทของตัวกำหนดเวลาจะเป็น SEC("tracepoint/sched/sched_switch") โดยที่ sched คือ ชื่อของระบบย่อยการติดตาม และ sched_switch คือชื่อ ของเหตุการณ์การติดตาม ดูข้อมูลเพิ่มเติมเกี่ยวกับ tracepoint ได้ในเอกสารประกอบเคอร์เนลเหตุการณ์การติดตาม
skfilter ฟังก์ชันโปรแกรมเป็นตัวกรองซ็อกเก็ตเครือข่าย
schedcls ฟังก์ชันโปรแกรมเป็นตัวแยกประเภทการจราจรของข้อมูลในเครือข่าย
cgroupskb, cgroupsock โปรแกรมจะทำงานเมื่อใดก็ตามที่กระบวนการใน CGroup สร้างซ็อกเก็ต AF_INET หรือ AF_INET6

ดูประเภทเพิ่มเติม ได้ในซอร์สโค้ดตัวโหลด

ตัวอย่างเช่น โปรแกรม myschedtp.c ต่อไปนี้จะเพิ่มข้อมูลเกี่ยวกับ PID ของงานล่าสุดที่ทำงานใน CPU ที่เฉพาะเจาะจง โปรแกรมนี้บรรลุเป้าหมายโดยการสร้างแผนที่และกำหนดฟังก์ชัน tp_sched_switch ซึ่งสามารถแนบกับเหตุการณ์การติดตาม sched:sched_switch ได้ ดูข้อมูลเพิ่มเติมได้ที่ การแนบโปรแกรมกับ tracepoint

#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

หากต้องการให้ระบบบิลด์ของ 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 คือชื่อของไฟล์ eBPF C ตัวโหลด Android จะสร้างและ ปักหมุดแต่ละโปรแกรมไว้ที่ /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME

    ตัวอย่างเช่น สำหรับตัวอย่าง tracepoint sched_switch ก่อนหน้านี้ใน myschedtp.c ระบบจะสร้างไฟล์โปรแกรมและปักหมุดไว้ที่ /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch

  • สำหรับแผนที่ที่สร้างขึ้น โดยสมมติว่า MAPNAME คือชื่อของแผนที่ และ FILENAME คือชื่อของไฟล์ eBPF C ตัวโหลด Android จะสร้างและ ปักหมุดแต่ละแผนที่ไว้ที่ /sys/fs/bpf/map_FILENAME_MAPNAME

    ตัวอย่างเช่น สำหรับตัวอย่าง tracepoint sched_switch ก่อนหน้านี้ใน myschedtp.c ระบบจะสร้างไฟล์แผนที่และปักหมุดไว้ที่ /sys/fs/bpf/map_myschedtp_cpu_pid_map

  • bpf_obj_get() ในไลบรารี BPF ของ Android จะแสดงตัวบอกไฟล์จากไฟล์ /sys/fs/bpf ที่ปักหมุดไว้ ตัวระบุไฟล์นี้สามารถใช้สำหรับการดำเนินการเพิ่มเติม เช่น การอ่านแผนที่หรือการแนบโปรแกรมกับ tracepoint

ไลบรารี BPF ของ Android

ไลบรารี BPF ของ Android มีชื่อว่า libbpf_android.so และเป็นส่วนหนึ่งของอิมเมจระบบ ไลบรารีนี้มอบความสามารถ eBPF ระดับต่ำที่จำเป็นสำหรับการสร้างและอ่านแผนที่ สร้างโพรบ, tracepoint และบัฟเฟอร์ perf ให้แก่ผู้ใช้

แนบโปรแกรมกับ tracepoint

ระบบจะโหลดโปรแกรม tracepoint โดยอัตโนมัติเมื่อบูต หลังจากโหลดแล้ว คุณต้องเปิดใช้งานโปรแกรม tracepoint โดยทำตามขั้นตอนต่อไปนี้

  1. เรียก bpf_obj_get() เพื่อรับ fd ของโปรแกรมจากตำแหน่งของไฟล์ที่ปักหมุดไว้ ดูข้อมูลเพิ่มเติมได้ที่ ไฟล์ที่พร้อมใช้งานใน sysfs
  2. เรียก bpf_attach_tracepoint() ในไลบรารี BPF โดยส่ง fd ของโปรแกรมและชื่อ tracepoint

ตัวอย่างโค้ดต่อไปนี้แสดงวิธีแนบ tracepoint 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 มีคลาส android::BpfMap ที่ใช้เทมเพลต C++ เพื่อสร้างอินสแตนซ์ BpfMap ตามประเภทคีย์และค่าสำหรับแผนที่ที่ต้องการ ตัวอย่างโค้ดก่อนหน้านี้แสดงการใช้ BpfMap ที่มีคีย์และค่าเป็นจำนวนเต็ม จำนวนเต็มอาจเป็นโครงสร้างโดยพลการก็ได้

ดังนั้น คลาส BpfMap ที่ใช้เทมเพลตจะช่วยให้คุณกำหนดออบเจ็กต์ BpfMap ที่กำหนดเองซึ่งเหมาะสำหรับแผนที่นั้นๆ ได้ จากนั้นคุณจะเข้าถึงแผนที่ได้โดยใช้ฟังก์ชันที่สร้างขึ้นเองซึ่งตระหนักถึงประเภท ทำให้โค้ดสะอาดขึ้น

ดูข้อมูลเพิ่มเติมเกี่ยวกับ BpfMap ได้ที่ ซอร์สของ Android

แก้ไขข้อบกพร่อง

ระหว่างการบูต ระบบจะบันทึกข้อความหลายรายการที่เกี่ยวข้องกับการโหลด BPF หากกระบวนการโหลดล้มเหลวไม่ว่าด้วยเหตุผลใดก็ตาม ระบบจะแสดงข้อความในบันทึกโดยละเอียดใน Logcat การกรองบันทึก logcat ตาม bpf จะพิมพ์ข้อความทั้งหมดและข้อผิดพลาดโดยละเอียดระหว่างเวลาโหลด เช่น ข้อผิดพลาดของเครื่องมือตรวจสอบ eBPF

ตัวอย่าง eBPF ใน Android

โปรแกรมต่อไปนี้ใน AOSP แสดงตัวอย่างเพิ่มเติมของการใช้ eBPF

ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ eBPF เพื่อรวบรวมการจัดการหน่วยความจำเคอร์เนลที่มีค่าใช้จ่ายต่ำ และฮิสโทแกรมเวลาในการตอบสนองด้านพลังงานสำหรับ statsd ได้ที่รวบรวมข้อมูลการวัดและส่งข้อมูลเคอร์เนลด้วย eBPF