eBPF로 커널 확장

eBPF(Extended Berkeley Packet Filter)는 사용자 제공 eBPF 프로그램을 실행하여 커널 기능을 확장하는 커널 내 가상 머신입니다. 이러한 프로그램은 커널의 프로브나 이벤트에 연결하여 유용한 커널 통계를 수집하고 모니터링하고 디버그하는 데 사용할 수 있습니다. 프로그램은 bpf(2) syscall을 사용하여 커널로 로드되며 사용자가 eBPF 머신 명령의 바이너리 blob으로 제공합니다. Android 빌드 시스템은 이 문서에 설명된 간단한 빌드 파일 문법을 사용하여 C 프로그램을 eBPF로 컴파일하는 기능을 지원합니다.

eBPF 내부 기능 및 아키텍처에 관한 자세한 내용은 브렌든 그레그의 eBPF 페이지를 참고하세요.

Android에는 부팅 시 eBPF 프로그램을 로드하는 eBPF 로더와 라이브러리가 포함되어 있습니다.

Android BPF 로더

Android 부팅 중에는 /system/etc/bpf/에 있는 모든 eBPF 프로그램이 로드됩니다. 이러한 프로그램은 C 프로그램에서 Android 빌드 시스템이 빌드한 바이너리 객체이며 Android 소스 트리의 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은 kprobe 처리 중인 커널 함수의 이름이어야 합니다. kprobe에 관한 자세한 내용은 kprobe 커널 설명서를 참조하세요.
tracepoint PROGFUNC를 tracepoint에 연결합니다. PROGNAMESUBSYSTEM/EVENT 형식이어야 합니다. 예를 들어, 함수를 스케줄러 컨텍스트 전환 이벤트에 연결하기 위한 tracepoint 섹션은 SEC("tracepoint/sched/sched_switch")일 수 있습니다. 여기서 sched는 트레이스 하위 시스템의 이름이고 sched_switch는 트레이스 이벤트의 이름입니다. tracepoint에 대한 자세한 정보는 트레이스 이벤트 커널 설명서를 확인하세요.
skfilter 프로그램이 네트워킹 소켓 필터로 작동합니다.
schedcls 프로그램이 네트워크 트래픽 분류자로 작동합니다.
cgroupskb, cgroupsock CGroup의 프로세스가 AF_INET 또는 AF_INET6 소켓을 만들 때마다 프로그램이 실행됩니다.

로더 소스 코드에서 더 많은 유형을 찾을 수 있습니다.

예를 들어 다음 myschedtp.c 프로그램은 특정 CPU에서 실행된 최신 작업 PID에 관한 정보를 추가합니다. 이 프로그램은 맵을 만들고 sched:sched_switch 트레이스 이벤트에 연결할 수 있는 tp_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 파일에 항목을 만들어야 합니다. 예를 들어 이름이 bpf_test.c인 eBPF 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 시스템은 /system/etc/bpf/에서 모든 eBPF 객체를 자동으로 로드하고 프로그램에 필요한 맵을 만들며, 로드된 프로그램을 맵과 함께 bpf 파일 시스템에 고정합니다. 이러한 파일은 eBPF 프로그램과 추가로 상호작용하거나 맵을 읽는 데 사용할 수 있습니다. 이 섹션에서는 이러한 파일의 이름을 지정하는 데 사용되는 규칙과 sysfs에서의 파일 위치를 설명합니다.

다음 파일이 생성되어 고정됩니다.

  • 로드된 모든 프로그램에서 PROGNAME은 프로그램의 이름으로, FILENAME은 eBPF C 파일의 이름으로 가정하고 Android 로더는 /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME에서 각 프로그램을 생성하고 고정합니다.

    예를 들어 myschedtp.c에서 위의 sched_switch tracepoint 예의 경우 프로그램 파일이 생성되어 /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch에 고정됩니다.

  • 생성된 모든 맵의 경우 MAPNAME은 맵의 이름으로, FILENAME은 eBPF C 파일의 이름으로 가정하고 Android 로더는 각 맵을 생성해 /sys/fs/bpf/map_FILENAME_MAPNAME에 고정합니다.

    예를 들어 myschedtp.c에서 위의 sched_switch tracepoint 예의 경우 맵 파일이 생성되어 /sys/fs/bpf/map_myschedtp_cpu_pid_map에 고정됩니다.

  • Android BPF 라이브러리의 bpf_obj_get()은 고정된 /sys/fs/bpf 파일에서 파일 설명자를 반환합니다. 이 파일 설명자는 맵을 읽거나 프로그램을 tracepoint에 연결하는 등의 추가 작업에 사용할 수 있습니다.

Android BPF 라이브러리

Android BPF 라이브러리의 이름은 libbpf_android.so이며 시스템 이미지의 일부입니다. 이 라이브러리는 사용자에게 맵 생성 및 읽기, 그리고 프로브, tracepoint, perf 버퍼 생성에 필요한 하위 수준 eBPF 기능을 제공합니다.

tracepoint에 프로그램 연결

tracepoint 프로그램은 부팅 시 자동으로 로드됩니다. 로드한 후에는 다음 단계를 사용하여 tracepoint 프로그램을 활성화해야 합니다.

  1. bpf_obj_get()을 호출하여 고정된 파일의 위치에서 프로그램 fd를 가져옵니다. 자세한 내용은 sysfs에서 사용할 수 있는 파일을 참고하세요.
  2. BPF 라이브러리에서 bpf_attach_tracepoint()를 호출하여 프로그램 fd와 tracepoint 이름을 전달합니다.

다음 코드 샘플은 이전 myschedtp.c 소스 파일에 정의된 sched_switch tracepoint를 연결하는 방법을 보여줍니다(오류 검사는 표시되지 않음).

  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 라이브러리에는 C++ 템플릿을 사용하여 해당하는 맵의 키 및 값의 유형을 기반으로 BpfMap을 인스턴스화하는 android::BpfMap 클래스가 포함되어 있습니다. 이전 코드 샘플은 키와 값이 정수인 BpfMap을 사용하는 방법을 보여줍니다. 정수는 임의의 구조일 수도 있습니다.

따라서 템플릿화된 BpfMap 클래스를 사용하면 특정 맵에 적합한 맞춤 BpfMap 객체를 쉽게 정의할 수 있습니다. 그런 다음 유형을 인식하는 맞춤 생성된 함수를 사용하여 맵에 액세스할 수 있는데, 덕분에 코드가 더 깔끔하게 정리됩니다.

BpfMap에 관한 자세한 내용은 Android 소스를 참조하세요.

디버깅 문제

부팅 중에는 BPF 로드와 관련된 여러 메시지가 로깅됩니다. 어떠한 이유로든 로드 프로세스에 실패하는 경우 자세한 로그 메시지가 logcat에 제공됩니다. logcat 로그를 'bpf'로 필터링하면 eBPF 확인자 오류와 같이 로드 중 발생한 모든 메시지 및 자세한 오류가 출력됩니다.

Android의 eBPF 예

AOSP의 다음 프로그램은 eBPF를 사용하는 추가 예를 제공합니다.

  • netd eBPF C 프로그램은 소켓 필터링 및 통계 수집과 같은 다양한 목적으로 Android의 네트워킹 데몬(netd)에서 사용됩니다. 이 프로그램의 사용 방법을 보려면 eBPF 트래픽 모니터 소스를 확인하세요.

  • time_in_state eBPF C 프로그램은 전력을 계산하는 데 사용된 여러 CPU 주파수에서 Android 앱이 소비한 시간을 계산합니다.

  • Android 12에서 gpu_mem eBPF C 프로그램은 각 프로세스 및 전체 시스템의 총 GPU 메모리 사용량을 추적합니다. 이 프로그램은 GPU 메모리 프로파일링에 사용됩니다.