objpool (Lock-Free Per-CPU Object Pool)

리눅스 커널의 lock-free per-CPU 오브젝트 풀(Object Pool)인 objpool을 심층 분석합니다. include/linux/objpool.hlib/objpool.c에 정의된 이 메커니즘은 Linux 6.4에서 도입되었으며, NMI(Non-Maskable Interrupt)를 포함한 모든 인터럽트(Interrupt) 컨텍스트에서 안전하게 오브젝트를 할당/반환할 수 있도록 설계되었습니다. kprobe, fprobe, rethook의 핵심 인프라로 사용되며, per-CPU LIFO 스택 기반의 lock-free 알고리즘으로 높은 성능을 달성합니다.

전제 조건: 메모리 관리(Memory Management) 개요Slab Allocator(SLUB), Per-CPU 변수 문서를 먼저 읽으세요. objpool은 per-CPU 구조와 lock-free 프로그래밍 개념 위에 구축되므로, 이들의 기본 원리를 이해해야 합니다.
일상 비유: objpool은 볼링장의 신발 대여 카운터와 비슷합니다. 각 레인(CPU)마다 신발 선반(per-CPU 슬롯)이 있고, 손님은 자기 레인 선반에서 신발을 꺼내(pop) 사용한 뒤 다시 올려놓습니다(push). 다른 레인과 아무런 조율 없이 독립적으로 동작하므로 줄 서기(lock)가 필요 없습니다. 모든 신발이 사용 중이면 빈손으로 돌아가야 합니다(NULL 반환).

핵심 요약

  • Lock-Free Per-CPU 풀 -- 각 CPU마다 독립적인 오브젝트 슬롯을 유지하여 락(Lock) 없이 할당/반환이 가능합니다.
  • NMI-Safe -- NMI 핸들러 내부에서도 안전하게 사용할 수 있습니다. 이는 kmalloc이나 mempool로는 불가능한 일입니다.
  • LIFO 스택 -- 각 per-CPU 슬롯은 head/tail 인덱스로 관리되는 순환 배열(Circular Array) 기반 LIFO 스택입니다.
  • 사전 할당(Pre-allocation) -- 초기화 시 모든 오브젝트를 미리 할당하므로 런타임에 메모리 할당이 발생하지 않습니다.
  • kprobe/fprobe/rethook 전용 -- kretprobe_instance, fprobe 데이터, rethook_node를 풀링하는 데 주로 사용됩니다.
  • Linux 6.4+ 도입 -- 기존 freelist(llist) 기반 풀링을 대체하여 NMI 안전성과 per-CPU 캐시 친화성을 확보했습니다.

단계별 이해

objpool의 동작 원리를 일상적인 비유로 단계별로 이해해 봅니다.

볼링장 신발 대여 시스템

볼링장에 4개의 레인(CPU 0~3)이 있고, 각 레인에 전용 신발 선반이 있습니다.

볼링장 신발 대여 비유: per-CPU Object Pool 레인 0 (CPU 0) 신발 A 신발 B 신발 C head=3, tail=0 3개 사용 가능 잠금(Lock) 불필요 레인 1 (CPU 1) 신발 D 대여중 신발 F head=2, tail=0 2개 사용 가능 잠금(Lock) 불필요 레인 2 (CPU 2) 신발 G 신발 H 신발 I head=3, tail=0 3개 사용 가능 잠금(Lock) 불필요 레인 3 (CPU 3) 대여중 대여중 대여중 head=0, tail=0 0개 사용 가능 다음 요청: NULL 반환 각 레인(CPU)은 독립적인 선반(per-CPU slot)을 가지므로 락이 필요 없습니다. 신발을 꺼내는 것(pop)은 head를 감소시키고, 돌려놓는 것(push)은 head를 증가시킵니다. NMI가 발생해도 동일한 방식으로 안전하게 동작합니다 (선점(Preemption) 비활성화 불필요). objpool_pop() = 선반에서 신발 꺼내기 (head-- 후 entries[head] 반환) objpool_push() = 선반에 신발 돌려놓기 (entries[head] = obj 후 head++)

핵심 포인트는 다음과 같습니다.

개요

왜 objpool이 필요한가

커널에서 오브젝트를 할당하는 일반적인 방법은 kmalloc/kmem_cache_alloc입니다. 그러나 이러한 할당자는 다음과 같은 상황에서 사용할 수 없습니다.

기존 방식의 한계

Linux 6.4 이전에는 kretprobe가 freelist(llist 기반)를 사용했습니다. 이 방식에는 다음과 같은 문제가 있었습니다.

항목freelist (llist)objpool
NMI 안전성llist_add는 NMI-safe이지만, llist_del_first는 CAS 루프 중 ABA 문제 가능per-CPU 슬롯으로 ABA 문제 원천 차단
캐시 친화성전역 리스트이므로 CPU 간 캐시 라인(Cache Line) 바운싱per-CPU 슬롯으로 캐시 라인 독점
공정성LIFO 전역 스택이므로 특정 CPU 편중CPU별 균등 분배
확장성CPU 수 증가 시 경합 증가CPU 수에 무관하게 O(1)
기존: freelist (전역 경합) 신규: objpool (per-CPU 독립) 전역 llist_head CPU 0 CPU 1 CPU 2 CAS 경합 + 캐시 바운싱 NMI 중 ABA 문제 가능 CPU 0 슬롯 [obj][obj][obj] head=3 CPU 1 슬롯 [obj][obj][ ] head=2 CPU 2 슬롯 [obj][obj][obj] head=3 CPU 0 CPU 1 CPU 2 경합 없음 (per-CPU 독립) NMI-safe (재진입 안전)

objpool은 이러한 문제를 per-CPU 슬롯 분리를 통해 근본적으로 해결합니다. 각 CPU는 자신의 슬롯에서만 pop/push하므로 다른 CPU와의 동기화가 필요 없습니다.

구조체 분석

struct objpool_head

objpool의 최상위 관리 구조체입니다. include/linux/objpool.h에 정의되어 있습니다.

/* include/linux/objpool.h */
struct objpool_head {
    int                         obj_size;    /* 오브젝트 크기 (바이트) */
    int                         nr_objs;     /* 전체 오브젝트 수 */
    int                         nr_cpus;     /* num_possible_cpus() */
    int                         capacity;    /* 슬롯당 엔트리 수 (nr_objs/nr_cpus + nr_objs) */
    gfp_t                       gfp;         /* 할당 플래그 */
    refcount_t                  ref;         /* 참조 카운트 */
    unsigned long               flags;       /* OBJPOOL_NR_OBJECT_BIT 등 */
    struct objpool_slot __percpu *slots;      /* per-CPU 슬롯 배열 */
    objpool_fini_cb             release;     /* 해제 콜백 */
    void                        *context;    /* 콜백에 전달할 컨텍스트 */
};
코드 설명
  • obj_size풀에 저장되는 각 오브젝트의 크기입니다. 초기화 시 지정되며 이후 변경되지 않습니다.
  • nr_objs풀 전체에서 관리하는 오브젝트의 총 수입니다. 초기화 시 사전 할당됩니다.
  • nr_cpusnum_possible_cpus() 값을 캐싱합니다. 핫플러그(Hotplug)로 CPU가 추가되어도 초기화 시점의 값을 사용합니다.
  • capacity각 per-CPU 슬롯이 저장할 수 있는 최대 엔트리 수입니다. 오브젝트가 CPU 간 이동할 수 있으므로 nr_objs보다 크게 설정됩니다.
  • slotsper-CPU로 할당된 objpool_slot 포인터입니다. 각 CPU마다 독립적인 슬롯을 가집니다.
  • release풀이 완전히 해제될 때 호출되는 콜백 함수입니다. objpool_fini에서 참조 카운트가 0이 되면 호출됩니다.

struct objpool_slot

각 CPU에 할당되는 per-CPU 슬롯 구조체입니다. LIFO 스택으로 동작합니다.

/* lib/objpool.c */
struct objpool_slot {
    uint32_t        head;       /* 다음 pop 위치 (소비자 인덱스) */
    uint32_t        tail;       /* 다음 push 위치 (생산자 인덱스) */
    uint32_t        last;       /* entries[] 배열 크기 마스크 */
    uint32_t        mask;       /* capacity - 1 (2의 거듭제곱 마스크) */
    void            *entries[]; /* 오브젝트 포인터 배열 (flexible array) */
};
코드 설명
  • head다음에 pop할 위치를 가리킵니다. pop 시 head - 1 위치의 오브젝트를 꺼내고 head를 감소시킵니다.
  • tail유효한 오브젝트의 시작 위치입니다. head - tail이 현재 사용 가능한 오브젝트 수입니다.
  • maskcapacity가 2의 거듭제곱이 되도록 올림(Round-up)한 뒤 -1한 값입니다. 인덱스를 순환시킬 때 index & mask로 모듈러 연산을 비트 AND로 대체합니다.
  • entries[]오브젝트 포인터를 저장하는 flexible array입니다. 슬롯 구조체 뒤에 연속적으로 배치됩니다.
objpool_head / objpool_slot 메모리 레이아웃 struct objpool_head obj_size | nr_objs | nr_cpus | capacity gfp | ref | flags slots (per-CPU 포인터) release | context CPU 0: objpool_slot head=3 tail=0 mask=7 last=8 obj_0 obj_1 obj_2 NULL NULL ... entries[0..7] (tail..head-1 = 유효) CPU 1: objpool_slot head=2 tail=0 mask=7 last=8 obj_3 obj_4 NULL NULL ... entries[0..7] (tail..head-1 = 유효) CPU N: objpool_slot head=4 tail=1 mask=7 last=8 (반환됨) obj_8 obj_9 obj_10 ... entries[0..7] (tail..head-1 = 유효, tail=1이므로 [0] 건너뜀) 유효한 오브젝트 빈 슬롯 (NULL) per-CPU 포인터

슬롯 용량 계산

각 per-CPU 슬롯의 capacity는 전체 오브젝트 수보다 크게 설정됩니다. 이는 오브젝트가 하나의 CPU에서 pop되고 다른 CPU에서 push될 수 있기 때문입니다.

/* lib/objpool.c - objpool_init_percpu_slots() */
/* 슬롯 용량 = nr_objs / nr_cpus + nr_objs (넉넉하게) */
int capacity = roundup_pow_of_two(pool->nr_objs / pool->nr_cpus + pool->nr_objs);
/* 최소 capacity = roundup_pow_of_two(nr_objs + nr_objs / nr_cpus) */
/* 예: nr_objs=25, nr_cpus=4 → capacity = roundup_pow_of_two(25/4 + 25) = 32 */
코드 설명
  • roundup_pow_of_twocapacity를 2의 거듭제곱으로 올림합니다. 이를 통해 index % capacityindex & mask로 대체하여 나눗셈 연산을 피합니다.
  • nr_objs / nr_cpus + nr_objs최악의 경우 모든 오브젝트가 하나의 CPU에 몰릴 수 있으므로, 각 슬롯은 전체 오브젝트 수를 수용할 수 있어야 합니다.

Lock-Free 알고리즘

per-CPU LIFO 스택

각 per-CPU 슬롯은 순환 배열(Circular Array)을 사용하는 LIFO(Last-In, First-Out) 스택입니다. headtail 인덱스가 오브젝트의 유효 범위를 나타냅니다.

objpool_slot LIFO 동작: pop과 push 1단계: 초기 상태 (3개 보유) obj_A obj_B obj_C - - tail=0 head=3 2단계: objpool_pop() -- obj_C 반환 obj_A obj_B - - - tail=0 head=2 obj_C (반환됨) 3단계: objpool_push(obj_X) -- obj_X 추가 obj_A obj_B obj_X - - tail=0 head=3 obj_X (삽입) NMI 재진입 시나리오 프로세스 컨텍스트 (CPU 0) 1. head=3 읽기 2. head-- (head=2) 3. obj = entries[2] -- NMI 발생 (2와 3 사이) -- 4. NMI: head=2 읽기 → head-- → obj=entries[1] (다른 오브젝트) 5. NMI 복귀 후: entries[2]는 여전히 유효 → 안전! 핵심 원리 head는 단조 감소/증가하며 NMI가 중간에 pop하더라도 서로 다른 인덱스의 entries를 접근하므로 충돌 없음

NMI-Safe 설계 원리

objpool이 NMI에서도 안전한 이유는 다음과 같습니다.

  1. per-CPU 격리: 각 CPU는 자기 슬롯에서만 pop/push하므로 다른 CPU와의 경합이 없습니다.
  2. 단조 인덱스(Monotonic Index): head는 pop 시 감소, push 시 증가합니다. NMI가 중간에 끼어들어도 서로 다른 entries[] 위치를 접근합니다.
  3. 비파괴적 읽기: pop 시 head를 먼저 감소시킨 후 entries[head & mask]를 읽습니다. NMI가 추가로 pop하면 head가 더 감소할 뿐, 이미 감소된 위치의 데이터는 보존됩니다.
  4. 락 미사용: 어떤 종류의 락이나 cmpxchg 루프도 사용하지 않으므로, NMI에 의한 교착(Deadlock)이 원리적으로 불가능합니다.

Cross-CPU 반환

오브젝트가 CPU 0에서 pop되고 CPU 1에서 push될 수 있습니다. 이 경우 CPU 1의 head가 증가하고, CPU 0의 오브젝트 수는 변하지 않습니다. 시간이 지나면 특정 CPU에 오브젝트가 편중될 수 있지만, capacity가 충분히 크게 설정되어 있으므로 오버플로는 발생하지 않습니다.

핵심 API

objpool_init

오브젝트 풀을 초기화하고 모든 오브젝트를 사전 할당합니다.

/* include/linux/objpool.h */
int objpool_init(struct objpool_head *pool,
                 int nr_objs, int obj_size,
                 gfp_t gfp, void *context,
                 objpool_init_obj_cb objinit,
                 objpool_fini_cb release);
코드 설명
  • pool초기화할 objpool_head 구조체 포인터입니다.
  • nr_objs사전 할당할 오브젝트의 총 수입니다. 런타임에 동적으로 늘릴 수 없습니다.
  • obj_size각 오브젝트의 크기(바이트)입니다. 내부적으로 정렬(Alignment)이 적용될 수 있습니다.
  • gfp오브젝트와 슬롯 메모리 할당 시 사용할 GFP 플래그입니다. 보통 GFP_KERNEL을 사용합니다.
  • objinit각 오브젝트 할당 후 호출되는 초기화 콜백입니다. NULL이면 호출하지 않습니다.
  • release풀 해제 시(참조 카운트 0) 호출되는 콜백입니다. 호출자가 pool 구조체를 포함한 상위 구조체를 해제하는 데 사용합니다.

objpool_init의 내부 동작을 단계별로 살펴봅니다.

/* lib/objpool.c - objpool_init() 내부 */

/* 1. 기본 필드 초기화 */
pool->obj_size = obj_size;
pool->nr_objs  = nr_objs;
pool->nr_cpus  = num_possible_cpus();
pool->gfp      = gfp & ~__GFP_ZERO;
pool->context  = context;
pool->release  = release;
refcount_set(&pool->ref, nr_objs + 1);

/* 2. per-CPU 슬롯 할당 */
rc = objpool_init_percpu_slots(pool, nr_objs, context, objinit);

/* 3. 오브젝트를 각 CPU 슬롯에 라운드 로빈으로 분배 */
for (i = 0; i < nr_objs; i++) {
    int cpu = i % pool->nr_cpus;
    struct objpool_slot *slot = per_cpu_ptr(pool->slots, cpu);
    void *obj = objpool_alloc_object(pool);
    if (objinit)
        objinit(obj, context);
    slot->entries[slot->head & slot->mask] = obj;
    slot->head++;
}
코드 설명
  • refcount_set(&pool->ref, nr_objs + 1)참조 카운트를 오브젝트 수 + 1로 설정합니다. 각 오브젝트가 pop될 때 참조를 보유하고, +1은 풀 자체의 참조입니다. objpool_fini에서 -1, 각 오브젝트 push 시 -1하여 모두 반환되면 0이 됩니다.
  • i % pool->nr_cpus오브젝트를 라운드 로빈(Round-Robin)으로 CPU에 분배합니다. nr_objs=12, nr_cpus=4이면 각 CPU에 3개씩 배치됩니다.
  • slot->head++오브젝트를 추가할 때마다 head를 증가시킵니다. 초기화 후 head는 해당 CPU에 배치된 오브젝트 수가 됩니다.

objpool_pop (objpool_alloc)

풀에서 오브젝트를 하나 꺼냅니다. NMI를 포함한 모든 컨텍스트에서 호출할 수 있습니다.

/* include/linux/objpool.h */
static inline void *objpool_pop(struct objpool_head *pool)
{
    void *obj = NULL;
    unsigned long flags;
    int i, cpu;

    /* 선점(Preemption) 비활성화하여 CPU 고정 */
    cpu = raw_smp_processor_id();

    for (i = 0; i < pool->nr_cpus; i++) {
        int c = (cpu + i) % pool->nr_cpus;
        struct objpool_slot *slot = per_cpu_ptr(pool->slots, c);
        obj = objpool_try_get_slot(slot);
        if (obj)
            return obj;
    }
    return NULL;
}
코드 설명
  • raw_smp_processor_id()현재 CPU 번호를 얻습니다. smp_processor_id()와 달리 선점(Preemption) 검사를 하지 않으므로 NMI에서도 안전합니다.
  • for (i = 0; i < pool->nr_cpus; i++)먼저 현재 CPU의 슬롯을 시도하고, 비어 있으면 다른 CPU의 슬롯을 순회합니다. 이 폴백(Fallback) 메커니즘은 특정 CPU에 오브젝트가 없을 때 다른 CPU에서 빌려올 수 있게 합니다.
  • objpool_try_get_slot(slot)해당 슬롯에서 오브젝트를 꺼내는 실제 로직입니다. head > tail이면 head를 감소시키고 entries에서 오브젝트를 반환합니다.

objpool_push (objpool_free)

오브젝트를 풀에 반환합니다. 어떤 CPU에서든 호출할 수 있으며, 반환 시점의 현재 CPU 슬롯에 push됩니다.

/* include/linux/objpool.h */
static inline int objpool_push(void *obj, struct objpool_head *pool)
{
    int cpu = raw_smp_processor_id();
    struct objpool_slot *slot = per_cpu_ptr(pool->slots, cpu);

    /* LIFO push: entries[head] = obj; head++ */
    slot->entries[slot->head & slot->mask] = obj;
    smp_wmb();  /* entries 쓰기가 head 증가 전에 가시적이도록 */
    slot->head++;

    /* 참조 카운트 감소: 모든 오브젝트 반환 + fini 시 release 콜백 호출 */
    if (refcount_dec_and_test(&pool->ref))
        pool->release(pool, pool->context);

    return 0;
}
코드 설명
  • entries[slot->head & slot->mask] = obj현재 head 위치에 오브젝트를 저장합니다. & mask로 순환 인덱싱을 수행합니다.
  • smp_wmb()쓰기 메모리 배리어(Write Memory Barrier)입니다. entries 쓰기가 head 증가보다 먼저 다른 CPU에 가시적이 되도록 보장합니다.
  • refcount_dec_and_test참조 카운트를 1 감소시키고, 0이 되면 true를 반환합니다. 모든 오브젝트가 반환되고 objpool_fini도 호출된 경우에만 release 콜백이 실행됩니다.

objpool_fini / objpool_drop

풀을 해제합니다. 두 가지 변형이 있습니다.

/* 정상 종료: 참조 카운트 감소 → 0이면 release 콜백 */
void objpool_fini(struct objpool_head *pool);

/* 비상 해제: 즉시 모든 리소스 해제 (초기화 실패 시) */
void objpool_drop(struct objpool_head *pool);
코드 설명
  • objpool_fini참조 카운트를 1 감소시킵니다. 아직 사용 중인 오브젝트가 있으면 마지막 오브젝트가 push될 때 release가 호출됩니다. 이 메커니즘으로 인해 fini 후에도 오브젝트를 안전하게 반환할 수 있습니다.
  • objpool_dropobjpool_init이 중간에 실패했을 때 사용합니다. 이미 할당된 슬롯과 오브젝트를 즉시 해제하며, release 콜백은 호출하지 않습니다.

API 요약 표

함수컨텍스트설명
objpool_init()프로세스풀 초기화 + 오브젝트 사전 할당
objpool_pop()모든 컨텍스트 (NMI 포함)오브젝트 할당 (= objpool_alloc)
objpool_push()모든 컨텍스트 (NMI 포함)오브젝트 반환 (= objpool_free)
objpool_fini()프로세스풀 해제 요청 (지연 해제 가능)
objpool_drop()프로세스풀 즉시 해제 (초기화 실패 경로)

kprobe/fprobe 활용

kretprobe_instance 풀링

kretprobe는 함수의 리턴(Return) 시점에 핸들러를 실행하는 메커니즘입니다. 함수 진입 시 kretprobe_instance를 할당하고, 리턴 시 해제합니다. 함수가 재귀적으로 호출되거나 NMI에 의해 중단될 수 있으므로, 풀링 메커니즘이 NMI-safe해야 합니다.

/* kernel/kprobes.c - kretprobe 등록 시 objpool 초기화 */
static int kretprobe_init_inst_pool(struct kretprobe *rp)
{
    int num = rp->maxactive;
    if (num <= 0)
        num = max_t(int, 10, 2 * num_possible_cpus());

    return objpool_init(&rp->rph->pool,
                        num,
                        rp->data_size + sizeof(struct kretprobe_instance),
                        GFP_KERNEL, rp,
                        kretprobe_init_inst, kretprobe_fini_pool);
}
코드 설명
  • rp->maxactive동시에 활성화될 수 있는 kretprobe 인스턴스의 최대 수입니다. 0이면 기본값(2 * num_possible_cpus(), 최소 10)을 사용합니다.
  • rp->data_size + sizeof(struct kretprobe_instance)각 인스턴스의 크기입니다. 사용자 정의 데이터(data_size)를 포함하여 하나의 오브젝트로 관리합니다.
  • kretprobe_init_inst각 오브젝트 초기화 콜백입니다. kretprobe_instancerph 포인터를 설정합니다.

kretprobe 핸들러가 실행되는 흐름은 다음과 같습니다.

kretprobe에서의 objpool 사용 흐름 함수 진입 pre_handler 호출 objpool_pop() kretprobe_instance 할당 리턴 주소 저장 ret_addr → trampoline 대상 함수 실행 ... 함수 리턴 trampoline으로 점프 ret_handler 호출 사용자 핸들러 실행 objpool_push() kretprobe_instance 반환 원래 리턴 주소로 정상 실행 재개 NMI 재진입 안전성 함수 A 진입 → objpool_pop(inst_1) → NMI 발생 → 함수 B 진입 → objpool_pop(inst_2) → NMI 종료 inst_1과 inst_2는 같은 CPU의 다른 entries[] 위치에서 할당되므로 충돌 없음

fprobe 데이터 전달

fprobe는 ftrace 기반의 함수 프로브(Function Probe)입니다. 함수 진입/종료 핸들러 간에 데이터를 전달할 때 objpool을 사용합니다.

/* kernel/trace/fprobe.c - fprobe의 objpool 초기화 */
static int fprobe_init_rethook(struct fprobe *fp, int num)
{
    struct fprobe_rethook_node *fnh;
    int size;

    if (num <= 0)
        num = max_t(int, 10, 2 * num_possible_cpus());

    size = sizeof(*fnh) + fp->entry_data_size;

    /* rethook이 내부적으로 objpool을 사용 */
    fp->rethook = rethook_alloc(fp->ops.func, fprobe_rethook_handler,
                                 size, num);
    return fp->rethook ? 0 : -ENOMEM;
}
코드 설명
  • fp->entry_data_size진입 핸들러에서 종료 핸들러로 전달할 사용자 데이터의 크기입니다. 이 데이터가 objpool 오브젝트 안에 포함됩니다.
  • rethook_allocrethook을 할당합니다. 내부적으로 objpool_init을 호출하여 rethook_node 풀을 생성합니다.

rethook 활용

rethook은 함수 리턴 후킹(Return Hooking)의 공통 프레임워크입니다. kretprobe와 fprobe 모두 rethook을 통해 objpool을 사용합니다.

rethook_node 풀링

/* kernel/trace/rethook.c */
struct rethook *rethook_alloc(void *data,
                              rethook_handler_t handler,
                              int size, int num)
{
    struct rethook *rh;

    rh = kzalloc(sizeof(struct rethook), GFP_KERNEL);
    if (!rh)
        return NULL;

    rh->data    = data;
    rh->handler = handler;
    refcount_set(&rh->ref, 1);

    /* objpool 초기화: rethook_node를 num개 사전 할당 */
    if (objpool_init(&rh->pool, num, size,
                     GFP_KERNEL, rh,
                     rethook_init_node, rethook_free_pool)) {
        kfree(rh);
        return NULL;
    }

    return rh;
}
코드 설명
  • sizerethook_node의 크기(사용자 데이터 포함)입니다. 이 크기의 오브젝트가 num개 사전 할당됩니다.
  • rethook_init_node각 노드의 초기화 콜백입니다. rethook_noderethook 포인터를 설정합니다.
  • rethook_free_pool풀 해제 콜백입니다. 모든 노드가 반환된 후 rethook 구조체를 kfree합니다.

rethook_node 할당/반환

/* kernel/trace/rethook.c - 리턴 후킹 시점 */
struct rethook_node *rethook_try_get(struct rethook *rh)
{
    struct rethook_node *node;

    if (unlikely(!refcount_inc_not_zero(&rh->ref)))
        return NULL;

    /* objpool에서 노드 할당 */
    node = (struct rethook_node *)objpool_pop(&rh->pool);
    if (node)
        node->rethook = rh;
    else
        refcount_dec(&rh->ref);

    return node;
}

void rethook_recycle(struct rethook_node *node)
{
    struct rethook *rh = node->rethook;

    /* objpool에 노드 반환 */
    objpool_push(node, &rh->pool);

    if (refcount_dec_and_test(&rh->ref))
        rethook_free(rh);
}
코드 설명
  • refcount_inc_not_zerorethook이 이미 해제 중인지 확인합니다. 0이면 해제 중이므로 노드를 할당하지 않습니다.
  • objpool_pop(&rh->pool)per-CPU 슬롯에서 rethook_node를 꺼냅니다. NMI 컨텍스트에서도 안전합니다.
  • objpool_push(node, &rh->pool)사용이 끝난 노드를 현재 CPU의 슬롯에 반환합니다. cross-CPU 반환이 가능합니다.

성능 특성

Lock-Free 장점

특성설명
시간 복잡도pop/push 모두 O(1)입니다. 현재 CPU 슬롯에서 즉시 처리됩니다.
캐시 친화성per-CPU 슬롯은 해당 CPU의 L1/L2 캐시에 상주합니다. 다른 CPU의 캐시를 무효화하지 않습니다.
확장성CPU 수에 무관한 성능입니다. 128 코어(Core) 시스템에서도 경합이 없습니다.
선점 안전선점(Preemption) 비활성화 없이 동작합니다. NMI에서도 안전합니다.
예측 가능 지연최악의 경우에도 nr_cpus번의 슬롯을 순회하면 됩니다.

per-CPU 캐시 친화성

per-CPU 캐시 친화성 CPU 0 L1 캐시: slot_0 상주 pop/push: L1 히트(Hit) 캐시 미스(Miss) 없음 CPU 1 L1 캐시: slot_1 상주 pop/push: L1 히트(Hit) 캐시 미스(Miss) 없음 CPU 2 L1 캐시: slot_2 상주 pop/push: L1 히트(Hit) 캐시 미스(Miss) 없음 CPU N L1 캐시: slot_N 상주 pop/push: L1 히트(Hit) 캐시 미스(Miss) 없음 전역 리스트 방식 llist_head를 모든 CPU가 접근 → 매번 캐시 라인 무효화 → MESI Invalidate + 캐시 미스 objpool per-CPU 방식 각 CPU가 자기 슬롯만 접근 → 캐시 라인 독점 (Exclusive) → 항상 L1 히트, 경합 없음

메모리 오버헤드

objpool의 메모리 사용량을 계산해 봅니다.

/* 메모리 계산 예시: nr_objs=25, obj_size=128, nr_cpus=4 */

/* per-CPU 슬롯 하나의 크기 */
int capacity = roundup_pow_of_two(25/4 + 25);  /* = 32 */
size_t slot_size = sizeof(struct objpool_slot) + capacity * sizeof(void *);
/* = 16 + 32 * 8 = 272 바이트 */

/* 전체 슬롯 메모리 = nr_cpus * slot_size */
/* = 4 * 272 = 1,088 바이트 */

/* 오브젝트 메모리 = nr_objs * obj_size */
/* = 25 * 128 = 3,200 바이트 */

/* 총 메모리 = 슬롯 + 오브젝트 + objpool_head */
/* ≈ 1,088 + 3,200 + 64 = 4,352 바이트 */

슬롯 배열의 용량이 roundup_pow_of_two로 올림되므로, 실제 필요한 것보다 슬롯 메모리가 더 사용될 수 있습니다. 그러나 이 오버헤드는 lock-free 성능 이점에 비하면 미미합니다.

확장성 분석

objpool의 확장성을 CPU 수에 따라 분석합니다. 각 시나리오에서의 예상 지연 시간을 표로 정리합니다.

CPU 수pop 지연 (최선)pop 지연 (최악)경합비고
1~5 ns~5 ns없음단일 CPU, 항상 로컬 슬롯
4~5 ns~25 ns없음폴백(Fallback) 시 4개 슬롯 순회
16~5 ns~90 ns없음폴백 시 캐시 미스 가능
128~5 ns~700 ns없음모든 슬롯 순회는 극히 드묾

최선의 경우(현재 CPU 슬롯에 오브젝트 존재) 지연은 CPU 수에 무관합니다. 최악의 경우(모든 CPU 슬롯을 순회)에도 락 경합이 없으므로 다른 CPU의 성능에 영향을 주지 않습니다. 이는 spinlock 기반 mempool과의 가장 큰 차이점입니다.

Cross-CPU 반환의 영향

오브젝트가 CPU A에서 pop되고 CPU B에서 push되면, 시간이 지남에 따라 오브젝트가 특정 CPU에 편중될 수 있습니다. 이 현상의 영향을 분석합니다.

벤치마크 비교

Wu Qiang의 원본 패치에서 보고된 벤치마크 결과입니다 (8 CPU 시스템, kretprobe). 동일한 함수에 kretprobe를 설치하고 초당 처리량을 측정한 것입니다.

메커니즘ops/sec (8 CPU)상대 성능
freelist (llist) 기반~2,100,0001.0x (기준)
objpool (per-CPU)~3,800,0001.8x

per-CPU 분리에 의한 캐시 라인 경합 제거가 약 80%의 성능 향상을 가져왔습니다. CPU 수가 많을수록 이 격차는 더 벌어집니다. 이는 전역 llist_head에 대한 CAS 경합이 CPU 수에 비례하여 증가하기 때문이며, objpool은 CPU 수와 무관하게 일정한 성능을 유지합니다.

특히 NMI 컨텍스트에서의 안정성 향상이 가장 중요한 기여입니다. 기존 freelist 방식에서는 NMI가 CAS 루프 중간에 발생하면 ABA 문제로 인한 데이터 손상이 이론적으로 가능했지만, objpool은 이를 원천적으로 차단합니다.

디버깅

objpool 고갈 진단

모든 오브젝트가 사용 중이면 objpool_popNULL을 반환합니다. kretprobe에서 이 상황이 발생하면 해당 함수 호출의 리턴 후킹이 누락됩니다.

/* kretprobe에서 풀 고갈 시 nmissed 카운터 증가 */
static int pre_handler_kretprobe(struct kprobe *p,
                                   struct pt_regs *regs)
{
    struct kretprobe_instance *ri;

    ri = objpool_pop(&rp->rph->pool);
    if (!ri) {
        /* 풀 고갈: 이 리턴은 추적되지 않음 */
        rp->nmissed++;
        return 0;
    }
    /* ... 정상 처리 ... */
}

per-CPU 분포 확인

오브젝트가 특정 CPU에 편중되었는지 확인하려면 각 슬롯의 head - tail 값을 조회합니다.

/* 디버깅용: per-CPU 슬롯 상태 출력 */
static void objpool_debug_dump(struct objpool_head *pool)
{
    int cpu;
    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot = per_cpu_ptr(pool->slots, cpu);
        pr_info("objpool CPU%d: head=%u tail=%u avail=%u capacity=%u\n",
                cpu, slot->head, slot->tail,
                slot->head - slot->tail,
                slot->mask + 1);
    }
}
코드 설명
  • head - tail현재 해당 CPU 슬롯에서 사용 가능한 오브젝트 수입니다. uint32_t 언더플로는 발생하지 않습니다 (head >= tail 보장).
  • mask + 1슬롯의 전체 용량입니다. mask는 capacity-1이므로 +1하면 원래 capacity가 됩니다.

nmissed 모니터링

kretprobe의 풀 고갈 빈도를 확인합니다.

# kretprobe nmissed 확인 (debugfs)
cat /sys/kernel/debug/kprobes/list
# 출력 예시:
# ffffffff81234560 k do_sys_openat2+0x0 [OPTIMIZED]
# ffffffff81234560 r do_sys_openat2+0x0 nmissed=3

# nmissed가 높으면 maxactive 증가 필요
# echo 'r:myprobe do_sys_openat2 maxactive=100' > kprobe_events

maxactive 적정값 결정

kretprobe의 maxactive 값은 동시에 활성 상태인 인스턴스의 최대 수를 결정합니다. 이 값이 너무 작으면 nmissed가 증가하고, 너무 크면 메모리가 낭비됩니다.

maxactive 결정 가이드

기본값: max(10, 2 * num_possible_cpus()) — 4 CPU 시스템에서는 10, 128 CPU 시스템에서는 256입니다.

고주파 함수 (do_sys_openat2 등)
기본값의 4~8배: 40~80 (4 CPU 기준)
재귀적 함수 (lookup_slow 등)
재귀 깊이 × num_cpus: 재귀 깊이 8이면 32 (4 CPU 기준)
NMI 컨텍스트에서 호출되는 함수
기본값 + num_cpus: 14 (4 CPU 기준). NMI는 CPU당 1개 추가 인스턴스만 필요합니다.

일반적인 문제와 해결

증상원인해결
nmissed 지속 증가maxactive 부족maxactive2 * num_cpus * 평균_동시_호출_깊이로 증가
특정 CPU에서만 NULL 반환cross-CPU 편중대부분 자연 해소됨. 심각하면 nr_objs 증가
objpool_init 실패 (-ENOMEM)메모리 부족nr_objs 감소 또는 obj_size 최적화
오브젝트 반환 누락 (리소스 누수)push 경로 빠짐에러 경로에서 objpool_push 호출 확인
release 콜백 호출 안 됨오브젝트 미반환모든 pop된 오브젝트가 push되었는지 확인

ftrace를 이용한 추적

# objpool_pop/push 호출 추적
echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
echo 'p:objpool_pop_trace objpool_pop' >> /sys/kernel/debug/tracing/kprobe_events
echo 'p:objpool_push_trace objpool_push' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe

BPF를 이용한 고급 추적

# bpftrace로 objpool pop/push 빈도 모니터링
bpftrace -e '
kprobe:objpool_try_get_slot {
    @pop_count[cpu] = count();
}
kprobe:objpool_try_add_slot {
    @push_count[cpu] = count();
}
interval:s:5 {
    print(@pop_count);
    print(@push_count);
    clear(@pop_count);
    clear(@push_count);
}
'
# bpftrace로 objpool 고갈 이벤트 추적
bpftrace -e '
kretprobe:objpool_pop /retval == 0/ {
    @null_returns[cpu] = count();
    @null_stack[kstack] = count();
}
interval:s:10 {
    print(@null_returns);
    print(@null_stack);
}
'

crash 도구를 이용한 사후 분석

시스템 크래시 덤프에서 objpool 상태를 분석하는 방법입니다.

# crash 도구에서 objpool_head 구조체 확인
crash> struct objpool_head 0xffff888100123400
struct objpool_head {
  obj_size = 192,
  nr_objs = 25,
  nr_cpus = 4,
  capacity = 32,
  gfp = 3264,
  ref = {
    refs = {
      counter = 18
    }
  },
  slots = 0x00000000deadbeef,
  release = 0xffffffff81234560,
  context = 0xffff888100456000
}

# per-CPU 슬롯 상태 확인
crash> p/d ((struct objpool_slot *)per_cpu(slots, 0))->head
$1 = 15
crash> p/d ((struct objpool_slot *)per_cpu(slots, 0))->tail
$2 = 8
# → CPU 0에 7개 오브젝트 보유 (head - tail = 7)

소스 코드 워크스루

objpool_try_get_slot 내부 추적

objpool_pop이 호출하는 objpool_try_get_slot의 실제 구현을 분석합니다.

/* lib/objpool.c */
static inline void *objpool_try_get_slot(struct objpool_slot *slot)
{
    void *obj;
    uint32_t head, tail;

    /* head와 tail을 읽기 */
    head = READ_ONCE(slot->head);
    tail = READ_ONCE(slot->tail);

    /* 비어있으면 NULL 반환 */
    if (head == tail)
        return NULL;

    /* head를 먼저 감소 (LIFO: 가장 최근에 push된 것을 pop) */
    head = head - 1;
    WRITE_ONCE(slot->head, head);

    /* 오브젝트를 읽기 전에 배리어 */
    smp_rmb();

    /* entries에서 오브젝트 포인터 획득 */
    obj = slot->entries[head & slot->mask];

    /* 해당 위치를 NULL로 클리어 (디버깅 용도) */
    slot->entries[head & slot->mask] = NULL;

    return obj;
}
코드 설명
  • READ_ONCE(slot->head)컴파일러 최적화를 방지하고 한 번만 읽도록 강제합니다. volatile 시맨틱을 부여합니다.
  • head == tail슬롯이 비어 있는 조건입니다. head와 tail이 같으면 사용 가능한 오브젝트가 없습니다.
  • WRITE_ONCE(slot->head, head)head를 먼저 감소시킵니다. NMI가 이 시점에 발생하면 NMI 핸들러는 이미 감소된 head를 보고 다른 위치에서 pop합니다.
  • smp_rmb()읽기 메모리 배리어입니다. head 쓰기와 entries 읽기의 순서를 보장합니다.
  • entries[head & slot->mask]순환 인덱싱으로 오브젝트를 읽습니다. mask가 2의 거듭제곱 - 1이므로 모듈러 연산과 동일합니다.

objpool_try_add_slot 내부 추적

/* lib/objpool.c */
static inline int objpool_try_add_slot(void *obj,
                                        struct objpool_slot *slot)
{
    uint32_t head, tail;

    head = READ_ONCE(slot->head);
    tail = READ_ONCE(slot->tail);

    /* 슬롯이 꽉 찼는지 확인 */
    if (head - tail >= slot->last)
        return -ENOSPC;

    /* 오브젝트를 현재 head 위치에 저장 */
    slot->entries[head & slot->mask] = obj;

    /* 쓰기 배리어: entries 쓰기 후 head 증가 */
    smp_wmb();

    /* head 증가 */
    WRITE_ONCE(slot->head, head + 1);

    return 0;
}
코드 설명
  • head - tail >= slot->last슬롯 오버플로 방지입니다. last는 entries 배열의 크기이며, 이를 초과하면 저장할 수 없습니다.
  • entries[head & slot->mask] = obj오브젝트 포인터를 현재 head 위치에 저장합니다. 순환 인덱싱을 사용합니다.
  • smp_wmb()쓰기 배리어입니다. entries에 오브젝트가 쓰여진 후에 head가 증가하도록 순서를 보장합니다. 다른 컨텍스트(NMI 등)가 head를 읽을 때 이미 유효한 오브젝트가 entries에 있어야 합니다.

per-CPU 슬롯 초기화 워크스루

/* lib/objpool.c - objpool_init_percpu_slots() */
static int objpool_init_percpu_slots(struct objpool_head *pool,
                                      int nr_objs,
                                      void *context,
                                      objpool_init_obj_cb objinit)
{
    int i, cpu;

    /* capacity 계산: 2의 거듭제곱으로 올림 */
    pool->capacity = roundup_pow_of_two(nr_objs / pool->nr_cpus + nr_objs);

    /* per-CPU 슬롯 메모리 할당 */
    pool->slots = __alloc_percpu(
        sizeof(struct objpool_slot) + pool->capacity * sizeof(void *),
        sizeof(void *));
    if (!pool->slots)
        return -ENOMEM;

    /* 각 CPU 슬롯 초기화 */
    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot = per_cpu_ptr(pool->slots, cpu);
        slot->head = 0;
        slot->tail = 0;
        slot->last = pool->capacity;
        slot->mask = pool->capacity - 1;
    }

    /* 오브젝트 할당 및 라운드 로빈 분배 */
    for (i = 0; i < nr_objs; i++) {
        void *obj;
        cpu = i % pool->nr_cpus;

        obj = kvmalloc_node(pool->obj_size, pool->gfp,
                             cpu_to_node(cpu));
        if (!obj)
            return -ENOMEM;

        if (objinit)
            objinit(obj, context);

        /* 해당 CPU 슬롯에 push */
        objpool_add_scattered(obj, pool, cpu);
    }
    return 0;
}
코드 설명
  • __alloc_percpuper-CPU 메모리를 할당합니다. 각 CPU에 sizeof(objpool_slot) + capacity * sizeof(void*) 크기의 메모리가 배치됩니다.
  • cpu_to_node(cpu)해당 CPU의 NUMA 노드를 반환합니다. 오브젝트를 해당 CPU의 로컬 메모리에서 할당하여 NUMA 친화성을 확보합니다.
  • objpool_add_scattered지정된 CPU의 슬롯에 오브젝트를 추가합니다. 초기화 시점이므로 경합 없이 안전합니다.

objpool_fini 내부 추적

/* lib/objpool.c */
void objpool_fini(struct objpool_head *pool)
{
    int count, cpu;

    /* 풀에 남아 있는 오브젝트 수 세기 */
    count = 0;
    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot = per_cpu_ptr(pool->slots, cpu);
        count += slot->head - slot->tail;
    }

    /* 풀에 있는 오브젝트 수만큼 refcount 감소 */
    if (count > 0)
        refcount_sub(count, &pool->ref);

    /* 자체 참조(-1) 해제 */
    if (refcount_dec_and_test(&pool->ref))
        objpool_free(pool);
}
코드 설명
  • count += slot->head - slot->tail각 CPU 슬롯에 남아 있는 오브젝트를 셉니다. 이 오브젝트들은 아무도 사용하지 않으므로 참조를 해제합니다.
  • refcount_sub(count, &pool->ref)풀에 있는 오브젝트 수만큼 참조 카운트를 한 번에 감소시킵니다. 아직 사용 중인 오브젝트가 있으면 그 수만큼 참조가 남습니다.
  • refcount_dec_and_test풀 자체의 참조(+1)를 해제합니다. 사용 중인 오브젝트가 없으면 이 시점에서 0이 되어 objpool_free가 호출됩니다.

참조 카운팅 흐름 정리

objpool의 참조 카운팅 메커니즘은 안전한 지연 해제(Deferred Free)를 보장합니다.

objpool 참조 카운팅 흐름 (nr_objs=3 예시) 시간 objpool_init() ref = 3 + 1 = 4 pop(obj_A) ref = 4 (불변) pop(obj_B) ref = 4 (불변) objpool_fini() ref = 4-1-1 = 2 push(obj_A) ref = 2-1 = 1 push(obj_B) ref = 0 → release! init: ref = nr_objs + 1 = 4 (오브젝트 3개 + 풀 자체 1개) fini: 풀에 남은 오브젝트(1개) + 풀 자체(1개) = 2 감소 → ref = 2 push: 각 반환 시 -1 → 마지막 push에서 ref = 0 → release 콜백 실행 refcount 변화 그래프 4 3 2 1 0 init pop A pop B fini push A push B

이 설계의 핵심은 objpool_fini 호출 후에도 아직 사용 중인 오브젝트를 안전하게 반환할 수 있는 점입니다. 마지막 오브젝트가 push될 때 비로소 release 콜백이 실행되어 모든 리소스가 해제됩니다.

실전 사용 예제

커널 모듈에서 objpool을 직접 사용하는 예제입니다.

#include <linux/module.h>
#include <linux/objpool.h>

struct my_context {
    struct objpool_head pool;
    /* 추가 필드 ... */
};

struct my_object {
    unsigned long timestamp;
    int           cpu;
    char          data[64];
};

/* 오브젝트 초기화 콜백 */
static int my_obj_init(void *obj, void *context)
{
    struct my_object *my = obj;
    memset(my, 0, sizeof(*my));
    return 0;
}

/* 풀 해제 콜백 */
static void my_pool_release(struct objpool_head *pool, void *context)
{
    struct my_context *ctx = context;
    pr_info("objpool released, all objects returned\n");
    kfree(ctx);
}

/* 모듈 초기화 */
static int __init my_init(void)
{
    struct my_context *ctx;
    int ret;

    ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;

    /* 20개의 my_object를 사전 할당 */
    ret = objpool_init(&ctx->pool,
                        20,                    /* nr_objs */
                        sizeof(struct my_object), /* obj_size */
                        GFP_KERNEL,             /* gfp */
                        ctx,                    /* context */
                        my_obj_init,            /* objinit */
                        my_pool_release);       /* release */
    if (ret) {
        kfree(ctx);
        return ret;
    }

    /* 오브젝트 사용 예시 */
    {
        struct my_object *obj = objpool_pop(&ctx->pool);
        if (obj) {
            obj->timestamp = ktime_get_ns();
            obj->cpu = raw_smp_processor_id();
            snprintf(obj->data, sizeof(obj->data), "hello");

            /* 사용 후 반환 */
            objpool_push(obj, &ctx->pool);
        }
    }

    return 0;
}

/* 모듈 종료 */
static void __exit my_exit(void)
{
    /* objpool_fini 호출 → 모든 오브젝트 반환 후 release 콜백 */
    /* ctx는 release 콜백에서 kfree됨 */
    objpool_fini(&global_ctx->pool);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
코드 설명
  • objpool_init(&ctx->pool, 20, ...)20개의 my_object를 사전 할당합니다. 이후 objpool_pop으로 즉시 할당할 수 있습니다.
  • my_pool_release모든 오브젝트가 반환되고 objpool_fini가 호출된 후 실행됩니다. 여기서 ctx를 해제합니다. ctxobjpool_head를 포함하므로 이 순서가 중요합니다.
  • objpool_fini(&global_ctx->pool)풀 해제를 요청합니다. 사용 중인 오브젝트가 있으면 마지막 오브젝트가 push될 때 release가 호출됩니다.

에러 처리 패턴

objpool 초기화가 중간에 실패할 수 있습니다. 이 경우 objpool_drop을 사용하여 이미 할당된 리소스를 정리합니다.

/* 초기화 실패 처리 */
int ret = objpool_init(&pool, nr_objs, obj_size,
                      GFP_KERNEL, ctx, init_cb, release_cb);
if (ret) {
    /* objpool_init이 실패하면 내부적으로 objpool_drop을 호출하여 */
    /* 이미 할당된 슬롯과 오브젝트를 해제합니다. */
    /* 호출자는 pool 구조체를 포함한 상위 구조체만 해제하면 됩니다. */
    kfree(ctx);
    return ret;
}

/* 주의: objpool_init 실패 시 release 콜백은 호출되지 않습니다. */
/* release 콜백에서 ctx를 kfree하더라도, 실패 경로에서는 직접 해제해야 합니다. */

관련 메커니즘 비교

메커니즘NMI-safeLock-freepre-allocper-CPU동적 크기주요 용도
objpool O O O O X kprobe/fprobe/rethook
mempool X X (spinlock) O X X 블록 I/O 등 긴급 할당
kmem_cache X 부분적 (SLUB) X 부분적 O 범용 오브젝트 캐싱
percpu_ref X O X O - 참조 카운팅 (풀링 아님)
llist (freelist) 부분적 O (CAS) O X X 범용 lock-free 리스트
오브젝트 풀링 메커니즘 비교 objpool per-CPU LIFO 슬롯 Lock-free, NMI-safe 사전 할당, 고정 크기 O(1) pop/push kprobe/fprobe/rethook 전용 모든 컨텍스트에서 사용 가능 mempool 전역 리스트 + spinlock Lock 필요, NMI 불가 사전 할당 + fallback alloc O(1) 할당 (경합 시 대기) 블록 I/O, 파일시스템 인터럽트 컨텍스트: 제한적 kmem_cache (SLUB) per-CPU slab + 부분 리스트 Fast path lock-free 온디맨드 할당, 동적 크기 O(1) fast / O(N) slow 범용 커널 오브젝트 캐싱 NMI 불가 (slub_lock) 선택 가이드 NMI/IRQ 컨텍스트에서 고정 수 오브젝트 → objpool 메모리 부족 시에도 보장된 할당 → mempool 동적 수, 범용 캐싱 프로세스 컨텍스트 → kmem_cache objpool의 핵심 차별점: NMI-safe + per-CPU lock-free + 사전 할당 (런타임 할당 0회) 대가: 고정 크기 풀, 동적 확장 불가, 오브젝트 고갈 시 NULL 반환

사용 시나리오 정리

상세 동작 비교

mempool과의 차이

mempoolinclude/linux/mempool.h에 정의되어 있으며, 메모리 압박(Memory Pressure) 상황에서도 할당을 보장하는 비상 풀입니다.

/* mempool의 할당 흐름 (비교용) */
void *mempool_alloc(struct mempool *pool, gfp_t gfp)
{
    /* 1. 일반 할당 시도 */
    element = pool->alloc(gfp, pool->pool_data);
    if (element)
        return element;

    /* 2. 실패하면 spinlock 획득 후 비상 풀에서 할당 */
    spin_lock_irqsave(&pool->lock, flags);  /* ← NMI에서 교착! */
    if (pool->curr_nr) {
        element = pool->elements[--pool->curr_nr];
        spin_unlock_irqrestore(&pool->lock, flags);
        return element;
    }
    spin_unlock_irqrestore(&pool->lock, flags);

    /* 3. 비상 풀도 비면 대기 (블로킹) */
    prepare_to_wait(&pool->wait, ...);
    schedule();  /* ← 인터럽트 컨텍스트에서 불가! */
}
코드 설명
  • spin_lock_irqsavemempool은 비상 풀 접근 시 spinlock을 사용합니다. NMI 핸들러가 이미 이 락을 보유한 상태에서 NMI가 재진입하면 교착이 발생합니다.
  • schedule()비상 풀마저 비면 메모리가 확보될 때까지 대기합니다. 인터럽트 컨텍스트에서는 sleep이 불가능하므로 호출할 수 없습니다.

kmem_cache (SLUB)와의 차이

/* SLUB의 할당 흐름 (비교용) */
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags)
{
    /* Fast path: per-CPU freelist에서 즉시 할당 (lock-free) */
    obj = __slab_alloc_node(...);  /* cmpxchg 사용 */
    if (obj)
        return obj;

    /* Slow path: 새 slab 페이지 할당 (GFP 플래그 필요) */
    page = alloc_pages(flags, ...);  /* ← NMI에서 불가! */
    /* ... slab 초기화 ... */
}

SLUB의 fast path는 per-CPU freelist를 사용하여 lock-free이지만, freelist가 비면 새 slab 페이지를 할당해야 하고, 이 과정에서 buddy allocator의 zone lock이 필요합니다. 따라서 NMI 컨텍스트에서는 사용할 수 없습니다.

llist (lockless list)와의 차이

/* llist의 pop 동작 (비교용) */
struct llist_node *llist_del_first(struct llist_head *head)
{
    struct llist_node *entry, *old_entry, *next;

    entry = smp_load_acquire(&head->first);
    for (;;) {
        if (entry == NULL)
            return NULL;
        old_entry = entry;
        next = READ_ONCE(entry->next);
        /* CAS 루프: 다른 CPU가 head를 변경하면 재시도 */
        entry = cmpxchg(&head->first, old_entry, next);
        if (entry == old_entry)   /* 성공 */
            return entry;
        /* 실패: 다시 읽고 재시도 (ABA 문제 가능) */
    }
}
코드 설명
  • cmpxchg(&head->first, ...)CAS(Compare-And-Swap) 연산입니다. 전역 head를 atomic하게 변경하지만, 이 과정에서 모든 CPU가 동일한 캐시 라인을 경합합니다.
  • ABA 문제NMI가 CAS 루프 중에 발생하여 head를 변경한 뒤 원래 값으로 되돌리면, CAS가 성공하지만 리스트 구조가 손상될 수 있습니다. objpool은 per-CPU 구조이므로 이 문제가 원천적으로 불가능합니다.

설계 트레이드오프(Trade-off)

장점단점
NMI-safe: 어떤 컨텍스트에서도 안전고정 크기: 런타임에 오브젝트 추가/제거 불가
Lock-free: O(1) pop/push, 경합 없음메모리 낭비: capacity가 2의 거듭제곱으로 올림됨
per-CPU 캐시 친화성Cross-CPU 편중: 시간이 지나면 분포 불균형
간단한 API: init/pop/push/fini고갈 시 NULL: 블로킹(Blocking) 대기 없음
참조 카운팅으로 안전한 해제범용성 부족: kprobe/fprobe/rethook 전용

향후 발전 방향

커널 버전별 변화

커널 버전변경 사항
v6.4objpool 최초 도입 (include/linux/objpool.h, lib/objpool.c). kretprobe를 freelist에서 objpool로 전환합니다.
v6.5rethook이 objpool을 사용하도록 전환됩니다. fprobe도 rethook을 통해 간접적으로 objpool을 사용합니다.
v6.6+objpool_init의 에러 처리 개선, capacity 계산 최적화가 적용됩니다.

자주 하는 실수

핵심 정리: objpool은 "NMI 컨텍스트에서 안전한 오브젝트 할당"이라는 단 하나의 목표에 최적화된 메커니즘입니다. per-CPU LIFO 스택, 사전 할당, lock-free 설계를 결합하여 이 목표를 달성합니다. 범용 할당자(kmalloc, kmem_cache)나 범용 풀(mempool)로는 불가능한 이 특수한 요구사항을 정확히 충족합니다.

Per-CPU 슬롯 내부 구조 심층

objpool의 핵심 자료 구조는 struct objpool_slot입니다. 각 CPU마다 하나씩 할당되며, 내부에 entries[] 순환 배열과 head/tail 인덱스를 사용하여 LIFO 스택을 구현합니다. 이 섹션에서는 entries[] 배열의 물리적 배치, head/tail 인덱스의 의미, 그리고 push/pop 연산의 5단계 과정을 상세히 분석합니다.

entries[] 배열의 메모리 배치

objpool_slot은 가변 길이 구조체입니다. 헤더 뒤에 entries[] 배열이 이어지며, 배열의 크기는 capacity 필드가 결정합니다. entries[]의 각 원소는 오브젝트 포인터(void *)가 아니라, 오브젝트 풀 전체 배열 내에서의 인덱스입니다. 실제 오브젝트 주소는 objpool_head.obj_arr[index]로 변환합니다.

/* include/linux/objpool.h - 슬롯 구조체 */
struct objpool_slot {
    uint32_t head;      /* pop 위치 (소비자 인덱스) */
    uint32_t tail;      /* push 위치 (생산자 인덱스) */
    uint32_t last;      /* 마지막 push 위치 (tail 캐시) */
    uint32_t mask;      /* capacity - 1 (2의 거듭제곱 마스크) */
    void *entries[];    /* 오브젝트 포인터 순환 배열 */
};

/* capacity는 항상 2의 거듭제곱 → mask = capacity - 1
 * 인덱스 계산: slot->entries[idx & slot->mask]
 * 이렇게 하면 모듈로 연산 없이 비트 AND로 순환 가능 */

head와 tail은 단조 증가(monotonically increasing)하는 32비트 정수입니다. 실제 배열 인덱스는 head & mask 또는 tail & mask로 계산합니다. 이 설계 덕분에 head와 tail이 같은 값을 가질 때 빈 슬롯, tail - head가 capacity와 같을 때 가득 찬 슬롯임을 O(1)에 판별할 수 있습니다.

/* 슬롯 상태 판별 매크로 (개념적) */
#define SLOT_EMPTY(s)  ((s)->head == (s)->tail)
#define SLOT_FULL(s)   ((s)->tail - (s)->head == (s)->mask + 1)
#define SLOT_COUNT(s)  ((s)->tail - (s)->head)

/* head/tail 단조 증가 예시:
 * capacity=4, mask=3일 때
 * head=5 → 실제 인덱스 = 5 & 3 = 1
 * tail=8 → 실제 인덱스 = 8 & 3 = 0
 * 현재 오브젝트 수 = 8 - 5 = 3 */

LIFO push/pop 5단계

objpool의 push와 pop은 각각 5단계로 구성됩니다. 락 없이 동작하기 위해 headtail 인덱스를 원자적으로 조작하며, try_cmpxchg_release/acquire 시맨틱을 활용합니다.

pop 연산 (오브젝트 획득)

  1. 현재 CPU 슬롯 조회: this_cpu_ptr(pool->cpu_slots)로 현재 CPU의 슬롯 포인터를 가져옵니다.
  2. head 읽기: READ_ONCE(slot->head)로 현재 head 값을 가져옵니다.
  3. 비어있는지 확인: head == READ_ONCE(slot->tail)이면 이 슬롯은 비어 있으므로 다른 CPU 슬롯에서 시도합니다(steal).
  4. head 전진(CAS): try_cmpxchg_release(&slot->head, &head, head + 1)로 head를 원자적으로 1 증가시킵니다. 실패하면 2단계로 돌아갑니다.
  5. 오브젝트 반환: slot->entries[head & slot->mask]에서 오브젝트 포인터를 꺼내 반환합니다.

push 연산 (오브젝트 반환)

  1. 현재 CPU 슬롯 조회: this_cpu_ptr(pool->cpu_slots)로 현재 CPU의 슬롯을 가져옵니다.
  2. tail 읽기: READ_ONCE(slot->tail)로 현재 tail 값을 가져옵니다.
  3. 오브젝트 저장: slot->entries[tail & slot->mask] = obj로 오브젝트 포인터를 배열에 기록합니다.
  4. 쓰기 배리어: smp_wmb()로 오브젝트 포인터가 entries[]에 완전히 기록된 후에 tail이 전진하도록 보장합니다.
  5. tail 전진: WRITE_ONCE(slot->tail, tail + 1)로 tail을 1 증가시켜 새 오브젝트가 가시적이 되도록 합니다.
/* objpool_pop 실제 구현 (include/linux/objpool.h, 단순화) */
static inline void *objpool_pop(struct objpool_head *pool)
{
    struct objpool_slot *slot;
    uint32_t head, tail;
    int cpu, i;

    /* 1단계: 현재 CPU 슬롯 조회 */
    cpu = raw_smp_processor_id();
    for (i = 0; i < num_possible_cpus(); i++) {
        slot = per_cpu_ptr(pool->cpu_slots,
                           (cpu + i) % num_possible_cpus());
        /* 2단계: head 읽기 */
        head = READ_ONCE(slot->head);
        do {
            /* 3단계: 비어있는지 확인 */
            tail = READ_ONCE(slot->tail);
            if (head == tail)
                break;  /* 이 슬롯은 비어 있음 → 다음 CPU 시도 */
            /* 4단계: head 전진 (CAS) */
        } while (!try_cmpxchg_release(&slot->head,
                                       &head, head + 1));
        if (head != tail) {
            /* 5단계: 오브젝트 반환 */
            return slot->entries[head & slot->mask];
        }
    }
    return NULL;  /* 모든 슬롯이 비어 있음 */
}
objpool_slot: LIFO push/pop 동작 entries[] (capacity=8, mask=7) [0] obj_2 [1] obj_5 [2] obj_1 [3] obj_7 [4] --- [5] --- [6] --- [7] --- head=0 tail=4 Pop (오브젝트 획득): ① head 읽기: head=0 ② 비어있는지 확인: head(0) != tail(4) → 비어있지 않음 ③ CAS(&head, 0, 1): head를 0→1로 원자적 전진 ④ entries[0 & 7] = entries[0] → obj_2 반환 ⑤ 결과: head=1, tail=4, 남은 오브젝트 3개 Push (오브젝트 반환): ① tail 읽기: tail=4 ② entries[4 & 7] = entries[4] ← obj_3 저장 ③ smp_wmb(): 쓰기 배리어로 순서 보장 ④ WRITE_ONCE(tail, 5): tail을 4→5로 전진 ⑤ 결과: head=0, tail=5, 보유 오브젝트 5개 pop 후 상태 [0]--- [1]obj5 [2]obj1 [3]obj7 head=1, tail=4 (3개) push 후 상태 [0]obj2 [1]obj5 [2]obj1 [3]obj7 [4]obj3 head=0, tail=5 (5개) Steal (다른 CPU에서 가져오기): 로컬 슬롯이 비어있으면 (cpu+1) % nr_cpus부터 순회하며 pop 시도 → 캐시 미스 발생하지만 NULL 반환보다 나은 최후 수단 objpool_head: 전체 CPU 슬롯 배치 (nr_cpus=4, nr_objs=16) objpool_head nr_objs=16, nr_cpus=4, obj_size=64 CPU 0 슬롯 head=3, tail=7, mask=7 4개 오브젝트 보유 CPU 1 슬롯 head=10, tail=15, mask=7 5개 오브젝트 보유 CPU 2 슬롯 head=5, tail=5, mask=7 비어 있음 (head==tail) CPU 3 슬롯 head=0, tail=8, mask=7 가득 참 (tail-head==8) steal: CPU2가 CPU1에서 pop 시도 obj_arr[0..15]: 사전 할당된 16개 오브젝트 kvmalloc(16 * (sizeof(void*) + 64), GFP_KERNEL) → 연속 메모리 블록 entries[]에는 obj_arr 인덱스(0~15)가 저장됨 → 포인터 크기 절약, 캐시 친화적
핵심 포인트: push는 producer 단독 접근이므로 CAS 없이 WRITE_ONCE만으로 충분합니다. 반면 pop은 steal로 인해 여러 CPU가 동일 슬롯에 접근할 수 있으므로 try_cmpxchg_release가 필요합니다. 이 비대칭 설계가 push 경로의 성능을 극대화합니다.

capacity 계산과 2의 거듭제곱 정렬

각 per-CPU 슬롯의 capacity는 nr_objs를 CPU 수로 나눈 값을 2의 거듭제곱으로 올림한 값입니다. 이 정렬은 모듈로 연산을 비트 AND로 대체하여 성능을 최적화합니다.

/* lib/objpool.c - capacity 계산 (단순화) */
static int objpool_init_percpu_slots(struct objpool_head *pool,
                                     int nr_objs, int obj_size,
                                     gfp_t gfp)
{
    int nr_cpus = num_possible_cpus();
    /* CPU당 최소 오브젝트 수: nr_objs / nr_cpus, 최소 4 */
    int capacity = max(roundup_pow_of_two(nr_objs / nr_cpus + 1), 4UL);
    int slot_size;
    int cpu;

    /* 슬롯 크기: 헤더 + entries[] 배열 */
    slot_size = struct_size_t(struct objpool_slot, entries, capacity);

    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot;
        /* per-CPU 슬롯 할당 (NUMA-aware) */
        slot = kvzalloc_node(slot_size, gfp, cpu_to_node(cpu));
        if (!slot)
            return -ENOMEM;
        slot->mask = capacity - 1;
        /* per-CPU 포인터에 저장 */
        *per_cpu_ptr(pool->cpu_slots, cpu) = slot;
    }

    pool->capacity = capacity;
    return 0;
}

/* 예시: nr_objs=20, nr_cpus=4
 * → 20/4 + 1 = 6 → roundup_pow_of_two(6) = 8
 * → capacity=8, mask=7
 * → 총 슬롯 메모리: 4 * (16 + 8*8) = 4 * 80 = 320 바이트 */

steal 알고리즘 상세

로컬 슬롯이 비어있을 때 실행되는 steal 알고리즘은 라운드 로빈 방식으로 다른 CPU의 슬롯을 순회합니다. steal은 원격 캐시라인 접근을 유발하므로 성능 비용이 높지만, NULL을 반환하는 것보다 낫습니다.

steal과 ABA 문제: steal은 원격 슬롯의 head를 CAS로 전진시킵니다. 32비트 head가 단조 증가하므로 ABA 문제가 발생하지 않습니다. head가 같은 값으로 돌아오려면 232번의 push/pop 사이클이 필요한데, 이는 현실적으로 불가능합니다. 이것이 순환 배열에서 인덱스를 모듈로 래핑하지 않고 단조 증가시키는 핵심 이유입니다.

NMI-Safe 설계 원리

NMI(Non-Maskable Interrupt)는 커널에서 가장 제한적인 인터럽트 컨텍스트입니다. kmalloc, mutex, spinlock_t(irqsave 포함) 등 거의 모든 동기화/할당 메커니즘이 NMI 내에서 사용할 수 없습니다. objpool은 이 극한 환경에서도 안전하게 동작하도록 설계되었습니다.

NMI에서 일반 할당이 불가능한 이유

NMI는 다른 모든 인터럽트(하드웨어 인터럽트, 소프트 인터럽트 포함)를 선점할 수 있으며, 비활성화할 수 없습니다. 이로 인해 다음과 같은 문제가 발생합니다:

per-CPU + lock-free로 NMI-safe 달성

objpool이 NMI-safe를 달성하는 핵심 원리는 다음 세 가지입니다:

  1. per-CPU 분리: 각 CPU가 독립적인 슬롯을 가지므로 CPU 간 경합이 없습니다. NMI가 현재 CPU의 슬롯에 접근해도 다른 CPU의 슬롯과 충돌하지 않습니다.
  2. 락 미사용: try_cmpxchgWRITE_ONCE/READ_ONCE만 사용합니다. CAS 실패 시 데드락이 아니라 단순히 재시도합니다.
  3. 사전 할당: 모든 오브젝트가 objpool_init 시점에 할당되므로 런타임에 메모리 할당자를 호출할 필요가 없습니다.
/* NMI에서 objpool 사용이 안전한 이유 - 시나리오 분석 */

/* 시나리오: CPU 0에서 kretprobe 핸들러 실행 중 NMI 발생 */
void kretprobe_handler(void) {
    /* ① 프로세스 컨텍스트에서 pop (head=3→4) */
    struct kretprobe_instance *ri = objpool_pop(&rp->rph->pool);
    /* ... ri 사용 중 ... */

    /* ★ 여기서 NMI 발생! */
    /* NMI 핸들러 내부의 다른 kretprobe도 같은 풀에서 pop 시도 */
    /* → head=4→5 (CAS 성공, 다른 슬롯 인덱스이므로 충돌 없음) */

    /* ② NMI 핸들러 복귀 후 계속 진행 */
    objpool_push(ri, &rp->rph->pool);  /* tail 전진 */
}

/* 핵심: head의 CAS는 NMI 핸들러와 일반 핸들러가 서로 다른 값으로
 * 경합하므로 한쪽은 성공하고 다른 쪽은 재시도합니다.
 * 데드락이 발생할 수 있는 지점이 존재하지 않습니다. */

NMI 중첩 시나리오

x86 아키텍처에서 NMI는 이론적으로 중첩될 수 있습니다(NMI 핸들러 실행 중 다시 NMI 발생). 이 극단적인 경우에도 objpool은 안전합니다:

NMI 중첩 시나리오

같은 CPU에서 head를 두 번 전진시키는 경우입니다. 초기 슬롯 상태: head=10, tail=14 (4개 오브젝트)

  1. [NMI-1 진입] pop 시작: head=10을 읽고, CAS(&head, 10, 11) 성공 → entries[10 & mask]를 반환합니다. NMI-1 처리 중...
  2. [NMI-2 진입] pop 시작 (NMI 중첩): head=11을 읽고 (NMI-1이 이미 전진시킴), CAS(&head, 11, 12) 성공 → entries[11 & mask]를 반환합니다. NMI-2 처리 후 push → tail 전진합니다.
  3. [NMI-2 복귀]
  4. NMI-1이 push → tail 전진합니다.
  5. [NMI-1 복귀]

결과: 모든 오브젝트가 정상적으로 반환됩니다. CAS의 원자성이 동일 CPU 내 중첩도 처리합니다.

NMI 중첩 시 objpool 동작 타임라인 시간 → 프로세스 pop(head 3→4) push(tail 6→7) NMI-1 pop(h:4→5) push(t:6→7) NMI! NMI-2 pop(h:5→6) push(t:6→7) NMI! 슬롯 상태 변화 (CPU 0, capacity=8) 초기: head=3, tail=6 → 3개 오브젝트 프로세스 pop 후: head=4, tail=6 → 2개 NMI-1 pop 후: head=5, tail=6 → 1개 NMI-2 pop 후: head=6, tail=6 → 0개 (비어있음) 모두 push 후: head=6, tail=9 → 3개 (복원됨) 모든 시점에서 head <= tail 불변식이 유지되며, 데드락 없이 정상 동작합니다
주의: NMI 중첩이 슬롯의 모든 오브젝트를 소진할 수 있습니다. nr_objs를 설정할 때 NMI 중첩 깊이를 고려해야 합니다. kretprobe의 maxactive 기본값이 2 * num_possible_cpus()인 이유가 바로 이것입니다 -- 각 CPU에서 프로세스 컨텍스트와 NMI 컨텍스트에서 동시에 하나씩 사용할 수 있도록 보장합니다.

메모리 순서 보장 분석

objpool의 NMI-safe 동작은 올바른 메모리 순서(Memory Ordering)에 의존합니다. 각 연산에서 사용되는 배리어(Barrier)를 분석합니다:

연산 메모리 순서 보장 내용
try_cmpxchg_release(&head) Release head 전진 전에 이전 entries[] 읽기가 완료됨을 보장합니다. 다른 CPU가 이 슬롯의 같은 위치에 push할 때 stale 데이터를 읽지 않도록 합니다.
smp_wmb() (push 내부) Write Barrier entries[]에 오브젝트 포인터를 기록한 후에 tail이 전진하도록 보장합니다. tail 전진이 먼저 보이면 pop에서 초기화되지 않은 포인터를 읽을 수 있습니다.
READ_ONCE(slot->tail) Volatile Read 컴파일러가 tail 읽기를 최적화(캐싱)하지 않도록 합니다. NMI가 push로 tail을 변경해도 pop에서 최신 값을 읽습니다.
WRITE_ONCE(slot->tail, ...) Volatile Write tail 업데이트가 원자적으로 가시적이 되도록 합니다. 중간 상태(torn write)가 관찰되지 않습니다.

이 배리어 조합은 x86에서는 대부분 no-op입니다. x86의 TSO(Total Store Ordering) 모델에서 smp_wmb()는 컴파일러 배리어로만 동작하고, try_cmpxchgLOCK CMPXCHG 명령어로 컴파일되어 암묵적 풀 배리어를 제공합니다. 따라서 x86에서 objpool의 배리어 오버헤드는 사실상 0입니다. ARM64에서는 smp_wmb()DMB ISHST로 변환되어 약간의 비용이 발생합니다.

kretprobe 활용 상세

kretprobe는 함수 반환 시점을 후킹하는 메커니즘입니다. 함수 진입 시 kretprobe_instance를 할당하고, 반환 시 핸들러를 호출한 뒤 인스턴스를 반환합니다. Linux 6.4부터 이 인스턴스 풀링에 objpool이 사용됩니다.

kretprobe_instance 생명주기

kretprobe_instance는 함수 호출 하나당 하나씩 소비되며, 함수 반환 시 재활용됩니다. 이 생명주기에서 objpool의 pop/push가 정확히 한 번씩 호출됩니다.

/* kernel/kprobes.c - kretprobe_instance 정의 */
struct kretprobe_instance {
    union {
        struct freelist_node freelist;  /* 구 버전 호환 */
    };
    struct rcu_head rcu;
    char data[];  /* 사용자 정의 데이터 (kretprobe.data_size) */
};

/* kretprobe 등록: objpool 초기화 */
int register_kretprobe(struct kretprobe *rp)
{
    int ret;
    /* maxactive가 0이면 기본값 = 2 * num_possible_cpus() */
    if (rp->maxactive <= 0)
        rp->maxactive = max_t(int, 10, 2 * num_possible_cpus());

    /* objpool 초기화: maxactive개의 kretprobe_instance 사전 할당 */
    ret = objpool_init(&rp->rph->pool,
                       rp->maxactive,       /* nr_objs */
                       sizeof(struct kretprobe_instance) + rp->data_size,
                       GFP_KERNEL,
                       rp->rph,             /* context */
                       kretprobe_init_inst,  /* objpool_init_obj_cb */
                       kretprobe_fini_pool); /* objpool_fini_cb */
    return ret;
}

pre_handler에서 pop, post에서 push

kretprobe의 동작은 pre_handler_kretprobe(함수 진입)와 __kretprobe_trampoline_handler(함수 반환)에서 이루어집니다:

/* kernel/kprobes.c - 함수 진입 시 (pre_handler) */
static int pre_handler_kretprobe(struct kprobe *p, struct pt_regs *regs)
{
    struct kretprobe *rp = container_of(p, struct kretprobe, kp);
    struct kretprobe_instance *ri;

    /* ★ objpool에서 인스턴스 pop (NMI-safe) */
    ri = objpool_pop(&rp->rph->pool);
    if (!ri) {
        /* 모든 인스턴스가 사용 중 → 이 호출은 추적하지 않음 */
        rp->nmissed++;
        return 0;
    }

    /* 반환 주소를 kretprobe_trampoline으로 교체 */
    arch_prepare_kretprobe(ri, regs);
    /* 현재 태스크의 ri 목록에 추가 */
    llist_add(&ri->llist, &current->kretprobe_instances);
    return 0;
}

/* kernel/kprobes.c - 함수 반환 시 */
static __used void *__kretprobe_trampoline_handler(struct pt_regs *regs)
{
    struct kretprobe_instance *ri;
    /* current->kretprobe_instances에서 ri 꺼내기 */
    ri = /* ... llist에서 pop ... */;

    /* 사용자 핸들러 호출 */
    if (rp->handler)
        rp->handler(ri, regs);

    /* ★ objpool에 인스턴스 push (반환) */
    objpool_push(ri, &rp->rph->pool);

    return (void *)ri->ret_addr;  /* 원래 반환 주소 복원 */
}

maxactive와 nmissed

maxactive는 objpool에 사전 할당할 kretprobe_instance 수를 결정합니다. 동시에 활성화될 수 있는 대상 함수 호출 수보다 커야 합니다. nmissed는 인스턴스 부족으로 추적하지 못한 호출 수입니다.

/* maxactive 설정 가이드라인 */

/* 기본값: 2 * num_possible_cpus()
 * - 각 CPU에서 프로세스 컨텍스트 1개 + NMI 1개 동시 사용 가능
 * - 대부분의 경우 충분하지만, 재귀 함수나 장시간 실행 함수는 부족할 수 있음 */

/* nmissed 모니터링 */
static void check_kretprobe_status(struct kretprobe *rp)
{
    /* /sys/kernel/debug/kprobes/list에서도 확인 가능 */
    if (rp->nmissed > 0) {
        pr_warn("kretprobe %pS: nmissed=%lu (maxactive=%d)\n",
                rp->kp.addr, rp->nmissed, rp->maxactive);
        /* nmissed가 0보다 크면 maxactive를 늘려야 합니다 */
    }
}

/* 재귀 함수의 경우 maxactive 계산:
 * maxactive = num_possible_cpus() * max_recursion_depth * 2
 * 예: 4코어, 최대 재귀 깊이 10 → maxactive = 4 * 10 * 2 = 80 */
kretprobe_instance 생명주기 objpool (사전 할당된 ri 풀) maxactive개 보유 pop pre_handler_kretprobe ① ri = objpool_pop() ② arch_prepare_kretprobe(ri) 대상 함수 실행 ri가 llist에 보관 ret_addr 교체 상태 objpool (ri가 반환됨) push trampoline_handler ① rp->handler(ri, regs) ② objpool_push(ri) kretprobe_trampoline 함수 ret 시 진입 원래 ret_addr 복원 함수 반환 시 trampoline 진입 nmissed 경로 (풀 고갈 시) objpool (비어있음) head == tail pop → NULL pre_handler: ri == NULL rp->nmissed++ → 이 호출 추적 건너뜀 nmissed > 0이면 maxactive를 늘려야 합니다: echo 'maxactive=100' >> kretprobe 설정
디버깅 팁: cat /sys/kernel/debug/kprobes/list에서 각 kretprobe의 nmissed 값을 확인할 수 있습니다. nmissed가 0보다 크면 objpool이 고갈되었다는 의미이므로 maxactive를 늘려야 합니다.

kretprobe: freelist에서 objpool로의 전환

Linux 6.4 이전에는 kretprobe가 freelist(llist 기반)를 사용했습니다. objpool로 전환된 이유를 비교합니다:

특성 freelist (6.3 이전) objpool (6.4+)
자료 구조 전역 lock-less linked list per-CPU 순환 배열 LIFO
NMI-safe O (cmpxchg 기반) O (per-CPU + cmpxchg)
캐시라인 경합 높음 (전역 head 공유) 없음 (per-CPU 분리)
CAS 실패율 (8코어) ~40% (전역 경합) ~1% (steal 시에만)
메모리 오버헤드 낮음 (포인터 1개/노드) 약간 높음 (per-CPU 슬롯 배열)
NUMA 인식 X O (kvzalloc_node)
확장성 (32+ CPU) 매우 나쁨 선형 스케일링

핵심 개선점은 캐시라인 분리입니다. freelist의 전역 head 포인터는 모든 CPU가 CAS로 경합하므로 L1 캐시라인이 지속적으로 무효화(invalidate)됩니다. objpool은 각 CPU가 자신의 슬롯만 접근하므로 캐시라인이 안정적으로 유지됩니다.

fprobe/rethook 통합 상세

fprobe는 ftrace 기반의 고성능 함수 프로브입니다. 반환 값 추적이 필요할 때 내부적으로 rethook을 사용하며, rethook은 다시 objpoolrethook_node를 풀링합니다. 이 3단 구조(fprobe → rethook → objpool)를 상세히 분석합니다.

fprobe → rethook → objpool 3단 구조

fprobe의 exit_handler가 설정되면 rethook이 활성화되고, rethook은 objpool을 통해 rethook_node를 관리합니다:

/* kernel/trace/fprobe.c - fprobe 등록 시 rethook 초기화 */
int register_fprobe(struct fprobe *fp, const char *filter, const char *notfilter)
{
    /* exit_handler가 있으면 rethook 필요 */
    if (fp->exit_handler) {
        int num = fp->nr_maxactive ? : 2 * num_possible_cpus();
        fp->rethook = rethook_alloc((void *)fp,
                                     fprobe_exit_handler,
                                     num,
                                     sizeof(struct fprobe_rethook_node));
        /* rethook_alloc 내부에서 objpool_init 호출 */
    }
    /* ... ftrace 등록 ... */
    return 0;
}

/* kernel/trace/rethook.c - rethook 초기화 */
struct rethook *rethook_alloc(void *data,
                              rethook_handler_t handler,
                              int num, int data_size)
{
    struct rethook *rh;
    rh = kzalloc(sizeof(*rh), GFP_KERNEL);
    rh->handler = handler;
    rh->data = data;

    /* ★ objpool 초기화: num개의 rethook_node 사전 할당 */
    if (objpool_init(&rh->pool, num,
                     sizeof(struct rethook_node) + data_size,
                     GFP_KERNEL, rh,
                     rethook_init_node, rethook_fini_pool))
        goto err;
    return rh;
}

rethook_try_get과 rethook_recycle

/* kernel/trace/rethook.c - rethook_node 획득/반환 */

/* 함수 진입 시: rethook_node 획득 */
struct rethook_node *rethook_try_get(struct rethook *rh)
{
    struct rethook_node *node;

    if (unlikely(!rethook_valid(rh)))
        return NULL;

    /* ★ objpool에서 pop */
    node = objpool_pop(&rh->pool);
    if (node)
        node->rethook = rh;
    return node;
}

/* 함수 반환 후: rethook_node 반환 */
void rethook_recycle(struct rethook_node *node)
{
    /* ★ objpool에 push */
    objpool_push(node, &node->rethook->pool);
}

/* fprobe entry에서 rethook 사용 */
static void fprobe_handler(unsigned long ip, unsigned long parent_ip,
                            struct ftrace_ops *ops, struct ftrace_regs *fregs)
{
    struct fprobe *fp = container_of(ops, struct fprobe, ops);
    struct rethook_node *rhn = NULL;

    if (fp->exit_handler) {
        rhn = rethook_try_get(fp->rethook);  /* objpool_pop */
        if (!rhn) {
            fp->nmissed++;
            return;
        }
    }
    /* entry_handler 호출 */
    if (fp->entry_handler)
        fp->entry_handler(fp, ip, parent_ip, fregs);

    if (rhn)
        rethook_hook(rhn, fregs, true);  /* 반환 주소 교체 */
}
fprobe → rethook → objpool 3단 구조 fprobe 레이어 register_fprobe() fprobe_handler() entry_handler() fprobe_exit_handler() rethook 레이어 rethook_alloc() rethook_try_get() rethook_hook() rethook_recycle() objpool 레이어 objpool_init() objpool_pop() objpool_push() per-CPU slot[0] rethook_node 오브젝트들이 per-CPU 슬롯에 분산 배치됩니다
주의: fprobe의 nr_maxactive와 kretprobe의 maxactive는 동일한 역할을 합니다 -- 둘 다 objpool의 nr_objs로 전달됩니다. fprobe가 내부적으로 rethook을 거치므로 한 단계 더 간접적이지만, 성능 특성은 동일합니다.

fprobe와 kretprobe 선택 기준

fprobe와 kretprobe는 모두 objpool을 사용하지만, 사용 방식에 차이가 있습니다:

특성 kretprobe fprobe
기반 기술 int3 breakpoint (kprobe) ftrace (function tracer)
objpool 경로 직접 사용 rethook 경유
성능 (entry) ~1,000ns (int3 오버헤드) ~100ns (ftrace nop 교체)
다중 함수 프로브 함수당 1개 등록 글로브 패턴으로 일괄 등록
반환 값 추적 handler에서 직접 접근 exit_handler 설정 시 rethook 활성화
NMI 내 프로브 O (objpool NMI-safe) O (objpool NMI-safe)
권장 용도 레거시 코드, 단일 함수 새 코드, 다중 함수, 고성능

fprobe는 kretprobe의 상위 호환으로 설계되었으며, 새로운 트레이싱 코드에서는 fprobe 사용이 권장됩니다. 두 메커니즘 모두 내부적으로 objpool의 동일한 pop/push 경로를 사용하므로 오브젝트 풀링 성능은 동일합니다.

rethook_node 전체 생명주기

rethook_node의 생명주기를 시간 순서대로 정리합니다:

  1. 초기화 (rethook_alloc): objpool_init으로 num개의 rethook_node를 사전 할당합니다. 각 노드의 rethook 필드는 NULL입니다.
  2. 획득 (rethook_try_get): objpool_pop으로 노드를 꺼냅니다. node->rethook = rh로 소속 rethook을 기록합니다.
  3. 설치 (rethook_hook): 현재 태스크의 반환 주소 스택에 노드를 삽입합니다. 원래 반환 주소를 node->ret_addr에 저장하고, 반환 주소를 rethook_trampoline으로 교체합니다.
  4. 트리거 (rethook_trampoline_handler): 대상 함수 반환 시 트램펄린이 호출됩니다. rh->handler를 호출하여 사용자 핸들러(fprobe의 exit_handler)를 실행합니다.
  5. 반환 (rethook_recycle): objpool_push로 노드를 풀에 되돌립니다. 다음 함수 호출에서 재사용됩니다.

성능 벤치마크와 대안 비교

objpool의 성능을 다른 커널 오브젝트 풀링/할당 메커니즘과 정량적으로 비교합니다. 벤치마크는 동일한 크기의 오브젝트를 반복적으로 할당/해제하는 시나리오에서 측정합니다.

벤치마크 방법론

다음 4가지 메커니즘을 비교합니다:

/* 벤치마크 코드 개요 (간략화) */
#define BENCH_ITERATIONS  1000000
#define OBJ_SIZE          64

static void bench_objpool(struct objpool_head *pool)
{
    int i;
    ktime_t start = ktime_get();
    for (i = 0; i < BENCH_ITERATIONS; i++) {
        void *obj = objpool_pop(pool);
        if (obj)
            objpool_push(obj, pool);
    }
    pr_info("objpool: %lld ns/op\n",
            ktime_to_ns(ktime_sub(ktime_get(), start)) / BENCH_ITERATIONS);
}

/* 대표적인 결과 (x86_64, 4코어, 6.4 커널):
 *
 * 메커니즘           | 단일 CPU  | 4 CPU 병렬 | NMI-safe
 * ------------------|----------|-----------|--------
 * objpool           |   15 ns  |    18 ns  |   O
 * kmem_cache (SLUB) |   25 ns  |    30 ns  |   X
 * mempool           |   45 ns  |   120 ns  |   X
 * llist (freelist)  |   20 ns  |    85 ns  |   O
 *
 * objpool은 단일 CPU에서 가장 빠르고, 멀티 CPU에서도
 * per-CPU 분리 덕분에 거의 선형 스케일링을 달성합니다.
 * llist는 NMI-safe하지만 전역 리스트의 CAS 경합으로
 * 멀티 CPU에서 성능이 급격히 저하됩니다. */
오브젝트 풀링 메커니즘 성능 비교 동시 CPU 수 지연 시간 (ns/op) 0 30 60 90 120 150 1 CPU 4 CPU 8 CPU 15ns 18ns 20ns 25ns 30ns 40ns 20ns 85ns 130ns 45ns 120ns 150ns objpool (NMI-safe) kmem_cache llist (NMI-safe) mempool
핵심 분석: objpool은 CPU 수가 증가해도 지연 시간이 거의 변하지 않습니다(15ns → 20ns). 이는 per-CPU 분리 덕분에 캐시라인 경합이 없기 때문입니다. 반면 llist와 mempool은 전역 자료 구조를 공유하므로 CPU 수에 비례하여 성능이 저하됩니다. kmem_cache는 per-CPU freelist 덕분에 상대적으로 양호하지만, 슬랩 리필(refill) 시 스핀락이 필요하므로 NMI에서 사용할 수 없습니다.

objpool 선택 기준

조건 권장 메커니즘 이유
NMI 컨텍스트에서 사용 objpool 유일한 NMI-safe 풀
고정 크기 + 높은 빈도 kmem_cache (SLUB) 범용성, 풍부한 디버깅 지원
할당 실패 허용 불가 mempool 예약 풀 + 대기 가능
단순 LIFO + NMI 불필요 per-CPU llist 헤더만으로 구현 가능
오브젝트 수 동적 변화 kmem_cache 또는 mempool objpool은 초기화 후 크기 고정

커널 모듈 예제

objpool을 직접 사용하는 커널 모듈 예제입니다. /proc/objpool_demo 인터페이스를 통해 pop/push 연산을 테스트할 수 있습니다.

모듈 소스 코드

/* objpool_demo.c - objpool 사용 데모 커널 모듈 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/objpool.h>
#include <linux/slab.h>

#define POOL_SIZE    32    /* 사전 할당할 오브젝트 수 */

struct demo_object {
    unsigned long id;       /* 오브젝트 식별 번호 */
    unsigned long pop_cpu;  /* 마지막으로 pop한 CPU */
    unsigned long pop_count;/* pop된 총 횟수 */
    ktime_t last_pop_time;  /* 마지막 pop 시각 */
};

static struct objpool_head demo_pool;
static atomic_long_t total_pops = ATOMIC_LONG_INIT(0);
static atomic_long_t total_pushes = ATOMIC_LONG_INIT(0);
static atomic_long_t total_misses = ATOMIC_LONG_INIT(0);

/* 오브젝트 초기화 콜백: objpool_init에서 각 오브젝트마다 호출 */
static int demo_obj_init(void *obj, void *context)
{
    struct demo_object *dobj = obj;
    static atomic_long_t next_id = ATOMIC_LONG_INIT(0);

    dobj->id = atomic_long_inc_return(&next_id);
    dobj->pop_cpu = -1;
    dobj->pop_count = 0;
    dobj->last_pop_time = 0;
    pr_debug("초기화: obj[%lu] at %px\n", dobj->id, dobj);
    return 0;
}

/* 풀 해제 콜백 */
static int demo_pool_fini(struct objpool_head *head, void *context)
{
    pr_info("풀 해제 완료 (total pops=%ld, pushes=%ld, misses=%ld)\n",
            atomic_long_read(&total_pops),
            atomic_long_read(&total_pushes),
            atomic_long_read(&total_misses));
    return 0;
}

/* pop + 작업 + push 테스트 */
static void demo_pop_push(void)
{
    struct demo_object *obj;

    obj = objpool_pop(&demo_pool);
    if (!obj) {
        atomic_long_inc(&total_misses);
        pr_warn("pop 실패: 풀 고갈 (CPU %d)\n",
                raw_smp_processor_id());
        return;
    }

    atomic_long_inc(&total_pops);

    /* 오브젝트 사용: 메타데이터 업데이트 */
    obj->pop_cpu = raw_smp_processor_id();
    obj->pop_count++;
    obj->last_pop_time = ktime_get();

    pr_debug("pop: obj[%lu] on CPU %lu (count=%lu)\n",
             obj->id, obj->pop_cpu, obj->pop_count);

    /* 오브젝트 반환 */
    objpool_push(obj, &demo_pool);
    atomic_long_inc(&total_pushes);
}

/* /proc/objpool_demo 읽기: 풀 상태 표시 */
static int demo_proc_show(struct seq_file *m, void *v)
{
    seq_printf(m, "=== objpool_demo 상태 ===\n");
    seq_printf(m, "풀 크기 (nr_objs):    %d\n", POOL_SIZE);
    seq_printf(m, "오브젝트 크기:         %zu bytes\n",
               sizeof(struct demo_object));
    seq_printf(m, "총 pop 횟수:          %ld\n",
               atomic_long_read(&total_pops));
    seq_printf(m, "총 push 횟수:         %ld\n",
               atomic_long_read(&total_pushes));
    seq_printf(m, "총 miss 횟수:         %ld\n",
               atomic_long_read(&total_misses));
    seq_printf(m, "num_possible_cpus:    %d\n",
               num_possible_cpus());
    return 0;
}

/* /proc/objpool_demo 쓰기: "pop" 명령으로 테스트 실행 */
static ssize_t demo_proc_write(struct file *file,
                                const char __user *buf,
                                size_t count, loff_t *ppos)
{
    char cmd[16];
    size_t len = min(count, sizeof(cmd) - 1);

    if (copy_from_user(cmd, buf, len))
        return -EFAULT;
    cmd[len] = '\0';

    if (strncmp(cmd, "pop", 3) == 0) {
        demo_pop_push();
    } else if (strncmp(cmd, "burst", 5) == 0) {
        int i;
        for (i = 0; i < 100; i++)
            demo_pop_push();
        pr_info("100회 burst pop/push 완료\n");
    }
    return count;
}

static int demo_proc_open(struct inode *inode, struct file *file)
{
    return single_open(file, demo_proc_show, NULL);
}

static const struct proc_ops demo_proc_ops = {
    .proc_open    = demo_proc_open,
    .proc_read    = seq_read,
    .proc_write   = demo_proc_write,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

static struct proc_dir_entry *proc_entry;

static int __init demo_init(void)
{
    int ret;

    /* objpool 초기화 */
    ret = objpool_init(&demo_pool,
                       POOL_SIZE,
                       sizeof(struct demo_object),
                       GFP_KERNEL,
                       NULL,             /* context */
                       demo_obj_init,    /* init 콜백 */
                       demo_pool_fini);  /* fini 콜백 */
    if (ret) {
        pr_err("objpool_init 실패: %d\n", ret);
        return ret;
    }

    /* /proc 인터페이스 생성 */
    proc_entry = proc_create("objpool_demo", 0666, NULL,
                              &demo_proc_ops);
    if (!proc_entry) {
        objpool_fini(&demo_pool);
        return -ENOMEM;
    }

    pr_info("모듈 로드: %d개 오브젝트, 각 %zu 바이트\n",
            POOL_SIZE, sizeof(struct demo_object));
    return 0;
}

static void __exit demo_exit(void)
{
    proc_remove(proc_entry);
    objpool_fini(&demo_pool);
    pr_info("모듈 언로드\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("objpool 데모 커널 모듈");
MODULE_AUTHOR("Linux Kernel Documentation");

Makefile

# Makefile - objpool_demo 빌드
obj-m += objpool_demo.o

# 커널 빌드 디렉터리 (환경에 맞게 수정)
KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

# 사용법:
#   make                              # 모듈 빌드
#   sudo insmod objpool_demo.ko       # 모듈 로드
#   cat /proc/objpool_demo            # 상태 확인
#   echo "pop" > /proc/objpool_demo   # 단일 pop/push 테스트
#   echo "burst" > /proc/objpool_demo # 100회 burst 테스트
#   cat /proc/objpool_demo            # 결과 확인
#   sudo rmmod objpool_demo           # 모듈 언로드
#   dmesg | tail -20                  # 커널 로그 확인
실습 순서: 모듈을 로드한 후 echo "burst" > /proc/objpool_demo로 100회 pop/push를 실행하고, cat /proc/objpool_demo로 총 pop/push/miss 횟수를 확인합니다. dmesg에서 각 오브젝트의 CPU 번호와 pop 횟수를 확인하면 per-CPU 분산이 실제로 어떻게 동작하는지 볼 수 있습니다.
주의: 이 예제는 학습용입니다. 실제 프로덕션 코드에서 objpool을 직접 사용하는 경우는 드물며, 대부분 kretprobe/fprobe/rethook을 통해 간접적으로 사용합니다. objpool API는 커널 내부 API이므로 버전 간 호환성이 보장되지 않습니다.

모듈 테스트 및 검증

모듈을 로드한 후 다음 명령으로 동작을 검증할 수 있습니다:

## 1. 모듈 빌드 및 로드
make
sudo insmod objpool_demo.ko

## 2. 초기 상태 확인
cat /proc/objpool_demo
# === objpool_demo 상태 ===
# 풀 크기 (nr_objs):    32
# 오브젝트 크기:         32 bytes
# 총 pop 횟수:          0
# 총 push 횟수:         0
# 총 miss 횟수:         0
# num_possible_cpus:    4

## 3. 단일 pop/push 테스트
echo "pop" > /proc/objpool_demo
cat /proc/objpool_demo
# 총 pop 횟수:          1
# 총 push 횟수:         1

## 4. burst 테스트 (100회 연속 pop/push)
echo "burst" > /proc/objpool_demo
cat /proc/objpool_demo
# 총 pop 횟수:          101
# 총 push 횟수:         101
# 총 miss 횟수:         0

## 5. 커널 로그 확인
dmesg | grep objpool_demo
# objpool_demo: 모듈 로드: 32개 오브젝트, 각 32 바이트
# objpool_demo: 100회 burst pop/push 완료

## 6. 멀티 코어 테스트 (여러 터미널에서 동시 실행)
# 터미널 1:
for i in $(seq 1 100); do echo "burst" > /proc/objpool_demo; done
# 터미널 2:
for i in $(seq 1 100); do echo "burst" > /proc/objpool_demo; done

## 7. 결과 확인: miss가 0이면 per-CPU 분산이 잘 동작하는 것
cat /proc/objpool_demo
# 총 pop 횟수:          20101
# 총 push 횟수:         20101
# 총 miss 횟수:         0   ← 오브젝트 충분

## 8. 모듈 언로드
sudo rmmod objpool_demo
dmesg | tail -5
# objpool_demo: 풀 해제 완료 (total pops=20101, pushes=20101, misses=0)
# objpool_demo: 모듈 언로드

오류 처리 패턴

objpool을 사용하는 모듈에서 주의해야 할 오류 처리 패턴을 정리합니다:

설계 패턴: objpool을 사용하는 코드는 항상 "사전 할당 → 런타임 pop/push → 전량 반환 → 해제"의 4단계 생명주기를 따릅니다. 이 패턴은 kretprobe, rethook, fprobe 모두에서 동일하며, objpool을 직접 사용하는 커스텀 모듈에서도 반드시 준수해야 합니다.

커널 소스 심층 분석

이 섹션에서는 objpool의 실제 커널 소스 코드를 단계별로 추적하며, 초기화부터 해제까지의 전체 흐름을 분석합니다. Linux 6.4+ 기준으로 include/linux/objpool.hlib/objpool.c의 핵심 구현을 다룹니다.

objpool_init: per-CPU 슬롯 메모리 배치

objpool_init은 per-CPU 슬롯을 할당하고 오브젝트를 라운드 로빈으로 분배합니다. 이 과정에서 NUMA 노드 인식 할당과 캐시 라인 정렬(Cache Line Alignment)이 이루어집니다.

/* lib/objpool.c - objpool_init 전체 흐름 (v6.4+ 기준) */
int objpool_init(struct objpool_head *pool,
                 int nr_objs, int object_size,
                 gfp_t gfp, void *context,
                 objpool_init_obj_cb objinit,
                 objpool_fini_cb release)
{
    int rc, cpu, i;

    /* 1단계: 메타데이터 초기화 */
    memset(pool, 0, sizeof(*pool));
    pool->nr_objs  = nr_objs;
    pool->obj_size = ALIGN(object_size, sizeof(void *));
    pool->nr_cpus  = num_possible_cpus();
    pool->gfp      = gfp & ~__GFP_ZERO;
    pool->context  = context;
    pool->release  = release;

    /* 2단계: capacity 계산 - 2의 거듭제곱으로 올림 */
    pool->capacity = roundup_pow_of_two(
        nr_objs / pool->nr_cpus + nr_objs);

    /* 3단계: per-CPU 슬롯 배열 할당
     * __alloc_percpu는 각 CPU에 독립적 메모리를 배치하며,
     * 캐시 라인 경계에 정렬됩니다 (일반적으로 64바이트) */
    pool->slots = __alloc_percpu(
        sizeof(struct objpool_slot) +
            pool->capacity * sizeof(void *),
        sizeof(void *));
    if (!pool->slots)
        return -ENOMEM;

    /* 4단계: 각 CPU 슬롯 초기화 */
    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot =
            per_cpu_ptr(pool->slots, cpu);
        slot->head = 0;
        slot->tail = 0;
        slot->last = pool->capacity;
        slot->mask = pool->capacity - 1;
    }

    /* 5단계: 오브젝트 사전 할당 및 라운드 로빈 분배
     * NUMA 인식: 각 오브젝트를 해당 CPU의 NUMA 노드에서 할당 */
    for (i = 0; i < nr_objs; i++) {
        void *obj;
        cpu = i % pool->nr_cpus;

        /* kvmalloc_node: NUMA 로컬 메모리에서 할당 */
        obj = kvmalloc_node(pool->obj_size, pool->gfp,
                             cpu_to_node(cpu));
        if (!obj) {
            objpool_free(pool);
            return -ENOMEM;
        }

        /* 사용자 초기화 콜백 호출 */
        if (objinit) {
            rc = objinit(obj, context);
            if (rc) {
                kvfree(obj);
                objpool_free(pool);
                return rc;
            }
        }

        /* 해당 CPU 슬롯에 push */
        {
            struct objpool_slot *slot =
                per_cpu_ptr(pool->slots, cpu);
            slot->entries[slot->head & slot->mask] = obj;
            slot->head++;
        }
    }

    /* 6단계: 참조 카운트 설정
     * nr_objs + 1: 각 오브젝트에 1, 풀 자체에 1 */
    refcount_set(&pool->ref, nr_objs + 1);

    return 0;
}
코드 설명
  • ALIGN(object_size, sizeof(void *))오브젝트 크기를 포인터 크기 단위로 정렬합니다. 64비트 시스템에서 8바이트 정렬이 됩니다. 정렬되지 않은 접근은 일부 아키텍처에서 성능 저하나 예외를 유발합니다.
  • __alloc_percpuper-CPU 영역에 메모리를 할당합니다. 커널의 per-CPU 할당자는 각 CPU의 메모리를 독립된 영역에 배치하므로, 서로 다른 CPU의 슬롯이 동일한 캐시 라인을 공유하지 않습니다. 이것이 false sharing을 방지하는 핵심입니다.
  • kvmalloc_node(... cpu_to_node(cpu))NUMA 시스템에서 오브젝트를 해당 CPU의 로컬 NUMA 노드 메모리에서 할당합니다. 원격 노드 접근은 추가 지연(50~100ns)을 유발하므로 이 최적화가 중요합니다.
  • refcount_set(&pool->ref, nr_objs + 1)참조 카운트를 오브젝트 수 + 1로 초기화합니다. fini에서 -1, 풀에 남은 오브젝트 수만큼 추가 감소합니다. 사용 중인 오브젝트가 push될 때마다 -1하여 최종적으로 0이 되면 release 콜백이 실행됩니다.
objpool_init: per-CPU 슬롯 메모리 배치 (nr_objs=12, nr_cpus=4) __alloc_percpu가 각 CPU 슬롯을 독립된 캐시 라인에 배치하여 false sharing을 방지합니다 CPU 0 슬롯 (캐시 라인 A) head=3 tail=0 mask=15 obj_0 obj_4 obj_8 --- entries[0..15]: 3개 유효 NUMA Node 0 로컬 메모리 64B 캐시 라인 경계 (독점 소유) CPU 1 슬롯 (캐시 라인 B) head=3 tail=0 mask=15 obj_1 obj_5 obj_9 --- entries[0..15]: 3개 유효 NUMA Node 0 로컬 메모리 64B 캐시 라인 경계 (독점 소유) CPU 2 슬롯 (캐시 라인 C) head=3 tail=0 mask=15 obj_2 obj_6 obj_10 --- entries[0..15]: 3개 유효 NUMA Node 1 로컬 메모리 64B 캐시 라인 경계 (독점 소유) CPU 3 슬롯 (캐시 라인 D) head=3 tail=0 mask=15 obj_3 obj_7 obj_11 --- entries[0..15]: 3개 유효 NUMA Node 1 로컬 메모리 64B 캐시 라인 경계 (독점 소유) 라운드 로빈 분배: i % nr_cpus obj_0→CPU0, obj_1→CPU1, obj_2→CPU2, obj_3→CPU3, obj_4→CPU0, obj_5→CPU1, ... 12개 오브젝트 / 4 CPU = CPU당 3개씩 균등 분배 (capacity=16, 여유 공간 13개) False Sharing 방지 메커니즘 False Sharing 발생 시 (가상) CPU 0 head와 CPU 1 head가 같은 캐시 라인에 배치 → CPU 0이 head를 수정하면 CPU 1의 캐시 무효화 → CPU 1이 head를 읽으려면 메모리에서 다시 로드 필요 → MESI Invalidate 연쇄: 성능 50~80% 저하 → CPU 수에 비례하여 악화 objpool 실제 설계: per-CPU 분리 __alloc_percpu가 각 CPU에 독립된 캐시 라인 배치 → CPU 0이 head를 수정해도 CPU 1 캐시에 영향 없음 → 각 CPU가 자기 캐시 라인을 Exclusive 모드로 소유 → 항상 L1 히트, CPU 수 무관하게 일정한 성능 → pop/push 모두 ~5ns (L1 캐시 접근 시간)

objpool_pop: Fast Path vs Slow Path (Steal)

objpool_pop은 두 가지 경로를 가집니다. Fast Path는 현재 CPU의 로컬 슬롯에서 즉시 오브젝트를 꺼내는 것이고, Slow Path는 로컬 슬롯이 비어있을 때 다른 CPU의 슬롯에서 오브젝트를 가져오는(steal) 것입니다.

/* objpool_pop 내부 흐름 분석 */

/* Fast Path: 로컬 슬롯에 오브젝트 존재 (~15ns) */
void *objpool_pop(struct objpool_head *pool)
{
    int cpu = raw_smp_processor_id();
    struct objpool_slot *slot = per_cpu_ptr(pool->slots, cpu);
    uint32_t head = READ_ONCE(slot->head);
    uint32_t tail = READ_ONCE(slot->tail);

    /* Fast Path: 로컬 슬롯에 오브젝트가 있음 */
    if (likely(head != tail)) {
        /* CAS로 head 전진 (NMI 재진입 보호) */
        if (try_cmpxchg_release(&slot->head, &head, head + 1))
            return slot->entries[head & slot->mask];
        /* CAS 실패 = NMI가 먼저 pop → 재시도 */
    }

    /* Slow Path: 로컬 슬롯이 비어있거나 CAS 실패 */
    return objpool_pop_slow(pool, cpu);
}

/* Slow Path: 다른 CPU 슬롯에서 steal (~50~200ns) */
static noinline void *objpool_pop_slow(
    struct objpool_head *pool, int cpu)
{
    int i;
    /* 현재 CPU 다음부터 모든 CPU를 순회 */
    for (i = 1; i < pool->nr_cpus; i++) {
        int target = (cpu + i) % pool->nr_cpus;
        struct objpool_slot *slot =
            per_cpu_ptr(pool->slots, target);
        uint32_t head, tail;

        head = READ_ONCE(slot->head);
        do {
            tail = READ_ONCE(slot->tail);
            if (head == tail)
                break;  /* 이 슬롯도 비어있음 */
        } while (!try_cmpxchg_release(
                &slot->head, &head, head + 1));

        if (head != tail)
            return slot->entries[head & slot->mask];
    }
    return NULL;  /* 모든 CPU가 비어있음: 풀 고갈 */
}
코드 설명
  • likely(head != tail)컴파일러 힌트입니다. 대부분의 경우 로컬 슬롯에 오브젝트가 있으므로 이 분기가 참일 확률이 높습니다. CPU의 분기 예측기가 이 경로를 최적화합니다.
  • try_cmpxchg_release원자적 비교-교환(Compare-And-Exchange)입니다. head 값이 예상과 같으면 head+1로 변경합니다. NMI가 먼저 head를 변경했으면 실패하고, 변경된 head 값이 head 변수에 저장됩니다.
  • noinlineSlow path를 별도 함수로 분리하고 인라인을 금지합니다. Fast path의 코드 크기를 줄여 명령어 캐시(I-cache) 효율을 높입니다.
  • (cpu + i) % pool->nr_cpus현재 CPU 다음부터 순회하여 특정 CPU에 steal이 집중되는 것을 방지합니다. 이 순회 순서는 공정성(fairness)을 보장합니다.
objpool_pop: Fast Path vs Slow Path (Steal) objpool_pop(pool) 호출 로컬 CPU 슬롯: head != tail ? 예 (Fast Path) CAS(head, head+1): L1 캐시 히트 entries[head & mask] 반환 (~15ns) 아니오 (Slow Path) CPU (cpu+1) % nr_cpus 슬롯 시도 원격 슬롯: head != tail ? CAS steal: 캐시 미스 발생 (~50-200ns) 원격 entries[] 반환 (50~200ns) 아니오 다음 CPU 시도 NULL 반환 (고갈) nr_cpus회 반복 Fast Path: ~15ns (L1 히트) Slow Path: ~50-200ns (캐시 미스) NULL: 풀 고갈 (nr_objs 부족)

objpool_fini: RCU와 참조 카운팅을 통한 안전한 해제

objpool_fini는 즉시 메모리를 해제하지 않습니다. 대신 참조 카운팅(Reference Counting)을 활용하여 모든 사용 중인 오브젝트가 반환된 후에 비로소 release 콜백을 호출합니다. 이 메커니즘은 RCU(Read-Copy-Update)의 지연 해제 패턴과 유사합니다.

/* lib/objpool.c - objpool_fini 상세 흐름 */
void objpool_fini(struct objpool_head *pool)
{
    int count, cpu;

    /* 1단계: 풀에 남아있는 (미사용) 오브젝트 수 계산 */
    count = 0;
    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot =
            per_cpu_ptr(pool->slots, cpu);
        count += READ_ONCE(slot->head) -
                 READ_ONCE(slot->tail);
    }

    /* 2단계: 미사용 오브젝트의 참조를 한 번에 해제
     * 이 오브젝트들은 아무도 사용하지 않으므로 안전 */
    if (count > 0)
        refcount_sub(count, &pool->ref);

    /* 3단계: 풀 자체의 참조(-1) 해제
     * 사용 중인 오브젝트가 없으면 여기서 ref=0 → release */
    if (refcount_dec_and_test(&pool->ref))
        objpool_free(pool);

    /* 사용 중인 오브젝트가 있는 경우:
     * ref = (nr_objs + 1) - count - 1 = 사용중_오브젝트수
     * 각 오브젝트가 push될 때 refcount_dec_and_test가
     * 호출되며, 마지막 오브젝트 반환 시 ref=0 → release */
}

/* objpool_free: 실제 리소스 해제 */
static void objpool_free(struct objpool_head *pool)
{
    int cpu;

    if (!pool->slots)
        return;

    /* 각 CPU 슬롯의 오브젝트를 해제 */
    for_each_possible_cpu(cpu) {
        struct objpool_slot *slot =
            per_cpu_ptr(pool->slots, cpu);
        uint32_t h = slot->head, t = slot->tail;
        while (h != t) {
            void *obj = slot->entries[h & slot->mask];
            if (obj)
                kvfree(obj);
            h++;
        }
    }

    /* per-CPU 슬롯 메모리 해제 */
    free_percpu(pool->slots);
    pool->slots = NULL;

    /* release 콜백 호출 (호출자의 상위 구조체 해제) */
    if (pool->release)
        pool->release(pool, pool->context);
}
코드 설명
  • refcount_sub(count, &pool->ref)풀에 남아 있는 오브젝트의 참조를 한 번에 감소시킵니다. 이 오브젝트들은 사용 중이 아니므로 즉시 해제할 수 있습니다.
  • refcount_dec_and_test풀 자체의 참조(+1)를 해제합니다. 아직 사용 중인 오브젝트가 있으면 ref > 0이므로 objpool_free는 호출되지 않습니다.
  • kvfree(obj)kvmalloc으로 할당된 오브젝트를 해제합니다. vmalloc 또는 kmalloc 중 어느 것으로 할당되었는지 자동 감지하여 올바른 해제 함수를 호출합니다.
  • free_percpu(pool->slots)per-CPU 영역에 할당된 슬롯 메모리를 해제합니다. __alloc_percpu의 대응 해제 함수입니다.

objpool_slot 구조체 내부: 순환 버퍼 설계

objpool_slot은 per-CPU 순환 버퍼(Circular Buffer)입니다. 이 구조체가 어떻게 LIFO 스택을 구현하면서도 NMI-safe를 유지하는지 상세히 분석합니다.

/* objpool_slot 메모리 레이아웃 상세
 *
 * 주소:  | offset 0  | offset 4  | offset 8  | offset 12 |
 *        |-----------|-----------|-----------|-----------|
 *        |   head    |   tail    |   last    |   mask    |
 *        |  (4바이트)  |  (4바이트)  |  (4바이트)  |  (4바이트)  |
 *
 * 주소:  | offset 16         | offset 24         | ...
 *        |-------------------|-------------------|---
 *        | entries[0] (8B)   | entries[1] (8B)   | ...
 *        | (void * 포인터)    | (void * 포인터)    |
 *
 * 총 크기: 16 + capacity * 8 바이트
 * capacity=16일 때: 16 + 128 = 144 바이트
 *
 * head와 tail의 관계:
 * - head >= tail 항상 성립 (head는 pop에서 증가)
 * - head == tail: 슬롯이 비어 있음
 * - head - tail == capacity: 슬롯이 가득 참 (이론상)
 * - 실제 entries[] 인덱스: value & mask (순환)
 *
 * 단조 증가 설계의 이점:
 * - head와 tail 각각 32비트 → 2^32까지 증가 가능
 * - ABA 문제 방지: 같은 인덱스로 돌아오려면 2^32회 순환 필요
 * - uint32_t 오버플로 시에도 차이 연산(head-tail)은 정확 */

struct objpool_slot {
    uint32_t head;      /* consumer index (pop 시 증가) */
    uint32_t tail;      /* producer index (push 시 증가) */
    uint32_t last;      /* capacity (최대 항목 수) */
    uint32_t mask;      /* capacity - 1 (비트 AND 마스크) */
    void    *entries[];  /* flexible array: 오브젝트 포인터 */
};

/* 핵심 불변식(Invariant):
 * 1) 0 <= (head - tail) <= last
 * 2) entries[head & mask] .. entries[(tail-1) & mask] 이 유효 범위
 * 3) head는 pop에서만 증가, tail은 push에서만 증가
 * 4) 동일 CPU에서 pop은 CAS, push는 WRITE_ONCE (비대칭)
 * 5) cross-CPU steal 시에만 원격 슬롯의 head를 CAS로 전진 */
objpool_slot 순환 버퍼: 물리 메모리와 논리 뷰 물리 메모리 레이아웃 (capacity=8, mask=7) head=13 tail=17 last=8 mask=7 offset 0 offset 16: [0] --- [1] obj7 [2] --- [3] --- [4] --- [5] obj2 [6] obj5 [7] obj9 head&7=5 tail&7=1 논리 뷰: 단조 증가 인덱스 매핑 논리 인덱스: 13 14 15 16 17 물리 인덱스: 13&7=5 14&7=6 15&7=7 16&7=0 17&7=1 head (pop 시작) tail (push 위치) objpool vs mempool_t: 내부 구조 비교 objpool: per-CPU 순환 배열 CPU0 슬롯 CPU1 슬롯 CPU2 슬롯 CPU3 슬롯 lock 없음, CAS만 사용 NMI-safe, per-CPU 캐시 독점 고갈 시 NULL 반환 (비블로킹) 고정 크기, 런타임 확장 불가 mempool_t: 전역 배열 + spinlock spinlock_t lock elements[0..min_nr-1] (전역 배열) spin_lock_irqsave 사용 NMI 불안전 (데드락 위험) 고갈 시 대기(블로킹) 가능 백업 할당자(alloc_fn) 폴백

mempool_t와의 상세 비교: 언제 무엇을 사용해야 하는가

mempool_t(include/linux/mempool.h)과 objpool은 모두 사전 할당 풀이지만, 설계 목표와 사용 컨텍스트가 근본적으로 다릅니다.

비교 항목objpoolmempool_t
설계 목표NMI-safe, lock-free, 최소 지연할당 보장(OOM에서도 성공), 대기 가능
내부 구조per-CPU 순환 배열 (LIFO)전역 배열 + spinlock + 대기 큐
동기화CAS + 메모리 배리어spin_lock_irqsave + wait_queue
NMI 안전성O (핵심 설계 목표)X (spinlock 데드락 위험)
하드 IRQ 안전성O부분적 (할당만, 대기 불가)
고갈 시 동작NULL 반환 (즉시)대기 후 재시도 (GFP_KERNEL) 또는 NULL (GFP_ATOMIC)
백업 할당자없음 (사전 할당만)있음 (alloc_fn으로 새로 할당 시도)
캐시 친화성O (per-CPU, L1 히트)X (전역 배열, 캐시 바운싱)
CPU 확장성O(1) 모든 CPUspinlock 경합으로 CPU 수에 비례하여 저하
메모리 오버헤드높음 (per-CPU 슬롯 * CPU 수)낮음 (단일 배열)
크기 변경X (초기화 후 고정)O (mempool_resize)
주요 용도kprobe/fprobe/rethook블록 I/O, 파일시스템 트랜잭션
/* mempool_t 사용 예시 (비교용) */
static struct kmem_cache *my_cache;
static mempool_t *my_mempool;

/* 초기화: 16개 오브젝트를 사전 할당 */
my_cache = kmem_cache_create("my_obj", 128, 0, 0, NULL);
my_mempool = mempool_create_slab_pool(16, my_cache);

/* 할당: spinlock 획득 → NMI에서 불가 */
void *obj = mempool_alloc(my_mempool, GFP_KERNEL);
/* GFP_KERNEL: 메모리 부족 시 대기 (sleep) 가능
 * GFP_ATOMIC: 대기 불가, 풀 고갈 시 NULL */

/* 해제 */
mempool_free(obj, my_mempool);

/* 정리 */
mempool_destroy(my_mempool);
kmem_cache_destroy(my_cache);

/* 결론:
 * - NMI/하드IRQ에서 안전한 할당이 필요 → objpool
 * - OOM에서도 할당 보장이 필요 → mempool_t
 * - 두 요구사항이 동시에 필요한 경우는 없음
 *   (NMI에서는 대기 불가, 대기가 가능하면 NMI가 아님) */

구현 예제

kretprobe 오브젝트 풀 사용 예제

kretprobe를 등록하고 함수 반환 값을 추적하는 커널 모듈입니다. objpool이 내부적으로 어떻게 사용되는지 보여줍니다.

/* kretprobe_trace.c - kretprobe를 통한 함수 반환값 추적 */
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/slab.h>

/* 사용자 정의 데이터: kretprobe_instance.data[]에 저장 */
struct trace_data {
    ktime_t entry_time;  /* 함수 진입 시각 */
    int     entry_cpu;   /* 진입 시 CPU 번호 */
};

/* 함수 진입 핸들러: objpool_pop이 내부에서 호출됨 */
static int entry_handler(struct kretprobe_instance *ri,
                          struct pt_regs *regs)
{
    struct trace_data *data = (struct trace_data *)ri->data;

    /* 이 시점에서 ri는 이미 objpool에서 pop된 상태입니다.
     * pre_handler_kretprobe → objpool_pop → 이 핸들러 */
    data->entry_time = ktime_get();
    data->entry_cpu  = raw_smp_processor_id();

    return 0;  /* 0: 정상 진행, 음수: 후킹 취소 */
}

/* 함수 반환 핸들러: 이후 objpool_push가 호출됨 */
static int ret_handler(struct kretprobe_instance *ri,
                        struct pt_regs *regs)
{
    struct trace_data *data = (struct trace_data *)ri->data;
    unsigned long retval = regs_return_value(regs);
    s64 delta_ns = ktime_to_ns(
        ktime_sub(ktime_get(), data->entry_time));

    pr_info("do_sys_openat2: ret=%ld, latency=%lldns, "
            "entry_cpu=%d, exit_cpu=%d\n",
            retval, delta_ns,
            data->entry_cpu, raw_smp_processor_id());

    /* 이 핸들러 반환 후 ri는 objpool_push로 풀에 반환됩니다.
     * cross-CPU 반환 가능: entry_cpu != exit_cpu일 수 있음 */
    return 0;
}

static struct kretprobe my_kretprobe = {
    .handler     = ret_handler,
    .entry_handler = entry_handler,
    .data_size   = sizeof(struct trace_data),
    .maxactive   = 0,  /* 0 = 기본값 max(10, 2*num_cpus) */
    .kp.symbol_name = "do_sys_openat2",
};

static int __init trace_init(void)
{
    int ret = register_kretprobe(&my_kretprobe);
    if (ret) {
        pr_err("kretprobe 등록 실패: %d\n", ret);
        return ret;
    }

    /* 이 시점에서 objpool이 초기화되어 있습니다:
     * - maxactive개의 kretprobe_instance가 사전 할당
     * - 각 instance 크기 = sizeof(kretprobe_instance) + sizeof(trace_data)
     * - per-CPU 슬롯에 라운드 로빈으로 분배 */
    pr_info("kretprobe 등록: %s (maxactive=%d)\n",
            my_kretprobe.kp.symbol_name,
            my_kretprobe.maxactive);
    return 0;
}

static void __exit trace_exit(void)
{
    unregister_kretprobe(&my_kretprobe);
    pr_info("kretprobe 해제: nmissed=%lu\n",
            my_kretprobe.nmissed);
    /* unregister_kretprobe 내부에서 objpool_fini가 호출됩니다.
     * 사용 중인 인스턴스가 있으면 반환 후 release 콜백 실행 */
}

module_init(trace_init);
module_exit(trace_exit);
MODULE_LICENSE("GPL");
코드 설명
  • .data_size = sizeof(struct trace_data)각 kretprobe_instance에 추가로 trace_data 크기만큼의 공간이 할당됩니다. 이 공간은 objpool 오브젝트의 일부로 관리됩니다. entry_handler에서 데이터를 저장하고 ret_handler에서 읽습니다.
  • .maxactive = 00으로 설정하면 기본값 max(10, 2 * num_possible_cpus())가 사용됩니다. 4코어 시스템에서 10개, 128코어 시스템에서 256개가 됩니다. 이 값이 objpool의 nr_objs로 전달됩니다.
  • entry_cpu != exit_cpu함수 실행 중에 프로세스 마이그레이션이 발생하면 진입 CPU와 반환 CPU가 다를 수 있습니다. 이 경우 objpool_push는 반환 CPU의 슬롯에 인스턴스를 넣으므로 cross-CPU 반환이 됩니다.

커스텀 고빈도 할당 풀 예제

네트워크 패킷 처리와 같은 고빈도 할당 시나리오에서 objpool을 사용하는 예제입니다. 인터럽트 컨텍스트에서 안전하게 버퍼를 할당/해제합니다.

/* highfreq_pool.c - 고빈도 할당용 커스텀 objpool 예제 */
#include <linux/module.h>
#include <linux/objpool.h>
#include <linux/interrupt.h>
#include <linux/hrtimer.h>

#define POOL_OBJS     64   /* CPU당 16개 기준 (4 CPU) */
#define BUF_SIZE      256  /* 각 버퍼 크기 */

struct pool_context {
    struct objpool_head pool;
    struct hrtimer      timer;
    atomic_long_t      alloc_count;
    atomic_long_t      free_count;
    atomic_long_t      miss_count;
};

struct packet_buf {
    u64  timestamp;
    u16  len;
    u8   cpu;
    u8   flags;
    char data[BUF_SIZE - 12];  /* 패딩 조정 */
};

static struct pool_context *ctx;

/* 오브젝트 초기화 콜백 */
static int buf_init(void *obj, void *context)
{
    memset(obj, 0, sizeof(struct packet_buf));
    return 0;
}

/* 풀 해제 콜백: 컨텍스트 구조체 해제 */
static int pool_release(struct objpool_head *head, void *context)
{
    struct pool_context *pctx = context;
    pr_info("풀 해제: alloc=%ld, free=%ld, miss=%ld\n",
            atomic_long_read(&pctx->alloc_count),
            atomic_long_read(&pctx->free_count),
            atomic_long_read(&pctx->miss_count));
    kfree(pctx);
    return 0;
}

/* 고빈도 할당/해제 시뮬레이션 (hrtimer 콜백, IRQ 컨텍스트) */
static enum hrtimer_restart timer_callback(struct hrtimer *timer)
{
    struct pool_context *pctx =
        container_of(timer, struct pool_context, timer);
    struct packet_buf *buf;

    /* IRQ 컨텍스트에서 objpool_pop 호출: 안전 */
    buf = objpool_pop(&pctx->pool);
    if (!buf) {
        atomic_long_inc(&pctx->miss_count);
        goto restart;
    }

    atomic_long_inc(&pctx->alloc_count);

    /* 버퍼 사용: 타임스탬프와 CPU 기록 */
    buf->timestamp = ktime_get_ns();
    buf->cpu = raw_smp_processor_id();
    buf->len = 64;

    /* 즉시 반환 (실제로는 DMA 완료 후 반환 등) */
    objpool_push(buf, &pctx->pool);
    atomic_long_inc(&pctx->free_count);

restart:
    hrtimer_forward_now(timer, ns_to_ktime(100000));  /* 100us */
    return HRTIMER_RESTART;
}

static int __init highfreq_init(void)
{
    int ret;

    ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;

    /* objpool 초기화: 64개의 packet_buf 사전 할당 */
    ret = objpool_init(&ctx->pool,
                       POOL_OBJS,
                       sizeof(struct packet_buf),
                       GFP_KERNEL,
                       ctx,
                       buf_init,
                       pool_release);
    if (ret) {
        kfree(ctx);
        return ret;
    }

    /* hrtimer 시작: 100us 간격으로 pop/push 반복 */
    hrtimer_init(&ctx->timer, CLOCK_MONOTONIC,
                 HRTIMER_MODE_REL);
    ctx->timer.function = timer_callback;
    hrtimer_start(&ctx->timer,
                  ns_to_ktime(100000),
                  HRTIMER_MODE_REL);

    pr_info("고빈도 풀 시작: %d개 오브젝트, %zuB/obj\n",
            POOL_OBJS,
            sizeof(struct packet_buf));
    return 0;
}

static void __exit highfreq_exit(void)
{
    hrtimer_cancel(&ctx->timer);
    /* objpool_fini: 모든 오브젝트 반환 후 pool_release 호출
     * pool_release에서 ctx를 kfree하므로 이후 ctx 접근 불가 */
    objpool_fini(&ctx->pool);
}

module_init(highfreq_init);
module_exit(highfreq_exit);
MODULE_LICENSE("GPL");
코드 설명
  • hrtimer 콜백hrtimer 콜백은 하드 IRQ 컨텍스트에서 실행됩니다. kmalloc(GFP_ATOMIC)은 실패할 수 있지만, objpool_pop은 사전 할당된 오브젝트를 반환하므로 메모리 압박의 영향을 받지 않습니다.
  • POOL_OBJS = 644 CPU 시스템에서 CPU당 16개의 오브젝트를 확보합니다. hrtimer가 100us마다 1개씩 사용하므로, pop-push 사이의 시간이 극히 짧아 고갈될 가능성이 거의 없습니다.
  • container_ofhrtimer 구조체의 주소에서 pool_context의 주소를 역산합니다. 이 패턴은 커널에서 콜백 함수에 컨텍스트를 전달하는 표준 방법입니다.
  • hrtimer_cancel 후 objpool_fini먼저 타이머를 취소하여 새로운 pop이 발생하지 않도록 한 뒤 objpool_fini를 호출합니다. 이 순서가 중요합니다. 타이머를 먼저 취소하지 않으면 fini 후에도 pop이 발생할 수 있습니다.
실무 참고: objpool은 커널 내부 API이므로 안정적인 ABI가 보장되지 않습니다. 실제 네트워크 드라이버에서는 page_pool이나 napi_alloc_skb 등 네트워크 전용 풀링 메커니즘을 사용하는 것이 적절합니다. 위 예제는 objpool의 IRQ 안전성과 고빈도 특성을 보여주기 위한 학습용입니다.

참고 자료