Hash Table 심화 (Kernel Hash Table In-Depth)

Linux 커널의 해시 테이블 구현 전반을 다룹니다. hlist 자료구조, <linux/hashtable.h> API, 리사이즈 가능한 rhashtable, 커널 해시 함수(hash_long, jhash), RCU 보호 해시 테이블, 그리고 conntrack·inode·pid 등 실제 커널 활용 사례까지 종합적으로 설명합니다.

개요 (Overview)

해시 테이블(Hash Table)은 키(key)를 해시 함수로 변환하여 버킷(bucket)에 매핑하는 자료구조입니다. 평균 O(1) 시간에 검색·삽입·삭제가 가능하여, 대량의 객체를 빠르게 룩업해야 하는 커널 서브시스템 전반에서 핵심적으로 사용됩니다.

리눅스 커널에서 해시 테이블이 사용되는 대표적인 영역:

커널은 두 가지 주요 해시 테이블 구현을 제공합니다:

커널 해시 테이블은 체이닝(chaining) 방식으로 충돌을 해결합니다. 각 버킷은 hlist(hash list)로 구현된 단일 연결 리스트이며, 이는 메모리 효율성과 구현 단순성을 모두 달성합니다.

hlist 구조 (hlist_head / hlist_node)

해시 테이블의 각 버킷은 struct hlist_head로 표현되고, 체인 내의 각 노드는 struct hlist_node입니다. 일반적인 list_head와 달리 hlist는 비원형(non-circular) 단방향 리스트로 설계되었습니다.

/* include/linux/types.h */
struct hlist_head {
    struct hlist_node *first;
};

struct hlist_node {
    struct hlist_node *next;
    struct hlist_node **pprev;  /* 이전 노드의 next 포인터를 가리킴 */
};

왜 pprev는 이중 포인터인가?

hlist_nodepprevhlist_node *가 아닌 hlist_node **인 이유는 두 가지 핵심적인 이점 때문입니다:

/* hlist 노드 삭제 — 위치에 관계없이 동일한 코드 */
static inline void __hlist_del(struct hlist_node *n)
{
    struct hlist_node *next = n->next;
    struct hlist_node **pprev = n->pprev;

    WRITE_ONCE(*pprev, next);      /* 이전 → 다음으로 연결 */
    if (next)
        WRITE_ONCE(next->pprev, pprev);
}

hlist vs list_head 비교

특성list_headhlist_head/hlist_node
구조원형 이중 연결비원형 단방향(pprev 이중 포인터)
헤드 크기포인터 2개 (16바이트/x86_64)포인터 1개 (8바이트/x86_64)
노드 크기포인터 2개포인터 2개
tail 접근O(1)O(n)
빈 상태 판단head->next == headhead->first == NULL
주요 용도범용 리스트해시 테이블 버킷

주요 hlist 매크로/함수

/* 초기화 */
HLIST_HEAD(name);                          /* 정적 선언 + 초기화 */
HLIST_HEAD_INIT                             /* { .first = NULL } */
INIT_HLIST_HEAD(&head);                     /* 동적 초기화 */
INIT_HLIST_NODE(&node);                     /* 노드 초기화 */

/* 삽입/삭제 */
hlist_add_head(&node, &head);               /* 헤드 앞에 삽입 */
hlist_add_before(&node, &next_node);        /* 특정 노드 앞에 삽입 */
hlist_add_behind(&node, &prev_node);        /* 특정 노드 뒤에 삽입 */
hlist_del(&node);                            /* 삭제 */
hlist_del_init(&node);                      /* 삭제 후 초기화 */

/* 순회 */
hlist_for_each(pos, head)                    /* hlist_node* 순회 */
hlist_for_each_safe(pos, n, head)            /* 삭제 안전 순회 */
hlist_for_each_entry(obj, head, member)      /* 구조체 순회 */
hlist_for_each_entry_safe(obj, n, head, member) /* 삭제 안전 구조체 순회 */
hlist_entry(ptr, type, member)               /* container_of wrapper */

커널 해시 함수 (<linux/hash.h>)

좋은 해시 테이블 성능의 핵심은 해시 함수의 품질입니다. 커널은 다양한 용도에 맞는 여러 해시 함수를 제공합니다.

Golden Ratio 해시 (hash_long / hash_32 / hash_64)

가장 기본적인 커널 해시 함수로, 정수 키를 해싱할 때 사용합니다. Fibonacci Hashing이라고도 불리며, 황금비(Golden Ratio)에서 유도된 상수를 곱합니다.

/* include/linux/hash.h */

/* 2^32 / phi = 2^32 * (sqrt(5)-1)/2 */
#define GOLDEN_RATIO_32  0x61C88647

/* 2^64 / phi */
#define GOLDEN_RATIO_64  0x61C8864680B583EBull

static inline u32 hash_64(u64 val, unsigned int bits)
{
    return (u32)(val * GOLDEN_RATIO_64) >> (64 - bits);
}

static inline u32 hash_32(u32 val, unsigned int bits)
{
    return (val * GOLDEN_RATIO_32) >> (32 - bits);
}

/* 아키텍처 워드 크기에 맞는 해시 */
#if BITS_PER_LONG == 64
#define hash_long(val, bits)  hash_64((u64)(val), bits)
#else
#define hash_long(val, bits)  hash_32((u32)(val), bits)
#endif
💡

bits 파라미터는 해시 테이블의 버킷 수를 2의 거듭제곱으로 표현한 지수입니다. 예를 들어 1024개 버킷이면 bits = 10입니다. 결과값은 항상 0 ~ (2^bits - 1) 범위입니다.

포인터 해싱 (hash_ptr)

static inline u32 hash_ptr(const void *ptr, unsigned int bits)
{
    return hash_long((unsigned long)ptr, bits);
}

Jenkins Hash (jhash)

가변 길이 데이터(문자열, 구조체 등)를 해싱할 때 사용합니다. Bob Jenkins가 설계한 해시 함수로, <linux/jhash.h>에 정의됩니다. 주로 네트워킹 서브시스템에서 튜플(src IP, dst IP, src port, dst port) 해싱에 사용됩니다.

/* include/linux/jhash.h */

/* 가변 길이 바이트 배열 해싱 */
u32 jhash(const void *key, u32 length, u32 initval);

/* 2개 word 해싱 (빠름) */
u32 jhash_2words(u32 a, u32 b, u32 initval);

/* 3개 word 해싱 */
u32 jhash_3words(u32 a, u32 b, u32 c, u32 initval);

/* 1개 word 해싱 */
u32 jhash_1word(u32 a, u32 initval);

네트워크 튜플 해싱 예시 (conntrack):

static u32 hash_conntrack_raw(const struct nf_conntrack_tuple *tuple,
                              unsigned int zoneid,
                              const struct net *net)
{
    unsigned int n;
    u32 seed;

    get_random_once(&nf_conntrack_hash_rnd, sizeof(nf_conntrack_hash_rnd));
    seed = nf_conntrack_hash_rnd ^ zoneid;

    n = (sizeof(tuple->src) + sizeof(tuple->dst.u3)) / sizeof(u32);
    return jhash2((u32 *)tuple, n,
                  seed ^ (u32)(unsigned long)net ^
                  ((__force __u16)tuple->dst.u.all << 16) |
                  tuple->dst.protonum);
}

기타 해시 함수

함수헤더용도
xxhash32() / xxhash64()<linux/xxhash.h>고성능 범용 해시, btrfs 체크섬 등
full_name_hash()<linux/namei.h>dcache 파일명 해싱
hashlen_string()<linux/stringhash.h>문자열 해시 + 길이 동시 반환
arch_fast_hash()아키텍처별CRC32c 등 하드웨어 가속 해시
siphash()<linux/siphash.h>HashDoS 방어용 보안 해시 (net_hash_mix)

보안 주의: 사용자 제어 가능한 키(네트워크 패킷 등)를 해싱할 때는 jhash에 랜덤 initval을 사용하거나, siphash를 사용하여 HashDoS(해시 충돌 공격)를 방어해야 합니다. hash_long은 정수 키 전용이며 보안 해시가 아닙니다.

<linux/hashtable.h> API

커널 3.7에서 도입된 <linux/hashtable.h>는 고정 크기 해시 테이블을 위한 간결한 매크로 세트를 제공합니다. 내부적으로 hlist_head 배열과 hash_long()을 사용합니다.

선언과 초기화

#include <linux/hashtable.h>

/* 정적 해시 테이블 선언 (2^bits 버킷) */
DEFINE_HASHTABLE(my_ht, 10);  /* 1024 버킷 */

/* 또는 동적으로 */
struct hlist_head my_ht[1 << 10];
hash_init(my_ht);  /* 모든 버킷을 NULL로 초기화 */

DEFINE_HASHTABLE(name, bits)는 다음과 동일합니다:

struct hlist_head name[1 << (bits)] =
    { [0 ... ((1 << (bits)) - 1)] = HLIST_HEAD_INIT };

삽입, 삭제, 검색

struct my_object {
    int key;
    char *data;
    struct hlist_node hash_node;
};

/* 삽입: key를 해시하여 버킷에 추가 */
struct my_object *obj = kmalloc(sizeof(*obj), GFP_KERNEL);
obj->key = 42;
obj->data = "hello";
hash_add(my_ht, &obj->hash_node, obj->key);

/* 삭제 */
hash_del(&obj->hash_node);

/* 검색: 특정 key와 동일한 버킷의 모든 노드를 순회 */
struct my_object *cur;
int search_key = 42;
hash_for_each_possible(my_ht, cur, hash_node, search_key) {
    if (cur->key == search_key) {
        pr_info("found: %s\n", cur->data);
        break;
    }
}

hash_for_each_possible()은 동일 버킷의 모든 노드를 순회합니다. 해시 충돌이 있을 수 있으므로 반드시 키 값을 직접 비교하여 원하는 객체인지 확인해야 합니다.

전체 API 레퍼런스

매크로/함수설명
DEFINE_HASHTABLE(name, bits)정적 해시 테이블 선언 (2^bits 버킷)
hash_init(ht)모든 버킷을 빈 상태로 초기화
hash_add(ht, node, key)key를 해시하여 해당 버킷에 node 삽입
hash_del(node)노드 삭제
hash_del_init(node)노드 삭제 후 초기화 (재삽입 가능)
hash_for_each(ht, bkt, obj, member)전체 테이블 순회 (모든 버킷)
hash_for_each_safe(ht, bkt, tmp, obj, member)삭제 안전 전체 순회
hash_for_each_possible(ht, obj, member, key)특정 key의 버킷만 순회
hash_for_each_possible_safe(ht, obj, tmp, member, key)삭제 안전 버킷 순회
hash_empty(ht)테이블이 비어있는지 확인

완전한 사용 예제

#include <linux/module.h>
#include <linux/hashtable.h>
#include <linux/slab.h>

/* 8-bit hash → 256 버킷 */
DEFINE_HASHTABLE(pid_cache, 8);

struct pid_entry {
    pid_t               pid;
    char                comm[TASK_COMM_LEN];
    struct hlist_node   hash_node;
};

static void add_pid(pid_t pid, const char *comm)
{
    struct pid_entry *entry;

    entry = kmalloc(sizeof(*entry), GFP_KERNEL);
    if (!entry)
        return;

    entry->pid = pid;
    strscpy(entry->comm, comm, TASK_COMM_LEN);

    hash_add(pid_cache, &entry->hash_node, pid);
}

static struct pid_entry *find_pid(pid_t pid)
{
    struct pid_entry *entry;

    hash_for_each_possible(pid_cache, entry, hash_node, pid) {
        if (entry->pid == pid)
            return entry;
    }
    return NULL;
}

static void remove_pid(pid_t pid)
{
    struct pid_entry *entry = find_pid(pid);
    if (entry) {
        hash_del(&entry->hash_node);
        kfree(entry);
    }
}

static void dump_all(void)
{
    struct pid_entry *entry;
    unsigned int bkt;

    hash_for_each(pid_cache, bkt, entry, hash_node)
        pr_info("[bucket %u] pid=%d comm=%s\n",
                bkt, entry->pid, entry->comm);
}

static void cleanup_all(void)
{
    struct pid_entry *entry;
    struct hlist_node *tmp;
    unsigned int bkt;

    hash_for_each_safe(pid_cache, bkt, tmp, entry, hash_node) {
        hash_del(&entry->hash_node);
        kfree(entry);
    }
}

rhashtable (Resizable Hash Table)

<linux/rhashtable.h>는 커널 3.17(Thomas Graf 제안, commit 7e1e7763)에서 도입된 자동 리사이즈 해시 테이블입니다. 고정 크기 hashtable.h와 달리, 요소 수에 따라 버킷 배열을 동적으로 확장/축소하여 로드 팩터를 일정 범위로 유지합니다. 네트워킹 서브시스템의 고성능 요구사항에서 탄생했으며, 현재는 conntrack, nftables, netlabel, TIPC, IMA 등 커널 전반에서 사용됩니다.

rhashtable의 핵심 특징:

내부 구조 (Internal Architecture)

rhashtable은 세 가지 핵심 구조체로 구성됩니다:

/* include/linux/rhashtable-types.h */

/* 해시 테이블 전체를 대표하는 구조체 */
struct rhashtable {
    struct bucket_table __rcu  *tbl;       /* 현재 버킷 테이블 (RCU 보호) */
    unsigned int               key_len;   /* 키 바이트 길이 */
    unsigned int               max_elems; /* 최대 요소 수 (0=무제한) */
    struct rhashtable_params    p;         /* 파라미터 복사본 */
    bool                       rhlist;    /* rhltable 모드 여부 */
    struct work_struct         run_work;  /* 비동기 리사이즈 work */
    struct mutex               mutex;     /* 리사이즈 직렬화 */
    spinlock_t                 lock;      /* 리사이즈 상태 보호 */
    atomic_t                   nelems;    /* 현재 요소 수 */
};

/* 버킷 배열 + 메타데이터 */
struct bucket_table {
    unsigned int               size;       /* 버킷 수 (항상 2의 거듭제곱) */
    unsigned int               nest;       /* 중첩 테이블 깊이 */
    unsigned int               hash_rnd;   /* 랜덤 해시 시드 */
    struct list_head           walkers;    /* 활성 워커 리스트 */
    struct rcu_head            rcu;        /* RCU 해제용 */
    struct bucket_table __rcu  *future_tbl; /* 리사이즈 중 새 테이블 */
    lockdep_map_p              dep_map;    /* lockdep 추적 */
    struct rhash_lock_head __rcu *buckets[] __counted_by(size);
};

/* 각 요소에 내장되는 해시 노드 */
struct rhash_head {
    struct rhash_head __rcu    *next;     /* 체인 내 다음 노드 */
};

bucket_tablebuckets[]Flexible Array Member로 선언되어 있어, 테이블 생성 시 kvmalloc(sizeof(*tbl) + nbuckets * sizeof(tbl->buckets[0]))로 한 번에 할당됩니다. 이는 별도의 포인터 배열을 할당하는 것보다 캐시 지역성이 좋습니다.

Nulls 마커와 테이블 전환 감지

rhashtable은 일반 hlist가 아닌 hlist_nulls 기반 체인을 사용합니다. 체인의 끝(NULL)에 버킷 인덱스를 인코딩한 nulls 마커를 배치하여, 리사이즈 중 읽기 측에서 자신이 올바른 버킷을 순회하고 있는지 검증할 수 있습니다.

/*
 * Nulls 마커 원리:
 *
 * 체인 끝이 단순 NULL이 아닌 (bucket_index << 1) | 1 값.
 * 읽기 측이 체인을 순회한 후, 끝의 nulls 값이
 * 자신이 시작한 버킷과 일치하는지 확인.
 * 불일치하면 리사이즈로 인해 요소가 이동된 것이므로 재시도.
 */

/* 읽기 측 패턴 (내부 구현) */
restart:
    head = rht_bucket(tbl, hash);
    rht_for_each_rcu(he, head) {
        if (rht_key_matches(params, he, key))
            return he;
    }

    /* nulls 마커 검증 */
    if (get_nulls_value(he) != hash)
        goto restart;  /* 버킷이 바뀌었으므로 재시도 */

per-bucket 잠금 전략

rhashtable의 쓰기 연산은 버킷 단위 spinlock으로 직렬화됩니다. 각 버킷의 첫 번째 포인터에 잠금 비트를 인코딩하는 bit-spin-lock 기법을 사용합니다. 이는 별도의 lock 배열 없이 버킷 포인터의 최하위 비트(LSB)를 활용하는 방식입니다.

/*
 * Per-bucket locking 구조:
 *
 * buckets[i]는 struct rhash_lock_head* 타입.
 * 포인터의 bit 0을 spinlock으로 사용:
 *   bit 0 = 0 → 잠금 해제 (unlocked)
 *   bit 0 = 1 → 잠금 설정 (locked)
 *
 * 장점:
 *   - 별도 lock 배열 불필요 → 메모리 절약
 *   - 캐시 라인을 데이터와 공유 → 미스 감소
 *   - 서로 다른 버킷의 삽입/삭제는 완전 병렬
 */

/* 내부 잠금 헬퍼 (lib/rhashtable.c) */
static inline void rht_lock(struct bucket_table *tbl,
                            struct rhash_lock_head __rcu **bkt)
{
    local_bh_disable();
    bit_spin_lock(0, (unsigned long *)bkt);
    lock_map_acquire(&tbl->dep_map);
}

static inline void rht_unlock(struct bucket_table *tbl,
                              struct rhash_lock_head __rcu **bkt)
{
    lock_map_release(&tbl->dep_map);
    bit_spin_unlock(0, (unsigned long *)bkt);
    local_bh_enable();
}
💡

local_bh_disable()가 호출되는 이유: rhashtable은 softirq 컨텍스트(예: 네트워크 수신 경로)에서도 사용되므로, 데드락을 방지하기 위해 하단 반쪽(bottom half)을 비활성화합니다.

rhashtable_params 상세

rhashtable_params는 해시 테이블의 동작 방식을 결정하는 구성 구조체입니다. rhashtable_init()에 전달하면 내부에 복사되어 테이블 수명 동안 유지됩니다.

struct rhashtable_params {
    u16                  nelem_hint;          /* 초기 요소 수 힌트 (초기 버킷 수 계산) */
    u16                  key_len;             /* 키 바이트 길이 (obj_hashfn 사용 시 0) */
    u16                  key_offset;          /* 구조체 내 키 시작 오프셋 */
    u16                  head_offset;         /* 구조체 내 rhash_head 오프셋 */
    unsigned int         max_size;            /* 최대 버킷 수 (0=무제한) */
    u16                  min_size;            /* 최소 버킷 수 */
    bool                 automatic_shrinking; /* 자동 축소 활성화 */
    rht_hashfn_t         hashfn;              /* 키 → 해시 함수 (기본: jhash) */
    rht_obj_hashfn_t     obj_hashfn;          /* 객체 → 해시 함수 (복합 키용) */
    rht_obj_cmpfn_t      obj_cmpfn;           /* 커스텀 비교 함수 (선택) */
};
필드필수기본값설명
head_offset필수-구조체 내 rhash_head 위치. offsetof()로 지정
key_offset조건부-키 위치. obj_hashfn 미사용 시 필수
key_len조건부-키 바이트 크기. obj_hashfn 미사용 시 필수
hashfn선택jhashu32 (*)(const void *key, u32 len, u32 seed). 키 데이터를 해시
obj_hashfn선택-u32 (*)(const void *obj, u32 len, u32 seed). 복합 키 등 객체에서 직접 해시 계산
obj_cmpfn선택memcmpint (*)(struct rhashtable_compare_arg *, const void *obj). 키 일치 판단
nelem_hint선택0예상 요소 수. 초기 버킷 수를 최적화 (nelem_hint/load_factor)
min_size선택64축소 시 최소 버킷 수
max_size선택0확장 시 최대 버킷 수. 0이면 2^31까지 허용
automatic_shrinking선택false요소 삭제 시 자동 축소 여부

기본 사용 예:

#include <linux/rhashtable.h>

struct my_entry {
    u32                    key;
    char                   value[64];
    struct rhash_head      rhash;   /* rhashtable 노드 */
};

static const struct rhashtable_params my_rht_params = {
    .head_offset     = offsetof(struct my_entry, rhash),
    .key_offset      = offsetof(struct my_entry, key),
    .key_len         = sizeof(u32),
    .automatic_shrinking = true,
    .nelem_hint      = 1024,          /* 초기 요소 수 힌트 */
    .min_size        = 256,           /* 최소 버킷 수 */
    .max_size        = 0,             /* 0 = 제한 없음 */
};

복합 키 사용 예 (obj_hashfn + obj_cmpfn):

/* 복합 키: (src_ip, dst_ip, protocol) 3-tuple */
struct flow_key {
    __be32  src_ip;
    __be32  dst_ip;
    u8      protocol;
};

struct flow_entry {
    struct flow_key    fkey;
    u64                packets;
    u64                bytes;
    struct rhash_head  rhash;
};

static u32 flow_obj_hashfn(const void *data, u32 len, u32 seed)
{
    const struct flow_entry *f = data;
    return jhash(&f->fkey, sizeof(f->fkey), seed);
}

static u32 flow_key_hashfn(const void *data, u32 len, u32 seed)
{
    return jhash(data, sizeof(struct flow_key), seed);
}

static int flow_obj_cmpfn(struct rhashtable_compare_arg *arg,
                          const void *obj)
{
    const struct flow_key *key = arg->key;
    const struct flow_entry *f = obj;
    return memcmp(&f->fkey, key, sizeof(*key));
}

static const struct rhashtable_params flow_params = {
    .head_offset  = offsetof(struct flow_entry, rhash),
    .key_offset   = offsetof(struct flow_entry, fkey),
    .key_len      = sizeof(struct flow_key),
    .hashfn       = flow_key_hashfn,
    .obj_hashfn   = flow_obj_hashfn,
    .obj_cmpfn    = flow_obj_cmpfn,
};

기본 API

struct rhashtable my_rht;

/* 초기화: 파라미터 기반으로 버킷 테이블 할당 */
int ret = rhashtable_init(&my_rht, &my_rht_params);
if (ret)
    return ret;  /* -ENOMEM: 초기 버킷 할당 실패 */

/* 삽입: per-bucket lock 획득 후 체인 앞에 삽입 */
struct my_entry *entry = kzalloc(sizeof(*entry), GFP_KERNEL);
entry->key = 100;
ret = rhashtable_insert_fast(&my_rht, &entry->rhash, my_rht_params);
if (ret) {
    kfree(entry);
    return ret;  /* -ENOMEM: 리사이즈 실패, -EEXIST: 키 중복 */
}

/* 검색: RCU 보호 하에 lock-free 수행 */
u32 search_key = 100;
struct my_entry *found;
rcu_read_lock();
found = rhashtable_lookup_fast(&my_rht, &search_key, my_rht_params);
if (found)
    pr_info("found value: %s\n", found->value);
rcu_read_unlock();

/* 삭제: per-bucket lock 획득 후 체인에서 제거 */
ret = rhashtable_remove_fast(&my_rht, &entry->rhash, my_rht_params);

/* 정리 (모든 요소 제거 후) */
rhashtable_destroy(&my_rht);

전체 API 레퍼런스

함수컨텍스트설명
rhashtable_init()process해시 테이블 초기화, 초기 버킷 할당
rhashtable_destroy()process빈 테이블 해제 (요소가 남아있으면 경고)
rhashtable_free_and_destroy()process콜백으로 모든 요소 해제 후 테이블 파괴
rhashtable_insert_fast()any요소 삽입. 중복 키 시 -EEXIST
rhashtable_lookup_fast()rcu키로 요소 검색. RCU read lock 내에서 호출
rhashtable_remove_fast()any요소 삭제. 삭제 후 RCU grace period 대기 필요
rhashtable_lookup_insert_fast()any원자적 검색+삽입. 없으면 삽입, 있으면 -EEXIST
rhashtable_lookup_get_insert_fast()any검색+삽입. 기존 엔트리 포인터 반환 또는 삽입
rhashtable_replace_fast()any기존 요소를 새 요소로 원자적 교체
rhashtable_walk_enter()process순회 반복자(iterator) 등록
rhashtable_walk_start()any순회 시작, RCU read lock 획득
rhashtable_walk_next()rcu다음 요소 반환. 리사이즈 시 -EAGAIN
rhashtable_walk_stop()any순회 일시 정지, RCU read lock 해제
rhashtable_walk_exit()process순회 반복자 해제

원자적 검색+삽입

검색과 삽입을 원자적으로 수행하는 편의 함수:

/* 키가 없으면 삽입, 이미 있으면 -EEXIST */
ret = rhashtable_lookup_insert_fast(&my_rht, &entry->rhash, my_rht_params);

/* 키가 있으면 기존 엔트리 반환, 없으면 삽입 */
struct my_entry *existing;
existing = rhashtable_lookup_get_insert_fast(&my_rht, &entry->rhash, my_rht_params);
if (IS_ERR(existing))
    return PTR_ERR(existing);  /* 에러 (-ENOMEM 등) */
if (existing)
    pr_info("already exists\n");  /* 기존 엔트리 */
else
    pr_info("newly inserted\n");  /* 새로 삽입됨 */

원자적 교체 (Replace)

rhashtable_replace_fast()는 동일 키의 기존 요소를 새 요소로 원자적으로 교체합니다. 읽기 측은 교체 전후의 어느 한 쪽만 관측하며, 중간 상태는 보이지 않습니다.

struct my_entry *old_entry = find_entry(&my_rht, key);
struct my_entry *new_entry = kzalloc(sizeof(*new_entry), GFP_KERNEL);
new_entry->key = old_entry->key;
strscpy(new_entry->value, "updated", sizeof(new_entry->value));

ret = rhashtable_replace_fast(&my_rht, &old_entry->rhash,
                              &new_entry->rhash, my_rht_params);
if (!ret) {
    synchronize_rcu();   /* 또는 call_rcu() */
    kfree(old_entry);   /* 이전 요소를 안전하게 해제 */
}

자동 리사이즈 메커니즘

rhashtable의 리사이즈는 다단계 과정으로 진행됩니다:

확장 (Grow) 트리거 조건

축소 (Shrink) 트리거 조건

리사이즈 과정 상세

  1. 트리거: 삽입/삭제 시 rht_grow_above_75() 또는 rht_shrink_below_30() 검사
  2. 예약: schedule_work(&ht->run_work)로 work queue에 리사이즈 작업 등록
  3. Mutex 획득: ht->mutex로 동시 리사이즈 방지
  4. 새 테이블 할당: kvmalloc()으로 2배(확장) 또는 1/2(축소) 크기의 bucket_table 할당
  5. future_tbl 설정: 현재 tbl->future_tbl = new_tbl로 연결 (RCU publish)
  6. 요소 마이그레이션: 버킷별로 lock을 잡고, 각 요소를 재해싱하여 새 테이블로 이동
  7. 테이블 교체: rcu_assign_pointer(ht->tbl, new_tbl)로 현재 테이블 전환
  8. 이전 테이블 해제: call_rcu()로 grace period 이후 이전 bucket_table 해제
rhashtable 리사이즈 과정 (4 → 8 버킷 확장) ① 현재 테이블 (tbl) ② 새 테이블 (future_tbl) bucket_table (size=4) [0] [1] [2] [3] A E B C F D bucket_table (size=8) [0] [1] [2] [3] [4] [5] [6] [7] A C D E B F ③ 요소 재해싱: hash(A) % 8 = 0 hash(E) % 8 = 4 (이동!) hash(B) % 8 = 5 (이동!) hash(C) % 8 = 2 hash(F) % 8 = 6 (이동!) hash(D) % 8 = 3 ④ 마이그레이션 완료 후: rcu_assign_pointer(ht->tbl, new_tbl)

리사이즈 중 삽입future_tbl(새 테이블)에만 수행됩니다. 검색은 현재 테이블에서 시작하되, future_tbl이 존재하면 새 테이블도 탐색합니다. 이 설계 덕분에 리사이즈 진행 중에도 정확성이 보장됩니다.

중첩 테이블(Nested Table): 대규모 테이블(버킷 수 > PAGE_SIZE / sizeof(bucket))에서 kvmalloc()이 연속 메모리 할당에 실패하면, rhashtable은 중첩 페이지 테이블 방식으로 폴백합니다. bucket_table.nest 필드가 중첩 깊이를 추적하며, 각 레벨은 단일 페이지 크기의 포인터 배열입니다.

rhltable (중복 키 허용)

일반 rhashtable은 키 중복을 허용하지 않습니다(-EEXIST 반환). rhltable은 동일 키에 여러 엔트리를 허용하는 변형으로, 각 엔트리를 rhlist_head로 연결합니다. nftables set의 interval element 등에서 사용됩니다.

struct my_multi_entry {
    u32                     key;
    u32                     value;
    struct rhlist_head       rhlist;  /* rhltable 노드 (rhash_head 확장) */
};

struct rhltable my_rhlt;

/* 초기화 */
rhltable_init(&my_rhlt, &my_rht_params);

/* 삽입: 동일 키 허용 (중복 시에도 성공) */
rhltable_insert(&my_rhlt, &entry->rhlist, my_rht_params);

/* 검색: 동일 키의 모든 엔트리를 연결 리스트로 반환 */
struct rhlist_head *list, *tmp;
rcu_read_lock();
list = rhltable_lookup(&my_rhlt, &search_key, my_rht_params);
if (list) {
    rhl_for_each_entry_rcu(entry, tmp, list, rhlist) {
        pr_info("key=%u value=%u\n", entry->key, entry->value);
    }
}
rcu_read_unlock();

/* 삭제: 특정 엔트리만 제거 */
rhltable_remove(&my_rhlt, &entry->rhlist, my_rht_params);

rhlist_headrhash_head를 내장하며, 동일 키의 엔트리들을 별도의 연결 리스트(struct rhlist_head *list)로 묶습니다. 해시 체인에는 키당 하나의 "대표 노드"만 들어가고, 나머지는 대표 노드에서 분기하는 리스트에 연결됩니다.

순회 API (Walk Iterator)

rhashtable은 리사이즈에 안전한 순회 API를 제공합니다. hash_for_each와 달리, 순회 중간에 sleep이 가능하고 리사이즈가 발생해도 정확성이 보장됩니다.

struct rhashtable_iter iter;

/* 1. 반복자 등록 (process 컨텍스트) */
rhashtable_walk_enter(&my_rht, &iter);

/* 2. 순회 시작 (RCU read lock 획득) */
rhashtable_walk_start(&iter);

/* 3. 요소 순회 */
struct my_entry *entry;
while ((entry = rhashtable_walk_next(&iter)) != NULL) {
    if (IS_ERR(entry)) {
        if (PTR_ERR(entry) == -EAGAIN)
            continue;  /* 리사이즈 발생 → 재시도 */
        break;         /* 기타 에러 */
    }
    pr_info("key=%u value=%s\n", entry->key, entry->value);
}

/* 4. 순회 종료 (RCU read lock 해제) */
rhashtable_walk_stop(&iter);

/* 5. 반복자 해제 */
rhashtable_walk_exit(&iter);
💡

중간 sleep이 필요한 경우: rhashtable_walk_stop()으로 RCU lock을 해제한 후 sleep하고, rhashtable_walk_start()로 다시 시작하면 이전 위치에서 계속됩니다. 리사이즈가 발생했을 경우 rhashtable_walk_next()-EAGAIN을 반환하여 위치를 재조정합니다.

정리와 해제

rhashtable을 해제할 때는 반드시 모든 요소를 먼저 제거해야 합니다. rhashtable_free_and_destroy()는 콜백 함수를 통해 이를 자동화합니다.

/* 방법 1: rhashtable_free_and_destroy (권장) */
static void my_entry_free(void *ptr, void *arg)
{
    struct my_entry *entry = ptr;
    kfree(entry);
}

/* 모든 요소에 대해 my_entry_free 호출 후 테이블 해제 */
rhashtable_free_and_destroy(&my_rht, my_entry_free, NULL);

/* 방법 2: Walk API로 수동 정리 */
struct rhashtable_iter iter;
rhashtable_walk_enter(&my_rht, &iter);
rhashtable_walk_start(&iter);

struct my_entry *entry;
while ((entry = rhashtable_walk_next(&iter)) != NULL) {
    if (IS_ERR(entry))
        continue;
    rhashtable_remove_fast(&my_rht, &entry->rhash, my_rht_params);
    kfree_rcu(entry, rcu);  /* RCU grace period 후 해제 */
}

rhashtable_walk_stop(&iter);
rhashtable_walk_exit(&iter);
rhashtable_destroy(&my_rht);

rhashtable_destroy()는 테이블에 요소가 남아있으면 WARN_ON을 출력합니다. 절대 요소가 남아있는 상태에서 호출하지 마세요. 메모리 누수가 발생합니다. 반드시 rhashtable_free_and_destroy()를 사용하거나 모든 요소를 수동으로 제거하세요.

동시성 모델 요약

연산보호 메커니즘블로킹비고
검색 (lookup_fast) RCU read lock non-blocking 리사이즈 중에도 lock-free
삽입 (insert_fast) per-bucket bit-spinlock non-blocking (spinlock) 동일 버킷만 직렬화, 다른 버킷은 병렬
삭제 (remove_fast) per-bucket bit-spinlock non-blocking 삭제 후 RCU grace period 대기 필요
리사이즈 ht->mutex + per-bucket lock blocking (mutex, kvmalloc) work queue에서 비동기 수행
순회 (walk_*) RCU read lock + walker 등록 sleepable (stop/start 사이) 리사이즈 시 -EAGAIN으로 재조정

hashtable.h vs rhashtable 비교

특성hashtable.hrhashtable
크기고정 (컴파일 시 결정)동적 (자동 확장/축소)
버킷 구조hlist_head[]rhash_lock_head[] + nulls
해시 함수hash_long()jhash() (커스텀 가능)
잠금사용자 책임per-bucket spinlock 내장
RCU 지원_rcu 접미사 매크로기본 내장
중복 키지원 (해시 충돌과 동일 취급)기본 불허, rhltable로 지원
순회hash_for_eachrhashtable_walk_* (리사이즈 안전)
메모리스택/전역에 직접 할당kvmalloc() 동적 할당
초기화DEFINE_HASHTABLE / hash_initrhashtable_init()
적합한 경우요소 수 예측 가능, 단순한 구조요소 수 변동, 고성능 동시성 필요

RCU 해시 테이블

<linux/hashtable.h>는 RCU로 보호되는 해시 테이블 연산을 별도의 _rcu 접미사 매크로로 제공합니다. 이를 사용하면 읽기 측에서 lock 없이 해시 테이블을 안전하게 순회할 수 있습니다.

RCU 해시 테이블 API

/* RCU 보호 삽입 — 쓰기 측에서 적절한 lock 보유 필요 */
hash_add_rcu(ht, &obj->hash_node, key);

/* RCU 보호 삭제 */
hash_del_rcu(&obj->hash_node);

/* RCU 읽기 측 전체 순회 */
rcu_read_lock();
hash_for_each_rcu(ht, bkt, obj, member) {
    /* obj를 읽기 전용으로 접근 */
}
rcu_read_unlock();

/* RCU 읽기 측 버킷 순회 (검색) */
rcu_read_lock();
hash_for_each_possible_rcu(ht, obj, member, key) {
    if (obj->key == search_key) {
        /* found */
        break;
    }
}
rcu_read_unlock();

읽기-쓰기 분리 패턴

static DEFINE_HASHTABLE(my_ht, 10);
static DEFINE_SPINLOCK(my_ht_lock);  /* 쓰기 측 보호 */

/* 읽기 경로: lock-free */
struct my_object *lookup(int key)
{
    struct my_object *obj;

    rcu_read_lock();
    hash_for_each_possible_rcu(my_ht, obj, hash_node, key) {
        if (obj->key == key) {
            rcu_read_unlock();
            return obj;
        }
    }
    rcu_read_unlock();
    return NULL;
}

/* 쓰기 경로: spinlock으로 보호 */
void insert(struct my_object *obj)
{
    spin_lock(&my_ht_lock);
    hash_add_rcu(my_ht, &obj->hash_node, obj->key);
    spin_unlock(&my_ht_lock);
}

/* 삭제 경로 */
void delete(struct my_object *obj)
{
    spin_lock(&my_ht_lock);
    hash_del_rcu(&obj->hash_node);
    spin_unlock(&my_ht_lock);

    synchronize_rcu();   /* 또는 call_rcu()로 비동기 */
    kfree(obj);
}
💡

rhashtable는 내부적으로 RCU를 사용하여 읽기 연산을 보호합니다. rhashtable_lookup_fast()rcu_read_lock() 안에서 호출해야 합니다. 별도의 _rcu 접미사 매크로가 필요 없습니다.

충돌 해결과 성능

체이닝 (Separate Chaining)

커널 해시 테이블은 체이닝 방식으로 해시 충돌을 해결합니다. 동일 버킷에 해시되는 모든 요소는 hlist 체인으로 연결됩니다. Open addressing(선형/이차 탐사)과 비교할 때 다음과 같은 장점이 있습니다:

버킷 크기 선택 가이드

DEFINE_HASHTABLE(name, bits)bits 값 선택 기준:

bits버킷 수hlist_head 크기 (x86_64)권장 요소 수
416128 bytes~16
82562 KB~256
101,0248 KB~1,024
124,09632 KB~4,096
1665,536512 KB~65,536
201,048,5768 MB~1M
💡

일반적으로 로드 팩터(요소 수 / 버킷 수)를 0.5 ~ 1.0 사이로 유지하는 것이 좋습니다. 요소 수가 예측 불가능하면 rhashtable을 사용하세요.

캐시 라인 최적화

해시 테이블 성능에 영향을 미치는 캐시 관련 요소:

커널 활용 사례

conntrack (nf_conntrack)

Netfilter의 연결 추적 테이블은 커널의 가장 대표적인 rhashtable 활용 사례 중 하나입니다. 네트워크 연결의 5-tuple(프로토콜, 소스/목적지 IP, 소스/목적지 포트)을 키로 해시하여 초당 수백만 패킷의 연결 상태를 빠르게 조회합니다.

/* include/net/netfilter/nf_conntrack.h */
struct nf_conntrack_tuple_hash {
    struct hlist_nulls_node    hnnode;    /* nulls 마커가 있는 hlist */
    struct nf_conntrack_tuple  tuple;
};

/* 하나의 연결은 방향별 2개의 tuple_hash를 가짐 */
struct nf_conn {
    struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
    unsigned long                 status;      /* 연결 상태 비트맵 */
    possible_net_t                ct_net;
    struct hlist_node             nat_bysource;
    /* ... */
    spinlock_t                    lock;
    u32                           timeout;     /* jiffies 기반 만료 */
    u16                           mark;
    /* ... */
};

/* net/netfilter/nf_conntrack_core.c */
/* conntrack 해시 테이블 구조: hlist_nulls_head 배열 */
/* net->ct.hash: 동적 크기 해시, nf_conntrack_htable_size 버킷 */
/* 해시 함수: jhash2() + 랜덤 시드(nf_conntrack_hash_rnd) */

/* 연결 검색 경로 (패킷 수신 hot path) */
static struct nf_conntrack_tuple_hash *
____nf_conntrack_find(struct net *net,
                     const struct nf_conntrack_zone *zone,
                     const struct nf_conntrack_tuple *tuple,
                     u32 hash)
{
    struct nf_conntrack_tuple_hash *h;
    struct hlist_nulls_head *ct_hash;
    struct hlist_nulls_node *n;
    unsigned int bucket, hsize;

begin:
    nf_conntrack_get_ht(&ct_hash, &hsize);
    bucket = reciprocal_scale(hash, hsize);

    hlist_nulls_for_each_entry_rcu(h, n, &ct_hash[bucket], hnnode) {
        if (nf_ct_key_equal(h, tuple, zone, net))
            return h;
    }

    /* nulls 마커 검증: 리사이즈로 인한 버킷 이동 감지 */
    if (get_nulls_value(n) != bucket) {
        /* 테이블이 바뀌었으므로 재시도 */
        goto begin;
    }
    return NULL;
}
💡

conntrack 해시 테이블 크기는 /proc/sys/net/netfilter/nf_conntrack_buckets로 조회/설정하고, 최대 엔트리 수는 nf_conntrack_max로 제한합니다. 일반적으로 nf_conntrack_max = nf_conntrack_buckets × 4(로드 팩터 4) 관계입니다.

inode 해시 테이블

/* fs/inode.c */
static struct hlist_head *inode_hashtable;

/* inode 검색: superblock + inode number로 해시 */
struct inode *ilookup(struct super_block *sb, unsigned long ino)
{
    struct hlist_head *head = inode_hashtable +
                              hash(sb, ino);
    /* head에서 hlist 순회로 검색 */
}

PID 해시

/* kernel/pid.c */
struct pid {
    refcount_t           count;
    unsigned int         level;
    spinlock_t           lock;
    struct hlist_head    tasks[PIDTYPE_MAX];
    struct hlist_head    inodes;
    wait_queue_head_t    wait_pidfd;
    struct rcu_head      rcu;
    struct upid          numbers[];  /* pid namespace 계층 */
};

/* struct upid는 해시 테이블에 연결 */
struct upid {
    int                  nr;        /* PID 번호 */
    struct pid_namespace *ns;
    struct hlist_node    pid_chain; /* pid_hash[]에 연결 */
};

dentry 캐시 (dcache)

/* fs/dcache.c */
static struct hlist_bl_head *dentry_hashtable;

/* 해시 키: parent dentry + 파일명 해시 */
/* full_name_hash()로 파일명 해싱 */
/* hlist_bl: bit-locked hlist (비트 스핀락 내장) */

네트워크 소켓 해시

/* TCP established 소켓 해시 (inet_ehash_bucket) */
/* 키: local addr/port + remote addr/port */
/* 빠른 수신 패킷 → 소켓 매핑에 핵심 */

/* UDP 소켓 해시 (udp_table) */
/* 키: local addr/port */
/* reuseport 지원 시 추가 해시로 분산 */

모듈 심볼 해시

/* kernel/module/main.c */
/* EXPORT_SYMBOL로 등록된 심볼을 해시 테이블로 검색 */
/* 모듈 로딩 시 심볼 해석(symbol resolution)에 사용 */

해시 테이블 구조 다이어그램

Hash Table 구조 (DEFINE_HASHTABLE, 8 buckets) hlist_head[] [0] key=256 key=512 NULL [1] NULL [2] key=42 NULL [3] NULL [4] key=1028 key=7 key=2052 NULL [5] NULL [6] key=99 NULL [7] NULL 범례 hlist_head (8B) hlist_node + data next 포인터

자료구조 선택 가이드

기준 Hash Table Red-Black Tree XArray Linked List
검색 O(1) 평균 O(log n) O(log n) O(n)
삽입/삭제 O(1) 평균 O(log n) O(log n) O(1) 위치 알 때
정렬 순회 불가 O(n) 정렬 순서 O(n) 인덱스 순서 O(n) 삽입 순서
범위 검색 불가 효율적 효율적 O(n)
키 타입 정수, 구조체, 문자열 비교 가능한 모든 키 정수 (unsigned long) N/A
메모리 버킷 배열 + 노드 노드당 3 포인터 radix tree 노드 노드당 포인터 2개
동적 크기 rhashtable만 가능 자동 자동 자동
대표 사용처 inode 캐시, conntrack, PID CFS, VMA, ext4 extent 페이지 캐시, IDR task 리스트, 타이머
💡

선택 기준 요약: 정확한 키 매칭(exact match)이 주된 연산이면 → Hash Table. 범위 검색이나 정렬 순회가 필요하면 → rbtree. 정수 키 + 희소(sparse) 데이터면 → XArray. 순서 유지가 목적이면 → Linked List.

디버깅 팁

crash 유틸리티로 해시 테이블 덤프

# crash 셸에서 해시 테이블 구조 확인
crash> struct hlist_head inode_hashtable 10

# 특정 버킷의 체인 순회
crash> list hlist_node -H 0xffff...addr -s my_object.key,data

# DEFINE_HASHTABLE로 선언된 전역 해시 테이블
crash> p my_ht
crash> struct hlist_head my_ht 256

drgn으로 해시 테이블 분석

# drgn 스크립트: 해시 테이블 통계
import drgn
from drgn.helpers.linux.list import hlist_for_each_entry

prog = drgn.program_from_kernel()

# 각 버킷의 체인 길이 측정
ht = prog["my_ht"]
for i in range(len(ht)):
    count = 0
    for entry in hlist_for_each_entry("struct my_object",
                                       ht[i], "hash_node"):
        count += 1
    if count > 0:
        print(f"bucket[{i}]: {count} entries")

일반적 실수와 해결

실수증상해결
hash_for_each_possible에서 키 비교 누락 잘못된 객체 반환 (해시 충돌 시) 반드시 if (obj->key == search_key) 확인
hash_del 없이 kfree dangling pointer → 커널 패닉 hash_del()kfree() 순서 준수
RCU 해시에서 rcu_read_lock 누락 use-after-free, KASAN 경고 읽기 경로에 rcu_read_lock() 감싸기
순회 중 hash_del (safe 미사용) 무한 루프 또는 스킵 hash_for_each_safe / hash_for_each_possible_safe 사용
버킷 수가 너무 적음 긴 체인 → O(n) 검색 로드 팩터 확인, bits 값 증가 또는 rhashtable 전환
hash_add_rcu 시 쓰기 lock 미보유 동시 삽입 → 체인 손상 spinlock/mutex로 쓰기 측 직렬화
rhashtable에서 rhashtable_destroy 전 요소 미제거 메모리 누수 + WARN_ON rhashtable_free_and_destroy() 사용
rhashtable_remove_fast 후 즉시 kfree RCU 읽기 측 use-after-free kfree_rcu() 또는 synchronize_rcu()kfree()
rhashtable_walk_next-EAGAIN 미처리 순회 중 요소 누락 IS_ERR(entry) 검사 후 continue로 재시도
rhashtable_lookup_fastrcu_read_lock 밖에서 호출 KASAN/KCSAN 경고, 불안정한 결과 반드시 rcu_read_lock()/rcu_read_unlock() 내에서 호출
rhashtable_params에서 head_offset/key_offset 잘못 지정 해시 불일치, 검색 실패, 커널 패닉 offsetof() 매크로로 정확히 계산

CONFIG_DEBUG_OBJECTS를 활성화하면 hlist_node의 이중 해제, 초기화 누락 등을 탐지할 수 있습니다. CONFIG_PROVE_RCU는 RCU 해시 테이블의 잘못된 사용을 경고합니다.