Notifier Chain (알림 체인)

Notifier Chain은 Linux 커널의 이벤트 알림 메커니즘입니다. 발행자(publisher)가 이벤트를 발생시키면 등록된 구독자(subscriber) 콜백들이 순서대로 호출됩니다. 잠금 컨텍스트에 따라 4가지 변형(blocking, atomic, raw, srcu)을 제공하며, CPU 핫플러그·전원관리·네트워크 디바이스 이벤트 등 커널 전반에서 사용됩니다.

이 문서에서는 각 변형의 API 나열에 그치지 않고 "콜백이 실행되는 잠금 문맥"과 "콜백 내부에서 허용되는 동작"을 중심으로 설계 기준을 정리합니다. registration/unregistration의 수명 주기 규칙, 우선순위 체인의 부작용, NOTIFY_STOP 사용 시 전파 차단 영향, SRCU 비용 모델과 읽기 경로 이점까지 분석해 모듈 간 결합도를 낮추면서도 장애 전파를 제어할 수 있는 이벤트 설계 방법을 제공합니다.

전제 조건: 동기화 기법RCU 문서를 먼저 읽으세요. 각 Notifier Chain 변형은 서로 다른 잠금 메커니즘을 사용하므로, 뮤텍스·스핀락·RCU 개념이 필요합니다.
일상 비유: Notifier Chain은 신문 구독 시스템과 비슷합니다. 신문사(발행자)가 기사를 발행하면 구독자 목록을 순회하며 각자에게 배달합니다. 구독자 목록을 읽는 동안 새 구독자가 추가될 수 있어 적절한 동기화가 필요합니다.

핵심 요약

  • notifier_block — 구독자 콜백을 담는 기본 구조체 (priority 필드로 호출 순서 결정)
  • blocking_notifier_head — rwsem 보호, 슬립 가능 컨텍스트 (가장 일반적)
  • atomic_notifier_head — 스핀락 보호, 인터럽트 핸들러 내 사용 가능
  • raw_notifier_head — 잠금 없음, 호출자가 직접 동기화 책임
  • srcu_notifier_head — SRCU 보호, 콜백 내에서 체인 수정 허용

단계별 이해

  1. notifier_block 작성
    콜백 함수와 우선순위를 담은 struct notifier_block을 정의합니다. 우선순위가 높을수록 먼저 호출됩니다.
  2. 체인에 등록
    register_*_notifier() 함수로 관심 체인에 등록합니다. 반환값을 확인하여 중복 등록을 방지해야 합니다.
  3. 이벤트 수신
    체인 발행자가 *_notifier_call_chain()을 호출하면 등록된 모든 콜백이 순서대로 실행됩니다.
  4. 콜백 반환값
    NOTIFY_OK: 정상 처리, NOTIFY_STOP: 체인 순회 중단, NOTIFY_BAD: 오류(중단 없음), NOTIFY_DONE: 이벤트 무관.
  5. 등록 해제
    모듈 언로드 시 반드시 unregister_*_notifier()를 호출해야 합니다. 미해제 시 댕글링 포인터 패닉이 발생합니다.

개요

Notifier Chain의 핵심은 struct notifier_block의 단방향 연결 리스트입니다. 발행자는 이벤트 번호와 데이터를 인자로 체인을 순회하며 각 콜백을 호출합니다.

struct notifier_block

/* include/linux/notifier.h */
struct notifier_block {
    notifier_fn_t       notifier_call; /* 콜백 함수 포인터 */
    struct notifier_block __rcu *next; /* 다음 구독자 (RCU protected) */
    int                 priority;      /* 호출 우선순위 (높을수록 먼저) */
};

/* 콜백 함수 시그니처 */
typedef int (*notifier_fn_t)(struct notifier_block *nb,
                              unsigned long action,
                              void *data);

콜백 반환값

반환값의미체인 순회
NOTIFY_DONE이 이벤트에 관심 없음계속
NOTIFY_OK정상 처리 완료계속
NOTIFY_BAD처리 실패 (에러 신호)계속
NOTIFY_STOP처리 완료, 이후 콜백 불필요중단
NOTIFY_STOP_MASKNOTIFY_STOP에 포함된 비트 마스크중단

4가지 Notifier Chain 변형

4가지 Notifier Chain 타입 — 공통 구조 + 서로 다른 잠금 메커니즘 blocking rw_semaphore 슬립 가능 · 콜백 sleep OK 사용: PM, 클럭 ★ 가장 일반적 atomic spinlock_t IRQ 컨텍스트 · 콜백 sleep X 사용: 네트워크 이벤트 raw 잠금 없음 (호출자 책임) 제한 없음 · 특수 목적 사용: 부팅 초기 srcu srcu_struct 슬립 가능 · 콜백 중 등록/해제 OK 사용: CPU 핫플러그 공통 구조: notifier_block 단방향 연결 리스트 nb₁ priority=300 nb₂ priority=200 nb₃ priority=100 → NULL • priority 내림차순 정렬 (높을수록 먼저 호출) • 모든 타입이 동일한 연결 리스트 구조 사용 • 차이점은 잠금 메커니즘만 (위쪽 박스) 호출 순서: nb₁ → nb₂ → nb₃

Blocking Notifier Chain

가장 일반적인 변형으로, rwsem(읽기-쓰기 세마포어)으로 보호됩니다. 콜백에서 슬립이 허용되며, 프로세스 컨텍스트에서만 사용 가능합니다.

Blocking API

/* 선언 및 초기화 */
static BLOCKING_NOTIFIER_HEAD(my_chain);   /* 정적 초기화 */

/* 또는 동적 초기화 */
struct blocking_notifier_head my_chain;
BLOCKING_INIT_NOTIFIER_HEAD(&my_chain);

/* 구독자 등록/해제 */
int blocking_notifier_chain_register(struct blocking_notifier_head *nh,
                                       struct notifier_block *nb);
int blocking_notifier_chain_unregister(struct blocking_notifier_head *nh,
                                         struct notifier_block *nb);

/* 이벤트 발행 */
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
                                   unsigned long val, void *v);

Blocking Notifier 실전 예제

/* === 발행자(publisher) 측 === */
static BLOCKING_NOTIFIER_HEAD(voltage_change_chain);

/* 외부에서 등록/해제할 수 있도록 함수 공개 */
int register_voltage_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_register(&voltage_change_chain, nb);
}
EXPORT_SYMBOL(register_voltage_notifier);

int unregister_voltage_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_unregister(&voltage_change_chain, nb);
}
EXPORT_SYMBOL(unregister_voltage_notifier);

/* 이벤트 정의 */
#define VOLTAGE_CHANGE_PRE  0x01
#define VOLTAGE_CHANGE_POST 0x02

static void notify_voltage_change(unsigned long event, int mv)
{
    blocking_notifier_call_chain(&voltage_change_chain, event, (void *)(long)mv);
}

/* === 구독자(subscriber) 측 === */
static int my_voltage_cb(struct notifier_block *nb,
                           unsigned long event, void *data)
{
    int mv = (int)(long)data;
    if (event == VOLTAGE_CHANGE_PRE)
        pr_info("Voltage will change to %dmV\n", mv);
    return NOTIFY_OK;
}

static struct notifier_block my_nb = {
    .notifier_call = my_voltage_cb,
    .priority      = 100,  /* 높을수록 먼저 호출 */
};

static int __init my_init(void)
{
    return register_voltage_notifier(&my_nb);
}

static void __exit my_exit(void)
{
    unregister_voltage_notifier(&my_nb); /* 반드시 해제! */
}

Atomic Notifier Chain

스핀락으로 보호되어 인터럽트 핸들러에서도 호출 가능합니다. 콜백 내에서 슬립을 유발하는 어떠한 연산도 허용되지 않습니다.

/* 초기화 */
static ATOMIC_NOTIFIER_HEAD(netdev_chain);

/* API */
int atomic_notifier_chain_register(struct atomic_notifier_head *nh,
                                     struct notifier_block *nb);
int atomic_notifier_chain_unregister(struct atomic_notifier_head *nh,
                                       struct notifier_block *nb);
int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
                                  unsigned long val, void *v);
Atomic Notifier 제약: 콜백 함수 내에서 mutex_lock(), kmalloc(GFP_KERNEL), schedule() 등 슬립을 유발하는 함수를 호출하면 커널 패닉이 발생합니다.

SRCU Notifier Chain

SRCU(Sleepable RCU)로 보호되어 콜백 내에서 슬립이 허용되면서도, 콜백 실행 중 체인에서 등록/해제가 가능합니다. CPU 핫플러그 이벤트에 사용됩니다.

/* SRCU Notifier는 동적 초기화가 필수 */
struct srcu_notifier_head cpu_chain;

static int __init init_cpu_notifier(void)
{
    srcu_init_notifier_head(&cpu_chain);  /* SRCU struct 초기화 포함 */
    return 0;
}

/* 정리 시 반드시 호출 */
srcu_cleanup_notifier_head(&cpu_chain);

/* API */
int srcu_notifier_chain_register(struct srcu_notifier_head *nh,
                                   struct notifier_block *nb);
int srcu_notifier_chain_unregister(struct srcu_notifier_head *nh,
                                     struct notifier_block *nb);
int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
                               unsigned long val, void *v);

Raw Notifier Chain

잠금이 전혀 없는 최저수준 변형입니다. 동기화는 전적으로 호출자의 책임입니다. 시스템 초기화 단계나 이미 잠금이 보장된 특수 경로에서만 사용합니다.

/* 초기화 */
static RAW_NOTIFIER_HEAD(my_raw_chain);

/* API: 호출자가 적절한 잠금을 보유해야 함 */
int raw_notifier_chain_register(struct raw_notifier_head *nh,
                                  struct notifier_block *nb);
int raw_notifier_chain_unregister(struct raw_notifier_head *nh,
                                    struct notifier_block *nb);
int raw_notifier_call_chain(struct raw_notifier_head *nh,
                              unsigned long val, void *v);

Raw Notifier 실전 예제 — Reboot 알림

/* kernel/reboot.c: 시스템 재부팅/종료 알림 (Raw Notifier 사용)
 * 재부팅 경로는 이미 잠금 보유 상태이고 특수 컨텍스트이므로 raw 사용 */

/* 구독자 측 */
static int my_reboot_cb(struct notifier_block *nb,
                          unsigned long code, void *data)
{
    switch (code) {
    case SYS_RESTART:
        pr_info("System restarting\n");
        my_flush_all();       /* 데이터 플러시 */
        break;
    case SYS_HALT:
    case SYS_POWER_OFF:
        pr_info("System halting\n");
        my_hw_shutdown();     /* 하드웨어 안전 종료 */
        break;
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_reboot_nb = {
    .notifier_call = my_reboot_cb,
    .priority      = 128,   /* 0 = 기본, 양수 = 앞서 호출, 음수 = 나중 호출 */
};

static int __init my_init(void)
{
    return register_reboot_notifier(&my_reboot_nb);
}

static void __exit my_exit(void)
{
    unregister_reboot_notifier(&my_reboot_nb);
}
Raw Notifier 동기화 책임: raw_notifier_call_chain()은 어떤 잠금도 획득하지 않습니다. 등록/해제와 호출이 동시에 일어날 수 있는 환경에서는 use-after-free 문제가 발생합니다. 반드시 호출자가 적절한 직렬화를 보장해야 합니다.

4종 비교

항목blockingatomicrawsrcu
잠금 구조체struct rw_semaphorespinlock_t없음struct srcu_struct
콜백 내 sleep가능불가호출자 결정가능
IRQ 컨텍스트불가가능호출자 결정불가
콜백 중 등록불가 (데드락)불가 (데드락)가능 (주의)가능
초기화 매크로BLOCKING_NOTIFIER_HEADATOMIC_NOTIFIER_HEADRAW_NOTIFIER_HEADsrcu_init_notifier_head()
주 사용처전원관리, 클럭, 규제기네트워크, 디바이스부팅 초기, 특수CPU 핫플러그
등록 시 잠금rwsem writespin_lock_irqsave없음mutex
호출 시 잠금rwsem readspin_lock_irqsave + RCU read없음srcu_read_lock
PREEMPT_RT 호환가능제한적호출자 결정가능
grace period 필요아니오예 (RCU)아니오예 (SRCU)
메모리 오버헤드rw_semaphore (40B)spinlock_t (4B)포인터 (8B)srcu_struct (~200B)

4종 잠금 메커니즘 비교 다이어그램

4종 Notifier Chain — 잠금 메커니즘 상세 비교 Blocking 등록: rw_semaphore (W) 호출: rw_semaphore (R) 콜백 내 sleep 허용 IRQ 컨텍스트 불가 콜백 중 등록 불가 PM, 클럭, Regulator ★ 기본 선택 Atomic 등록: spin_lock_irqsave 호출: RCU read-side 콜백 내 sleep 불가 IRQ 컨텍스트 가능 콜백 중 등록 불가 netdev, panic IRQ 필요 시 Raw 등록: 잠금 없음 호출: 잠금 없음 모든 것 호출자 책임 동기화 보장 없음 콜백 중 등록 가능 reboot, 초기 부팅 ⚠ 특수 경로 전용 SRCU 등록: mutex 호출: srcu_read_lock 콜백 내 sleep 허용 IRQ 컨텍스트 불가 콜백 중 등록 가능 CPU 핫플러그 콜백 중 변경 시 각 head 구조체 내부 구성 blocking_notifier_head rw_semaphore rwsem; notifier_block *head; atomic_notifier_head spinlock_t lock; notifier_block *head; raw_notifier_head notifier_block *head; (잠금 필드 없음) srcu_notifier_head mutex lock; srcu_struct srcu; head; 모든 head → 동일한 notifier_block 단방향 연결 리스트 nb(pri=300) → nb(pri=200) → nb(pri=100) → NULL 차이점은 잠금 메커니즘뿐 — 콜백 구조·순회 로직·반환값 처리는 4종 모두 동일 선택 기준: 콜백 실행 컨텍스트(프로세스/IRQ) + 콜백 중 체인 변경 필요 여부

내부 동작 메커니즘

notifier_call_chain 내부 구조

모든 변형의 핵심은 notifier_call_chain()으로, 우선순위 정렬된 연결 리스트를 순회하며 콜백을 호출합니다.

/* kernel/notifier.c: 실제 순회 로직 (단순화) */
static int notifier_call_chain(struct notifier_block **nl,
                                  unsigned long val, void *v,
                                  int nr_to_call, int *nr_calls)
{
    int ret = NOTIFY_DONE;
    struct notifier_block *nb, *next_nb;

    nb = rcu_dereference_raw(*nl);

    while (nb && nr_to_call) {
        next_nb = rcu_dereference_raw(nb->next);

        if (nr_calls)
            (*nr_calls)++;

        ret = nb->notifier_call(nb, val, v);

        if (ret & NOTIFY_STOP_MASK) /* NOTIFY_STOP or NOTIFY_BAD with stop */
            break;

        nb = next_nb;
        nr_to_call--;
    }
    return ret;
}

notifier_call_chain 실행 흐름 다이어그램

notifier_call_chain() 내부 실행 흐름 *_notifier_call_chain() 잠금 획득 (blocking: rwsem_read / atomic: RCU) nb = rcu_dereference_raw(*nl) while (nb != NULL && nr_to_call > 0) next_nb = rcu_dereference_raw(nb->next) ret = nb->notifier_call(nb, val, v) ret & STOP_MASK? 예 (STOP) break (순회 중단) 아니오 nb = next_nb; nr_to_call--; 잠금 해제 + return ret 콜백 반환값 NOTIFY_DONE (0) NOTIFY_OK (1) NOTIFY_BAD (-2|STOP) NOTIFY_STOP (-1|STOP) STOP_MASK = 0x8000 BAD/STOP → 순회 중단

notifier_call_chain 상세 코드 분석

notifier_call_chain()은 4종 모든 변형의 핵심 순회 함수입니다. 각 변형은 이 함수를 잠금으로 감싸서 호출합니다. 핵심 매개변수인 nr_to_call은 최대 호출 횟수를 제한하며, nr_calls는 실제 호출된 횟수를 돌려줍니다.

/* kernel/notifier.c — 전체 구현 (Linux 6.x) */
static int notifier_call_chain(
    struct notifier_block **nl,
    unsigned long val, void *v,
    int nr_to_call, int *nr_calls)
{
    int ret = NOTIFY_DONE;
    struct notifier_block *nb, *next_nb;

    nb = rcu_dereference_raw(*nl);

    while (nb && nr_to_call) {
        /* 콜백 실행 전에 next를 미리 저장 —
         * 콜백이 자기 자신을 해제해도 안전하게 순회 가능 (SRCU/raw) */
        next_nb = rcu_dereference_raw(nb->next);

        if (nr_calls)
            (*nr_calls)++;

        /* 핵심: 콜백 호출 */
        ret = nb->notifier_call(nb, val, v);

        if (notifier_to_errno(ret)) {
            /* ret에 NOTIFY_STOP_MASK 비트가 설정됨
             * → NOTIFY_STOP 또는 NOTIFY_BAD */
            break;
        }

        nb = next_nb;
        nr_to_call--;
    }
    return ret;
}

/* blocking 변형: rwsem read lock으로 감싸기 */
int blocking_notifier_call_chain(
    struct blocking_notifier_head *nh,
    unsigned long val, void *v)
{
    int ret = NOTIFY_DONE;

    if (rcu_access_pointer(nh->head)) {
        down_read(&nh->rwsem);       /* 읽기 잠금 획득 */
        ret = notifier_call_chain(&nh->head, val, v,
                                      -1, NULL);
        up_read(&nh->rwsem);         /* 읽기 잠금 해제 */
    }
    return ret;
}

/* atomic 변형: RCU read-side critical section */
int atomic_notifier_call_chain(
    struct atomic_notifier_head *nh,
    unsigned long val, void *v)
{
    int ret;

    rcu_read_lock();              /* RCU 읽기 진입 */
    ret = notifier_call_chain(&nh->head, val, v,
                                  -1, NULL);
    rcu_read_unlock();            /* RCU 읽기 종료 */
    return ret;
}

콜백 반환값 처리 로직

콜백 반환값은 비트 마스크로 처리됩니다. NOTIFY_STOP_MASK (0x8000) 비트가 설정되면 체인 순회가 중단됩니다.

/* include/linux/notifier.h — 반환값 정의 */
#define NOTIFY_DONE       0x0000   /* 관심 없음, 계속 */
#define NOTIFY_OK         0x0001   /* 처리 완료, 계속 */
#define NOTIFY_STOP_MASK  0x8000   /* 중단 비트 */
#define NOTIFY_BAD        (NOTIFY_STOP_MASK|0x0002)
                                     /* 0x8002: 에러 + 중단 */
#define NOTIFY_STOP       (NOTIFY_STOP_MASK|0x0001)
                                     /* 0x8001: 성공 + 중단 */

/* 변환 함수: 반환값에서 에러 코드 추출 */
static inline int notifier_from_errno(int err)
{
    if (err)
        return NOTIFY_STOP_MASK | (NOTIFY_OK - err);
    return NOTIFY_OK;
}

static inline int notifier_to_errno(int ret)
{
    return -(ret & ~NOTIFY_STOP_MASK);  /* STOP_MASK 제거 후 부호 반전 */
}
NOTIFY_BAD vs NOTIFY_STOP: 두 값 모두 체인 순회를 중단하지만 의미가 다릅니다. NOTIFY_STOP은 "이벤트를 성공적으로 처리했으니 더 이상 전파할 필요 없음"이고, NOTIFY_BAD는 "에러가 발생했으며 발행자가 이벤트를 취소해야 함"입니다. blocking_notifier_call_chain()의 반환값에 notifier_to_errno()를 적용하면 에러 코드를 추출할 수 있습니다.

등록 시 우선순위 정렬

/* 등록 시 우선순위 내림차순으로 연결 리스트에 삽입 */
static int notifier_chain_register(struct notifier_block **nl,
                                     struct notifier_block *n)
{
    while ((*nl) != NULL) {
        if ((*nl)->priority < n->priority)
            break;   /* 삽입 위치 발견 */
        nl = &((*nl)->next);
    }
    n->next = *nl;
    rcu_assign_pointer(*nl, n);
    return 0;
}
/* → 등록 시마다 O(n) 탐색, 호출 시 순서 보장 */
우선순위 관례: 커널 내부 코드는 일반적으로 우선순위 0(기본값)을 사용합니다. 드라이버가 특정 핸들러보다 앞서 처리해야 하면 양수 값을, 나중에 처리해야 하면 음수 값을 사용하세요. 동일 우선순위의 등록 순서는 LIFO(나중 등록이 먼저 호출)입니다.

우선순위 정렬 구조 다이어그램

Notifier Block 우선순위 정렬 및 삽입 과정 1단계: 초기 체인 nb_A pri=200 nb_B pri=100 nb_C pri=0 → NULL 2단계: nb_D (pri=150) 삽입 nb_D (신규) pri=150 nb_A (200) 200 > 150 → 넘김 nb_B (100) 100 < 150 → 여기! 삽입 위치 발견 3단계: 삽입 후 체인 nb_A pri=200 nb_D pri=150 nb_B pri=100 nb_C pri=0 → NULL 호출 순서: A(200)→D(150)→B(100)→C(0) 동일 우선순위 (pri=0) 등록 순서 영향 등록 순서: X(pri=0) → Y(pri=0) → Z(pri=0) 체인 순서: Z → Y → X → NULL (동일 우선순위: 나중 등록이 앞에 = LIFO) 주의사항 모듈 로드 순서에 의존하는 설계는 취약합니다. 명시적 우선순위를 사용하세요. 알고리즘 복잡도: 등록 O(n), 호출 O(n), 해제 O(n) — n = 체인의 notifier_block 수

등록/해제 생명주기 다이어그램

Notifier Block 등록/해제 생명주기 등록 경로 (Register) 1. struct notifier_block 정의 .notifier_call = 콜백, .priority = N 2. *_notifier_chain_register(head, nb) 잠금 획득 → 우선순위 정렬 삽입 → 잠금 해제 활성 상태 (Active) 이벤트 발생 시 콜백 호출됨 nb 구조체는 반드시 유효해야 함 해제 경로 (Unregister) 4. *_notifier_chain_unregister(head, nb) 잠금 획득 → 리스트에서 제거 → 잠금 해제 5. Grace Period 대기 (atomic/srcu) RCU/SRCU: 진행 중인 콜백 완료 보장 비활성 상태 (Inactive) nb 구조체 재사용/해제 안전 모듈 언로드 가능 모듈 exit 해제 누락 시 위험 시나리오 모듈 A: nb 등록 모듈 A 언로드 (unregister 누락!) 이벤트 발생 → 댕글링 포인터 → 커널 패닉 (NULL deref / UAF) 올바른 해제 패턴 module_exit 함수에서: 1. unregister_*_notifier(&nb); 2. 반환값 확인 (0 = 성공) 3. 이후에만 모듈 리소스 해제 체크리스트 - __exit 함수에 unregister 확인 - devm_* 래퍼 사용 고려 (자동 해제) - 에러 경로에서도 해제 보장 (goto err)

커널 주요 Notifier Chain 목록

전원 관리 (Blocking)

/* kernel/power/main.c: PM 상태 전환 알림 */
int register_pm_notifier(struct notifier_block *nb);
int unregister_pm_notifier(struct notifier_block *nb);

/* PM 이벤트 값 */
// PM_HIBERNATION_PREPARE : 하이버네이션 준비
// PM_POST_HIBERNATION    : 하이버네이션 복구 후
// PM_SUSPEND_PREPARE     : 서스펜드 진입 전
// PM_POST_SUSPEND        : 서스펜드 복구 후

static int my_pm_cb(struct notifier_block *nb,
                     unsigned long event, void *data)
{
    if (event == PM_SUSPEND_PREPARE)
        flush_my_work();  /* 서스펜드 전 작업 정리 */
    return NOTIFY_OK;
}

네트워크 디바이스 (Atomic)

/* net/core/dev.c: 네트워크 디바이스 이벤트 */
int register_netdevice_notifier(struct notifier_block *nb);
int unregister_netdevice_notifier(struct notifier_block *nb);

/* 이벤트: NETDEV_UP, NETDEV_DOWN, NETDEV_REGISTER, NETDEV_UNREGISTER, */
/*         NETDEV_CHANGE, NETDEV_CHANGEMTU, NETDEV_CHANGEADDR 등 */

static int my_netdev_cb(struct notifier_block *nb,
                          unsigned long event, void *ptr)
{
    struct net_device *dev = netdev_notifier_info_to_dev(ptr);
    if (event == NETDEV_UP)
        pr_info("%s is up\n", dev->name);
    return NOTIFY_DONE;
}

CPU 핫플러그 (SRCU 기반 state machine)

/* Linux 4.10+: cpuhp state machine으로 교체됨 */
/* 직접 notifier 대신 cpuhp_setup_state() 사용 권장 */
int cpuhp_setup_state(enum cpuhp_state state,
                       const char *name,
                       int (*startup)(unsigned int cpu),
                       int (*teardown)(unsigned int cpu));

/* 예제 */
static int my_cpu_online(unsigned int cpu)
{
    pr_info("CPU %u online\n", cpu);
    return 0;
}

static int __init my_init(void)
{
    return cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "my:online",
                              my_cpu_online, NULL);
}
CPU 핫플러그 진화: Linux 4.10부터 기존 register_cpu_notifier()는 deprecated되었고, cpuhp_setup_state() 기반 state machine으로 전환되었습니다. 내부적으로 여전히 SRCU Notifier를 사용합니다.

CPU Hotplug Notifier 실행 시퀀스

CPU Hotplug State Machine — Notifier 실행 시퀀스 CPU Online 경로 CPUHP_OFFLINE (초기 상태) CPUHP_BRINGUP_CPU CPUHP_AP_ONLINE (startup 콜백) CPUHP_AP_ONLINE_DYN (드라이버) CPUHP_ONLINE (최종) CPU Offline 경로 CPUHP_ONLINE (시작) CPUHP_AP_ONLINE_DYN (teardown) CPUHP_TEARDOWN_CPU IRQ 마이그레이션 CPUHP_OFFLINE (최종) cpuhp_setup_state() 등록 구조 cpuhp_setup_state() 호출 state = CPUHP_AP_ONLINE_DYN name = "mydrv:online" startup = my_cpu_online(cpu) teardown = my_cpu_offline(cpu) 반환값: 동적 할당된 state 번호 내부 동작 1. cpuhp_state[] 배열에 콜백 등록 2. 기존 온라인 CPU에 startup 즉시 실행 3. 이후 CPU online 시 자동 호출 4. CPU offline 시 teardown 자동 호출 5. cpuhp_remove_state()로 해제

CPU Hotplug 실전 코드

/* 완전한 CPU Hotplug 드라이버 예제 */
#include <linux/cpuhotplug.h>
#include <linux/percpu.h>

static DEFINE_PER_CPU(struct my_cpu_data, cpu_data);
static enum cpuhp_state hp_state;

static int my_cpu_online(unsigned int cpu)
{
    struct my_cpu_data *data = per_cpu_ptr(&cpu_data, cpu);

    pr_info("CPU %u online: 리소스 초기화\n", cpu);
    data->active = true;
    data->counter = 0;
    return 0;  /* 0: 성공, 음수: 실패 → CPU online 중단 */
}

static int my_cpu_offline(unsigned int cpu)
{
    struct my_cpu_data *data = per_cpu_ptr(&cpu_data, cpu);

    pr_info("CPU %u offline: 리소스 정리\n", cpu);
    data->active = false;
    flush_my_cpu_work(cpu);  /* CPU별 작업 완료 */
    return 0;
}

static int __init my_init(void)
{
    int ret;

    /* CPUHP_AP_ONLINE_DYN: 동적 state 번호 할당
     * 등록 즉시 모든 온라인 CPU에 startup 콜백 실행 */
    ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                              "mydrv:online",
                              my_cpu_online,
                              my_cpu_offline);
    if (ret < 0)
        return ret;

    hp_state = ret;  /* 해제 시 필요한 state 번호 저장 */
    return 0;
}

static void __exit my_exit(void)
{
    /* 모든 온라인 CPU에 teardown 콜백 실행 후 등록 해제 */
    cpuhp_remove_state(hp_state);
}

Netdevice Notifier 실전 사용 흐름

Netdevice Notifier — 이벤트 발생부터 콜백까지 이벤트 원인 ip link set eth0 up ip addr add 10.0.0.1/24 MTU 변경 / MAC 변경 디바이스 등록/해제 net/core/dev.c call_netdevice_notifiers() → raw_notifier_call_chain() netdev_chain (raw) 구독자 1: 브리지 모듈 구독자 2: bonding 드라이버 구독자 3: VLAN 모듈 구독자 4: 사용자 드라이버 주요 Netdevice 이벤트 (include/linux/netdevice.h) 상태 변경 NETDEV_UP — 인터페이스 활성화 NETDEV_DOWN — 인터페이스 비활성화 NETDEV_CHANGE — 링크 상태 변경 NETDEV_GOING_DOWN — 비활성화 직전 NETDEV_FEAT_CHANGE — 기능 변경 콜백: netdev_notifier_info └→ struct net_device *dev └→ struct netlink_ext_ack 속성 변경 NETDEV_CHANGEMTU — MTU 변경 NETDEV_CHANGEADDR — MAC 주소 변경 NETDEV_CHANGENAME — 이름 변경 NETDEV_PRE_TYPE_CHANGE NETDEV_POST_TYPE_CHANGE 생명주기 NETDEV_REGISTER — 디바이스 등록 NETDEV_UNREGISTER — 디바이스 해제 NETDEV_PRE_UP — 활성화 직전 NETDEV_RELEASE — 디바이스 해제 중 NETDEV_JOIN — 브리지 합류 주의: netdev_chain은 실제로 raw_notifier_head — 네트워크 서브시스템이 RTNL 잠금으로 직렬화

Netdevice Notifier 완전한 예제

/* 네트워크 디바이스 이벤트 전체 처리 예제 */
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/notifier.h>

static int my_netdev_event(struct notifier_block *nb,
                            unsigned long event, void *ptr)
{
    struct net_device *dev = netdev_notifier_info_to_dev(ptr);

    /* 특정 디바이스만 필터링 */
    if (!(dev->flags & IFF_UP) && event != NETDEV_UP)
        return NOTIFY_DONE;

    switch (event) {
    case NETDEV_REGISTER:
        pr_info("[%s] 디바이스 등록됨\n", dev->name);
        break;
    case NETDEV_UP:
        pr_info("[%s] 인터페이스 UP (MTU=%d)\n",
                dev->name, dev->mtu);
        /* 필터 규칙 설치 등 */
        break;
    case NETDEV_DOWN:
        pr_info("[%s] 인터페이스 DOWN\n", dev->name);
        /* 필터 규칙 제거 등 */
        break;
    case NETDEV_CHANGEMTU:
        pr_info("[%s] MTU 변경: %d\n", dev->name, dev->mtu);
        break;
    case NETDEV_UNREGISTER:
        pr_info("[%s] 디바이스 해제 중\n", dev->name);
        break;
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_netdev_nb = {
    .notifier_call = my_netdev_event,
    .priority      = 0,    /* 기본 우선순위 */
};

static int __init my_init(void)
{
    return register_netdevice_notifier(&my_netdev_nb);
}

static void __exit my_exit(void)
{
    unregister_netdevice_notifier(&my_netdev_nb);
}
module_init(my_init);
module_exit(my_exit);
register_netdevice_notifier의 특수 동작: 등록 시 기존에 이미 등록된 모든 네트워크 디바이스에 대해 NETDEV_REGISTER + NETDEV_UP 이벤트를 즉시 재생(replay)합니다. 이를 통해 등록 시점에 이미 존재하는 디바이스도 빠짐없이 처리할 수 있습니다.

PM Notifier 실전 상세 예제

/* 전원 관리 Notifier — 서스펜드/하이버네이션 전후 처리 */
#include <linux/suspend.h>
#include <linux/notifier.h>

static int my_pm_notifier(struct notifier_block *nb,
                           unsigned long action, void *data)
{
    switch (action) {
    case PM_HIBERNATION_PREPARE:
        pr_info("하이버네이션 준비 중\n");
        my_save_state();
        break;
    case PM_SUSPEND_PREPARE:
        pr_info("서스펜드 준비 중\n");
        my_flush_buffers();
        my_disable_polling();
        break;
    case PM_POST_SUSPEND:
        pr_info("서스펜드 복귀\n");
        my_enable_polling();
        my_restore_state();
        break;
    case PM_POST_HIBERNATION:
        pr_info("하이버네이션 복귀\n");
        my_restore_state();
        break;
    case PM_RESTORE_PREPARE:
        /* 하이버네이션 이미지 복원 직전 */
        break;
    case PM_POST_RESTORE:
        /* 하이버네이션 이미지 복원 실패 시 */
        break;
    }
    return NOTIFY_OK;
}

static struct notifier_block my_pm_nb = {
    .notifier_call = my_pm_notifier,
    .priority      = 0,
};

/* 등록/해제 */
register_pm_notifier(&my_pm_nb);
unregister_pm_notifier(&my_pm_nb);

Reboot Notifier 실전 상세 예제

/* Reboot Notifier — 시스템 재부팅/종료/halt 처리
 * kernel/reboot.c에서 restart_handler_list 관리
 * 이 체인은 blocking_notifier_head (restart_handler) 또는
 * raw_notifier_head (reboot_notifier_list) 사용 */
#include <linux/reboot.h>

static int my_reboot_handler(struct notifier_block *nb,
                              unsigned long action, void *data)
{
    switch (action) {
    case SYS_RESTART:
        pr_emerg("시스템 재시작 — 하드웨어 안전 종료\n");
        my_hw_safe_shutdown();
        my_flush_nvram();      /* 비휘발 메모리 플러시 */
        break;
    case SYS_HALT:
        pr_emerg("시스템 정지\n");
        my_hw_safe_shutdown();
        break;
    case SYS_POWER_OFF:
        pr_emerg("시스템 전원 끄기\n");
        my_hw_power_down_sequence();
        break;
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_reboot_nb = {
    .notifier_call = my_reboot_handler,
    .priority      = 200,  /* 높은 우선순위 — 먼저 실행 */
};

/* register_reboot_notifier()는 내부적으로
 * blocking_notifier_chain_register(&reboot_notifier_list, nb) 호출 */
register_reboot_notifier(&my_reboot_nb);
unregister_reboot_notifier(&my_reboot_nb);

기타 주요 Notifier Chain

체인등록 함수이벤트 예시변형
Rebootregister_reboot_notifier()SYS_RESTART, SYS_HALTraw
inet 주소 변경register_inetaddr_notifier()NETDEV_UP, NETDEV_DOWNblocking
inet6 주소 변경register_inet6addr_notifier()NETDEV_UP, NETDEV_DOWNblocking
클럭 변경clk_notifier_register()PRE_RATE_CHANGE, POST_RATE_CHANGEblocking
Regulator 변경regulator_register_notifier()REGULATOR_EVENT_ENABLEblocking
디스플레이 패널드라이버별 register_notifierPANEL_EVENT_BLANK, UNBLANKblocking
PANICatomic_notifier_chain_register(&panic_notifier_list, nb)0 (단일 이벤트)atomic
FB (Framebuffer)fb_register_client()FB_EVENT_BLANK, SUSPENDblocking

inet 주소 변경 알림 예제

/* 네트워크 인터페이스 IP 주소 변경 감지 */
#include <linux/inetdevice.h>

static int my_inetaddr_cb(struct notifier_block *nb,
                             unsigned long event, void *ptr)
{
    struct in_ifaddr *ifa = ptr;
    struct net_device *dev = ifa->ifa_dev->dev;

    switch (event) {
    case NETDEV_UP:
        pr_info("%s: IP %pI4 added\n", dev->name, &ifa->ifa_address);
        break;
    case NETDEV_DOWN:
        pr_info("%s: IP %pI4 removed\n", dev->name, &ifa->ifa_address);
        break;
    }
    return NOTIFY_OK;
}

static struct notifier_block my_inetaddr_nb = {
    .notifier_call = my_inetaddr_cb,
};

register_inetaddr_notifier(&my_inetaddr_nb);

PANIC Notifier 예제

/* 커널 패닉 직전에 호출 — atomic 컨텍스트 */
static int my_panic_cb(struct notifier_block *nb,
                         unsigned long val, void *data)
{
    const char *msg = data;  /* 패닉 메시지 문자열 */

    /* 하드웨어 상태 레지스터 덤프 등 긴급 처리 */
    my_hw_emergency_dump();

    return NOTIFY_DONE;  /* NOTIFY_STOP 금지: 패닉을 막을 수 없음 */
}

static struct notifier_block my_panic_nb = {
    .notifier_call = my_panic_cb,
    .priority      = 200,  /* 높은 우선순위로 먼저 실행 */
};

/* panic_notifier_list는 atomic_notifier_head */
atomic_notifier_chain_register(&panic_notifier_list, &my_panic_nb);

성능 고려사항

항목내용
등록 비용O(n) — 우선순위 정렬 삽입, 등록은 드물게 발생하므로 무시 가능
호출 비용O(n) 콜백 수 — 콜백이 많을수록 지연 증가
Blocking 오버헤드rwsem 읽기 획득/해제 (경합 없을 때 매우 빠름)
Atomic 오버헤드스핀락 획득/해제 (IRQ 비활성화 포함)
SRCU 오버헤드srcu_read_lock/unlock — 빠른 경로지만 grace period 비용
콜백 최소화: Notifier Chain 콜백은 이벤트 경로에서 동기적으로 실행됩니다. 콜백이 오래 걸리면 전체 시스템의 이벤트 처리 지연이 발생합니다. 무거운 작업은 work_queue로 위임하고 콜백에서는 플래그만 설정하세요.

디버깅

ftrace로 Notifier Chain 추적

ftracefunction 트레이서와 function_graph 트레이서를 사용하면 notifier chain 콜백의 호출 순서와 소요 시간을 정밀하게 추적할 수 있습니다.

# 1. notifier_call_chain 함수 추적
echo 'notifier_call_chain' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

# 2. function_graph로 콜백 호출 계층 확인
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'blocking_notifier_call_chain' > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 이벤트 발생 후 확인
cat /sys/kernel/debug/tracing/trace

# 출력 예시:
# | blocking_notifier_call_chain() {
#   | down_read() { ... }
#   | notifier_call_chain() {
#     | my_pm_notifier() { ... }        ← 콜백 1
#     | driver_xyz_notifier() { ... }    ← 콜백 2
#   }
#   | up_read() { ... }
# }

# 3. 특정 notifier chain만 필터링 (예: PM)
echo 'pm_notifier_call_chain' > /sys/kernel/debug/tracing/set_ftrace_filter
echo funcgraph-duration > /sys/kernel/debug/tracing/trace_options
echo funcgraph-proc > /sys/kernel/debug/tracing/trace_options

# 4. 추적 종료 및 정리
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo nop > /sys/kernel/debug/tracing/current_tracer

lockdep을 이용한 잠금 순서 검증

# 커널 빌드 옵션 (디버그 커널에서 활성화)
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_PROVE_LOCKING=y
CONFIG_LOCKDEP=y

# lockdep이 검출하는 Notifier Chain 관련 문제:
# 1. blocking_notifier 콜백에서 같은 체인의 register/unregister → 데드락
# 2. atomic_notifier 콜백에서 sleep 함수 호출 → BUG: scheduling while atomic
# 3. 중첩된 notifier chain 호출의 잠금 역순 → lockdep 경고

# lockdep 통계 확인
cat /proc/lockdep_stats
# lock_stat으로 경합 분석
cat /proc/lock_stat | grep notifier

printk를 이용한 콜백 디버깅

/* 콜백 진입/종료 추적 패턴 */
static int my_debug_cb(struct notifier_block *nb,
                         unsigned long event, void *data)
{
    pr_debug("[%s] event=%lu data=%p\n", __func__, event, data);

    /* 콜백 소요 시간 측정 */
    ktime_t start = ktime_get();

    /* ... 실제 처리 ... */

    pr_debug("[%s] took %lld ns\n", __func__,
             ktime_to_ns(ktime_sub(ktime_get(), start)));

    return NOTIFY_OK;
}

/* dynamic debug로 런타임에 활성화/비활성화 */
/* echo 'func my_debug_cb +p' > /sys/kernel/debug/dynamic_debug/control */

변형 선택 가이드

Notifier Chain 타입 선택 가이드 시작 IRQ/인터럽트 컨텍스트에서 이벤트를 발생시키시겠습니까? (인터럽트 핸들러 내부에서 notifier_call_chain 호출) atomic 아니오 콜백 A 실행 중 다른 콜백 B가 체인에 등록/해제할 수 있어야 합니까? (예: CPU 핫플러그 — CPU.online 콜백 중 offline 핸들러 등록) srcu 아니오 시스템 초기화 단계이거나, 이미 외부에서 동기화가 보장된 특수 경로입니까? (호출자가 잠금을 직접 관리하거나, 단일 스레드 초기화 단계) raw (잠금 없음) 아니오 blocking (rwsem) 기본값: blocking — 대부분의 경우 이 타입을 사용하세요

주의사항 및 흔한 실수

실수 1 — 등록 해제 누락: 모듈 언로드 시 unregister_*_notifier()를 호출하지 않으면, 해제된 메모리의 콜백 포인터가 남아 있어 이후 이벤트 발생 시 커널 패닉이 발생합니다. 이는 가장 흔하면서도 치명적인 버그입니다.
실수 2 — Atomic 콜백에서 sleep: atomic_notifier_call_chain()은 RCU read-side critical section에서 실행됩니다. 콜백 내에서 mutex_lock(), kmalloc(GFP_KERNEL), msleep(), schedule() 등 슬립을 유발하는 함수를 호출하면 BUG: scheduling while atomic 패닉이 발생합니다.
/* 잘못된 예: atomic notifier 콜백에서 sleep */
static int bad_atomic_cb(struct notifier_block *nb,
                           unsigned long event, void *data)
{
    struct my_data *d = kmalloc(sizeof(*d), GFP_KERNEL); /* BUG! sleep 가능 */
    mutex_lock(&my_mutex);   /* BUG! sleep 가능 */
    return NOTIFY_OK;
}

/* 올바른 예: atomic 안전한 할당 사용 */
static int good_atomic_cb(struct notifier_block *nb,
                            unsigned long event, void *data)
{
    struct my_data *d = kmalloc(sizeof(*d), GFP_ATOMIC); /* OK: 비수면 */
    spin_lock(&my_lock);     /* OK: 비수면 */
    /* 또는: 무거운 작업은 workqueue로 위임 */
    schedule_work(&my_work); /* OK: 작업 예약만 함 */
    return NOTIFY_OK;
}
실수 3 — Blocking Notifier에서 재진입: blocking_notifier_call_chain()은 내부적으로 rwsem 읽기 잠금을 획득합니다. 콜백 내에서 같은 체인에 register/unregister를 시도하면 쓰기 잠금을 요청하게 되어 데드락이 발생합니다. 이 경우 SRCU Notifier를 사용해야 합니다.
실수 4 — 콜백에서 무한 루프 유발: Notifier 콜백 A가 이벤트를 발생시켜 같은 체인을 재호출하면 무한 재귀가 발생합니다. 특히 netdevice notifier에서 콜백 내에서 dev_change_flags()를 호출하면 NETDEV_CHANGE 이벤트가 다시 발생하여 순환 호출이 됩니다.
/* 잘못된 예: 콜백에서 같은 체인 이벤트 재발생 */
static int bad_recursive_cb(struct notifier_block *nb,
                              unsigned long event, void *data)
{
    struct net_device *dev = netdev_notifier_info_to_dev(data);
    if (event == NETDEV_UP) {
        /* 위험! 이 호출이 NETDEV_CHANGE를 발생시킬 수 있음 */
        dev_change_flags(dev, dev->flags | IFF_PROMISC, NULL);
    }
    return NOTIFY_DONE;
}

/* 올바른 예: workqueue로 위임 */
static int good_deferred_cb(struct notifier_block *nb,
                              unsigned long event, void *data)
{
    if (event == NETDEV_UP) {
        /* 비동기로 처리하여 재귀 회피 */
        schedule_work(&my_promisc_work);
    }
    return NOTIFY_DONE;
}
실수 5 — NOTIFY_STOP 남용: 이벤트를 처리한 첫 번째 콜백이 NOTIFY_STOP을 반환하면 이후 콜백은 호출되지 않습니다. 이는 우선순위가 높은 핸들러가 이벤트를 독점적으로 처리할 때 유용하지만, 다른 모듈의 정상적인 이벤트 수신을 방해할 수 있습니다. NOTIFY_STOP은 정확한 의미를 이해한 경우에만 사용하세요.
실수 6 — 등록 순서에 의존: 모듈 로드 순서에 따라 콜백 실행 순서가 달라지는 설계는 취약합니다. 실행 순서가 중요하다면 반드시 priority 필드를 명시적으로 설정하세요. 동일 우선순위의 등록 순서는 LIFO이므로 모듈 로드 순서에 따라 결과가 달라질 수 있습니다.

흔한 실수 요약

실수증상해결 방법
unregister 누락모듈 언로드 후 커널 패닉 (UAF)module_exit에서 반드시 unregister
atomic 콜백에서 sleepBUG: scheduling while atomicGFP_ATOMIC / workqueue 위임
blocking 콜백에서 재등록데드락 (rwsem R→W)SRCU notifier 사용
콜백에서 같은 체인 이벤트 발생무한 재귀 / 스택 오버플로우workqueue로 비동기 처리
NOTIFY_STOP 남용다른 구독자 이벤트 수신 불가NOTIFY_OK / NOTIFY_DONE 사용
priority 미지정모듈 로드 순서에 의존적 동작명시적 priority 값 설정
스택 변수에 nb 선언함수 반환 후 댕글링 포인터static 또는 동적 할당
콜백 소요시간 과다이벤트 경로 지연, 시스템 지연최소한의 처리, 나머지 위임
다음 학습: