IDR/IDA (ID Allocator)

Linux 커널의 정수 ID 할당 메커니즘인 IDR(ID Radix tree)과 IDA(ID Allocator)의 설계 원리, API 사용법, 파일 디스크립터/PID/디바이스 번호 할당 등 실제 활용 사례를 다룹니다. 현재는 XArray 기반으로 재구현되었지만 레거시 API가 광범위하게 사용됩니다.

일상 비유: IDR은 번호표 발급기와 비슷합니다. 은행에서 순서대로 번호를 받고, 번호와 고객 정보를 매핑하며, 사용 후 번호를 반납하면 재사용됩니다.

핵심 요약

  • ID 할당 — 정수 ID를 자동으로 할당하고 포인터와 매핑합니다.
  • 빠른 검색 — ID로 O(log n) 시간에 포인터를 찾습니다.
  • 범위 지정 — 최소/최대 ID를 지정하여 특정 범위에서만 할당 가능합니다.
  • 자동 재사용 — 해제된 ID는 자동으로 재사용됩니다.
  • 두 가지 변형 — IDR(포인터 저장), IDA(비트맵만)
전제 조건: XArray 문서를 먼저 읽으세요. IDR/IDA는 내부적으로 XArray 위에 구현되어 있으므로, XArray의 저장/조회/잠금 모델을 이해하면 IDR의 동작을 더 깊이 파악할 수 있습니다.

단계별 이해

  1. IDR vs IDA 선택
    ID와 포인터를 함께 매핑해야 하면 IDR(idr_alloc/idr_find), ID만 필요하면 IDA(ida_alloc/ida_free)를 선택합니다. 새 코드라면 XArray 직접 사용도 고려합니다.
  2. 할당/해제 대칭성 확인
    모든 idr_alloc 성공 경로에 대응하는 idr_remove가 있는지 확인합니다. 오류 경로에서 누락하면 ID 고갈이 발생합니다.
  3. 범위와 동시성 설계
    할당 시 start/end 범위를 시스템 정책(RLIMIT_NOFILE 등)에 맞추고, 동시 접근 시 잠금/RCU 전략을 결정합니다.

개요 (Overview)

IDR(ID Radix tree)과 IDA(ID Allocator)는 정수 ID를 효율적으로 관리하는 자료구조입니다:

ℹ️

역사: IDR은 원래 radix tree로 구현되었으나, Linux 4.20부터 XArray로 재구현되었습니다. 하지만 기존 API는 호환성을 위해 유지되며, 커널 전체에서 여전히 광범위하게 사용됩니다.

IDR 내부 구조

IDR은 현재 XArray를 기반으로 구현되어 있으며, ID를 인덱스로 사용하는 희소 배열입니다:

IDR 내부 XArray 기반 구조 IDR 내부 구조 (XArray 기반) struct idr idr_rt: XArray idr_base: 0 idr_next: 3 (힌트) XArray로 위임 XArray 희소 배열 (Sparse Array) Level 0 (루트) xa_head (노드 포인터) Level 1 (내부 노드) Slot 0 ID 0-63 → xa_node Slot 1 ID 64-127 NULL ... Level 2 (리프 슬롯) ID: 0 → struct device* (ptr1: 0xffff...) ID: 1 → struct device* (ptr2: 0xffff...) ID: 2 NULL (해제됨) xa_node 내부 slot들 IDR 동작 원리 idr_alloc(idr, ptr, 0, 100, GFP_KERNEL) → XArray에서 0-99 범위의 빈 슬롯 검색 → 첫 번째 빈 ID 반환 (예: 2, ID 2가 해제 상태) → xa_store(&idr→idr_rt, 2, ptr, GFP_KERNEL) 호출 idr_find(idr, 1) → xa_load(&idr→idr_rt, 1) → ptr2 반환 idr_remove(idr, 2) → xa_erase(&idr→idr_rt, 2) → ID 2 재사용 가능

IDR API

기본 사용법

/* include/linux/idr.h */
struct idr {
    struct xarray idr_rt;
    unsigned int idr_base;
    unsigned int idr_next;
};

/* 초기화 */
DEFINE_IDR(my_idr);  /* 정적 */
void idr_init(struct idr *idr);  /* 동적 */

/* ID 할당 및 포인터 저장 */
int idr_alloc(struct idr *idr, void *ptr,
                int start, int end, gfp_t gfp);
/* 반환: 할당된 ID (음수는 에러) */
/* start: 최소 ID, end: 최대 ID (exclusive) */

/* ID로 포인터 찾기 */
void *idr_find(struct idr *idr, unsigned long id);

/* ID 제거 */
void *idr_remove(struct idr *idr, unsigned long id);

/* 전체 해제 */
void idr_destroy(struct idr *idr);

사용 예제

/* 간단한 IDR 사용 예 */
DEFINE_IDR(device_idr);

struct device {
    int id;
    char name[32];
};

/* 디바이스 등록 */
int register_device(struct device *dev)
{
    int id;

    /* ID 0-1000 범위에서 할당 */
    id = idr_alloc(&device_idr, dev, 0, 1000, GFP_KERNEL);
    if (id < 0)
        return id;

    dev->id = id;
    pr_info("Device registered with ID %d\n", id);
    return 0;
}

/* 디바이스 검색 */
struct device *find_device(int id)
{
    return idr_find(&device_idr, id);
}

/* 디바이스 제거 */
void unregister_device(int id)
{
    struct device *dev = idr_remove(&device_idr, id);
    if (dev) {
        pr_info("Device %d unregistered\n", id);
        kfree(dev);
    }
}

IDR 순회

/* 모든 엔트리 순회 */
int id;
struct device *dev;

idr_for_each_entry(&device_idr, dev, id) {
    pr_info("Device %d: %s\n", id, dev->name);
}

/* 특정 ID부터 순회 */
int start_id = 100;
idr_for_each_entry_continue(&device_idr, dev, start_id) {
    /* ... */
}

IDA API

IDA는 포인터 저장 없이 ID만 할당합니다. 메모리 효율적입니다:

/* include/linux/idr.h */
struct ida {
    struct xarray xa;
};

/* 초기화 */
DEFINE_IDA(my_ida);
void ida_init(struct ida *ida);

/* ID 할당 */
int ida_alloc(struct ida *ida, gfp_t gfp);
int ida_alloc_range(struct ida *ida, unsigned int min,
                       unsigned int max, gfp_t gfp);

/* ID 해제 */
void ida_free(struct ida *ida, unsigned int id);

/* 전체 해제 */
void ida_destroy(struct ida *ida);

/* 사용 예 */
DEFINE_IDA(minor_ida);

int alloc_minor(void)
{
    return ida_alloc_range(&minor_ida, 0, 255, GFP_KERNEL);
}

void free_minor(int minor)
{
    ida_free(&minor_ida, minor);
}

ID 할당/해제 과정

IDR에서 ID를 할당하고 해제할 때 내부 상태가 어떻게 변하는지 시각화:

IDR ID 할당/해제 과정 시각화 ID 할당/해제 과정 (5단계) Step 1: 초기 상태 DEFINE_IDR(dev_idr) idr_next: 0 XArray: 비어 있음 Step 2: idr_alloc(dev1) dev_idr idr_next: 1 ID 0 → dev1* xa_store(0, dev1) Step 3: idr_alloc(dev2, dev3) dev_idr idr_next: 3 ID 0 → dev1* ID 1 → dev2* ID 2 → dev3* Step 4: idr_remove(1) dev_idr idr_next: 3 ID 0 → dev1* ID 1 → NULL (제거) ID 2 → dev3* Step 5: idr_alloc(dev4) dev_idr (빈 ID 1 자동 재사용) idr_next: 3 ID 0 → dev1* ID 1 → dev4* (재사용) ID 2 → dev3* 자동 재사용 원리 • idr_alloc()은 빈 슬롯을 자동으로 찾아 재사용 • 메모리 단편화 방지 • O(log n) 검색 • XArray가 내부 관리

실사용 사례

파일 디스크립터

파일 디스크립터 할당은 IDR의 가장 중요한 사용 사례입니다. 범위 제약이 있는 예제:

/* fs/file.c */
struct files_struct {
    struct fdtable __rcu *fdt;
    unsigned long open_fds_init[1];  /* 초기 비트맵 */
    struct file __rcu *fd_array[NR_OPEN_DEFAULT];
    /* ... */
};

/* 프로세스별 파일 디스크립터 제한 */
int get_unused_fd_flags(unsigned flags)
{
    return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
}

/* __alloc_fd의 간소화된 구현 */
int __alloc_fd(struct files_struct *files,
               unsigned start, unsigned end, unsigned flags)
{
    unsigned int fd;
    struct fdtable *fdt;

    fdt = files_fdtable(files);

    /* start부터 end 범위에서 빈 fd 검색 */
    fd = start;
    while (fd < end) {
        if (!test_bit(fd, fdt->open_fds))
            break;
        fd++;
    }

    if (fd >= end)
        return -EMFILE;  /* 파일 디스크립터 고갈 */

    __set_open_fd(fd, fdt);
    return fd;
}
ℹ️

범위 제약 사례:

  • 표준 입출력 (0-2) — stdin/stdout/stderr는 항상 0, 1, 2번
  • 일반 파일 (3-1023) — 대부분의 프로세스 기본 제한
  • RLIMIT_NOFILE — ulimit -n으로 설정 (기본: 1024)
  • NR_OPEN (시스템 최대) — /proc/sys/fs/nr_open (기본: 1048576)

범위 지정 실전 예제

/* 특정 범위에서 fd 할당 예제 */

/* 예제 1: 100-200 범위에서 할당 */
int fd = __alloc_fd(current->files, 100, 200, 0);
if (fd < 0) {
    pr_err("범위 100-200에서 fd 할당 실패\n");
} else {
    pr_info("할당된 fd: %d\n", fd);
}

/* 예제 2: 0부터 시스템 제한까지 */
int fd = get_unused_fd_flags(O_CLOEXEC);
/* O_CLOEXEC: exec 시 자동으로 닫힘 */

/* 예제 3: dup2()로 특정 fd 강제 할당 */
int new_fd = 10;
if (dup2(old_fd, new_fd) < 0) {
    perror("dup2");
}

디바이스 번호

/* drivers/base/devtmpfs.c */
static DEFINE_IDA(devtmpfs_ida);

int alloc_chrdev_region(dev_t *dev, unsigned baseminor,
                        unsigned count, const char *name)
{
    int ret = ida_alloc_range(&devtmpfs_ida, baseminor,
                                 baseminor + count - 1, GFP_KERNEL);
    if (ret < 0)
        return ret;

    *dev = MKDEV(ret, baseminor);
    return 0;
}

PID 할당

/* kernel/pid.c */
struct pid_namespace {
    struct idr idr;
    /* ... */
};

struct pid *alloc_pid(struct pid_namespace *ns)
{
    struct pid *pid;
    int nr;

    pid = kmem_cache_alloc(pid_cachep, GFP_KERNEL);
    if (!pid)
        return NULL;

    nr = idr_alloc(&ns->idr, pid, 1, pid_max, GFP_KERNEL);
    if (nr < 0) {
        kmem_cache_free(pid_cachep, pid);
        return NULL;
    }

    pid->numbers[0].nr = nr;
    return pid;
}

IDR vs IDA 비교

특징 IDR IDA
포인터 저장 ✓ (ID → 포인터 매핑) ✗ (ID만 할당)
메모리 사용 더 많음 더 적음 (비트맵)
검색 idr_find() 불가능
적합한 용도 파일 디스크립터, IPC ID 디바이스 번호, 마이너 번호
순회 idr_for_each_entry 불가능

선택 기준 플로우차트

IDR과 IDA 중 어떤 것을 사용할지 결정하는 플로우차트:

IDR vs IDA 선택 의사결정 플로우차트 IDR vs IDA 선택 가이드 정수 ID 할당 필요 ID와 포인터를 함께 관리해야 하는가? YES ✓ IDR 사용 • idr_alloc() / idr_find() • ID → 포인터 매핑 • idr_for_each_entry 순회 새 코드라면 XArray 직접 권장 NO 포인터 없이 ID만 관리하면 충분한가? YES ✓ IDA 사용 • ida_alloc() / ida_free() • 메모리 효율적 (비트맵) NO XArray 직접 사용 • xa_alloc() / xa_load() • 새 코드에서 권장 (레거시 호환 불필요 시) 실사용 사례 IDR: 파일 디스크립터(fd → file*), IPC ID(msgid → msg_queue*), PID(nr → struct pid*) IDA: 디바이스 마이너 번호, 블록 디바이스 ID, 네트워크 인터페이스 인덱스
💡

간단한 선택 기준: ID로 객체를 찾아야 한다면 IDR, ID만 필요하다면 IDA를 사용하세요.

성능 특성

연산 시간 복잡도 설명
할당 (idr_alloc) O(log n) XArray 기반
검색 (idr_find) O(log n) 빠른 탐색
제거 (idr_remove) O(log n) 트리 재구성
순회 O(n) 모든 엔트리 방문
💡

최적화: IDR은 XArray 기반으로 재구현되어 이전 radix tree 구현보다 메모리 사용량이 40% 감소하고 성능이 20% 향상되었습니다.

XArray로 마이그레이션

새로운 코드에서는 IDR 대신 XArray를 직접 사용하는 것이 권장됩니다:

/* 기존 IDR 코드 */
DEFINE_IDR(my_idr);
int id = idr_alloc(&my_idr, ptr, 0, INT_MAX, GFP_KERNEL);
void *ptr = idr_find(&my_idr, id);
idr_remove(&my_idr, id);

/* XArray로 변환 */
DEFINE_XARRAY_ALLOC(my_xarray);
int id;
xa_alloc(&my_xarray, &id, ptr, XA_LIMIT(0, INT_MAX), GFP_KERNEL);
void *ptr = xa_load(&my_xarray, id);
xa_erase(&my_xarray, id);

IDR/IDA 운영 루틴

ID 할당기는 누수와 중복 할당이 가장 큰 리스크입니다. 할당/조회/해제의 대칭성을 코드 리뷰 기준으로 고정해야 합니다.

  1. 할당 실패 처리: idr_alloc/ida_alloc 반환값 검사
  2. 해제 대칭성: 모든 성공 경로에 대응하는 idr_remove/ida_free 확인
  3. 범위 정책: 최소/최대 ID 범위를 시스템 정책과 일치시킴
  4. 마이그레이션 판단: 신규 경로는 가능하면 XArray 직접 사용 고려
장애 유형원인 후보대응
ID 고갈해제 누락오류 경로 포함 해제 대칭성 점검
잘못된 포인터 조회ID 재사용 타이밍 경합락/RCU 정책 재검토
범위 밖 할당limit 설정 누락alloc 호출 시 범위 인자 고정