Notifier Chain (알림 체인(Notifier Chain))

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

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

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

핵심 요약

  • notifier_block — 구독자 콜백을 담는 기본 구조체 (priority 필드로 호출 순서 결정)
  • blocking_notifier_head — rwsem 보호, 슬립(Sleep) 가능 컨텍스트 (가장 일반적)
  • atomic_notifier_head — 스핀락 보호, 인터럽트(Interrupt) 핸들러(Handler) 내 사용 가능
  • 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의 단방향 연결 리스트(Linked List)입니다. 발행자는 이벤트 번호와 데이터를 인자로 체인을 순회하며 각 콜백을 호출합니다.

발행-구독 아키텍처

Notifier Chain은 발행-구독(Publish-Subscribe) 패턴을 커널 내부에서 구현한 것입니다. 이벤트 소스(발행자)가 체인 헤드를 통해 이벤트를 발행하면, 등록된 모든 구독자 콜백이 우선순위 순서대로 호출됩니다. 각 체인 헤드는 서로 다른 잠금 메커니즘을 사용하여 동시성을 제어합니다. 아래 다이어그램은 커널 전반의 주요 이벤트 소스가 어떻게 체인 헤드를 거쳐 구독자에게 전달되는지를 보여줍니다.

Notifier Chain 발행-구독(Pub-Sub) 아키텍처 이벤트 소스 (Publisher) CPU Hotplug Power Management Network Subsystem Memory Hotplug Reboot / Shutdown 체인 헤드 (Notifier Head) cpu_chain [SRCU] pm_chain_head [Blocking] netdev_chain [Atomic] memory_chain [Blocking] reboot_notifier [Raw] 구독자 (Subscriber Callbacks) slab_memory_callback() workqueue_cpu_up/down_cb() my_pm_handler() cpufreq_bp_suspend/resume() fib_netdev_event() [routing] bonding_netdev_event() slab_mem_going_online_cb() virtio_mem_memory_notifier() my_reboot_cb() [device flush] 데이터 흐름 call_chain(head, event_number, void *data) event_number: unsigned long (이벤트 종류) | data: 이벤트별 구조체 포인터 잠금 타입 범례 SRCU Blocking (rwsem) Atomic (spinlock+RCU) Raw (잠금 없음)

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₃

4종 내부 잠금 흐름 비교

아래 다이어그램은 4가지 Notifier Chain 변형이 등록(register), 호출(call_chain), 해제(unregister) 경로에서 각각 어떤 잠금을 획득하고 해제하는지를 병렬로 비교합니다. 같은 작업이라도 변형에 따라 잠금 경로가 완전히 다르며, 이 차이가 사용 가능한 컨텍스트와 제약 조건을 결정합니다.

4종 Notifier Chain — 등록/호출/해제 잠금 흐름 비교 Blocking Atomic Raw SRCU 등록 (register) down_write(&rwsem) 리스트 정렬 삽입 up_write(&rwsem) spin_lock_irqsave() rcu_assign_pointer spin_unlock_irqrestore() (잠금 없음) 리스트 정렬 삽입 호출자가 직렬화 보장 mutex_lock() rcu_assign_pointer mutex_unlock() 호출 (call_chain) down_read(&rwsem) nb->notifier_call() sleep 가능 같은 체인 수정 불가 up_read(&rwsem) rcu_read_lock() rcu_dereference(head) sleep 불가 스핀락 없이 순회 rcu_read_unlock() (잠금 없음) 직접 리스트 순회 제한 없음 콜백 중 등록 가능 동시성 보장 없음 idx = srcu_read_lock() nb->notifier_call() sleep 가능 콜백 중 등록/해제 가능 srcu_read_unlock(idx) 해제 (unregister) down_write(&rwsem) 리스트에서 제거 up_write(&rwsem) 즉시 안전 (grace period 불필요) spin_lock_irqsave() 리스트에서 제거 spin_unlock_irqrestore() RCU grace period 후 nb 해제 안전 (잠금 없음) 리스트에서 제거 호출자가 안전성 보장 동시 순회 중이면 위험 mutex_lock() 리스트에서 제거 mutex_unlock() synchronize_srcu() 대기 핵심 특성 읽기-쓰기 분리 잠금 다수 호출 병렬 가능 프로세스 컨텍스트 전용 읽기: 스핀락 없음 (RCU) 쓰기만 직렬화 IRQ 컨텍스트 허용 완전 무잠금 최소 오버헤드 호출자 책임 100% Per-CPU 카운터 추적 콜백 중 체인 수정 안전 메모리 비용 최대 선택 가이드 요약 프로세스 컨텍스트 + 단순 이벤트 → Blocking | IRQ 컨텍스트 필요 → Atomic | 외부 잠금 존재 → Raw | 콜백 중 체인 변경 → SRCU 확실하지 않으면 Blocking을 선택하세요 — 가장 안전하고 일반적인 변형입니다

커스텀 Notifier Chain 구현

기존 커널 체인을 사용하는 것 외에, 자체 서브시스템용 Notifier Chain을 처음부터 설계하고 구현하는 방법을 설명합니다. 발행자 모듈이 체인 헤드를 소유하고, 별도의 구독자 모듈이 콜백을 등록하는 완전한 예제입니다.

발행자 모듈 (Publisher)

/* my_publisher.c — 커스텀 Notifier Chain 발행자 모듈 */
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <linux/timer.h>

/* === 이벤트 정의 (헤더 파일로 분리 권장) === */
#define MY_EVENT_TEMP_HIGH    0x01  /* 온도 상한 초과 */
#define MY_EVENT_TEMP_NORMAL  0x02  /* 온도 정상 복귀 */
#define MY_EVENT_TEMP_CRIT    0x03  /* 온도 임계 초과 */

/* 이벤트 데이터 구조체 */
struct my_temp_event {
    int           sensor_id;    /* 센서 번호 */
    int           temperature;  /* 밀리섭씨 (mC) */
    int           threshold;    /* 트리거 임계값 */
    ktime_t       timestamp;    /* 이벤트 발생 시간 */
};

/* === 체인 헤드: 정적 초기화 === */
static BLOCKING_NOTIFIER_HEAD(my_temp_chain);

/* === 외부 모듈용 등록/해제 API === */
int my_temp_register_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_register(&my_temp_chain, nb);
}
EXPORT_SYMBOL_GPL(my_temp_register_notifier);

int my_temp_unregister_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_unregister(&my_temp_chain, nb);
}
EXPORT_SYMBOL_GPL(my_temp_unregister_notifier);

/* === 이벤트 발행 함수 === */
static int my_temp_notify(unsigned long event,
                           struct my_temp_event *data)
{
    int ret;

    data->timestamp = ktime_get();
    ret = blocking_notifier_call_chain(&my_temp_chain, event, data);
    ret = notifier_to_errno(ret);

    if (ret)
        pr_warn("temp notifier returned error: %d\n", ret);

    return ret;
}

/* === 시뮬레이션: 주기적 온도 이벤트 발생 === */
static struct delayed_work temp_work;
static int simulated_temp = 45000; /* 45.000도C */

static void temp_check_worker(struct work_struct *work)
{
    struct my_temp_event evt = {
        .sensor_id   = 0,
        .temperature = simulated_temp,
        .threshold   = 85000,  /* 85도C */
    };

    if (simulated_temp > 95000)
        my_temp_notify(MY_EVENT_TEMP_CRIT, &evt);
    else if (simulated_temp > 85000)
        my_temp_notify(MY_EVENT_TEMP_HIGH, &evt);

    /* 시뮬레이션: 온도 점진적 상승 */
    simulated_temp += 5000;
    if (simulated_temp > 100000)
        simulated_temp = 45000;

    schedule_delayed_work(&temp_work, msecs_to_jiffies(5000));
}

static int __init my_publisher_init(void)
{
    pr_info("my_publisher: temperature notifier chain initialized\n");
    INIT_DELAYED_WORK(&temp_work, temp_check_worker);
    schedule_delayed_work(&temp_work, msecs_to_jiffies(1000));
    return 0;
}

static void __exit my_publisher_exit(void)
{
    cancel_delayed_work_sync(&temp_work);
    pr_info("my_publisher: unloaded\n");
}

module_init(my_publisher_init);
module_exit(my_publisher_exit);
MODULE_LICENSE("GPL");

구독자 모듈 (Subscriber)

/* my_subscriber.c — 발행자의 체인에 등록하는 구독자 모듈 */
#include <linux/module.h>
#include <linux/notifier.h>

/* 발행자 헤더 (실제로는 별도 .h 파일) */
extern int my_temp_register_notifier(struct notifier_block *nb);
extern int my_temp_unregister_notifier(struct notifier_block *nb);

#define MY_EVENT_TEMP_HIGH    0x01
#define MY_EVENT_TEMP_NORMAL  0x02
#define MY_EVENT_TEMP_CRIT    0x03

struct my_temp_event {
    int     sensor_id;
    int     temperature;
    int     threshold;
    ktime_t timestamp;
};

/* === 콜백 구현 === */
static int my_temp_handler(struct notifier_block *nb,
                            unsigned long event, void *data)
{
    struct my_temp_event *evt = data;

    switch (event) {
    case MY_EVENT_TEMP_HIGH:
        pr_warn("[subscriber] sensor %d: HIGH %d.%03d C (threshold %d.%03d)\n",
                evt->sensor_id,
                evt->temperature / 1000, evt->temperature % 1000,
                evt->threshold / 1000, evt->threshold % 1000);
        /* 예: 팬 속도 증가, 클럭 스로틀링 등 */
        break;

    case MY_EVENT_TEMP_CRIT:
        pr_emerg("[subscriber] sensor %d: CRITICAL %d.%03d C!\n",
                 evt->sensor_id,
                 evt->temperature / 1000, evt->temperature % 1000);
        /* 긴급 조치: 워크로드 중단, 안전 종료 시작 */
        return NOTIFY_BAD;  /* 발행자에게 위험 상태 알림 */

    case MY_EVENT_TEMP_NORMAL:
        pr_info("[subscriber] sensor %d: temperature normalized\n",
                evt->sensor_id);
        break;
    }
    return NOTIFY_OK;
}

/* notifier_block은 반드시 static (스택 변수 금지!) */
static struct notifier_block my_temp_nb = {
    .notifier_call = my_temp_handler,
    .priority      = 100,  /* 다른 구독자보다 먼저 실행 */
};

static int __init my_subscriber_init(void)
{
    int ret = my_temp_register_notifier(&my_temp_nb);
    if (ret) {
        pr_err("[subscriber] failed to register: %d\n", ret);
        return ret;
    }
    pr_info("[subscriber] registered with priority %d\n",
            my_temp_nb.priority);
    return 0;
}

static void __exit my_subscriber_exit(void)
{
    my_temp_unregister_notifier(&my_temp_nb);
    pr_info("[subscriber] unregistered\n");
}

module_init(my_subscriber_init);
module_exit(my_subscriber_exit);
MODULE_LICENSE("GPL");
설계 요점 정리:
  • API 캡슐화: 체인 헤드를 직접 노출하지 않고, register/unregister 래퍼 함수를 EXPORT_SYMBOL_GPL로 공개합니다. 이렇게 하면 체인 타입 변경(예: blocking에서 SRCU로) 시 구독자 코드를 수정할 필요가 없습니다.
  • 이벤트 데이터 구조체: void *data에 전달할 구조체를 명확히 정의합니다. 발행자와 구독자가 같은 헤더를 공유해야 합니다.
  • 에러 전파: 발행자는 notifier_to_errno()로 반환값을 확인하여, 구독자가 NOTIFY_BAD를 반환한 경우 적절한 조치를 취합니다.
  • 정적 할당: struct notifier_block은 반드시 static이나 동적 할당(kmalloc)이어야 합니다. 스택 변수는 함수 반환 후 무효가 됩니다.

Blocking Notifier Chain

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

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); /* 반드시 해제! */
}

rwsem 잠금 동작 상세

Blocking notifier는 내부적으로 rw_semaphore(읽기-쓰기 세마포어)를 사용합니다. 이벤트 발행 시 down_read()로 읽기 잠금을 획득하고 체인을 순회합니다. 구독자 등록/해제 시에는 down_write()로 쓰기 잠금을 획득합니다. 읽기 잠금은 여러 CPU에서 동시에 획득할 수 있어 이벤트 발행의 병렬성이 보장됩니다. 그러나 쓰기 잠금은 모든 읽기 잠금이 해제될 때까지 대기해야 하므로, 콜백 실행 중 등록/해제가 지연될 수 있습니다.

Blocking Notifier — rwsem 동시성 타임라인 시간 → CPU 0 (call_chain) down_read 콜백 순회 (nb₁→nb₂→nb₃) up_read CPU 1 (call_chain) down_read 콜백 순회 (동시 실행) up_read CPU 2 (register) down_write 대기 (readers 완료 대기) 리스트 수정 + up_write 동시 읽기 허용 구간 마지막 reader 해제 후 rwsem 동작 규칙 1. down_read(): 여러 CPU가 동시 획득 가능 → call_chain 병렬 실행 2. down_write(): 모든 reader가 해제될 때까지 대기 → register/unregister 직렬화 3. writer 대기 중에도 새 reader는 진입 가능 (writer starvation 주의) 4. 콜백에서 같은 체인의 register/unregister 호출 → rwsem read→write 데드락!

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() 등 슬립을 유발하는 함수를 호출하면 커널 패닉(Kernel Panic)이 발생합니다.

RCU 기반 내부 구현

Atomic notifier의 핵심은 읽기 경로(call_chain)와 쓰기 경로(register/unregister)가 서로 다른 동기화를 사용하는 점입니다. 읽기 경로는 rcu_read_lock()으로 보호되어 스핀락 없이 체인을 순회합니다. 쓰기 경로만 spin_lock_irqsave()로 리스트를 수정합니다. 이 설계 덕분에 여러 CPU가 동시에 이벤트를 발행해도 스핀락 경합이 발생하지 않습니다. notifier_blocknext 포인터에 __rcu 어노테이션이 붙는 이유도 RCU로 보호되기 때문입니다.

Atomic Notifier — RCU 읽기/쓰기 분리 읽기 경로 (call_chain) — 스핀락 없음 rcu_read_lock() rcu_dereference(head) → nb₁ → nb₂ → nb₃ 콜백 호출 (sleep 불가) rcu_read_unlock() 쓰기 경로 (register/unregister) — 스핀락 보호 spin_lock_irqsave() rcu_assign_pointer() 리스트 포인터 수정 spin_unlock_irqrestore() Grace Period (유예 기간) CPU 0: rcu_read_lock 순회 중 unlock CPU 1: rcu_read_lock 순회 중 unlock 모든 reader 통과 → 이전 nb 안전 해제 핵심: 읽기 경로에서 스핀락을 잡지 않습니다 읽기: rcu_read_lock (preemption disable만) → 경합 없음 → 높은 확장성 쓰기: spin_lock_irqsave → 짧은 임계 구간 → 포인터만 변경 후 즉시 해제

내부 구현 코드

/* kernel/notifier.c — atomic notifier 내부 구현 (간략화) */

int atomic_notifier_chain_register(struct atomic_notifier_head *nh,
                                     struct notifier_block *n)
{
    unsigned long flags;
    int ret;

    spin_lock_irqsave(&nh->lock, flags);
    ret = notifier_chain_register(&nh->head, n);  /* rcu_assign_pointer 사용 */
    spin_unlock_irqrestore(&nh->lock, flags);
    return ret;
}

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();
    return ret;
}

/* notifier_call_chain 내부 순회 */
static int notifier_call_chain(struct notifier_block **nl,
                                unsigned long val, void *v, ...)
{
    struct notifier_block *nb, *next_nb;

    nb = rcu_dereference_raw(*nl);  /* __rcu 포인터 역참조 */
    while (nb) {
        next_nb = rcu_dereference_raw(nb->next);
        ret = nb->notifier_call(nb, val, v);
        if (ret & NOTIFY_STOP_MASK)
            break;
        nb = next_nb;
    }
    return ret;
}

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);

SRCU vs RCU: 설계 차이와 비용

일반 RCU에서 rcu_read_lock()은 선점을 비활성화(Preemption Disable)합니다. 이 때문에 RCU 읽기 구간 내에서는 슬립이 불가능합니다. SRCU는 이 제약을 해결하기 위해 Per-CPU 카운터 기반의 참조 추적을 사용합니다. srcu_read_lock()은 현재 CPU의 Per-CPU 카운터를 증가시키고 인덱스를 반환합니다. srcu_read_unlock()은 해당 카운터를 감소시킵니다. 선점을 비활성화하지 않으므로 콜백에서 슬립이 가능합니다.

SRCU의 비용은 일반 RCU보다 큽니다. srcu_struct는 Per-CPU 카운터 배열을 포함하여 약 200바이트(Byte) 이상의 메모리를 사용합니다. 반면 atomic notifier의 spinlock_t는 4바이트에 불과합니다. 따라서 SRCU notifier는 콜백에서 슬립이 반드시 필요하고, 콜백 내에서 체인 수정이 요구되는 경우에만 사용해야 합니다.

SRCU Notifier — Per-CPU 카운터 기반 읽기 보호 읽기 경로 (call_chain) — 슬립 허용 srcu_read_lock() idx = per_cpu++ 콜백 순회 + 실행 슬립 가능 (mutex, kmalloc OK) srcu_read_unlock() per_cpu[idx]-- 쓰기 경로 (register/unregister) — mutex 보호 mutex_lock() 리스트 수정 mutex_unlock() synchronize_srcu() 모든 reader 완료 대기 Per-CPU 카운터 메커니즘 CPU 0 count[0] = 1 CPU 1 count[0] = 0 CPU 2 count[0] = 2 CPU 3 count[0] = 0 synchronize_srcu(): 모든 CPU의 count 합이 0이 될 때까지 대기 비용 비교 spinlock_t (atomic): ~4B rw_semaphore (blocking): ~40B srcu_struct (srcu): ~200B+

SRCU Notifier 내부 구현 상세 분석

SRCU notifier는 4가지 변형 중 가장 복잡한 내부 구조를 가집니다. kernel/notifier.c의 실제 구현을 단계별로 분석합니다.

/* kernel/notifier.c — SRCU notifier 전체 구현 분석 */

/* srcu_notifier_head 구조체 */
struct srcu_notifier_head {
    struct mutex            mutex;  /* 쓰기 경로 직렬화 */
    struct srcu_struct      srcu;   /* SRCU 읽기 보호 (~200B) */
    struct notifier_block  *head;  /* 연결 리스트 시작 */
};

/* === 초기화: srcu_struct 동적 할당 필수 === */
int srcu_init_notifier_head(struct srcu_notifier_head *nh)
{
    mutex_init(&nh->mutex);
    if (init_srcu_struct(&nh->srcu))
        return -ENOMEM;       /* per-CPU 카운터 할당 실패 */
    nh->head = NULL;
    return 0;
}
/* 주의: BLOCKING_NOTIFIER_HEAD()처럼 정적 초기화 매크로 없음
 * srcu_struct는 per-CPU 메모리 할당이 필요하므로 반드시 동적 초기화 */

/* === 등록: mutex로 보호 === */
int srcu_notifier_chain_register(
    struct srcu_notifier_head *nh,
    struct notifier_block *n)
{
    int ret;

    mutex_lock(&nh->mutex);
    ret = notifier_chain_register(&nh->head, n);
    mutex_unlock(&nh->mutex);
    return ret;
}
/* mutex 사용 → 등록 중 슬립 가능
 * blocking의 down_write()와 달리 reader를 차단하지 않음
 * → 콜백 실행 중에도 등록이 가능한 핵심 차이 */

/* === 호출: SRCU 읽기 구간 === */
int srcu_notifier_call_chain(
    struct srcu_notifier_head *nh,
    unsigned long val, void *v)
{
    int ret, idx;

    idx = srcu_read_lock(&nh->srcu);
    /* idx: 현재 SRCU 인덱스 (0 또는 1, 더블 버퍼링)
     * srcu_read_lock은 선점을 비활성화하지 않음 → 슬립 가능
     * per-CPU 카운터를 증가시킬 뿐 */

    ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
    /* 이 구간에서:
     * - 콜백 내 mutex_lock(), kmalloc(GFP_KERNEL) 등 가능
     * - 콜백 내 같은 체인의 register/unregister 가능!
     *   (mutex는 비재귀지만, 쓰기는 mutex로, 읽기는 SRCU로 분리) */

    srcu_read_unlock(&nh->srcu, idx);
    /* per-CPU 카운터 감소 */

    return ret;
}

/* === 해제: mutex + synchronize_srcu === */
int srcu_notifier_chain_unregister(
    struct srcu_notifier_head *nh,
    struct notifier_block *n)
{
    int ret;

    mutex_lock(&nh->mutex);
    ret = notifier_chain_unregister(&nh->head, n);
    mutex_unlock(&nh->mutex);

    /* 핵심: 해제 후 grace period 대기
     * 진행 중인 srcu_read_lock() ~ srcu_read_unlock() 구간이
     * 모두 완료될 때까지 대기한 후 반환
     * → unregister 반환 후 n->notifier_call이 호출되지 않음 보장 */
    synchronize_srcu(&nh->srcu);

    return ret;
}

/* === 정리: per-CPU 메모리 해제 === */
void srcu_cleanup_notifier_head(
    struct srcu_notifier_head *nh)
{
    mutex_destroy(&nh->mutex);
    cleanup_srcu_struct(&nh->srcu);
    /* 반드시 모든 구독자가 해제된 후 호출해야 함 */
}

SRCU 더블 버퍼링 메커니즘

SRCU가 일반 RCU와 다른 핵심 특성은 더블 버퍼링(Double Buffering) 인덱스 방식입니다. srcu_read_lock()은 현재 활성 인덱스(0 또는 1)의 per-CPU 카운터를 증가시키고 그 인덱스를 반환합니다. synchronize_srcu()는 인덱스를 뒤집고(flip), 이전 인덱스의 모든 per-CPU 카운터가 0이 될 때까지 대기합니다.

SRCU 더블 버퍼링 — grace period와 인덱스 플립 시간 → 활성 idx idx = 0 (활성) idx = 1 (활성) — 인덱스 플립 후 synchronize_srcu() 호출 → 인덱스 플립 Reader A lock(0) 콜백 실행 중 (idx=0 카운터에 포함) unlock(0) Reader B lock(1) 콜백 실행 (idx=1 카운터) unlock(1) Writer (unregister) rm idx=0 대기 Reader A unlock(0) 후 안전 해제 가능 Per-CPU 카운터 상태 변화 count[0] (이전 인덱스) CPU0: 1 CPU1: 0 CPU2: 0 합=1 (대기 중) count[0] (Reader A unlock 후) CPU0: 0 CPU1: 0 CPU2: 0 합=0 (완료!) synchronize_srcu()는 인덱스를 플립한 후, 이전 인덱스의 모든 per-CPU 카운터 합이 0이 될 때까지 대기합니다. 새 reader는 새 인덱스를 사용하므로, 이전 인덱스의 카운터는 기존 reader가 unlock하면서 자연스럽게 감소합니다.
콜백 중 등록이 안전한 이유: Blocking notifier에서 콜백 내 등록이 데드락을 일으키는 이유는 call_chain이 rwsem read lock을 잡고, register가 rwsem write lock을 요청하기 때문입니다. SRCU notifier에서는 call_chain이 SRCU read lock(per-CPU 카운터)을 잡고, register는 별도의 mutex를 잡습니다. 두 잠금이 독립적이므로 교착이 발생하지 않습니다.

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 문제가 발생합니다. 반드시 호출자가 적절한 직렬화(Serialization)를 보장해야 합니다.

4종 비교

항목blockingatomicrawsrcu
잠금 구조체struct rw_semaphorespinlock_t없음struct srcu_struct
콜백 내 sleep가능불가호출자 결정가능
IRQ 컨텍스트불가가능호출자 결정불가
콜백 중 등록불가 (데드락)불가 (데드락)가능 (주의)가능
초기화 매크로(Macro)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)
메모리 오버헤드(Overhead)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)합니다. 이를 통해 등록 시점에 이미 존재하는 디바이스도 빠짐없이 처리할 수 있습니다.

네트워크 Notifier 실제 활용: 방화벽 규칙 자동 관리

아래 예제는 네트워크 인터페이스가 UP/DOWN될 때 자동으로 방화벽 규칙이나 라우팅 정책을 적용/제거하는 실전 패턴입니다. 이 패턴은 bonding, bridge, VLAN, nftables 등 주요 네트워크 서브시스템에서 실제로 사용됩니다.

/* 네트워크 Notifier 실전: 인터페이스별 정책 자동 관리 */
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/notifier.h>
#include <linux/inetdevice.h>
#include <linux/list.h>
#include <linux/slab.h>

/* 인터페이스별 관리 상태 */
struct my_iface_state {
    struct list_head   list;
    struct net_device  *dev;
    bool               rules_installed;
    u32                last_mtu;
};

static LIST_HEAD(iface_list);
static DEFINE_MUTEX(iface_mutex);

static struct my_iface_state *find_iface(struct net_device *dev)
{
    struct my_iface_state *s;
    list_for_each_entry(s, &iface_list, list) {
        if (s->dev == dev)
            return s;
    }
    return NULL;
}

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

    /* 물리 인터페이스만 대상 (loopback, veth 등 제외) */
    if (dev->type != ARPHRD_ETHER || !dev->dev.parent)
        return NOTIFY_DONE;

    /* 주의: netdev_chain은 raw notifier이므로
     * rtnl_lock()이 이미 보유된 상태 → mutex 사용 시 순서 주의 */

    switch (event) {
    case NETDEV_REGISTER:
        state = kzalloc(sizeof(*state), GFP_KERNEL);
        if (!state)
            return NOTIFY_BAD;  /* 등록 거부 */
        state->dev = dev;
        dev_hold(dev);  /* net_device 참조 카운터 증가 */
        list_add_tail(&state->list, &iface_list);
        pr_info("[my_fw] %s registered\n", dev->name);
        break;

    case NETDEV_UP:
        state = find_iface(dev);
        if (state && !state->rules_installed) {
            pr_info("[my_fw] %s UP — installing rules (MTU=%d)\n",
                    dev->name, dev->mtu);
            /* 방화벽 규칙/TC qdisc 설치 */
            state->rules_installed = true;
            state->last_mtu = dev->mtu;
        }
        break;

    case NETDEV_DOWN:
        state = find_iface(dev);
        if (state && state->rules_installed) {
            pr_info("[my_fw] %s DOWN — removing rules\n",
                    dev->name);
            state->rules_installed = false;
        }
        break;

    case NETDEV_CHANGEMTU:
        state = find_iface(dev);
        if (state && state->rules_installed) {
            pr_info("[my_fw] %s MTU %u → %u — updating rules\n",
                    dev->name, state->last_mtu, dev->mtu);
            /* MTU 기반 규칙 업데이트 */
            state->last_mtu = dev->mtu;
        }
        break;

    case NETDEV_UNREGISTER:
        state = find_iface(dev);
        if (state) {
            list_del(&state->list);
            dev_put(dev);  /* 참조 카운터 감소 */
            kfree(state);
            pr_info("[my_fw] %s unregistered\n", dev->name);
        }
        break;
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_netdev_nb = {
    .notifier_call = my_netdev_handler,
};

static int __init my_fw_init(void)
{
    /* register_netdevice_notifier는 기존 디바이스에 대해
     * NETDEV_REGISTER + NETDEV_UP을 즉시 재생(replay)합니다.
     * 따라서 init 시점에 이미 존재하는 디바이스도 처리됩니다. */
    return register_netdevice_notifier(&my_netdev_nb);
}

static void __exit my_fw_exit(void)
{
    struct my_iface_state *s, *tmp;

    unregister_netdevice_notifier(&my_netdev_nb);

    /* 남은 상태 정리 */
    list_for_each_entry_safe(s, tmp, &iface_list, list) {
        list_del(&s->list);
        dev_put(s->dev);
        kfree(s);
    }
}
netdev_chain의 rtnl_lock 주의사항: netdev_chainraw_notifier이지만, 모든 호출이 rtnl_lock()(뮤텍스) 보호 하에 이루어집니다. 따라서 콜백 내에서 rtnl_lock()을 다시 획득하면 데드락이 발생합니다. ASSERT_RTNL() 매크로로 rtnl 보유 여부를 확인할 수 있습니다. 또한 콜백 내에서 dev_change_flags(), dev_set_mtu() 등 네트워크 구성 함수를 직접 호출하면 동일 체인에 이벤트가 재발생하여 재진입 문제가 발생할 수 있습니다.

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) 콜백 수 — 콜백이 많을수록 지연(Latency) 증가
Blocking 오버헤드rwsem 읽기 획득/해제 (경합(Contention) 없을 때 매우 빠름)
Atomic 오버헤드스핀락 획득/해제 (IRQ 비활성화 포함)
SRCU 오버헤드srcu_read_lock/unlock — 빠른 경로지만 grace period 비용
콜백 최소화: Notifier Chain 콜백은 이벤트 경로에서 동기적으로 실행됩니다. 콜백이 오래 걸리면 전체 시스템의 이벤트 처리 지연이 발생합니다. 무거운 작업은 work_queue로 위임하고 콜백에서는 플래그만 설정하세요.

콜백 최소화 패턴: 즉시 처리 vs 지연 처리

Notifier 콜백에서 무거운 작업을 수행하면 전체 이벤트 전파 지연이 증가합니다. 아래 코드는 콜백을 최소화하고 실제 작업을 workqueue로 위임하는 권장 패턴입니다.

/* 콜백 최소화 패턴: 이벤트를 큐잉하고 workqueue에서 처리 */
struct my_event_work {
    struct work_struct   work;
    unsigned long        event;
    struct net_device   *dev;
};

static struct workqueue_struct *my_wq;

static void my_event_worker(struct work_struct *work)
{
    struct my_event_work *ew =
        container_of(work, struct my_event_work, work);

    /* workqueue 컨텍스트: 잠금, 슬립, 무거운 작업 모두 가능 */
    switch (ew->event) {
    case NETDEV_UP:
        rtnl_lock();
        install_heavy_filter_rules(ew->dev);
        rtnl_unlock();
        break;
    case NETDEV_DOWN:
        rtnl_lock();
        remove_heavy_filter_rules(ew->dev);
        rtnl_unlock();
        break;
    }
    dev_put(ew->dev);
    kfree(ew);
}

/* 콜백: 최소한의 작업만 수행 */
static int my_fast_netdev_cb(struct notifier_block *nb,
                              unsigned long event, void *ptr)
{
    struct net_device *dev = netdev_notifier_info_to_dev(ptr);
    struct my_event_work *ew;

    if (event != NETDEV_UP && event != NETDEV_DOWN)
        return NOTIFY_DONE;

    /* 콜백 내에서는 GFP_ATOMIC만 사용 (atomic notifier 호환) */
    ew = kmalloc(sizeof(*ew), GFP_ATOMIC);
    if (!ew)
        return NOTIFY_DONE;

    INIT_WORK(&ew->work, my_event_worker);
    ew->event = event;
    ew->dev = dev;
    dev_hold(dev);  /* workqueue 실행 전 dev 해제 방지 */

    queue_work(my_wq, &ew->work);
    return NOTIFY_DONE;
    /* 총 콜백 시간: ~100ns (kmalloc + queue_work만)
     * 실제 작업은 workqueue에서 비동기 처리 */
}

성능 측정 결과 참고

일반적인 Notifier chain 호출 오버헤드입니다 (x86_64, CONFIG_PREEMPT 기준, 콜백 본문 제외).

작업BlockingAtomicRawSRCU
call_chain 진입/종료 (콜백 0개)~50ns~20ns~5ns~80ns
call_chain (콜백 1개, nop)~80ns~40ns~15ns~120ns
register~200ns~100ns~30ns~250ns
unregister~200ns~100ns~30ns~50us (synchronize_srcu)
SRCU unregister 비용: SRCU notifier의 unregistersynchronize_srcu()를 호출하므로 수십 마이크로초에서 수 밀리초까지 소요될 수 있습니다. 이는 등록/해제가 빈번한 경우 병목이 될 수 있으므로, SRCU는 콜백 중 체인 수정이 반드시 필요한 경우에만 사용해야 합니다.

디버깅(Debugging)

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 사용
콜백에서 같은 체인 이벤트 발생무한 재귀 / 스택 오버플로(Stack Overflow)우workqueue로 비동기 처리
NOTIFY_STOP 남용다른 구독자 이벤트 수신 불가NOTIFY_OK / NOTIFY_DONE 사용
priority 미지정모듈 로드 순서에 의존적 동작명시적 priority 값 설정
스택 변수에 nb 선언함수 반환 후 댕글링 포인터static 또는 동적 할당
콜백 소요시간 과다이벤트 경로 지연, 시스템 지연최소한의 처리, 나머지 위임

데드락(Deadlock) 분석

Notifier chain에서 발생하는 데드락은 주로 잠금 중첩(Lock Nesting)과 재진입(Reentrancy)에서 비롯됩니다. 아래 다이어그램은 가장 흔한 3가지 데드락 시나리오와 각각의 해결 방법을 보여줍니다.

Notifier Chain 데드락 시나리오 분석 시나리오 1: rwsem R→W 데드락 Blocking 콜백에서 같은 체인 register call_chain → down_read(rwsem) 획득 콜백 A 실행 중... register → down_write(rwsem) 요청 DEADLOCK read lock 해제 대기 → 자기 자신 시나리오 2: 중첩 체인 잠금 역전 체인 A 콜백에서 체인 B 호출, 체인 B 콜백에서 체인 A 호출 CPU 0: lock(A) → cb → lock(B) CPU 1: lock(B) → cb → lock(A) AB/BA DEADLOCK lockdep이 이 패턴을 감지하여 경고합니다 해결: 체인 호출 순서를 항상 동일하게 유지 또는 workqueue로 비동기 위임 시나리오 3: Atomic에서 Sleep "scheduling while atomic" 패닉 rcu_read_lock() (선점 비활성) 콜백 실행 중... mutex_lock() / kmalloc(GFP_KERNEL) BUG: scheduling while atomic 선점 비활성 상태에서 schedule() 호출 해결 방법 요약 SRCU notifier 사용 또는 workqueue로 등록 위임 체인 호출 순서 규약 수립 lockdep 경고 반드시 수정 GFP_ATOMIC 사용 spin_lock + workqueue 위임 Notifier 콜백 내 잠금 사용 안전 규칙 Blocking 콜백 내 허용 mutex_lock() (다른 잠금) kmalloc(GFP_KERNEL) msleep(), schedule() 다른 blocking 체인 호출 같은 체인 register/unregister (금지!) Atomic 콜백 내 금지 mutex_lock() (금지) kmalloc(GFP_KERNEL) (금지) msleep(), schedule() (금지) spin_lock() (허용) kmalloc(GFP_ATOMIC) (허용) schedule_work() (허용) SRCU 콜백 내 허용 모든 Blocking 허용 항목 + 같은 체인 register/unregister 다른 SRCU 체인 호출 wait_for_completion() 같은 체인 synchronize_srcu (금지)

데드락 탐지: lockdep 출력 읽기

커널의 lockdep 서브시스템은 Notifier chain 관련 데드락을 런타임에 탐지합니다. 아래는 blocking notifier 콜백에서 같은 체인에 register를 시도했을 때 lockdep이 출력하는 경고 메시지 예시입니다.

# lockdep 경고 출력 예시
[  123.456789] ============================================
[  123.456790] WARNING: possible recursive locking detected
[  123.456791] --------------------------------------------
[  123.456792] kworker/0:1/123 is trying to acquire lock:
[  123.456793] ffff8880051a0000 (&nh->rwsem){++++}, at: blocking_notifier_chain_register+0x25/0x60
[  123.456795]
[  123.456796] but task is already holding lock:
[  123.456797] ffff8880051a0000 (&nh->rwsem){++++}, at: blocking_notifier_call_chain+0x1a/0x50
[  123.456799]
[  123.456800] other info that might help us debug this:
[  123.456801]  Possible unsafe locking scenario:
[  123.456802]        CPU0
[  123.456803]        ----
[  123.456804]   lock(&nh->rwsem);        # call_chain에서 read lock
[  123.456805]   lock(&nh->rwsem);        # register에서 write lock 시도
[  123.456806]
[  123.456807]  *** DEADLOCK ***

# 해결: 콜백에서 직접 register하지 말고 workqueue로 위임
# 또는 SRCU notifier로 전환

Workqueue 위임 패턴 상세

Blocking notifier 콜백에서 체인 수정이 필요한 경우, workqueue를 사용하여 데드락을 회피하는 패턴입니다.

/* 데드락 회피: blocking notifier 콜백에서 체인 수정 */
struct deferred_register {
    struct work_struct      work;
    struct notifier_block  *nb;
    bool                    is_register;  /* true=등록, false=해제 */
};

static void deferred_notifier_work(struct work_struct *work)
{
    struct deferred_register *dr =
        container_of(work, struct deferred_register, work);

    /* workqueue 컨텍스트: call_chain의 잠금 밖에서 실행 */
    if (dr->is_register)
        blocking_notifier_chain_register(&my_chain, dr->nb);
    else
        blocking_notifier_chain_unregister(&my_chain, dr->nb);

    kfree(dr);
}

/* 콜백 내에서 호출 */
static int my_callback(struct notifier_block *nb,
                        unsigned long event, void *data)
{
    if (event == SOME_EVENT) {
        struct deferred_register *dr;

        dr = kmalloc(sizeof(*dr), GFP_KERNEL);
        if (dr) {
            INIT_WORK(&dr->work, deferred_notifier_work);
            dr->nb = &another_nb;
            dr->is_register = true;
            schedule_work(&dr->work);
        }
    }
    return NOTIFY_OK;
}

die_chain / panic_notifier 상세

커널이 치명적 오류를 만나면 두 가지 알림 체인이 순서대로 동작합니다. die_chain은 oops/die 경로에서 호출되며, panic_notifier_list는 커널 패닉 직전 최후 정리 기회를 제공합니다. 두 체인 모두 Atomic Notifier이므로 콜백 내에서 슬립이 절대 허용되지 않습니다.

die_chain 구조

die_chainarch/x86/kernel/dumpstack.c(또는 아키텍처별 해당 파일)에서 notify_die()를 통해 호출됩니다. 이 함수는 트랩 핸들러, 페이지 폴트(Page Fault) 핸들러, MCE(Machine Check Exception) 경로 등에서 불립니다.

/* include/linux/kdebug.h */
enum die_val {
    DIE_OOPS = 1,
    DIE_INT3,          /* breakpoint */
    DIE_DEBUG,         /* debug exception */
    DIE_PANIC,         /* panic 직전 */
    DIE_NMI,           /* NMI */
    DIE_DIE,           /* 커널 die() 진입 */
    DIE_NMIUNKNOWN,    /* 출처 불명 NMI */
    DIE_NMIWATCHDOG,   /* NMI watchdog */
    DIE_KERNELDEBUG,   /* 커널 디버거 */
    DIE_TRAP,          /* 일반 트랩 */
    DIE_GPF,           /* General Protection Fault */
    DIE_CALL,          /* 소프트웨어 호출 */
    DIE_PAGE_FAULT,    /* 페이지 폴트 */
};

/* kernel/notifier.c */
static ATOMIC_NOTIFIER_HEAD(die_chain);

int register_die_notifier(struct notifier_block *nb)
{
    return atomic_notifier_chain_register(&die_chain, nb);
}

/* notify_die() — 아키텍처 트랩 핸들러에서 호출 */
int notify_die(enum die_val val, const char *str,
               struct pt_regs *regs, long err,
               int trap, int sig)
{
    struct die_args args = {
        .regs   = regs,
        .str    = str,
        .err    = err,
        .trapnr = trap,
        .signr  = sig,
    };
    return atomic_notifier_call_chain(&die_chain, val, &args);
}

panic_notifier_list

panic_notifier_listkernel/panic.cpanic() 함수에서 호출됩니다. 시스템이 더 이상 복구 불가능한 상태에서 최후의 정리를 수행합니다.

/* kernel/panic.c */
ATOMIC_NOTIFIER_HEAD(panic_notifier_list);
EXPORT_SYMBOL(panic_notifier_list);

void panic(const char *fmt, ...)
{
    /* ... 초기 설정 ... */
    atomic_notifier_call_chain(&panic_notifier_list,
                                PANIC_NOTIFIER, buf);
    /* ... kmsg dump, kdump, reboot ... */
}
콜백 내 제약 사항: die/panic 콜백은 NMI 컨텍스트에서도 호출될 수 있습니다. 따라서 spin_lock()조차 안전하지 않을 수 있으며, printk()도 NMI-safe 경로만 사용해야 합니다. 가능한 최소한의 작업(레지스터(Register) 덤프(Dump), LED 점멸, 하드웨어 리셋 트리거)만 수행하세요.

실제 사용 예

서브시스템등록 함수용도
kgdbregister_die_notifier()breakpoint/single-step 이벤트를 가로채 디버거로 전달
MCEregister_die_notifier()Machine Check Exception 처리 및 로깅
KVMregister_die_notifier()게스트 VM 내부 예외를 호스트에서 처리
perfregister_die_notifier()하드웨어 브레이크포인트 및 watchpoint 처리
kdump/kexecatomic_notifier_chain_register()패닉 시 크래시 덤프 커널 부팅
panic_blinkatomic_notifier_chain_register()패닉 시 키보드 LED 점멸

die_notifier 실전 구현: 커스텀 크래시 덤프

아래 예제는 커널 oops/die 이벤트를 가로채서 하드웨어 레지스터 상태를 비휘발 메모리(NVRAM)에 저장하는 die notifier의 실전 구현입니다. die 콜백은 매우 제한적인 환경에서 실행되므로 특별한 주의가 필요합니다.

/* 커스텀 die notifier: 하드웨어 상태를 비휘발 메모리에 저장
 * die 컨텍스트 제약: NMI 안전, sleep 불가, 최소 작업만 수행 */
#include <linux/module.h>
#include <linux/kdebug.h>
#include <linux/notifier.h>
#include <asm/io.h>

static void __iomem *nvram_base;

/* 크래시 정보 헤더 (NVRAM에 저장) */
struct crash_header {
    u32  magic;       /* 0xDEADC0DE */
    u32  die_val;     /* DIE_OOPS, DIE_GPF 등 */
    u64  timestamp;   /* RDTSC 값 */
    u64  rip;         /* 크래시 지점 명령 포인터 */
    u64  rsp;         /* 스택 포인터 */
    u64  cr2;         /* 페이지 폴트 주소 (해당 시) */
    int  trap_nr;     /* 트랩 번호 */
    int  error_code;  /* 에러 코드 */
} __packed;

static int my_die_handler(struct notifier_block *nb,
                           unsigned long val, void *data)
{
    struct die_args *args = data;
    struct crash_header hdr;

    /* NMI 안전한 작업만 수행 — 잠금 획득 금지 */

    switch (val) {
    case DIE_OOPS:
    case DIE_GPF:
    case DIE_TRAP:
    case DIE_PAGE_FAULT:
        hdr.magic      = 0xDEADC0DE;
        hdr.die_val    = (u32)val;
        hdr.timestamp  = rdtsc();
        hdr.rip        = args->regs ? args->regs->ip : 0;
        hdr.rsp        = args->regs ? args->regs->sp : 0;
        hdr.cr2        = (val == DIE_PAGE_FAULT) ?
                         read_cr2() : 0;
        hdr.trap_nr    = args->trapnr;
        hdr.error_code = (int)args->err;

        /* MMIO 쓰기: 잠금 없이 직접 접근 */
        memcpy_toio(nvram_base, &hdr, sizeof(hdr));
        break;

    case DIE_INT3:
        /* breakpoint — 디버거가 처리하도록 넘김 */
        return NOTIFY_DONE;

    case DIE_NMI:
        /* NMI watchdog — 타이머 상태만 간단히 기록 */
        writel(0xNMINMI00, nvram_base + sizeof(hdr));
        break;
    }

    /* NOTIFY_DONE: 다른 핸들러(kgdb, perf)도 이벤트 수신 가능
     * NOTIFY_STOP: 다른 핸들러 차단 — 일반적으로 사용하지 않음 */
    return NOTIFY_DONE;
}

static struct notifier_block my_die_nb = {
    .notifier_call = my_die_handler,
    .priority      = 0,  /* 기본: kgdb(INT_MAX)보다 낮은 우선순위 */
};

static int __init my_die_init(void)
{
    nvram_base = ioremap(0xFED10000, 4096);
    if (!nvram_base)
        return -ENOMEM;

    return register_die_notifier(&my_die_nb);
}

static void __exit my_die_exit(void)
{
    unregister_die_notifier(&my_die_nb);
    iounmap(nvram_base);
}
die 콜백 생존 규칙:
  • 잠금 획득 금지: die 경로는 이미 다른 잠금을 보유한 상태에서 진입할 수 있습니다. spin_lock()조차 데드락을 유발할 수 있습니다.
  • 메모리 할당 금지: kmalloc()은 내부적으로 잠금을 사용하므로 호출하면 안 됩니다.
  • MMIO만 사용: 비휘발 메모리에 기록할 때는 memcpy_toio()writel()처럼 잠금 없는 I/O만 사용합니다.
  • 최소 작업: 콜백 실행이 길어지면 watchdog 타임아웃으로 이중 패닉이 발생할 수 있습니다.
  • NOTIFY_STOP 주의: kgdb가 breakpoint를 처리해야 하는 경우, 먼저 등록된 핸들러가 NOTIFY_STOP을 반환하면 kgdb가 이벤트를 수신하지 못합니다.
die / panic Notifier 호출 경로 Oops / Trap NMI Watchdog MCE Page Fault BUG() notify_die() die_chain (Atomic) kgdb_handler perf_handler KVM_handler MCE_handler 복구 불가 시 panic() panic_notifier_list (Atomic) kdump, crash log, LED blink, reboot ...

Reboot Notifier Chain

시스템이 재부팅, 종료(halt), 전원 차단(power off)될 때 드라이버와 서브시스템에 정리 기회를 제공하는 Blocking Notifier Chain입니다. kernel/reboot.c에 정의되어 있으며, sys_reboot() 시스템 콜(System Call) 경로에서 호출됩니다.

Reboot 이벤트 종류

이벤트설명
SYS_RESTART0x0001시스템 재시작 (warm reboot)
SYS_HALT0x0002시스템 정지 (CPU halt)
SYS_POWER_OFF0x0003전원 완전 차단

API 및 등록

/* kernel/reboot.c */
static BLOCKING_NOTIFIER_HEAD(reboot_notifier_list);

int register_reboot_notifier(struct notifier_block *nb);
int unregister_reboot_notifier(struct notifier_block *nb);

/* devm 래퍼: device 수명과 자동 연결 */
int devm_register_reboot_notifier(struct device *dev,
                                   struct notifier_block *nb);

콜백 우선순위와 순서

reboot notifier는 우선순위가 높은 콜백부터 실행됩니다. 일반적으로 하드웨어 드라이버는 낮은 우선순위(먼저 호출되어 하드웨어 상태 저장), 파일시스템(Filesystem)은 높은 우선순위(나중에 호출되어 최종 플러시)로 등록합니다.

/* 예제: 커스텀 reboot notifier */
static int my_reboot_handler(struct notifier_block *nb,
                              unsigned long action, void *data)
{
    switch (action) {
    case SYS_RESTART:
        pr_info("my_driver: saving state before restart\n");
        my_hw_save_state();
        break;
    case SYS_HALT:
    case SYS_POWER_OFF:
        pr_info("my_driver: shutting down hardware\n");
        my_hw_shutdown();
        break;
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_reboot_nb = {
    .notifier_call = my_reboot_handler,
    .priority      = 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);
}

restart_handler_list: SoC별 리셋 핸들러

restart_handler_list는 실제 하드웨어 리셋을 수행하는 별도의 체인입니다. SoC 드라이버가 아키텍처별 리셋 메커니즘(watchdog 리셋, PMIC 리셋 등)을 등록하며, 우선순위가 가장 높은 핸들러 하나만 실행됩니다.

/* kernel/reboot.c */
void do_kernel_restart(char *cmd)
{
    atomic_notifier_call_chain(&restart_handler_list,
                                reboot_mode, cmd);
}

/* 드라이버 등록 예: watchdog 기반 리셋 */
static int wdt_restart(struct notifier_block *nb,
                        unsigned long action, void *data)
{
    writel(0x1, wdt_base + WDT_RESET_REG);
    mdelay(100);
    return NOTIFY_DONE;
}

static struct notifier_block wdt_restart_nb = {
    .notifier_call = wdt_restart,
    .priority      = 128,  /* 높은 우선순위: 선호 리셋 방법 */
};
reboot vs restart: reboot_notifier_list는 셧다운 직전 정리를 위한 Blocking 체인이고, restart_handler_list는 실제 하드웨어 리셋을 수행하는 Atomic 체인입니다. 전자는 여러 콜백이 모두 실행되지만, 후자는 NOTIFY_STOP을 반환하는 첫 핸들러에서 멈춥니다.

시스템 재부팅 전체 시퀀스

아래 다이어그램은 사용자 공간의 reboot 명령부터 실제 하드웨어 리셋까지의 전체 흐름에서 reboot notifier와 restart handler가 호출되는 시점을 보여줍니다.

시스템 재부팅 시퀀스 — Notifier Chain 호출 시점 reboot 명령 sys_reboot() kernel/reboot.c 1. reboot_notifier_list Blocking: 모든 콜백 순서대로 실행 drv_A: NVRAM 플러시 → drv_B: DMA 중지 → drv_C: 디바이스 레지스터 저장 → ... priority 내림차순 실행 2. device_shutdown() 각 디바이스의 .shutdown() 콜백 호출 dev_pm_ops 기반 디바이스별 정리 3. migrate_to_reboot_cpu() 재부팅 태스크를 CPU 0으로 마이그레이션 4. syscore_shutdown() 인터럽트 비활성, 타이머 정지, syscore 정리 5. do_kernel_restart() restart_handler_list (Atomic) 우선순위 최고 핸들러가 HW 리셋 수행 NOTIFY_STOP 반환 → 체인 중단 6. machine_restart() (아키텍처별) restart_handler 실패 시 fallback: ACPI/EFI 리셋 또는 triple fault로 강제 리셋 시간 흐름 두 체인의 역할 차이 reboot_notifier: 정리 작업 (모두 실행) NVRAM, DMA, 디바이스 상태 저장 restart_handler: HW 리셋 (하나만 실행) watchdog/PMIC/PSCI 리셋

devm_register_reboot_notifier 활용

devm_register_reboot_notifier()는 디바이스 수명 주기와 연동된 reboot notifier 등록 함수입니다. 디바이스가 제거될 때 자동으로 unregister가 호출되므로, module_exit에서 명시적으로 해제할 필요가 없습니다.

/* devm 기반 reboot notifier: 자동 수명 관리 */
static int my_reboot_cb(struct notifier_block *nb,
                          unsigned long action, void *data)
{
    struct my_device *mydev =
        container_of(nb, struct my_device, reboot_nb);

    switch (action) {
    case SYS_RESTART:
    case SYS_HALT:
    case SYS_POWER_OFF:
        /* 디바이스 고유 정리 작업 */
        my_hw_flush_and_stop(mydev);
        break;
    }
    return NOTIFY_DONE;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_device *mydev;
    int ret;

    mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
    if (!mydev)
        return -ENOMEM;

    mydev->reboot_nb.notifier_call = my_reboot_cb;
    mydev->reboot_nb.priority = 0;

    /* devm_: 디바이스 제거 시 자동 unregister */
    ret = devm_register_reboot_notifier(&pdev->dev,
                                         &mydev->reboot_nb);
    if (ret)
        return ret;

    /* remove 함수에서 unregister 불필요 — devm이 자동 처리 */
    return 0;
}

네트워크 Notifier Chain

리눅스 네트워크 스택(Network Stack)은 다양한 이벤트를 notifier chain으로 전파합니다. NIC 상태 변경, IP 주소 할당, 라우팅 테이블(Routing Table) 갱신 등 모든 네트워크 상태 변화가 구독자에게 알림됩니다.

netdev_chain 이벤트 목록

이벤트발생 시점콜백 데이터
NETDEV_REGISTER네트워크 디바이스 등록struct net_device *
NETDEV_UNREGISTER디바이스 등록 해제struct net_device *
NETDEV_UP인터페이스 활성화 (ifconfig up)struct net_device *
NETDEV_DOWN인터페이스 비활성화struct net_device *
NETDEV_CHANGE링크 상태/플래그 변경struct net_device *
NETDEV_CHANGEMTUMTU 변경struct net_device *
NETDEV_CHANGEADDRMAC 주소 변경struct net_device *
NETDEV_CHANGENAME인터페이스 이름 변경struct net_device *
NETDEV_PRE_UP인터페이스 활성화 직전struct net_device *
NETDEV_FEAT_CHANGEfeatures 변경 (offload 등)struct net_device *

call_netdevice_notifiers() 호출 경로

/* net/core/dev.c */
static RAW_NOTIFIER_HEAD(netdev_chain);

int call_netdevice_notifiers(unsigned long val,
                              struct net_device *dev)
{
    struct netdev_notifier_info info = {
        .dev = dev,
    };
    return call_netdevice_notifiers_info(val, &info);
}

/* 내부적으로 rtnl_lock()으로 보호 — raw이지만 사실상 blocking처럼 동작 */
static int call_netdevice_notifiers_info(unsigned long val,
                                          struct netdev_notifier_info *info)
{
    ASSERT_RTNL();  /* rtnl_lock 보유 확인 */
    return raw_notifier_call_chain(&netdev_chain, val, info);
}
Raw이지만 안전한 이유: netdev_chainraw_notifier이지만, 모든 등록/해제와 호출이 rtnl_lock()(뮤텍스) 보호 하에 이루어지므로 별도의 내부 잠금이 필요 없습니다. 이는 네트워크 서브시스템 고유의 Big Kernel Lock 스타일 동기화입니다.

netevent_notifier: 이웃 서브시스템 이벤트

/* net/core/netevent.c */
static ATOMIC_NOTIFIER_HEAD(netevent_notif_chain);

/* 이벤트 종류 */
enum netevent_notif_type {
    NETEVENT_NEIGH_UPDATE,       /* ARP/NDP 엔트리 갱신 */
    NETEVENT_REDIRECT,           /* ICMP 리다이렉트 수신 */
    NETEVENT_DELAY_PROBE_TIME_UPDATE, /* 탐사 간격 변경 */
    NETEVENT_IPV4_MPATH_HASH_UPDATE,  /* 멀티패스 해시 정책 변경 */
    NETEVENT_IPV6_MPATH_HASH_UPDATE,
};

inet/inet6addr_chain: IP 주소 변경 알림

/* net/ipv4/devinet.c */
BLOCKING_NOTIFIER_HEAD(inetaddr_chain);  /* IPv4 주소 변경 */
int register_inetaddr_notifier(struct notifier_block *nb);

/* net/ipv6/addrconf.c */
BLOCKING_NOTIFIER_HEAD(inet6addr_chain); /* IPv6 주소 변경 */
int register_inet6addr_notifier(struct notifier_block *nb);

/* 이벤트: NETDEV_UP, NETDEV_DOWN (주소 추가/삭제 시) */
/* 콜백 데이터: struct in_ifaddr * (IPv4) / struct inet6_ifaddr * (IPv6) */

fib_chain: 라우팅 테이블 변경

/* net/ipv4/fib_trie.c */
static ATOMIC_NOTIFIER_HEAD(fib_chain);

/* 이벤트 */
#define FIB_EVENT_ENTRY_REPLACE  0
#define FIB_EVENT_ENTRY_APPEND   1
#define FIB_EVENT_ENTRY_ADD      2
#define FIB_EVENT_ENTRY_DEL      3
#define FIB_EVENT_RULE_ADD       4
#define FIB_EVENT_RULE_DEL       5

/* switchdev 드라이버가 이를 구독하여 HW FIB 오프로드 수행 */
int register_fib_notifier(struct net *net,
                          struct notifier_block *nb,
                          void (*cb)(struct notifier_block *nb,
                                      unsigned long event, void *ptr),
                          struct netlink_ext_ack *extack);
네트워크 Notifier Chain 계층 구조 NIC Driver link up/down, MTU net/core/dev.c call_netdevice_notifiers() rtnl_lock() 보호 netdev_chain (Raw) : bonding, bridge, iptables ... netevent_chain (Atomic) : ARP/NDP 갱신 IP 주소 변경 ip addr add/del devinet.c / addrconf.c blocking_notifier_call_chain inetaddr_chain / inet6addr_chain (Blocking) 라우팅 테이블 ip route add/del fib_trie.c atomic_notifier_call_chain fib_chain (Atomic) : switchdev HW offload 체인 타입 요약: netdev_chain: Raw (rtnl_lock 외부 보호) inetaddr: Blocking (rwsem) fib/netevent: Atomic (RCU)

CPU 핫플러그 Notifier

Linux 4.10에서 기존 register_cpu_notifier() API는 deprecated되고, 상태 머신 기반의 cpuhp_setup_state()로 전환되었습니다. 새 API는 각 상태 전이마다 startup/teardown 콜백 쌍을 등록하여 대칭적 초기화/정리를 보장합니다.

핫플러그 상태 머신

CPU가 온라인/오프라인될 때 여러 단계(state)를 거칩니다. 각 단계마다 등록된 콜백이 호출되며, 실패 시 자동 롤백(Rollback)됩니다.

구간상태 범위실행 CPU슬립 가능용도
PREPARECPUHP_OFFLINE ~ CPUHP_BRINGUP_CPU부팅 CPU가능메모리 할당, 자료구조 준비
STARTINGCPUHP_BRINGUP_CPU ~ CPUHP_AP_ONLINE대상 CPU불가 (IRQ off)per-CPU 초기화, 타이머(Timer) 설정
ONLINECPUHP_AP_ONLINE ~ CPUHP_ONLINE대상 CPU가능고수준 서비스 시작

cpuhp_setup_state() API

/* include/linux/cpuhotplug.h */

/* 정적 상태: 미리 정의된 슬롯에 등록 */
int cpuhp_setup_state(enum cpuhp_state state,
                       const char *name,
                       int (*startup)(unsigned int cpu),
                       int (*teardown)(unsigned int cpu));

/* 동적 상태: CPUHP_AP_ONLINE_DYN에서 자동 할당 */
/* 반환값이 양수이면 할당된 state 번호 */
int cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "my:online",
                       my_cpu_online, my_cpu_offline);

/* no-call 변형: 기존 온라인 CPU에 대해 콜백 미호출 */
int cpuhp_setup_state_nocalls(enum cpuhp_state state,
                              const char *name,
                              int (*startup)(unsigned int cpu),
                              int (*teardown)(unsigned int cpu));

/* 인스턴스 기반: 여러 인스턴스가 같은 state 공유 */
int cpuhp_setup_state_multi(enum cpuhp_state state,
                            const char *name,
                            int (*startup)(unsigned int cpu,
                                           struct hlist_node *node),
                            int (*teardown)(unsigned int cpu,
                                            struct hlist_node *node));

인스턴스 기반 vs 싱글턴 콜백

싱글턴: cpuhp_setup_state()는 서브시스템 전체에 대한 단일 콜백입니다. 타이머 서브시스템, RCU, 워크큐 등 시스템당 하나만 필요한 초기화에 적합합니다.
인스턴스 기반: cpuhp_setup_state_multi()는 같은 상태에 여러 인스턴스를 등록할 수 있습니다. 예를 들어, 여러 블록 디바이스가 각각 per-CPU 완료 큐를 관리해야 할 때 사용합니다. 각 인스턴스는 cpuhp_state_add_instance()/cpuhp_state_remove_instance()로 동적 추가/제거됩니다.
/* 인스턴스 기반 예제: 블록 디바이스 per-CPU 큐 */
static enum cpuhp_state blk_mq_hp_state;

static int blk_mq_hctx_notify_online(unsigned int cpu,
                                      struct hlist_node *node)
{
    struct blk_mq_hw_ctx *hctx =
        hlist_entry_safe(node, struct blk_mq_hw_ctx, cpuhp_online);
    /* CPU가 온라인되면 해당 CPU의 하드웨어 큐 활성화 */
    blk_mq_tag_wakeup_all(hctx->tags, true);
    return 0;
}
CPU Hotplug 상태 머신 — 콜백 실행 컨텍스트 OFFLINE PREPARE 구간 부팅 CPU에서 실행 슬립 가능 STARTING 구간 대상 CPU에서 실행 슬립 불가 (IRQ off) ONLINE 구간 대상 CPU에서 실행 슬립 가능 ON 오프라인 경로 (teardown 콜백, 역순 실행) 상태별 등록 서브시스템 예시 상태 서브시스템 콜백 동작 CPUHP_PERF_PREPARE perf_event per-CPU 자료구조 할당 CPUHP_AP_IRQ_AFFINITY IRQ 서브시스템 인터럽트 어피니티 재설정 CPUHP_AP_SCHED_STARTING 스케줄러 런큐 활성화 CPUHP_AP_ONLINE_DYN 모듈/드라이버 동적 할당 (사용자 정의) CPUHP_AP_RCUTREE_ONLINE RCU Tree RCU 노드 활성화 CPUHP_AP_WORKQUEUE_ONLINE 워크큐 per-CPU 워커 풀 시작 CPUHP_ONLINE 최종 상태 CPU 완전 활성

PM (Power Management) Notifier

전원 관리(PM) notifier는 시스템 서스펜드/하이버네이션 전후에 드라이버와 서브시스템에 알림을 보내는 Blocking Notifier Chain입니다. 콜백에서 NOTIFY_BAD를 반환하면 서스펜드를 거부할 수 있습니다.

PM 이벤트 종류

이벤트시점거부 가능용도
PM_HIBERNATION_PREPARE하이버네이션 진입 전가능디스크 공간 확보, 캐시(Cache) 플러시(Flush)
PM_POST_HIBERNATION하이버네이션 복구 후불가리소스 재초기화
PM_SUSPEND_PREPARE서스펜드 진입 전가능작업 플러시, 하드웨어 상태 저장
PM_POST_SUSPEND서스펜드 복구 후불가하드웨어 재초기화
PM_RESTORE_PREPARE하이버네이션 이미지 복원 전가능복원 준비
PM_POST_RESTORE복원 실패 후불가정리

API 및 콜백 패턴

/* kernel/power/main.c */
static BLOCKING_NOTIFIER_HEAD(pm_chain_head);

int register_pm_notifier(struct notifier_block *nb);
int unregister_pm_notifier(struct notifier_block *nb);

/* 서스펜드 거부 예제 */
static int my_pm_handler(struct notifier_block *nb,
                          unsigned long event, void *data)
{
    switch (event) {
    case PM_SUSPEND_PREPARE:
        if (critical_operation_in_progress()) {
            pr_warn("my_driver: blocking suspend — critical op\n");
            return NOTIFY_BAD;  /* 서스펜드 거부! */
        }
        flush_all_pending_work();
        save_hardware_state();
        break;
    case PM_POST_SUSPEND:
        restore_hardware_state();
        break;
    }
    return NOTIFY_OK;
}

static struct notifier_block my_pm_nb = {
    .notifier_call = my_pm_handler,
    .priority      = 0,
};
NOTIFY_BAD 주의: PM_SUSPEND_PREPARE에서 NOTIFY_BAD를 반환하면 서스펜드가 거부됩니다. 하지만 이 시점에서 이미 호출된 다른 콜백들은 PM_POST_SUSPEND를 받아 rollback해야 합니다. PM 프레임워크가 이를 자동으로 처리하지만, 콜백 순서에 주의해야 합니다.

콜백 순서 의존성

PM notifier의 호출 순서는 우선순위에 의해 결정됩니다. 일반적으로 서브시스템 수준 콜백이 먼저 실행되고 개별 드라이버 콜백이 나중에 실행됩니다. 디바이스 수준의 서스펜드/리줌은 별도의 dev_pm_ops 프레임워크가 담당하며, PM notifier는 그 이전 단계입니다.

서스펜드 진행 순서:

  1. pm_notifier_call_chain(PM_SUSPEND_PREPARE) ← PM notifier
  2. suspend_freeze_processes() ← 프로세스 동결
  3. dpm_suspend_start() ← dev_pm_ops.prepare
  4. dpm_suspend() ← dev_pm_ops.suspend
  5. platform_suspend() ← 아키텍처별 진입

리줌은 역순:

  1. platform_resume()
  2. dpm_resume()
  3. dpm_resume_end()
  4. suspend_thaw_processes()
  5. pm_notifier_call_chain(PM_POST_SUSPEND) ← PM notifier

서스펜드/리줌 전체 시퀀스

아래 다이어그램은 시스템 서스펜드(Suspend)와 리줌(Resume) 과정에서 PM notifier가 호출되는 시점과 NOTIFY_BAD 롤백 경로를 보여줍니다.

PM 서스펜드/리줌 시퀀스 — Notifier 호출 시점 서스펜드 경로 (Suspend) PM_SUSPEND_PREPARE ← PM notifier 프로세스 동결 (freeze) dev_pm_ops.prepare dev_pm_ops.suspend platform_suspend() 리줌 경로 (Resume) platform_resume() dev_pm_ops.resume dev_pm_ops.complete 프로세스 해동 (thaw) PM_POST_SUSPEND ← PM notifier 하드웨어 진입/복귀 NOTIFY_BAD 롤백 경로 PM_SUSPEND_PREPARE cb₁: NOTIFY_OK cb₂: NOTIFY_OK cb₃: NOTIFY_BAD 서스펜드 거부! PM_POST_SUSPEND (롤백) 이미 호출된 cb₁, cb₂에게 PM_POST_SUSPEND 전달 → 상태 복구 PM notifier는 디바이스 서스펜드 이전/이후에 호출됩니다. NOTIFY_BAD 반환 시 프레임워크가 자동으로 PM_POST_SUSPEND를 발행하여 이전 콜백의 상태를 복구합니다.

메모리 Notifier Chain

메모리 핫플러그와 메모리 압박 상황에서 서브시스템에 알림을 전달하는 notifier chain 그룹입니다.

memory_chain: 메모리 핫플러그

/* mm/memory_hotplug.c */
static BLOCKING_NOTIFIER_HEAD(memory_chain);

int register_memory_notifier(struct notifier_block *nb);
int unregister_memory_notifier(struct notifier_block *nb);

메모리 핫플러그 이벤트

이벤트시점거부설명
MEM_GOING_ONLINE메모리 블록 온라인 전가능자료구조 확장 준비
MEM_ONLINE온라인 완료 후불가새 메모리 사용 시작
MEM_GOING_OFFLINE메모리 블록 오프라인 전가능페이지(Page) 마이그레이션 준비
MEM_OFFLINE오프라인 완료 후불가자료구조 축소
MEM_CANCEL_ONLINE온라인 취소됨불가MEM_GOING_ONLINE 롤백
MEM_CANCEL_OFFLINE오프라인 취소됨불가MEM_GOING_OFFLINE 롤백
/* 메모리 핫플러그 notifier 예제 */
static int my_memory_cb(struct notifier_block *nb,
                         unsigned long action, void *data)
{
    struct memory_notify *mn = data;

    switch (action) {
    case MEM_GOING_ONLINE:
        pr_info("memory block %lu going online (start_pfn=%lu, nr=%lu)\n",
                mn->start_pfn >> 15, mn->start_pfn, mn->nr_pages);
        /* 페이지 범위에 대한 자료구조 사전 할당 */
        if (expand_my_page_table(mn->start_pfn, mn->nr_pages))
            return NOTIFY_BAD;  /* 메모리 부족: 온라인 거부 */
        break;
    case MEM_CANCEL_ONLINE:
        shrink_my_page_table(mn->start_pfn, mn->nr_pages);
        break;
    case MEM_OFFLINE:
        shrink_my_page_table(mn->start_pfn, mn->nr_pages);
        break;
    }
    return NOTIFY_OK;
}

vmpressure notifier: 메모리 압박 알림

/* mm/vmpressure.c */
/* vmpressure는 memcg별 이벤트로, eventfd 기반 */
/* cgroup v2의 memory.pressure 인터페이스와 연동 */

/* 압박 수준 */
enum vmpressure_levels {
    VMPRESSURE_LOW,      /* 경미한 압박 (캐시 회수 시작) */
    VMPRESSURE_MEDIUM,   /* 중간 압박 (스왑 활발) */
    VMPRESSURE_CRITICAL, /* 위험 (OOM 임박) */
};

/* 사용자 공간에서 모니터링:
 * /sys/fs/cgroup//memory.pressure
 * PSI(Pressure Stall Information) 인터페이스
 */

OOM notifier

/* mm/oom_kill.c */
static BLOCKING_NOTIFIER_HEAD(oom_notify_list);

int register_oom_notifier(struct notifier_block *nb);
int unregister_oom_notifier(struct notifier_block *nb);

/* OOM notifier는 OOM killer 발동 전에 호출됨 */
/* 콜백이 메모리를 확보하면 NOTIFY_OK 반환 → OOM kill 회피 가능 */
static int my_oom_handler(struct notifier_block *nb,
                           unsigned long action, void *data)
{
    unsigned long freed = release_my_caches();
    if (freed > 0) {
        pr_info("my_driver: freed %lu pages, avoiding OOM\n", freed);
        return NOTIFY_OK;  /* OOM killer 호출 억제 시도 */
    }
    return NOTIFY_DONE;     /* 관심 없음 */
}
OOM notifier 주의: OOM notifier는 메모리 할당 실패 경로에서 호출되므로, 콜백 내에서 새 메모리를 할당하면 재귀 OOM을 유발할 수 있습니다. 이미 할당된 캐시를 해제하는 작업만 수행해야 합니다.

메모리 Notifier 이벤트 흐름

아래 다이어그램은 메모리 핫플러그 체인의 이벤트 상태 전이, vmpressure 알림 수준, OOM notifier의 동작을 한눈에 보여줍니다.

메모리 Notifier — 이벤트 흐름 종합 memory_chain: 메모리 핫플러그 상태 전이 MEM_GOING_ONLINE OK MEM_ONLINE BAD MEM_CANCEL_ONLINE MEM_GOING_OFFLINE OK MEM_OFFLINE BAD MEM_CANCEL_OFFLINE vmpressure: 메모리 압박 수준 LOW 캐시 회수 시작 MEDIUM 스왑 활발 CRITICAL OOM 임박 0% 메모리 압박 증가 → 100% oom_notify_list: OOM 발동 직전 최후의 방어선 할당 실패 __alloc_pages_slowpath OOM notifier 호출 캐시 해제 시도 해제 성공 재시도 → 할당 성공 해제 실패 OOM killer 발동 메모리 notifier는 핫플러그 거부(NOTIFY_BAD), 압박 수준 통지, OOM 회피 등 메모리 생명주기 전반을 관리합니다.

반환값 프로토콜

Notifier 콜백의 반환값은 체인 전파 동작을 제어하는 핵심 메커니즘입니다. 반환값은 단순 정수가 아니라 비트마스크 기반 프로토콜을 따릅니다.

반환값 정의

/* include/linux/notifier.h */
#define NOTIFY_DONE       0x0000   /* 관심 없음, 다음 콜백 계속 */
#define NOTIFY_OK         0x0001   /* 성공 처리, 다음 콜백 계속 */
#define NOTIFY_BAD        (NOTIFY_STOP_MASK | 0x0002)
                                    /* 에러/거부 + 즉시 중단 */
#define NOTIFY_STOP       (NOTIFY_STOP_MASK | NOTIFY_OK)
                                    /* 성공 + 즉시 중단 */

#define NOTIFY_STOP_MASK  0x8000   /* 이 비트가 설정되면 체인 중단 */

반환값 의미 비교

반환값비트체인 계속의미사용 시나리오
NOTIFY_DONE0x0000계속이 이벤트에 관심 없음대부분의 경우 기본 반환값
NOTIFY_OK0x0001계속성공적으로 처리함이벤트를 인지하고 처리한 경우
NOTIFY_BAD0x8002중단에러/거부PM_SUSPEND_PREPARE 거부, MEM_GOING_ONLINE 거부
NOTIFY_STOP0x8001중단성공 + 독점 처리die_notifier에서 kgdb가 이벤트를 처리한 경우

notifier_to_errno / errno_to_notifier 변환

/* include/linux/notifier.h */
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)
{
    ret &= ~NOTIFY_STOP_MASK;
    return ret > NOTIFY_OK ? NOTIFY_OK - ret : 0;
}

/* 사용 예: 호출자 측 */
ret = blocking_notifier_call_chain(&my_chain, EVENT, data);
ret = notifier_to_errno(ret);
if (ret) {
    pr_err("notifier returned error: %d\n", ret);
    goto rollback;
}
반환값에 따른 체인 전파 흐름 시나리오 1: NOTIFY_OK / NOTIFY_DONE (전파 계속) call_chain() CB-A: OK CB-B: DONE CB-C: OK 완료 시나리오 2: NOTIFY_STOP (독점 처리, 중단) call_chain() CB-A: OK CB-B: STOP CB-C: (미호출) CB-D: (미호출) 시나리오 3: NOTIFY_BAD (거부, 롤백 트리거) call_chain() CB-A: OK CB-B: BAD notifier_to_errno() 호출자: 작업 롤백
실전 패턴: NOTIFY_BAD는 PM 서스펜드 거부, 메모리 온라인 거부 등 "작업 취소"가 가능한 체인에서만 의미가 있습니다. 예를 들어, die_chain에서 NOTIFY_BAD를 반환해도 oops 자체를 되돌릴 수는 없습니다. 반면 NOTIFY_STOP은 "이 이벤트를 내가 완전히 처리했으니 다른 콜백은 볼 필요 없습니다"는 의미입니다 (kgdb의 breakpoint 처리가 대표적 예).

우선순위와 순서 제어

struct notifier_blockpriority 필드는 체인 내 콜백 실행 순서를 결정합니다. 높은 값이 먼저 실행되며, 같은 우선순위에서는 나중에 등록된 콜백이 먼저 실행됩니다(LIFO).

우선순위 메커니즘

/* kernel/notifier.c: 등록 시 정렬 삽입 */
static int notifier_chain_register(
    struct notifier_block **nl,
    struct notifier_block *n)
{
    while ((*nl) != NULL) {
        /* priority가 높은 것이 리스트 앞쪽 */
        if (n->priority > (*nl)->priority)
            break;
        if (n->priority == (*nl)->priority &&
            n == *nl)          /* 중복 등록 방지 */
            return -EEXIST;
        nl = &((*nl)->next);
    }
    n->next = *nl;
    rcu_assign_pointer(*nl, n);
    return 0;
}

커널 서브시스템별 우선순위 관례

우선순위 범위용도
INT_MAX최우선 처리 (디버거)kgdb die notifier
200~255하드웨어 리셋 핸들러watchdog restart_handler
100~199핵심 서브시스템KPTI, perf
1~99일반 서브시스템네트워크 필터, 브릿지
0 (기본값)일반 드라이버/모듈대부분의 콜백
-1 ~ -INT_MAX정리/최후 처리로깅, 통계 수집

우선순위 관련 문제

같은 우선순위에서의 LIFO: 두 모듈이 같은 우선순위(예: 0)로 등록하면, 나중에 로드된 모듈의 콜백이 먼저 실행됩니다. 모듈 로드 순서는 initcall 레벨, udev 규칙, systemd 서비스 순서에 따라 달라질 수 있어 비결정적입니다. 실행 순서가 중요하다면 반드시 명시적 우선순위를 설정하세요.
/* 안티패턴: 우선순위에 의존하는 취약한 설계 */
/* 모듈 A: 이벤트를 변환하여 전달 */
static struct notifier_block transformer_nb = {
    .notifier_call = transform_event,
    .priority      = 10,   /* 모듈 B보다 먼저 실행 의도 */
};

/* 모듈 B: 변환된 이벤트 소비 */
static struct notifier_block consumer_nb = {
    .notifier_call = consume_event,
    .priority      = 0,    /* 모듈 A 이후 실행 의도 */
};

/* 문제: 모듈 A가 로드되지 않으면 모듈 B는 원본 이벤트를 수신함
 * 해결: 모듈 간 직접 의존성이 있으면 notifier 대신 직접 함수 호출 사용
 */
설계 원칙: Notifier chain은 느슨한 결합(loose coupling)을 위한 메커니즘입니다. 콜백 간에 순서 의존성이 있다면 그것은 notifier의 오용 신호입니다. 의존성이 있는 콜백은 하나의 콜백으로 통합하거나, 별도의 호출 메커니즘을 사용하세요.

디버깅과 트레이싱

Notifier chain 콜백의 지연, 오류, 충돌을 진단하기 위한 다양한 커널 디버깅 기법을 소개합니다.

ftrace를 이용한 notifier 추적

# notifier_call_chain 함수 진입/종료 추적
cd /sys/kernel/debug/tracing
echo 'notifier_call_chain' > set_ftrace_filter
echo 'function_graph' > current_tracer
echo 1 > tracing_on

# 특정 notifier chain 호출 추적
echo 'blocking_notifier_call_chain raw_notifier_call_chain atomic_notifier_call_chain' > set_ftrace_filter

# 결과 확인
cat trace

# 출력 예:
#  1)               |  blocking_notifier_call_chain() {
#  1)               |    notifier_call_chain() {
#  1)   0.892 us    |      my_pm_handler();
#  1)   0.341 us    |      driver_suspend_cb();
#  1)   2.104 us    |    }
#  1)   2.561 us    |  }

Notifier 트레이스포인트

# 사용 가능한 notifier 관련 트레이스포인트 확인
ls /sys/kernel/debug/tracing/events/notifier/

# notifier 등록/해제 이벤트 활성화 (커널 빌드 옵션 의존)
echo 1 > /sys/kernel/debug/tracing/events/notifier/enable

# CPU hotplug 관련 트레이스포인트
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/enable
cat trace_pipe

BPF 기반 notifier 지연 분석

/* bpftrace 스크립트: notifier 콜백 지연 측정 */
/* bpftrace -e 아래 내용 */

/*
kprobe:notifier_call_chain
{
    @start[tid] = nsecs;
}

kretprobe:notifier_call_chain
/@start[tid]/
{
    $dur = nsecs - @start[tid];
    @latency = hist($dur);
    if ($dur > 1000000) {  // 1ms 이상
        printf("slow notifier chain: %d ns, comm=%s\n",
               $dur, comm);
    }
    delete(@start[tid]);
}
*/
# bpftrace로 느린 notifier 콜백 탐지
bpftrace -e '
kprobe:notifier_call_chain { @start[tid] = nsecs; }
kretprobe:notifier_call_chain /@start[tid]/ {
  $d = nsecs - @start[tid];
  @us = hist($d / 1000);
  if ($d > 1000000) {
    printf("SLOW %d us comm=%s\n", $d/1000, comm);
  }
  delete(@start[tid]);
}
'

kprobe를 이용한 notifier 콜백 인터셉트

kprobe를 사용하면 런타임에 특정 notifier 콜백의 진입/종료를 가로채서 인자 값, 반환값, 실행 시간을 기록할 수 있습니다. 모듈 수정 없이 디버깅이 가능하여 운영 환경에서 유용합니다.

/* kprobe 기반 notifier 콜백 모니터링 모듈 */
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/notifier.h>

/* notifier_call_chain의 콜백 호출 지점을 추적 */
static struct kprobe kp = {
    .symbol_name = "notifier_call_chain",
};

static int handler_pre(struct kprobe *p,
                        struct pt_regs *regs)
{
    /* x86_64: rdi=**nl, rsi=val, rdx=*v, rcx=nr_to_call */
    struct notifier_block **nl =
        (struct notifier_block **)regs->di;
    unsigned long val = regs->si;

    if (*nl) {
        pr_info("notifier_call_chain: event=%lu first_cb=%ps pri=%d\n",
                val, (*nl)->notifier_call, (*nl)->priority);
    }
    return 0;
}

static int __init kp_init(void)
{
    kp.pre_handler = handler_pre;
    return register_kprobe(&kp);
}

static void __exit kp_exit(void)
{
    unregister_kprobe(&kp);
}
module_init(kp_init);
module_exit(kp_exit);
MODULE_LICENSE("GPL");

Notifier Error Injection 프레임워크

커널은 lib/notifier-error-inject.c에 notifier 콜백의 에러 반환을 시뮬레이션하는 테스트 프레임워크를 제공합니다. 이를 통해 NOTIFY_BAD 반환 시 시스템의 롤백 동작을 검증할 수 있습니다.

# 커널 빌드 옵션
CONFIG_NOTIFIER_ERROR_INJECTION=m
CONFIG_CPU_NOTIFIER_ERROR_INJECT=m
CONFIG_PM_NOTIFIER_ERROR_INJECT=m
CONFIG_MEMORY_NOTIFIER_ERROR_INJECT=m

# PM notifier 에러 주입 예제
modprobe pm-notifier-error-inject

# 다음 PM_SUSPEND_PREPARE에서 -ENOMEM 반환하도록 설정
echo -12 > /sys/kernel/debug/notifier-error-inject/pm/actions/PM_SUSPEND_PREPARE/error

# 서스펜드 시도 → PM notifier가 NOTIFY_BAD 반환 → 서스펜드 거부
echo mem > /sys/power/state
# dmesg에서 확인: "PM: Some devices failed to suspend, or early wake event detected"

# 에러 주입 해제
echo 0 > /sys/kernel/debug/notifier-error-inject/pm/actions/PM_SUSPEND_PREPARE/error

# CPU hotplug 에러 주입
modprobe cpu-notifier-error-inject
echo -12 > /sys/kernel/debug/notifier-error-inject/cpu/actions/CPU_UP_PREPARE/error
# CPU online 시도가 거부됨
echo 1 > /sys/devices/system/cpu/cpu1/online

# 메모리 핫플러그 에러 주입
modprobe memory-notifier-error-inject
echo -12 > /sys/kernel/debug/notifier-error-inject/memory/actions/MEM_GOING_ONLINE/error
에러 주입 테스트 가치: Notifier error injection은 정상 경로에서는 테스트하기 어려운 롤백 경로를 검증합니다. PM notifier에서 NOTIFY_BAD가 반환되면 이미 호출된 콜백들에게 PM_POST_SUSPEND가 전달되어야 하는데, 이 롤백 경로에 버그가 있으면 서스펜드 실패 시 시스템이 불안정해집니다. 에러 주입으로 이런 엣지 케이스를 미리 검증하세요.

콜백 내 크래시 디버깅

해제 후 사용(Use-After-Free): Notifier 콜백에서 가장 흔한 크래시 원인은 unregister 없이 모듈이 언로드되어 콜백 함수 포인터가 댕글링 포인터가 되는 경우입니다. 이 크래시는 모듈 언로드 직후가 아니라 다음 이벤트 발생 시 나타나므로 원인 추적이 어렵습니다.
/* 크래시 진단을 위한 디버그 래퍼 */
static int debug_notifier_call_chain(
    struct notifier_block **nl,
    unsigned long val, void *v)
{
    struct notifier_block *nb;
    int ret;

    for (nb = rcu_dereference(*nl); nb; nb = rcu_dereference(nb->next)) {
        /* 콜백 주소가 유효한 커널 텍스트인지 검증 */
        if (!kernel_text_address((unsigned long)nb->notifier_call)) {
            WARN(1, "invalid notifier callback %px\n",
                 nb->notifier_call);
            continue;
        }
        ret = nb->notifier_call(nb, val, v);
        if (ret & NOTIFY_STOP_MASK)
            break;
    }
    return ret;
}

일반적 디버깅 시나리오

증상원인진단 방법해결
모듈 언로드 후 패닉unregister 누락ftrace로 등록/해제 쌍 확인module_exit에 unregister 추가
서스펜드 실패콜백에서 NOTIFY_BADpm_debug_messages=1 부트 파라미터거부하는 콜백 수정
데드락콜백 내 재등록lockdep 경고 메시지SRCU notifier 사용 또는 workqueue 위임
높은 지연콜백 과도한 작업bpftrace 지연 측정최소 작업 + workqueue 위임
이벤트 누락다른 콜백의 NOTIFY_STOPftrace function_graph로 체인 추적NOTIFY_STOP 사용 콜백 검토

설계 패턴과 모범 사례

Notifier chain을 올바르게 설계하고 사용하기 위한 패턴과 안티패턴을 정리합니다.

언제 Notifier Chain을 사용해야 하는가

적합한 경우부적합한 경우
구독자가 컴파일 시점에 알 수 없음구독자가 고정(1~2개)
이벤트 소스와 구독자 간 느슨한 결합 필요구독자 간 순서 의존성 있음
다수의 독립 모듈이 같은 이벤트 관심양방향 통신 필요
모듈이 동적으로 로드/언로드됨고성능 핫패스(per-packet 등)
커널 내부 서브시스템 간 이벤트 전달사용자 공간(User Space)과의 이벤트 전달

대안 메커니즘 비교

메커니즘결합도성능용도
Notifier Chain느슨보통커널 서브시스템 간 이벤트
직접 콜백강함높음1:1 관계, 고성능 필요
Workqueue느슨보통비동기 작업 위임
Netlink느슨낮음커널-사용자 공간 이벤트
Tracepoint느슨높음관찰/진단 목적
eventfd보통높음사용자 공간 이벤트 알림

모듈 언로드 시 안전한 해제 패턴

/* 패턴 1: 기본 — module_exit에서 해제 */
static struct notifier_block my_nb = {
    .notifier_call = my_callback,
};

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

static void __exit my_exit(void)
{
    unregister_netdevice_notifier(&my_nb);
    /* unregister 이후에만 모듈 리소스 해제 */
    kfree(my_data);
}

/* 패턴 2: devm — device 수명 자동 관리 */
static int my_probe(struct platform_device *pdev)
{
    return devm_register_reboot_notifier(&pdev->dev, &my_nb);
    /* 디바이스 제거 시 자동 unregister */
}

/* 패턴 3: 에러 경로 안전 — goto 패턴 */
static int __init my_init(void)
{
    int ret;

    ret = register_pm_notifier(&my_pm_nb);
    if (ret)
        return ret;

    ret = register_netdevice_notifier(&my_net_nb);
    if (ret)
        goto err_net;

    ret = register_reboot_notifier(&my_reboot_nb);
    if (ret)
        goto err_reboot;

    return 0;

err_reboot:
    unregister_netdevice_notifier(&my_net_nb);
err_net:
    unregister_pm_notifier(&my_pm_nb);
    return ret;
}

SRCU vs Blocking: 트레이드오프

Blocking Notifier: 읽기 경로(call_chain)에서 rwsem read lock을 획득합니다. 간단하고 예측 가능하지만, 콜백 내에서 같은 체인을 수정(register/unregister)하면 데드락이 발생합니다. 대부분의 경우 이 변형이 적합합니다.
SRCU Notifier: 읽기 경로에서 SRCU read lock을 사용하므로 콜백 내에서 체인 수정이 안전합니다. 단, SRCU의 grace period 비용(per-CPU 카운터 동기화)이 추가됩니다. 콜백에서 동적으로 구독을 변경해야 하는 경우에만 사용하세요.

커널 모듈(Kernel Module) Notifier 사용 템플릿

/* 완전한 커널 모듈 템플릿: notifier chain 사용 */
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/netdevice.h>
#include <linux/reboot.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);

    switch (event) {
    case NETDEV_UP:
        pr_info("[my_mod] %s is up\n", dev->name);
        break;
    case NETDEV_DOWN:
        pr_info("[my_mod] %s is down\n", dev->name);
        break;
    default:
        break;  /* 관심 없는 이벤트 무시 */
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_netdev_nb = {
    .notifier_call = my_netdev_event,
};

static int __init my_mod_init(void)
{
    int ret;

    ret = register_netdevice_notifier(&my_netdev_nb);
    if (ret) {
        pr_err("[my_mod] failed to register notifier: %d\n", ret);
        return ret;
    }

    pr_info("[my_mod] loaded\n");
    return 0;
}

static void __exit my_mod_exit(void)
{
    unregister_netdevice_notifier(&my_netdev_nb);
    pr_info("[my_mod] unloaded\n");
}

module_init(my_mod_init);
module_exit(my_mod_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Notifier Chain example module");

흔한 실수 요약

실수 1 — 스택 변수에 notifier_block 선언: struct notifier_block을 로컬 변수로 선언하고 등록하면, 함수 반환 후 메모리가 해제되어 댕글링 포인터가 됩니다. 반드시 static 또는 kmalloc으로 할당하세요.
실수 2 — 콜백 내 데드락: Blocking notifier 콜백에서 같은 체인의 register/unregister를 호출하면 rwsem read→write 데드락이 발생합니다. 같은 체인을 수정해야 한다면 SRCU notifier를 사용하거나 workqueue로 위임하세요.
실수 3 — 재진입 순환: netdev notifier 콜백에서 dev_set_mtu()를 호출하면 NETDEV_CHANGEMTU 이벤트가 발생하여 같은 콜백이 재진입됩니다. 재진입 가드(per-CPU 플래그)를 사용하거나 workqueue로 비동기 처리하세요.
/* 재진입 가드 패턴 */
static DEFINE_PER_CPU(int, in_notifier_cb);

static int my_netdev_cb(struct notifier_block *nb,
                          unsigned long event, void *ptr)
{
    if (__this_cpu_read(in_notifier_cb))
        return NOTIFY_DONE;  /* 재진입 방지 */

    __this_cpu_write(in_notifier_cb, 1);

    /* ... 작업 수행 ... */

    __this_cpu_write(in_notifier_cb, 0);
    return NOTIFY_DONE;
}

PREEMPT_RT 환경에서의 Notifier Chain

PREEMPT_RT(실시간 선점 패치)는 스핀락 시맨틱(Semantics)을 근본적으로 변경합니다. 일반 커널에서 spinlock_t는 선점과 인터럽트를 비활성화하지만, PREEMPT_RT에서는 rt_mutex로 대체되어 슬립이 가능한 잠금이 됩니다. 이 변화는 각 Notifier Chain 변형의 동작에 직접적인 영향을 미칩니다.

Atomic notifier의 경우, PREEMPT_RT에서 스핀락이 슬리핑 잠금으로 바뀌므로 등록 경로에서 슬립이 가능해집니다. 다만 읽기 경로는 여전히 rcu_read_lock()을 사용하므로, PREEMPT_RT에서 RCU 읽기 구간이 선점 가능해지는 효과가 있습니다. 이는 콜백 실행 중 더 높은 우선순위 태스크에 의해 선점될 수 있음을 의미합니다.

Blocking notifier와 SRCU notifier는 원래 슬립이 가능한 잠금을 사용하므로, PREEMPT_RT에서도 동작이 크게 변하지 않습니다. 따라서 RT 시스템에서는 blocking이나 SRCU notifier를 선호하는 것이 안전합니다.

Notifier 유형일반 커널PREEMPT_RTRT 영향
atomic spinlock + RCU (비선점) rt_mutex + RCU (선점 가능) 콜백 중 선점 가능, 지연 시간 변동
blocking rwsem (슬립 가능) rwsem (변화 없음) 영향 없음 (RT 안전)
srcu SRCU + mutex (슬립 가능) SRCU + mutex (변화 없음) 영향 없음 (RT 안전)
raw 잠금 없음 잠금 없음 호출자 동기화에 따라 다름
Atomic Notifier — 일반 커널 vs PREEMPT_RT 일반 커널 (mainline) 등록 경로: spin_lock_irqsave → IRQ 비활성 + 비선점 읽기 경로: rcu_read_lock → 선점 비활성 (sleep 불가) PREEMPT_RT 등록 경로: rt_mutex_lock → 슬립 가능, 우선순위 상속 읽기 경로: rcu_read_lock → 선점 가능 (콜백 중 선점) 콜백 실행 중 선점 불가 → 예측 가능한 완료 시간 → 긴 콜백 = 다른 태스크 지연 콜백 실행 중 고우선순위 태스크가 선점 가능 → RT 태스크 응답 시간 개선 → 콜백 완료 시간 비결정적 RT 환경 권장사항 blocking/SRCU notifier 사용 → RT에서도 동작 일관성 보장. atomic notifier는 RT에서 의미 변화 주의.

커널 Notifier Chain 전체 맵

Linux 커널에는 수십 개의 Notifier Chain이 여러 서브시스템에 걸쳐 정의되어 있습니다. 아래 다이어그램은 주요 Notifier Chain을 서브시스템별로 분류하고, 각 체인의 유형과 대표 이벤트를 정리한 것입니다.

Linux 커널 주요 Notifier Chain 전체 맵 서브시스템 체인 이름 유형 주요 이벤트 프로세스 스케줄링 cpu_chain SRCU CPU_UP_PREPARE, CPU_ONLINE, CPU_DEAD task_free_notifier Atomic 태스크 해제 시 알림 die_chain Atomic DIE_OOPS, DIE_GPF (커널 fault) 메모리 memory_chain Blocking MEM_GOING_ONLINE, MEM_OFFLINE oom_notify_list Blocking OOM killer 발동 전 알림 munmap_notifier Blocking VMA 해제 시 알림 네트워크 netdev_chain Raw NETDEV_UP, NETDEV_DOWN, NETDEV_CHANGEMTU inetaddr_chain Blocking NETDEV_UP (IPv4 주소 변경) inet6addr_chain Atomic NETDEV_UP (IPv6 주소 변경) netevent_notif_chain Atomic NETEVENT_NEIGH_UPDATE, REDIRECT 전원 관리 pm_chain_head Blocking PM_SUSPEND_PREPARE, PM_POST_SUSPEND reboot_notifier_list Blocking SYS_RESTART, SYS_HALT, SYS_POWER_OFF restart_handler_list Blocking 아키텍처별 재부팅 핸들러 디바이스 bus_notifier (bus_type) Blocking BUS_NOTIFY_ADD_DEVICE, BIND/UNBIND usb_notifier_list Blocking USB_DEVICE_ADD, USB_DEVICE_REMOVE panic_notifier_list Atomic 커널 패닉 직전 긴급 알림 보안 module_notify_list Blocking MODULE_STATE_COMING, GOING, LIVE keyboard_notifier_list Atomic KBD_KEYCODE, KBD_POST_KEYSYM 유형 범례 Blocking (rwsem, 가장 일반적) Atomic (spinlock+RCU) SRCU Raw (잠금 없음)

참고자료

공식 문서

LWN.net 및 주요 참고 글

커널 소스 경로

파일 역할
kernel/notifier.c 알림 체인 핵심 구현 — register/unregister/call 함수
include/linux/notifier.h notifier_block, 4종 notifier_head 구조체 및 매크로 정의
kernel/cpu.c CPU 핫플러그 notifier 등록 및 호출 경로
kernel/power/main.c PM notifier chain — suspend/resume 이벤트 알림
net/core/dev.c netdev_chain — 네트워크 디바이스 이벤트(NETDEV_UP 등) 알림
kernel/panic.c panic_notifier_list — 커널 패닉 직전 atomic notifier 호출
kernel/reboot.c reboot_notifier_list — 시스템 재부팅/종료 알림 체인
lib/notifier-error-inject.c notifier 오류 주입 테스트 프레임워크 구현

서적 및 학습 자료

블로그 및 커뮤니티 자료

다음 학습: