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

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

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

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

ตัวโหลด Android BPF

ในระหว่างการบูต Android โปรแกรม eBPF ทั้งหมดที่อยู่ใน /system/etc/bpf/ จะถูกโหลด โปรแกรมเหล่านี้เป็นออบเจ็กต์ไบนารีที่สร้างโดยระบบบิลด์ Android จากโปรแกรม C และมาพร้อมกับไฟล์ Android.bp ในแผนผังซอร์สของ Android ระบบ build เก็บอ็อบเจ็กต์ที่สร้างขึ้นไว้ที่ /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 เป็นฟังก์ชันที่เมื่อคอมไพล์แล้วจะถูกวางไว้ในส่วนของไฟล์ผลลัพธ์

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

เพื่อให้ระบบ Android build สามารถสร้างโปรแกรม 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

    ตัวอย่างเช่น สำหรับตัวอย่างจุดติดตาม 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 ที่ปักหมุดไว้ ตัวอธิบายไฟล์นี้สามารถใช้สำหรับการดำเนินการเพิ่มเติม เช่น การอ่านแผนที่ หรือการแนบโปรแกรมเข้ากับจุดติดตาม

ไลบรารี Android BPF

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

การแนบโปรแกรมเข้ากับจุดติดตาม

โปรแกรม Tracepoint จะถูกโหลดโดยอัตโนมัติเมื่อบูต หลังจากโหลดแล้ว ต้องเปิดใช้งานโปรแกรมจุดติดตามโดยใช้ขั้นตอนเหล่านี้:

  1. โทร bpf_obj_get() เพื่อรับโปรแกรม fd จากตำแหน่งของไฟล์ที่ปักหมุด สำหรับข้อมูลเพิ่มเติม โปรดดู ไฟล์ที่มีอยู่ใน 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 eBPF C ถูกใช้โดย networking daemon (netd) ใน Android เพื่อวัตถุประสงค์ต่างๆ เช่น การกรองซ็อกเก็ต และการรวบรวมสถิติ หากต้องการดูวิธีการใช้โปรแกรมนี้ ให้ตรวจสอบแหล่งที่มาของ การตรวจสอบการรับส่งข้อมูล eBPF

  • โปรแกรม time_in_state eBPF C คำนวณระยะเวลาที่แอป Android ใช้ที่ความถี่ CPU ที่แตกต่างกัน ซึ่งใช้ในการคำนวณพลังงาน

  • ใน Android 12 โปรแกรม gpu_mem eBPF C จะติดตามการใช้งานหน่วยความจำ GPU ทั้งหมดสำหรับแต่ละกระบวนการและสำหรับทั้งระบบ โปรแกรมนี้ใช้สำหรับการทำโปรไฟล์หน่วยความจำ GPU