System Programming

BPF 를 통한 Linux Performance 분석 #1 BPF Program 구성요소

uzguns 2024. 10. 9. 17:53

BPF란?

BPF는 리눅스의 subsystem으로 리눅스 커널 코드를 실행할 수 있는 샌드박스 엔진이다.

어떤 면에서는 jvm이나 chrome의 v8엔진과도 비슷하다.

 

BPF를 실행하는 과정은 간단하게 3단계로 이루어진다.

  • Load: BPF 프로그램을 커널에 로드, Verifier를 통해 검증
  • Attach: BPF 프로그램을 특정 이벤트에 연결(BPF는 이벤트 드리븐 방식임)
  • Callback: 이벤트가 발생할 때 BPF 프로그램이 실행됨

BPF Program

BPF를 사용하기 위해 필요한 dependency 는 다음과 같다.

# 리눅스 소스코드
$ uname -a
Linux toor-virtual-machine 6.8.0-45-generic #45~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Sep 11 15:25:05 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
$ apt-get install -y linux-image-6.8.0-45-generic linux-headers-6.8.0-45-generic

# libbpf-dev
$ apt install libbpf-dev
# 설치 후 확인
$ ls -la /usr/include/asm
lrwxrwxrwx 1 root root 20  8월  5  2021 /usr/include/asm -> x86_64-linux-gnu/asm

 

 

다음은 execve라는 시스템 콜이 발생할 때 실행되는 BPF 프로그램 예시이다.

#include <linux/bpf.h>
#define SEC(NAME) __attribute__((section(NAME), used))

static int (*bpf_trace_printk)(const char *fmt, int fmt_size, ...) = (void *)BPF_FUNC_trace_printk;

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
    char msg[] = "Hello, BPF World!\n";
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
}

char _license[] SEC("license") = "GPL";
  • section 매크로는 해당 프로그램이 어떤 BPF 실행 컨텍스트에 연결될 지를 결정한다.
    • socket
    • xdp
    • tracepoint
    • raw_tracepoint
    • kprobes / uprobes
    • perf_events
    • tc(traffic control)
    • cgroup skb
  • tracepoint는 다음 위치에서 확인할 수 있다.
$ cat /sys/kernel/debug/tracing/available_events | grep syscalls | grep sys_enter_execve
syscalls:sys_enter_execveat
syscalls:sys_enter_execve
  • bpf helper 프로그램은 다음 명령어로 확인할 수 있다.
$ man 7 bpf-helpers

 

llvm이 BPF를 지원하므로 C 코드를 BPF command로 컴파일하는 것이 가능하다. clang은 llvm을 백엔드로 사용하여 컴파일 하기 때문에 다음과 같이 c 코드를 BPF byte code로 변환할 수 있다.

$ clang -O2 -target bpf -c bpf_program.c -I/backup/bpf -o bpf_program.o

 

이렇게 생성된 바이트 코드는 커널에서 실행될 준비가 된 상태이다. 컴파일된 BPF 오브젝트 파일은 bpftool또는 libbpf에 의해 커널에 로드 되며

#include <stdio.h>
#include <bpf/libbpf.h>
#include "bpf_load.h"

int main(int argc, char **argv) {
    if (load_bpf_file("bpf_program.o") != 0) {
        printf("Failed to load BPF program\n");
        return -1;
    }

    read_trace_pipe();
    return 0;
}

BPF 시스템 콜을 통해 커널에서 실행된다.

$ sudo ./monitor-exec 
           <...>-57106   [001] ...21 84780.146610: bpf_trace_printk: Hello, BPF World!


           <...>-57111   [002] ...21 84780.150171: bpf_trace_printk: Hello, BPF World!


           <...>-57122   [003] ...21 84782.948802: bpf_trace_printk: Hello, BPF World!

프로그램을 종료하면 즉시 vm에서도 제거된다. 적재 프로그램을 종료해도 BPF 프로그램이 계속 남아 있게 만드는 방법도 있다. BPF 프로그램을 다른 프로세스 실행 유무와 무관하게 백그라운드에서 계속 실행하면서 시스템의 데이터를 수집하는 용도로 개발할 것이기 때문에 이는 중요한 문제이다.

 

BPF Maps

어떤 프로그램에 특정한 message를 전달함으로써 그 프로그램이 특정한 방식으로 행동하게 만드는 것은 소프트웨어 공학에서 널리 쓰이는 기법이다. 한 프로그램은 다른 프로그램에 message를 보냄으로써 그 프로그램의 행동 방식을 수정할 수 있다.

또한 다수의 프로그램이 서로 정보를 교환하는 수단으로 쓰인다.

BPF Map은 BPF 프로그램이 어플리케이션과 통신할 때 데이터의 자료구조이다. BPF 맵은 그 위치를 아는 모든 BPF 프로그램이 접근할 수 있다. BPF 맵은 어떤 타입의 데이터도 저장할 수 있지만 데이터의 크기를 명시해야한다.

커널은 BPF map의 key, value를 이진 blob으로 취급할 뿐 그 안에 담긴 data와 type이 무엇인지는 신경쓰지 않는다.

 

BPF 맵을 만드는 가장 직접적인 방법은 시스템 호출 bpf를 이용하는 것이다. 첫 인수를 BPF_MAP_CREATE로 지정해서 bpf를 호출하면 커널은 새 맵을 생성하고 그 맵과 연관된 파일 디스크립터를 리턴한다.

union bpf_attr {
        struct { /* BPF_MAP_CREATE 명령에서 사용되는 익명 구조체 */
                __u32   map_type;       /* enum bpf_map_type 중 하나 */
                __u32   key_size;       /* 키의 크기(바이트 단위) */
                __u32   value_size;     /* 값의 크기(바이트 단위) */
                __u32   max_entries;    /* 맵에 저장할 수 있는 최대 엔트리 수 */
                __u32   map_flags;      /* BPF_MAP_CREATE 관련 플래그
                                         * 위에서 정의됨.
                                         */
                __u32   inner_map_fd;   /* 내부 맵을 가리키는 파일 디스크립터(fd) */
                __u32   numa_node;      /* NUMA 노드 (BPF_F_NUMA_NODE가
                                         * 설정된 경우에만 유효).
                                         */
                char    map_name[BPF_OBJ_NAME_LEN]; /* 맵 이름 */
                __u32   map_ifindex;    /* 맵을 생성할 네트워크 장치의 ifindex */
                __u32   btf_fd;         /* BTF 타입 데이터를 가리키는 파일 디스크립터(fd) */
                __u32   btf_key_type_id;        /* 키의 BTF type_id */
                __u32   btf_value_type_id;      /* 값의 BTF type_id */
                __u32   btf_vmlinux_value_type_id;/* 커널 구조체가
                                                   * 맵 값으로 저장될 때의 BTF type_id
                                                   */
                /* 맵 타입별로 추가적인 필드
                 *
                 * BPF_MAP_TYPE_BLOOM_FILTER - 가장 낮은 4비트는
                 * 해시 함수의 수를 나타냄(0이면, 블룸 필터는 기본적으로
                 * 5개의 해시 함수를 사용).
                 */
                __u64   map_extra;
        };

예를 들어 다음은 키와 값이 unsigned int 타입인 hash table map을 생성하는 코드이다.

union bpf_attr my_map {
    .map_type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(int),
    .value_size = sizeof(int),
    .max_entries = 128,
    .map_flags = BPF_F_NO_PREALLOC,
}
int fd = bpf(BPF_MAP_CREATE, &my_map, sizeof(my_map));

 

커널에는 BPF map을 만들고 사용하는 것과 관련된 여러 convention 과 helper function 이 있다. BPF 관련 문서나 예제를 보면 시스템 콜을 직접 수행하는 것보다 이런 convention 들을 사용하는 경우가 더 많은데 이는 convention 쪽이 좀 더 읽기 쉽고 따르기 쉽기 때문이다. 

 

helper function bpf_create_map은 bpf system call을 한번 감싼 것으로 맵 생성에 필요한 여러 정보를 인수들로 받는다.

 

 

BPF Helpers

eBPF subsystem 은 pseudo-assembly 언어로 작성된 프로그램들로 구성되며, 여러 커널 훅 중 하나에 연결되어 특정 이벤트에 반응하여 실행된다. 이 프레임워크는 이전의 "classic" BPF(또는 "cBPF")와 여러 측면에서 다르며, 그 중 하나는 프로그램 내에서 특별한 함수(helper func)를 호출할 수 있다는 점이다. 이러한 함수들은 커널에서 정의된 허용된 helper 목록(white-list)에 제한된다.

이 helper들은 eBPF 프로그램이 시스템 또는 작업 중인 컨텍스트와 상호작용할 수 있도록 사용된다. 예를 들어, 이 함수들은 디버깅 메시지를 출력하거나, 특정 정보에 접근하는 데 사용할 수 있다.

 

Common Helpers:
- bpf_map_lookup_elem()
- bpf_map_update_elem()
- bpf_map_delete_elem()
- bpf_get_smp_processor_id()
- bpf_ktime_get_ns()

Special Helpers:
- bpf_trace_printk()
- bpf_probe_read()
- bpf_perf_event_read()
- bpf_xdp_adjust_meta()
- bpf_skb_change_head()

 

 

References