디바이스 드라이버 (Device Drivers)

Linux 커널 디바이스 드라이버 개발의 공통 골격을 장치 모델 관점에서 심층 정리합니다. character/block/network 디바이스 분류와 사용자 공간 인터페이스, platform/PCI/USB 기반 프로브-바인딩 생명주기, file_operations와 irq/workqueue/tasklet 분할 전략, DMA 매핑과 캐시 일관성, 전원관리(runtime PM/system suspend) 연계, 에러 경로와 자원 해제 패턴, tracepoint·dynamic debug 기반 디버깅까지 실무 드라이버 품질을 높이는 핵심 원칙을 다룹니다.

문서 구조 재정렬: 이 문서는 드라이버 공통 골격과 설계 원칙 중심으로 유지합니다. 세부 심화는 Network Device 드라이버, Device Tree, DMA 문서를 우선 참고하세요.
전제 조건: 커널 모듈인터럽트 문서를 먼저 읽으세요. 버스/열거/프로브 경로는 초기화 순서와 자원 등록 규칙이 핵심이므로, 모듈 로딩과 인터럽트 처리를 먼저 이해해야 합니다.
일상 비유: 이 주제는 터미널 입출고 게이트 운영과 비슷합니다. 차량(디바이스)이 들어오면 게이트 규칙(버스 규약)에 맞춰 배정하고 점검하듯이, 드라이버도 바인딩 규약을 정확히 따라야 합니다.

핵심 요약

  • 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
  • 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
  • IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
  • 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
  • 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.

단계별 이해

  1. 장치 수명주기 확인
    probe부터 remove까지 흐름을 점검합니다.
  2. 비동기 경로 설계
    IRQ, 워크큐, 타이머 역할을 분리합니다.
  3. 자원 정합성 검증
    DMA/클록/전원 참조를 교차 확인합니다.
  4. 현장 조건 테스트
    연결 끊김/복구/부하 상황을 재현합니다.
관련 표준: Device Tree Specification (하드웨어 기술 형식), PCIe 6.0 (디바이스 인터커넥트), ACPI 6.5 (디바이스 열거/전원) — 커널 디바이스 드라이버 프레임워크가 참조하는 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

디바이스 드라이버 개요

디바이스 드라이버는 커널과 하드웨어 사이의 인터페이스입니다. Linux에서는 세 가지 주요 디바이스 유형으로 분류합니다:

유형인터페이스예시
Character Device바이트 스트림, 순차 접근시리얼, 터미널, /dev/null
Block Device고정 크기 블록, 랜덤 접근디스크, SSD, USB 스토리지
Network Device패킷 기반, 소켓 API이더넷, WiFi

Character Device 드라이버

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;

static int my_open(struct inode *inode, struct file *file)
{
    pr_info("device opened\\n");
    return 0;
}

static ssize_t my_read(struct file *file,
    char __user *buf, size_t len, loff_t *off)
{
    char msg[] = "Hello from driver\\n";
    if (*off >= sizeof(msg))
        return 0;
    if (copy_to_user(buf, msg + *off, min(len, sizeof(msg) - *off)))
        return -EFAULT;
    *off += len;
    return len;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open  = my_open,
    .read  = my_read,
};

static int __init my_init(void)
{
    alloc_chrdev_region(&dev_num, 0, 1, "mydev");
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev_num, 1);
    my_class = class_create("mydev_class");
    device_create(my_class, NULL, dev_num, NULL, "mydev");
    return 0;
}

직접 해보기: Hello World 캐릭터 디바이스

간단한 캐릭터 디바이스 드라이버를 작성하고 유저 공간에서 읽기/쓰기를 테스트합니다.

난이도: 중급 ⏱️ 예상 소요 시간: 45분
⚙️ 환경 요구사항
  • OS: Ubuntu 22.04+ (커널 헤더 설치됨)
  • 권한: sudo 권한 (모듈 로드, 디바이스 노드 생성)
  • 사전 지식: 커널 모듈 기초 (module_init/exit)
실습 흐름
  1. 드라이버 소스 작성 (file_operations 구현)
  2. Makefile 작성 및 빌드
  3. 모듈 로드 및 디바이스 노드 확인
  4. 유저 공간에서 read/write 테스트
  5. 정리 및 언로드

1단계: 드라이버 소스 작성 (⏱️ 15분)

목표: 읽기/쓰기가 가능한 간단한 캐릭터 디바이스 드라이버 작성

chardev.c 파일 생성:

/* chardev.c - 간단한 캐릭터 디바이스 드라이버 */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "chardev"
#define CLASS_NAME  "chardev_class"
#define BUF_SIZE     256

static dev_t dev_num;
static struct cdev char_cdev;
static struct class *char_class = NULL;
static char device_buffer[BUF_SIZE] = "Hello from kernel!\\n";
static size_t buffer_size = 20;

/* open 핸들러 */
static int dev_open(struct inode *inode, struct file *file)
{
    pr_info("chardev: Device opened\\n");
    return 0;
}

/* read 핸들러 - 유저 공간으로 데이터 전송 */
static ssize_t dev_read(struct file *file,
                         char __user *user_buf,
                         size_t count,
                         loff_t *offset)
{
    size_t to_read;

    if (*offset >= buffer_size)
        return 0;  /* EOF */

    to_read = min(count, buffer_size - (size_t)*offset);

    if (copy_to_user(user_buf, device_buffer + *offset, to_read))
        return -EFAULT;

    *offset += to_read;
    pr_info("chardev: Read %zu bytes\\n", to_read);
    return to_read;
}

/* write 핸들러 - 유저 공간에서 데이터 수신 */
static ssize_t dev_write(struct file *file,
                          const char __user *user_buf,
                          size_t count,
                          loff_t *offset)
{
    size_t to_write = min(count, (size_t)BUF_SIZE - 1);

    if (copy_from_user(device_buffer, user_buf, to_write))
        return -EFAULT;

    device_buffer[to_write] = '\0';
    buffer_size = to_write;
    *offset = 0;  /* 다음 read는 처음부터 */

    pr_info("chardev: Wrote %zu bytes: %s\\n", to_write, device_buffer);
    return to_write;
}

/* release 핸들러 */
static int dev_release(struct inode *inode, struct file *file)
{
    pr_info("chardev: Device closed\\n");
    return 0;
}

/* file_operations 구조체 */
static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = dev_open,
    .read    = dev_read,
    .write   = dev_write,
    .release = dev_release,
};

static int __init chardev_init(void)
{
    /* 1. Major/Minor 번호 할당 */
    if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
        pr_err("Failed to allocate device number\\n");
        return -1;
    }
    pr_info("chardev: Allocated device number %d:%d\\n",
            MAJOR(dev_num), MINOR(dev_num));

    /* 2. cdev 초기화 및 추가 */
    cdev_init(&char_cdev, &fops);
    if (cdev_add(&char_cdev, dev_num, 1) < 0) {
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to add cdev\\n");
        return -1;
    }

    /* 3. 디바이스 클래스 생성 */
    char_class = class_create(CLASS_NAME);
    if (IS_ERR(char_class)) {
        cdev_del(&char_cdev);
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to create class\\n");
        return PTR_ERR(char_class);
    }

    /* 4. /dev/chardev 노드 생성 */
    if (IS_ERR(device_create(char_class, NULL, dev_num, NULL, DEVICE_NAME))) {
        class_destroy(char_class);
        cdev_del(&char_cdev);
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to create device\\n");
        return -1;
    }

    pr_info("chardev: Driver loaded successfully\\n");
    return 0;
}

static void __exit chardev_exit(void)
{
    device_destroy(char_class, dev_num);
    class_destroy(char_class);
    cdev_del(&char_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("chardev: Driver unloaded\\n");
}

module_init(chardev_init);
module_exit(chardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple character device driver");

2단계: 빌드 (⏱️ 5분)

Makefile 생성:

obj-m += chardev.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

빌드 실행:

make
✅ 예상 출력:
Building modules...
  CC [M]  chardev.o
  LD [M]  chardev.ko
Done.
      

3단계: 모듈 로드 및 확인 (⏱️ 5분)

# 모듈 로드
sudo insmod chardev.ko

# 디바이스 노드 확인
ls -l /dev/chardev

# Major/Minor 번호 확인
cat /proc/devices | grep chardev

# 커널 로그 확인
dmesg | tail -5
✅ 예상 출력:
$ ls -l /dev/chardev
crw------- 1 root root 237, 0 Jan 15 10:30 /dev/chardev

$ dmesg | tail -3
[ 100.123] chardev: Allocated device number 237:0
[ 100.124] chardev: Driver loaded successfully
      

4단계: 유저 공간 테스트 (⏱️ 10분)

# 읽기 테스트
sudo cat /dev/chardev

# 쓰기 테스트
echo "Hello from user!" | sudo tee /dev/chardev

# 다시 읽기
sudo cat /dev/chardev

# 커널 로그 확인
dmesg | tail -10
✅ 예상 출력:
$ sudo cat /dev/chardev
Hello from kernel!

$ echo "Hello from user!" | sudo tee /dev/chardev
Hello from user!

$ sudo cat /dev/chardev
Hello from user!

$ dmesg | tail -6
[ 200.123] chardev: Device opened
[ 200.124] chardev: Read 20 bytes
[ 200.125] chardev: Device closed
[ 200.126] chardev: Device opened
[ 200.127] chardev: Wrote 18 bytes: Hello from user!
[ 200.128] chardev: Device closed
      

5단계: 정리 (⏱️ 2분)

# 모듈 언로드
sudo rmmod chardev

# 디바이스 노드 삭제 확인
ls /dev/chardev  # "No such file or directory" 출력되면 정상

# 빌드 파일 정리
make clean

결과 검증

다음 단계

심화 문서: Character Device 심화 페이지에서 아래 주제를 상세히 다룹니다.
  • ioctl 핸들러 — ioctl 심화
  • poll/select/epoll 지원
  • mmap 구현
  • 동시성/잠금, miscdevice, 다중 인스턴스
  • 인터럽트 처리 — 인터럽트

Platform Driver

Platform Device 개념

PCI, USB 등 자동 열거(enumeration)가 가능한 버스와 달리, SoC(System-on-Chip) 내장 컨트롤러(UART, SPI, I2C, GPIO, 타이머 등)는 버스 프로토콜로 디바이스를 발견할 수 없습니다. 커널은 이러한 디바이스를 위해 Platform Bus라는 가상 버스를 제공합니다. Platform Bus는 실제 하드웨어 버스가 아니라, device-driver 매칭 인프라를 재사용하기 위한 소프트웨어 추상화입니다.

Platform Device의 하드웨어 정보(레지스터 주소, IRQ 번호 등)는 세 가지 방식으로 커널에 전달됩니다:

공급 방식설명대표 환경
Device TreeDTS/DTB 파일에 하드웨어 기술, 부트로더가 전달ARM, RISC-V, PowerPC
ACPIACPI 테이블(DSDT/SSDT)에 디바이스 기술x86 서버/데스크톱, ARM 서버
정적 등록 (레거시)보드 파일에서 platform_device_register() 직접 호출구형 ARM 보드 (비권장)
ℹ️

Device Tree의 상세 문법, 프로퍼티, OF API 등은 Device Tree 심화 섹션을 참고하세요.

struct platform_device 분석

struct platform_device는 platform bus에 연결된 디바이스 하나를 나타냅니다. include/linux/platform_device.h에 정의되어 있습니다.

struct platform_device {
    const char      *name;          /* 디바이스 이름 (name 매칭에 사용) */
    int             id;             /* 인스턴스 번호 (-1이면 단일) */
    struct device   dev;            /* 임베디드 generic device */
    struct resource *resource;       /* I/O, 메모리, IRQ 리소스 배열 */
    u32             num_resources;  /* resource 배열 크기 */
    const struct platform_device_id *id_entry; /* 매칭된 id_table 항목 */
    const char      *driver_override; /* 강제 드라이버 바인딩 */
};

핵심 필드별 용도:

필드용도
dev.platform_data보드 레벨 데이터 전달 (레거시). void * 포인터로 드라이버별 구조체 전달
dev.of_nodeDevice Tree에서 생성된 경우, 해당 DT 노드 (struct device_node *)
dev.fwnode통합 firmware 노드. DT와 ACPI를 추상화하는 struct fwnode_handle *
resourceMMIO 주소 범위, IRQ 번호, DMA 채널 등 하드웨어 리소스
driver_overridesysfs에서 수동으로 특정 드라이버를 강제 바인딩할 때 사용

struct resource는 디바이스의 하드웨어 리소스를 기술합니다:

struct resource {
    resource_size_t start;  /* 리소스 시작 주소/번호 */
    resource_size_t end;    /* 리소스 끝 주소/번호 (inclusive) */
    const char      *name;
    unsigned long   flags;  /* 리소스 유형 */
};

/* 주요 리소스 유형 플래그 */
IORESOURCE_MEM   /* MMIO 메모리 영역 */
IORESOURCE_IRQ   /* 인터럽트 번호 */
IORESOURCE_DMA   /* DMA 채널 */
IORESOURCE_IO    /* I/O 포트 (x86) */

struct platform_driver 분석

struct platform_driver는 platform device를 다루는 드라이버를 나타냅니다:

struct platform_driver {
    int  (*probe)(struct platform_device *);    /* 디바이스 발견 시 호출 */
    void (*remove)(struct platform_device *);   /* 디바이스 제거 시 호출 */
    void (*shutdown)(struct platform_device *); /* reboot/poweroff 시 호출 */
    int  (*suspend)(struct platform_device *, pm_message_t); /* 레거시 PM */
    int  (*resume)(struct platform_device *);   /* 레거시 PM */
    struct device_driver driver;   /* 임베디드 generic driver */
    const struct platform_device_id *id_table; /* name+data 기반 매칭 */
    bool prevent_deferred_probe;
};

driver 필드의 주요 하위 필드:

필드용도
driver.name드라이버 이름. sysfs 경로와 최후순위 매칭에 사용
driver.of_match_tableDevice Tree compatible 매칭 테이블
driver.acpi_match_tableACPI HID/CID 매칭 테이블
driver.pmstruct dev_pm_ops * — 최신 PM 콜백 (전원 관리 참고)
driver.ownerTHIS_MODULE (매크로가 자동 설정)

id_table은 이름 기반 매칭에 사용되며, 드라이버별 데이터를 전달할 수 있습니다:

struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];
    kernel_ulong_t driver_data;  /* 드라이버별 private 데이터 */
};

/* 사용 예: 여러 변형 디바이스를 하나의 드라이버로 지원 */
enum { CHIP_V1, CHIP_V2, CHIP_V3 };

static const struct platform_device_id my_ids[] = {
    { "my-chip-v1", CHIP_V1 },
    { "my-chip-v2", CHIP_V2 },
    { "my-chip-v3", CHIP_V3 },
    { }  /* sentinel */
};

매칭 메커니즘

Platform Bus의 platform_match() 함수는 다음 4단계 우선순위로 device-driver 매칭을 수행합니다:

우선순위매칭 방식비교 대상
1 (최고)driver_overridesysfs에서 수동 설정된 드라이버 이름과 driver.name 비교
2of_match_tableDT 노드의 compatible과 드라이버의 of_device_id 배열 비교
3acpi_match_tableACPI 디바이스 HID/CID와 acpi_device_id 배열 비교
4id_tableplatform_device.nameplatform_device_id.name 비교
5 (최저)driver.nameplatform_device.namedriver.name 문자열 비교

커널 내부의 platform_match() 핵심 로직:

/* drivers/base/platform.c */
static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* 1단계: driver_override (sysfs 강제 바인딩) */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* 2단계: Device Tree compatible 매칭 */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* 3단계: ACPI 매칭 */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* 4단계: id_table 매칭 */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* 5단계: driver.name 문자열 매칭 (fallback) */
    return (strcmp(pdev->name, drv->name) == 0);
}

각 매칭 방식별 드라이버 정의 예제:

/* Device Tree 매칭 (가장 일반적) */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-ctrl-v2", .data = &chip_v2_data },
    { .compatible = "vendor,my-ctrl-v1", .data = &chip_v1_data },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

/* ACPI 매칭 */
static const struct acpi_device_id my_acpi_ids[] = {
    { "VNDR0001", (kernel_ulong_t)&chip_v1_data },
    { "VNDR0002", (kernel_ulong_t)&chip_v2_data },
    { }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name           = "my-ctrl",
        .of_match_table = my_of_ids,
        .acpi_match_table = ACPI_PTR(my_acpi_ids),
    },
};

Platform Device 등록

Platform Device가 커널에 등록되는 세 가지 경로:

1. 정적 등록 (레거시 보드 파일)

DT 이전의 ARM 보드에서 사용하던 방식입니다. 현재는 비권장이지만 레거시 코드에서 여전히 볼 수 있습니다:

/* 레거시 보드 파일 (arch/arm/mach-xxx/) */
static struct resource my_resources[] = {
    [0] = {
        .start = 0x40000000,
        .end   = 0x40000FFF,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = 42,   /* IRQ 번호 */
        .end   = 42,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device my_pdev = {
    .name          = "my-ctrl",
    .id            = -1,
    .resource      = my_resources,
    .num_resources = ARRAY_SIZE(my_resources),
};

/* 보드 초기화 함수에서 */
platform_device_register(&my_pdev);

편의 함수로 간결하게 등록할 수도 있습니다:

/* 간편 등록 함수들 */
platform_device_register_simple("my-ctrl", -1, my_resources, 2);
platform_device_register_data(NULL, "my-ctrl", -1, &pdata, sizeof(pdata));
platform_device_register_resndata(NULL, "my-ctrl", -1,
    my_resources, 2, &pdata, sizeof(pdata));

2. Device Tree 기반 자동 생성

현대 임베디드 시스템의 표준 방식입니다. 부팅 시 커널이 DTB를 파싱하여 of_platform_populate()로 platform_device를 자동 생성합니다:

/* 커널 부팅 시 자동 실행 (drivers/of/platform.c) */
of_platform_default_populate(NULL, NULL, NULL);
    → DT의 최상위 "simple-bus" / "simple-mfd" 등의 자식 노드를
      순회하며 of_platform_device_create() 호출
    → struct platform_device 자동 생성 + 등록

3. ACPI 기반 자동 생성

x86 및 ARM 서버에서 ACPI 테이블의 디바이스 정보로 platform_device를 자동 생성합니다:

설명 요약:
  • ACPI 열거 — DSDT/SSDT의 Device() 오브젝트가 platform_device로 변환
  • ACPI 네임스페이스 예:
  • Device (MYCT) {
  • Name (_HID, "VNDR0001")
  • Name (_CRS, ResourceTemplate() {
  • Memory32Fixed(ReadWrite, 0x40000000, 0x1000)
  • Interrupt(ResourceConsumer, ...) { 42 }
  • })
  • }
  • → acpi_default_enumeration()이 platform_device로 변환

probe/remove 생명주기

probe()는 device-driver 매칭이 성공했을 때 커널이 호출하는 진입점입니다. remove()는 디바이스 언바인드 또는 드라이버 언로드 시 역순으로 정리합니다.

probe 호출 조건:

probe 전형적 순서:

static int my_probe(struct platform_device *pdev)
{
    struct my_priv *priv;
    void __iomem *base;
    int irq, ret;

    /* 1. private 데이터 할당 */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 2. 리소스 획득 — MMIO */
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);
    priv->base = base;

    /* 3. 리소스 획득 — IRQ */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;  /* 에러 코드 전파 */

    ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
                           0, dev_name(&pdev->dev), priv);
    if (ret)
        return ret;

    /* 4. 클록, 리셋, 레귤레이터 등 하드웨어 리소스 */
    priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
    if (IS_ERR(priv->clk))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->clk),
                              "failed to get clock\\n");

    /* 5. 하드웨어 초기화 */
    writel(CTRL_ENABLE, base + REG_CTRL);

    /* 6. 서브시스템 등록 (misc device, input, net 등) */
    ret = misc_register(&priv->miscdev);
    if (ret)
        return ret;

    /* 7. private 데이터를 디바이스에 연결 */
    platform_set_drvdata(pdev, priv);

    dev_info(&pdev->dev, "probed successfully\\n");
    return 0;
}
💡

dev_err_probe()-EPROBE_DEFER를 자동으로 감지하여 dev_dbg 수준으로 로깅하고, 그 외 에러는 dev_err로 출력합니다. probe deferral 메시지가 dmesg를 오염시키는 것을 방지하므로 적극 활용하세요.

remove — 역순 정리:

static void my_remove(struct platform_device *pdev)
{
    struct my_priv *priv = platform_get_drvdata(pdev);

    /* probe의 역순으로 정리 */
    misc_deregister(&priv->miscdev);

    /* devm_ 계열 리소스는 자동 해제되므로 별도 처리 불필요 */
    /* devm_kzalloc, devm_request_irq, devm_ioremap 등 */
}
ℹ️

devm_* (managed) API를 사용하면 remove에서 명시적 해제가 불필요합니다. 상세한 devm API 목록은 Managed Device Resources (devm) 섹션을 참고하세요.

shutdown 콜백:

shutdown()은 시스템 reboot 또는 poweroff 시 호출됩니다. DMA 전송 중단, 인터럽트 비활성화 등 하드웨어를 안전한 상태로 전환합니다:

static void my_shutdown(struct platform_device *pdev)
{
    struct my_priv *priv = platform_get_drvdata(pdev);

    /* 하드웨어를 안전한 상태로 전환 */
    writel(0, priv->base + REG_CTRL);      /* 컨트롤러 비활성화 */
    disable_irq(priv->irq);                /* 인터럽트 차단 */
}

리소스 접근 API

Platform 드라이버에서 디바이스의 하드웨어 리소스에 접근하는 API들:

API용도
platform_get_resource(pdev, type, index)타입별 N번째 리소스 구조체 반환
platform_get_resource_byname(pdev, type, name)이름으로 리소스 검색
platform_get_irq(pdev, index)N번째 IRQ 번호 반환 (DT/ACPI 추상화)
platform_get_irq_byname(pdev, name)이름으로 IRQ 검색
devm_platform_ioremap_resource(pdev, index)리소스 획득 + ioremap 한 번에 수행
devm_platform_ioremap_resource_byname(pdev, name)이름으로 리소스 획득 + ioremap
platform_get_drvdata(pdev)드라이버 private 데이터 반환
platform_set_drvdata(pdev, data)드라이버 private 데이터 저장

사용 예제:

/* 인덱스 기반 접근 */
struct resource *mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
int irq = platform_get_irq(pdev, 0);

/* 이름 기반 접근 (DT에서 reg-names, interrupt-names 지정 시) */
struct resource *cfg = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config");
int tx_irq = platform_get_irq_byname(pdev, "tx");
int rx_irq = platform_get_irq_byname(pdev, "rx");

/* 리소스 획득 + ioremap 통합 (권장) */
void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
    return PTR_ERR(base);

/* private 데이터 저장/조회 */
platform_set_drvdata(pdev, priv);          /* probe에서 */
struct my_priv *p = platform_get_drvdata(pdev);  /* remove 등에서 */
⚠️

platform_get_resource()devm_ioremap_resource()를 별도로 호출하는 것보다 devm_platform_ioremap_resource() 하나로 통합하는 것이 권장됩니다. NULL 체크와 영역 요청, 매핑을 한 번에 처리하여 코드가 간결해지고 에러 처리 누락을 방지합니다.

편의 매크로

module_platform_driver()는 모듈 init/exit 보일러플레이트를 자동 생성합니다:

/* include/linux/platform_device.h */
#define module_platform_driver(__platform_driver) \
    module_driver(__platform_driver, platform_driver_register, \
                  platform_driver_unregister)

/* 위 매크로는 아래와 동일한 코드를 생성 */
static int __init my_driver_init(void)
{
    return platform_driver_register(&my_driver);
}
module_init(my_driver_init);

static void __exit my_driver_exit(void)
{
    platform_driver_unregister(&my_driver);
}
module_exit(my_driver_exit);

관련 편의 매크로들:

매크로용도
module_platform_driver(drv)모듈용. init/exit에서 register/unregister 자동 생성
builtin_platform_driver(drv)빌트인 전용. device_initcall로 자동 등록
builtin_platform_driver_probe(drv, probe)빌트인 전용. probe를 __init 섹션에 배치하여 메모리 절약
MODULE_DEVICE_TABLE(of, ids)모듈 자동 로딩용 alias 생성. depmod가 인식
MODULE_DEVICE_TABLE(acpi, ids)ACPI 기반 모듈 자동 로딩용 alias 생성

MODULE_DEVICE_TABLE()modules.alias 파일에 항목을 추가하여, 디바이스 발견 시 udev가 해당 모듈을 자동 로드할 수 있게 합니다:

/* 드라이버 코드 */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-ctrl" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

/* → /lib/modules/.../modules.alias 에 추가됨:
 *   alias of:N*T*Cvendor,my-ctrlC* my_driver
 * → DT에 "vendor,my-ctrl" 노드가 있으면 udev가 my_driver.ko 자동 로드
 */

sysfs 인터페이스

Platform Bus는 sysfs를 통해 사용자 공간에 노출됩니다:

Platform Bus sysfs 구조 /sys/bus/platform/ devices/ ← 등록된 모든 platform device 40000000.uart → ../../../devices/platform/40000000.uart 40010000.spi → ../../../devices/platform/40010000.spi drivers/ ← 등록된 모든 platform driver my-ctrl/ bind ← 디바이스 이름 기록 시 수동 바인딩 unbind ← 디바이스 이름 기록 시 언바인딩 module → ../../../../module/my_ctrl 40000000.uart → ../../../../devices/platform/40000000.uart

수동 드라이버 바인딩/언바인딩:

# 디바이스를 현재 드라이버에서 분리
echo "40000000.uart" > /sys/bus/platform/drivers/my-ctrl/unbind

# driver_override로 다른 드라이버 강제 지정
echo "other-driver" > /sys/devices/platform/40000000.uart/driver_override

# 새 드라이버에 수동 바인딩
echo "40000000.uart" > /sys/bus/platform/drivers/other-driver/bind

# driver_override 해제 (빈 문자열 기록)
echo "" > /sys/devices/platform/40000000.uart/driver_override
💡

driver_override는 VFIO 등에서 디바이스를 사용자 공간 드라이버에 바인딩할 때 유용합니다. platform_match()의 최우선 매칭 경로이므로 DT/ACPI 매칭보다 우선합니다.

완전한 Platform Driver 예제:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>

struct my_priv {
    void __iomem   *base;
    struct clk     *clk;
    int            irq;
};

static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_priv *priv = data;
    u32 status = readl(priv->base + REG_STATUS);

    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    writel(status, priv->base + REG_STATUS);  /* ACK */
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_priv *priv;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    priv->clk = devm_clk_get_enabled(dev, NULL);
    if (IS_ERR(priv->clk))
        return dev_err_probe(dev, PTR_ERR(priv->clk), "clk failed\\n");

    return devm_request_irq(dev, priv->irq, my_irq_handler,
                            0, dev_name(dev), priv);
}

static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-ctrl-v2" },
    { .compatible = "vendor,my-ctrl" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .driver = {
        .name           = "my-ctrl",
        .of_match_table = my_of_ids,
    },
};
module_platform_driver(my_driver);

MODULE_DESCRIPTION("My Platform Controller Driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author Name");

DMA (Direct Memory Access)

심화 학습: DMA 매핑 API 전체(Coherent/Streaming/SG/Pool), IOMMU, SWIOTLB, CMA, DMA-BUF, P2P DMA, 캐시 일관성, 보안, 디버깅에 대한 종합 가이드는 DMA 심화 페이지를 참조하십시오.
#include <linux/dma-mapping.h>

/* Coherent DMA (CPU 캐시와 일관성 보장) */
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, buf, dma_handle);

/* Streaming DMA (방향 지정, 캐시 sync 필요) */
dma_addr_t dma = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
/* ... DMA 전송 ... */
dma_unmap_single(dev, dma, size, DMA_TO_DEVICE);
⚠️

DMA 매핑 후에는 반드시 dma_mapping_error()로 오류를 확인해야 합니다. IOMMU가 활성화된 시스템에서는 매핑 실패가 가능합니다.

Linux 디바이스 모델 (Device Model)

Linux 디바이스 모델은 커널 2.6에서 도입된 통합 프레임워크로, 모든 디바이스와 드라이버를 bus → device → driver의 계층으로 추상화합니다. 이 모델은 kobject 인프라 위에 구축되며, sysfs(/sys/)를 통해 사용자 공간에 노출됩니다. udev/systemd-udevd가 디바이스 이벤트를 수신하고, 전원 관리(PM)와 핫플러그가 일관된 인터페이스로 동작합니다.

struct device — 모든 디바이스의 기반

struct device는 커널에 등록되는 모든 디바이스의 공통 기반 구조체입니다. PCI, USB, platform, I2C 등 각 버스별 디바이스 구조체(pci_dev, usb_device, platform_device 등)는 이 구조체를 임베디드합니다.

/* include/linux/device.h (핵심 필드 발췌) */
struct device {
    struct kobject           kobj;           /* sysfs 디렉토리 노드 */
    struct device           *parent;         /* 부모 디바이스 (물리적 계층) */
    const struct device_type *type;          /* 디바이스 타입 (선택적) */

    struct bus_type         *bus;            /* 소속 버스 */
    struct device_driver    *driver;         /* 바인딩된 드라이버 (NULL이면 미바인딩) */

    void                    *platform_data;  /* 보드 레벨 데이터 (레거시) */
    void                    *driver_data;    /* 드라이버 private 데이터 */

    struct device_node      *of_node;        /* Device Tree 노드 */
    struct fwnode_handle    *fwnode;         /* 통합 FW 노드 (DT/ACPI) */

    struct class            *class;          /* 디바이스 클래스 (net, block 등) */
    dev_t                    devt;           /* /dev 노드 번호 (major:minor) */

    const struct dev_pm_ops *pm;             /* 전원 관리 콜백 */
    struct dev_pm_info       power;          /* PM 런타임 상태 */

    struct dma_map_ops      *dma_ops;        /* DMA 매핑 연산 */
    u64                     *dma_mask;       /* DMA 주소 마스크 */
    u64                      coherent_dma_mask;

    struct list_head         devres_head;    /* devm 리소스 리스트 */
};

주요 도우미 함수:

/* 디바이스 등록/해제 */
int device_register(struct device *dev);    /* device_initialize + device_add */
void device_unregister(struct device *dev); /* device_del + put_device */

/* 참조 카운팅 */
struct device *get_device(struct device *dev);  /* kobject_get 래퍼 */
void put_device(struct device *dev);              /* kobject_put 래퍼 */

/* driver_data 접근 */
void *dev_get_drvdata(const struct device *dev);
void dev_set_drvdata(struct device *dev, void *data);

/* 로깅 (dev_name 자동 포함) */
dev_err(&dev, "error: %d\\n", ret);
dev_warn(&dev, "warning\\n");
dev_info(&dev, "initialized\\n");
dev_dbg(&dev, "debug message\\n");
dev_err_probe(&dev, ret, "deferred 시 dbg, 그 외 err\\n");

struct device_driver — 드라이버 공통 구조체

struct device_driver는 모든 드라이버의 공통 기반입니다. 버스별 드라이버 구조체(pci_driver, platform_driver 등)가 이를 임베디드합니다.

/* include/linux/device/driver.h */
struct device_driver {
    const char              *name;           /* 드라이버 이름 */
    const struct bus_type   *bus;            /* 소속 버스 */
    struct module           *owner;          /* THIS_MODULE */

    const struct of_device_id   *of_match_table;   /* DT compatible 매칭 */
    const struct acpi_device_id *acpi_match_table; /* ACPI HID 매칭 */

    int  (*probe)(struct device *dev);           /* 버스별 probe가 호출 */
    void (*remove)(struct device *dev);          /* 언바인딩 시 호출 */
    void (*shutdown)(struct device *dev);        /* reboot/poweroff 시 */

    const struct dev_pm_ops *pm;                 /* PM 콜백 */
    const struct attribute_group **dev_groups;    /* 드라이버 sysfs 속성 */

    bool suppress_bind_attrs;  /* bind/unbind sysfs 파일 숨김 */
    enum probe_type probe_type; /* PROBE_DEFAULT_STRATEGY / PROBE_FORCE_SYNCHRONOUS */
};

struct bus_type — 버스 추상화

struct bus_type은 PCI, USB, Platform, I2C 등 버스 유형을 추상화합니다. 각 버스는 자신만의 device-driver 매칭 규칙과 probe 메커니즘을 정의합니다.

/* include/linux/device/bus.h */
struct bus_type {
    const char          *name;      /* 버스 이름 (sysfs: /sys/bus/NAME/) */
    const char          *dev_name;  /* 디바이스 이름 생성 접두어 */

    /* 핵심 콜백 */
    int  (*match)(struct device *dev, struct device_driver *drv);
    int  (*probe)(struct device *dev);
    void (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);

    /* uevent 환경변수 추가 (udev용) */
    int  (*uevent)(const struct device *dev, struct kobj_uevent_env *env);

    /* PM 콜백 */
    const struct dev_pm_ops *pm;

    /* 내부 데이터 */
    struct subsys_private *p;  /* klist_devices, klist_drivers, kset 등 */
};

/* 버스 등록/해제 */
int bus_register(const struct bus_type *bus);
void bus_unregister(const struct bus_type *bus);

커널에 등록된 주요 버스:

bus_typesysfs 경로match 방식디바이스 열거 방식
platform_bus_type/sys/bus/platform/DT compatible, ACPI HID, nameDT/ACPI/정적 등록
pci_bus_type/sys/bus/pci/vendor:device:class IDPCI 설정 공간 스캔
usb_bus_type/sys/bus/usb/vendor:product:class IDUSB 열거 프로토콜
i2c_bus_type/sys/bus/i2c/DT compatible, i2c_device_idDT/보드 파일/ACPI
spi_bus_type/sys/bus/spi/DT compatible, spi_device_idDT/보드 파일/ACPI
virtio_bus/sys/bus/virtio/device ID + feature bitsvirtio PCI/MMIO 스캔

struct class — 디바이스 분류

struct class는 기능별로 디바이스를 분류합니다. 버스(bus)가 물리적 연결을 나타낸다면, 클래스(class)는 논리적 기능을 나타냅니다. 같은 PCI 버스에 연결된 디바이스라도 네트워크 카드는 net 클래스, 그래픽 카드는 drm 클래스에 속합니다.

/* 클래스 정의 예 */
static const struct class my_class = {
    .name    = "my_subsystem",     /* /sys/class/my_subsystem/ */
    .devnode = my_devnode,           /* /dev 노드 이름/퍼미션 제어 */
};

/* 클래스 등록 */
int ret = class_register(&my_class);

/* 클래스 소속 디바이스 생성 — /dev 노드도 자동 생성 (udev 연동) */
struct device *dev = device_create(&my_class, parent,
                                    MKDEV(major, minor),
                                    priv, "my_dev%d", index);

/* 제거 */
device_destroy(&my_class, MKDEV(major, minor));
class_unregister(&my_class);
/sys/class/ 구조 /sys/class/ net/ 네트워크 인터페이스 eth0 → ../../devices/pci0000:00/0000:00:1f.6/net/eth0 block/ 블록 디바이스 sda → ../../devices/pci0000:00/.../sda input/ 입력 디바이스 event0 → ../../devices/.../input/event0 tty/ 터미널 디바이스 ttyS0 → ../../devices/platform/.../ttyS0 drm/ hwmon/ thermal/ gpio/ ※ class 디바이스는 모두 실제 디바이스 경로로의 심볼릭 링크

Device-Driver 바인딩 흐름

디바이스와 드라이버가 연결(bind)되는 과정은 디바이스 모델의 핵심 메커니즘입니다:

Device-Driver 바인딩 흐름 device_add(dev) driver_register(drv) bus->match(dev, drv) 매칭 성공 really_probe(dev, drv) bus->probe(dev) 또는 drv->probe(dev) return 0 → 바인딩 완료 성공 -EPROBE_DEFER → 재시도 큐 지연 에러 → 바인딩 실패 실패 kobject_uevent(KOBJ_BIND) → udev
device_add() 또는 driver_register() 시점에 버스의 match → probe 흐름이 실행됨
/* drivers/base/dd.c — 바인딩 핵심 흐름 (간략화) */
static int really_probe(struct device *dev, struct device_driver *drv)
{
    /* 1. dev->driver 설정 */
    dev->driver = drv;

    /* 2. 핀 설정 (pinctrl default state 적용) */
    pinctrl_bind_pins(dev);

    /* 3. DMA 설정 */
    dma_configure(dev);

    /* 4. probe 호출 — 버스 probe가 있으면 우선, 없으면 드라이버 probe */
    if (dev->bus->probe)
        ret = dev->bus->probe(dev);       /* platform_drv_probe() 등 */
    else if (drv->probe)
        ret = drv->probe(dev);

    /* 5. 결과 처리 */
    if (ret == -EPROBE_DEFER) {
        driver_deferred_probe_add(dev);  /* 재시도 큐에 추가 */
    } else if (ret == 0) {
        driver_bound(dev);               /* 바인딩 완료 */
        kobject_uevent(&dev->kobj, KOBJ_BIND);
    }
    return ret;
}

Deferred Probing (지연 프로빙)

커널 부팅 시 디바이스 간 의존성 때문에 probe 순서가 중요합니다. 예를 들어 SPI 컨트롤러 드라이버가 아직 로드되지 않은 상태에서 SPI 디바이스의 probe가 먼저 호출될 수 있습니다. 이때 -EPROBE_DEFER를 반환하면 커널이 해당 디바이스를 지연 큐(deferred probe list)에 넣고, 다른 probe가 성공할 때마다 재시도합니다.

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

    /* 의존하는 리소스가 아직 준비되지 않을 수 있음 */
    priv->clk = devm_clk_get(dev, NULL);
    if (IS_ERR(priv->clk)) {
        /* -EPROBE_DEFER이면 자동으로 재시도 큐에 추가됨 */
        return dev_err_probe(dev, PTR_ERR(priv->clk),
                              "failed to get clock\\n");
    }

    priv->regulator = devm_regulator_get(dev, "vdd");
    if (IS_ERR(priv->regulator))
        return dev_err_probe(dev, PTR_ERR(priv->regulator),
                              "failed to get regulator\\n");
    /* ... */
}

/* dev_err_probe()의 동작:
 *   - PTR_ERR가 -EPROBE_DEFER이면 → dev_dbg 수준 로깅 (dmesg 조용히)
 *   - 그 외 에러이면 → dev_err 수준 로깅
 *   - 두 경우 모두 에러 코드를 그대로 반환
 */
# 지연 프로빙 상태 확인
$ cat /sys/kernel/debug/devices_deferred
platform soc:spi@40010000: -517 (EPROBE_DEFER)
platform soc:i2c@40020000: -517 (EPROBE_DEFER)

# 부팅 완료 후에도 deferred 상태인 디바이스는 의존성 미해결
# 흔한 원인: 누락된 드라이버 모듈, DT 설정 오류, 순환 의존
💡

Deferred Probe 디버깅: /sys/kernel/debug/devices_deferred에서 대기 중인 디바이스 목록을 확인하세요. 커널 파라미터 driver_deferred_probe_timeout=30을 설정하면 지정된 초 이후 대기를 중단하고 경고를 출력합니다. fw_devlink=on은 많은 최신 메인라인 설정에서 기본 활성화되어 DT/ACPI 의존성을 자동 추적하지만, 실제 기본값은 커널/배포판 설정을 확인해야 합니다.

fwnode — 통합 펌웨어 노드

struct fwnode_handle은 Device Tree와 ACPI를 통합하는 추상화 계층입니다. 하나의 드라이버 코드로 DT 환경(ARM)과 ACPI 환경(x86 서버) 모두를 지원할 수 있습니다.

#include <linux/property.h>

/* fwnode 통합 API — DT/ACPI 구분 없이 프로퍼티 접근 */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    u32 freq;
    const char *label;

    /* DT: device_property → of_property_read_u32 */
    /* ACPI: device_property → acpi_dev_get_property */
    if (device_property_read_u32(dev, "clock-frequency", &freq))
        freq = 100000;  /* 기본값 */

    if (device_property_read_string(dev, "label", &label))
        label = "default";

    /* 불리언 프로퍼티 (존재 여부만 확인) */
    bool wakeup = device_property_read_bool(dev, "wakeup-source");

    /* 배열 프로퍼티 */
    u32 regs[4];
    device_property_read_u32_array(dev, "reg-offsets", regs, 4);

    /* 자식 노드 순회 */
    struct fwnode_handle *child;
    device_for_each_child_node(dev, child) {
        u32 addr;
        fwnode_property_read_u32(child, "reg", &addr);
        /* ... 자식 디바이스 설정 ... */
    }
    return 0;
}

fwnode 통합 API 주요 함수:

통합 API (fwnode/property)DT 전용 (of_*)ACPI 전용
device_property_read_u32()of_property_read_u32()acpi_dev_get_property()
device_property_read_string()of_property_read_string()acpi_dev_get_property()
device_property_read_bool()of_property_read_bool()-
device_get_match_data()of_device_get_match_data()acpi_device_get_match_data()
device_for_each_child_node()for_each_child_of_node()-
ℹ️

새로운 드라이버를 작성할 때는 of_* / acpi_* 전용 API 대신 통합 device_property_* API를 사용하세요. DT와 ACPI를 동시에 지원하는 드라이버를 작성할 수 있으며, 소프트웨어 노드(software_node)를 통한 단위 테스트도 가능해집니다.

struct device_type — 디바이스 세분화

device_type은 같은 버스에 속하지만 유형이 다른 디바이스를 구분합니다. USB 버스에서 디바이스(usb_device)와 인터페이스(usb_interface)가 대표적 예입니다.

struct device_type {
    const char *name;                           /* uevent DEVTYPE= 값 */
    const struct attribute_group **groups;       /* 추가 sysfs 속성 */
    int  (*uevent)(const struct device *, struct kobj_uevent_env *);
    char *(*devnode)(const struct device *, umode_t *, kuid_t *, kgid_t *);
    void (*release)(struct device *);
    const struct dev_pm_ops *pm;
};

/* 예: 블록 디바이스에서 disk_type과 partition_type 구분 */
/* net_device에서 이더넷, Wi-Fi, 브리지 등 구분 */
/* udev 규칙에서 DEVTYPE으로 매칭:
 *   SUBSYSTEM=="net", DEVTYPE=="wlan", ... */

디바이스 모델 전체 계층 구조

Linux Device Model 계층 구조 bus_type klist_devices (디바이스 목록) klist_drivers (드라이버 목록) struct device kobject (sysfs) of_node / fwnode struct device kobject (sysfs) of_node / fwnode device_driver probe / remove of/acpi_match device_driver probe / remove of/acpi_match match() → probe() class (논리적 그룹: net, block ...) dev->class /sys/bus/ | /sys/devices/ | /sys/class/ | /sys/block/
bus_type이 device와 driver를 연결하고, class가 디바이스를 논리적으로 분류

커스텀 버스 구현 예제

드라이버 서브시스템 개발자를 위한 커스텀 버스 타입 구현 예제입니다:

/* 커스텀 버스: 간단한 ID 기반 매칭 */
struct mybus_device {
    struct device dev;
    u32 device_id;
    const char *name;
};

struct mybus_driver {
    struct device_driver driver;
    const u32 *id_table;        /* 지원하는 device_id 배열 */
    int  (*probe)(struct mybus_device *);
    void (*remove)(struct mybus_device *);
};

/* match 콜백: device_id와 드라이버의 id_table 비교 */
static int mybus_match(struct device *dev, struct device_driver *drv)
{
    struct mybus_device *mdev = container_of(dev, struct mybus_device, dev);
    struct mybus_driver *mdrv = container_of(drv, struct mybus_driver, driver);

    for (const u32 *id = mdrv->id_table; *id; id++) {
        if (*id == mdev->device_id)
            return 1;  /* 매칭 성공 */
    }
    return 0;
}

/* probe 콜백: 버스 레벨에서 드라이버의 probe 호출 */
static int mybus_probe(struct device *dev)
{
    struct mybus_device *mdev = container_of(dev, struct mybus_device, dev);
    struct mybus_driver *mdrv = container_of(dev->driver,
                                              struct mybus_driver, driver);
    return mdrv->probe(mdev);
}

/* uevent: udev에 MYBUS_ID 환경변수 전달 */
static int mybus_uevent(const struct device *dev,
                        struct kobj_uevent_env *env)
{
    const struct mybus_device *mdev =
        container_of(dev, struct mybus_device, dev);
    return add_uevent_var(env, "MYBUS_ID=%u", mdev->device_id);
}

static const struct bus_type mybus_type = {
    .name   = "mybus",
    .match  = mybus_match,
    .probe  = mybus_probe,
    .uevent = mybus_uevent,
};

/* 초기화: /sys/bus/mybus/ 생성 */
bus_register(&mybus_type);

sysfs 디바이스 모델 전체 맵

/sys 최상위 구조와 디바이스 모델 관계 /sys/bus/ : 버스 타입별 관점 (platform/pci/usb/i2c/spi) - devices/: 등록 디바이스 심링크, drivers/: 등록 드라이버 + bind/unbind - drivers_autoprobe, uevent 로 동적 바인딩 제어 예: /sys/bus/platform/devices/40000000.uart /sys/devices/ : 물리적 디바이스 트리 (canonical 경로) - platform/40000000.uart/, pci0000:00/0000:00:1f.6/, ... - driver, of_node, uevent, power/ 등 속성 노드 포함 - 버스/클래스 경로는 대부분 이 경로를 가리키는 심링크 - 네트워크 인터페이스 예: .../net/eth0/ /sys/class/ : 기능별 논리 분류(심링크 집합) - net/eth0, block/sda, tty/ttyS0 ... - 실제 엔티티는 /sys/devices/... 경로에 존재 /sys/firmware/ : 펌웨어 관점 노드(Device Tree, ACPI, DMI) /sys/module/ : 로드된 커널 모듈 및 파라미터 - parameters/, holders/, drivers/ 링크 제공 예: /sys/module/my_driver/parameters/ 핵심 원칙 1) canonical 경로는 /sys/devices/ 2) /sys/bus/*/devices, /sys/class/* 는 주로 탐색 편의를 위한 심링크 뷰 3) 디바이스-드라이버 바인딩은 /sys/bus/*/drivers/* 의 bind/unbind로 제어 4) 문제 추적 시 udevadm info와 함께 /sys/devices 기준으로 역추적
⚠️

/sys/devices/실제 디렉토리(물리적 디바이스 계층)이고, /sys/bus/.../devices//sys/class/...심볼릭 링크입니다. 디바이스 정보를 읽을 때는 심링크를 따라가도 되지만, canonical 경로는 /sys/devices/ 아래에 있습니다. udevadm info --path=/sys/devices/...으로 디바이스의 전체 속성을 확인할 수 있습니다.

커널 Notifier Chain (이벤트 통지 체인)

Linux 커널은 다양한 서브시스템 간에 이벤트를 전파하기 위해 Notifier Chain 메커니즘을 제공합니다. 이는 Observer(관찰자) / Publish-Subscribe 패턴의 커널 구현으로, 특정 이벤트가 발생했을 때 미리 등록된 콜백 함수들을 순차적으로 호출합니다. CPU hotplug, 네트워크 디바이스 상태 변경, reboot, panic 등 커널의 핵심 이벤트 대부분이 이 메커니즘을 통해 전파됩니다.

Notifier Chain 개요

Notifier Chain의 핵심은 struct notifier_block입니다. 이벤트를 수신하려는 서브시스템은 콜백 함수와 우선순위를 담은 notifier_block을 체인에 등록하고, 이벤트 발생 시 체인에 등록된 모든 콜백이 우선순위 순서대로 호출됩니다.

/* include/linux/notifier.h */
struct notifier_block {
    notifier_fn_t notifier_call;  /* 콜백 함수 포인터 */
    struct notifier_block __rcu *next;  /* 다음 블록 (우선순위 정렬 연결 리스트) */
    int priority;  /* 우선순위 (높을수록 먼저 호출, 기본값 0) */
};

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

콜백 함수의 매개변수:

💡

Notifier Chain vs 대안: 직접 함수 호출은 두 서브시스템 간 강한 결합을 만들고, workqueue는 비동기 처리에 적합합니다. Notifier Chain은 동기적이면서 느슨한 결합이 필요할 때 — 즉, 여러 독립 서브시스템이 동일한 이벤트에 반응해야 하는 경우에 이상적입니다.

Notifier Chain 4가지 유형

커널은 사용 컨텍스트에 따라 4가지 유형의 Notifier Chain을 제공합니다. 각 유형은 잠금 메커니즘과 호출 가능한 컨텍스트가 다릅니다.

유형 잠금 방식 콜백에서 sleep 호출 컨텍스트 주요 사용처
Atomic spinlock + RCU 불가 인터럽트, atomic 컨텍스트 panic, die, reboot
Blocking rw_semaphore 가능 프로세스 컨텍스트만 netdevice, inetaddr, PM
Raw 잠금 없음 컨텍스트에 따라 호출자가 직접 동기화 저수준 CPU hotplug
SRCU SRCU (Sleepable RCU) 가능 프로세스 컨텍스트 PM notifier
/* 각 유형의 헤드 구조체 (include/linux/notifier.h) */
struct atomic_notifier_head {
    spinlock_t lock;
    struct notifier_block __rcu *head;
};

struct blocking_notifier_head {
    struct rw_semaphore rwsem;
    struct notifier_block __rcu *head;
};

struct raw_notifier_head {
    struct notifier_block __rcu *head;
};

struct srcu_notifier_head {
    struct mutex mutex;
    struct srcu_struct srcu;
    struct notifier_block __rcu *head;
};

각 유형별 초기화 매크로:

/* 정적 초기화 매크로 */
ATOMIC_NOTIFIER_HEAD(my_atomic_chain);
BLOCKING_NOTIFIER_HEAD(my_blocking_chain);
RAW_NOTIFIER_HEAD(my_raw_chain);

/* SRCU는 동적 초기화 필요 */
struct srcu_notifier_head my_srcu_chain;
srcu_init_notifier_head(&my_srcu_chain);

/* 동적 초기화 함수 (atomic/blocking/raw에도 사용 가능) */
ATOMIC_INIT_NOTIFIER_HEAD(&my_atomic_chain);
BLOCKING_INIT_NOTIFIER_HEAD(&my_blocking_chain);
RAW_INIT_NOTIFIER_HEAD(&my_raw_chain);
⚠️

유형 선택 기준: 인터럽트/NMI 컨텍스트에서 호출되면 Atomic, 콜백에서 sleep이 필요하면 Blocking 또는 SRCU를 사용하세요. Raw는 호출자가 동기화를 직접 관리해야 하므로 일반적으로 권장하지 않습니다. SRCU는 Blocking과 유사하지만 read-side가 SRCU 보호를 받아 콜백 해제가 더 안전합니다.

Notifier Chain API 상세

모든 유형은 동일한 패턴의 API를 따릅니다. {type}_notifier_chain_register(), {type}_notifier_chain_unregister(), {type}_notifier_call_chain()으로 구성됩니다.

등록 / 해제

/* Atomic Notifier Chain */
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);

/* Blocking Notifier 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);

/* Raw Notifier Chain */
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);

/* SRCU Notifier Chain */
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);

모든 등록 함수는 성공 시 0을 반환합니다. 등록 시 notifier_block우선순위 내림차순으로 연결 리스트에 삽입됩니다 (높은 priority가 먼저 호출됨).

통지 호출

/* 체인의 모든 콜백을 호출 */
int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
                                unsigned long val, void *v);
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
                                  unsigned long val, void *v);
int raw_notifier_call_chain(struct raw_notifier_head *nh,
                              unsigned long val, void *v);
int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
                               unsigned long val, void *v);

콜백 반환값

콜백 함수는 다음 상수 중 하나를 반환해야 합니다:

반환값 의미
NOTIFY_DONE 0x0000 이벤트에 관심 없음, 다음 콜백 계속
NOTIFY_OK 0x0001 정상 처리됨, 다음 콜백 계속
NOTIFY_STOP_MASK 0x8000 체인 순회 중단 플래그
NOTIFY_BAD NOTIFY_STOP_MASK | 0x0002 에러 발생, 체인 즉시 중단
NOTIFY_STOP NOTIFY_OK | NOTIFY_STOP_MASK 정상이지만 체인 중단 (나머지 콜백 건너뜀)
/* call_chain()의 반환값에서 errno 추출 */
static inline int notifier_to_errno(int ret)
{
    ret &= ~NOTIFY_STOP_MASK;
    return ret > NOTIFY_OK ? ret - 1 : 0;
}

/* errno를 notifier 반환값으로 변환 */
static inline int notifier_from_errno(int err)
{
    if (err)
        return NOTIFY_STOP_MASK | (NOTIFY_OK - err);
    return NOTIFY_OK;
}
ℹ️

NOTIFY_BAD의 용도: NOTIFY_BAD를 반환하면 이벤트 발생 주체에게 "거부"를 알릴 수 있습니다. 예를 들어, PM notifier에서 NOTIFY_BAD를 반환하면 suspend가 취소됩니다. call_chain() 호출자는 notifier_to_errno()로 반환값을 검사합니다.

커널 주요 Notifier Chain 목록

다음은 커널에서 가장 자주 사용되는 주요 Notifier Chain입니다:

Notifier Chain 유형 헤더/소스 주요 이벤트 등록 API
reboot_notifier_list Blocking kernel/reboot.c SYS_RESTART, SYS_HALT, SYS_POWER_OFF register_reboot_notifier()
panic_notifier_list Atomic kernel/panic.c 커널 패닉 발생 atomic_notifier_chain_register()
netdev_chain Raw net/core/dev.c NETDEV_UP, NETDEV_DOWN, NETDEV_CHANGE, ... register_netdevice_notifier()
inetaddr_chain Blocking net/ipv4/devinet.c NETDEV_UP, NETDEV_DOWN (IPv4 주소 변경) register_inetaddr_notifier()
inet6addr_chain Blocking net/ipv6/addrconf.c NETDEV_UP, NETDEV_DOWN (IPv6 주소 변경) register_inet6addr_notifier()
pm_chain_head Blocking kernel/power/main.c PM_SUSPEND_PREPARE, PM_POST_SUSPEND, ... register_pm_notifier()
module_notify_list Blocking kernel/module/main.c MODULE_STATE_LIVE, MODULE_STATE_COMING, ... register_module_notifier()
keyboard_notifier_list Atomic drivers/tty/vt/keyboard.c KBD_KEYCODE, KBD_KEYSYM, ... register_keyboard_notifier()
die_chain Atomic arch/*/kernel/traps.c DIE_OOPS, DIE_GPF, DIE_TRAP, ... register_die_notifier()
fb_notifier_list Blocking drivers/video/fbdev/core/fbmem.c FB_EVENT_MODE_CHANGE, FB_EVENT_BLANK, ... fb_register_client()

Notifier Chain 동작 흐름

Notifier Chain 동작 흐름 이벤트 발생 (예: netdev_change) raw_notifier_call_chain(&netdev_chain, val, dev) 우선순위 순서 nb1->notifier_call(nb1, val, dev) priority=10 NOTIFY_OK nb2->notifier_call(nb2, val, dev) priority=0 NOTIFY_DONE nb3->notifier_call(nb3, val, dev) priority=-5 NOTIFY_STOP nb4->notifier_call(...) 건너뜀! 최종 반환 호출자에게 NOTIFY_STOP 반환

실전 구현 예제

예제 1: 네트워크 디바이스 Notifier

네트워크 인터페이스의 상태 변경(UP/DOWN)을 감지하는 가장 일반적인 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);

    switch (event) {
    case NETDEV_UP:
        pr_info("netdev UP: %s (ifindex=%d)\\n",
                dev->name, dev->ifindex);
        break;
    case NETDEV_DOWN:
        pr_info("netdev DOWN: %s (ifindex=%d)\\n",
                dev->name, dev->ifindex);
        break;
    case NETDEV_REGISTER:
        pr_info("netdev REGISTER: %s\\n", dev->name);
        break;
    case NETDEV_UNREGISTER:
        pr_info("netdev UNREGISTER: %s\\n", dev->name);
        break;
    default:
        break;
    }

    return NOTIFY_DONE;
}

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

static int __init my_notifier_init(void)
{
    int ret;

    ret = register_netdevice_notifier(&my_netdev_nb);
    if (ret) {
        pr_err("netdev notifier 등록 실패: %d\\n", ret);
        return ret;
    }

    pr_info("netdev notifier 등록 완료\\n");
    return 0;
}

static void __exit my_notifier_exit(void)
{
    unregister_netdevice_notifier(&my_netdev_nb);
    pr_info("netdev notifier 해제 완료\\n");
}

module_init(my_notifier_init);
module_exit(my_notifier_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Network device notifier example");
💡

netdev_notifier_info_to_dev(): 커널 3.11부터 netdevice notifier의 data 매개변수는 struct net_device *가 아닌 struct netdev_notifier_info *입니다. 반드시 netdev_notifier_info_to_dev() 헬퍼를 사용하세요.

예제 2: 커스텀 Notifier Chain 정의 및 사용

자체 Notifier Chain을 정의하여 모듈 간 이벤트를 전파하는 예제입니다:

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

/* ===== 이벤트 정의 (공유 헤더에 배치) ===== */
#define MY_EVENT_START    0x01
#define MY_EVENT_STOP     0x02
#define MY_EVENT_ERROR    0x03

struct my_event_data {
    const char *name;
    int code;
};

/* ===== 이벤트 발행 측 (Producer) ===== */
static BLOCKING_NOTIFIER_HEAD(my_event_chain);

/* 외부 모듈이 사용할 등록/해제 API */
int my_register_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_register(&my_event_chain, nb);
}
EXPORT_SYMBOL_GPL(my_register_notifier);

int my_unregister_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_unregister(&my_event_chain, nb);
}
EXPORT_SYMBOL_GPL(my_unregister_notifier);

/* 이벤트 발생 시 호출 */
void my_fire_event(unsigned long event, struct my_event_data *data)
{
    int ret;

    ret = blocking_notifier_call_chain(&my_event_chain, event, data);
    if (notifier_to_errno(ret))
        pr_warn("이벤트 %lu 처리 중 에러 발생\\n", event);
}

/* ===== 이벤트 수신 측 (Consumer) ===== */
static int my_event_handler(struct notifier_block *nb,
                             unsigned long event, void *data)
{
    struct my_event_data *ev = data;

    switch (event) {
    case MY_EVENT_START:
        pr_info("[Consumer] START: name=%s, code=%d\\n",
                ev->name, ev->code);
        return NOTIFY_OK;
    case MY_EVENT_ERROR:
        pr_err("[Consumer] ERROR: name=%s, code=%d\\n",
               ev->name, ev->code);
        return notifier_from_errno(-EIO);  /* 에러 전파 */
    default:
        return NOTIFY_DONE;
    }
}

static struct notifier_block my_handler_nb = {
    .notifier_call = my_event_handler,
    .priority = 0,
};

static int __init consumer_init(void)
{
    return my_register_notifier(&my_handler_nb);
}

static void __exit consumer_exit(void)
{
    my_unregister_notifier(&my_handler_nb);
}

module_init(consumer_init);
module_exit(consumer_exit);
MODULE_LICENSE("GPL");

Notifier Chain 내부 구현

Notifier Chain의 핵심 구현은 kernel/notifier.c에 있습니다. 모든 유형이 공유하는 내부 함수를 살펴보겠습니다.

우선순위 기반 삽입

등록 시 notifier_chain_register()는 우선순위 내림차순으로 연결 리스트에 삽입합니다:

/* kernel/notifier.c — 핵심 내부 함수 (간략화) */
static int notifier_chain_register(
    struct notifier_block **nl,
    struct notifier_block *n)
{
    while (*nl != NULL) {
        if (unlikely((*nl) == n)) {
            WARN(1, "notifier already registered");
            return -EEXIST;
        }
        /* 우선순위 내림차순 정렬: 높은 값이 리스트 앞쪽 */
        if (n->priority > (*nl)->priority)
            break;
        nl = &((*nl)->next);
    }
    n->next = *nl;
    rcu_assign_pointer(*nl, n);
    return 0;
}

체인 순회 (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);

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

        if (nr_calls)
            (*nr_calls)++;

        /* NOTIFY_STOP_MASK이 설정되면 순회 중단 */
        if (ret & NOTIFY_STOP_MASK)
            break;

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

각 유형별 잠금 차이

/* Atomic: spinlock으로 등록 보호, RCU로 순회 보호 */
int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
                                unsigned long val, void *v)
{
    int ret;
    rcu_read_lock();
    ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
    rcu_read_unlock();
    return ret;
}

/* Blocking: rw_semaphore로 등록/순회 모두 보호 */
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
                                  unsigned long val, void *v)
{
    int ret;
    if (rcu_access_pointer(nh->head)) {
        down_read(&nh->rwsem);
        ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
        up_read(&nh->rwsem);
    } else {
        ret = NOTIFY_DONE;
    }
    return ret;
}

/* SRCU: Sleepable RCU로 순회 보호, mutex로 등록 보호 */
int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
                               unsigned long val, void *v)
{
    int ret, idx;
    idx = srcu_read_lock(&nh->srcu);
    ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
    srcu_read_unlock(&nh->srcu, idx);
    return ret;
}
ℹ️

RCU 보호의 의미: Atomic notifier의 순회는 rcu_read_lock()으로 보호됩니다. 이는 unregister() 후에도 RCU grace period가 완료될 때까지 해제된 notifier_block이 유효한 메모리를 참조함을 보장합니다. 따라서 unregister()와 콜백 실행 사이에 race condition이 발생하지 않습니다. Blocking notifier는 rw_semaphore가 이 역할을 대신합니다.

주의사항 및 디버깅

흔한 실수와 해결책

문제 원인 해결책
Atomic notifier 콜백에서 BUG/scheduling 콜백 내에서 sleep 가능 함수 호출 (mutex, kmalloc(GFP_KERNEL) 등) Blocking 유형으로 변경하거나, 콜백에서 workqueue로 지연 처리
모듈 언로드 시 크래시 module_exit에서 unregister 누락 반드시 exit에서 해제; devm_ 래퍼 활용 검토
Deadlock 콜백 내에서 동일 체인에 register/unregister 시도 콜백 안에서 체인 수정 금지; workqueue로 지연 처리
이벤트 누락 등록 전에 이미 발생한 이벤트 등록 후 현재 상태를 수동 확인하는 초기화 코드 추가
긴 체인으로 인한 지연 체인에 많은 콜백이 등록되어 순회 시간 증가 콜백을 가볍게 유지; 무거운 처리는 workqueue로 분리

Deadlock 시나리오 상세

/* 위험! Blocking notifier 콜백 내에서 동일 체인 수정 */
static int bad_callback(struct notifier_block *nb,
                         unsigned long event, void *data)
{
    /* call_chain()이 rwsem read-lock을 잡고 있는 상태에서
       unregister()가 write-lock을 요청 → DEADLOCK */
    blocking_notifier_chain_unregister(&chain, nb);  /* 절대 금지! */
    return NOTIFY_DONE;
}

/* 올바른 방법: workqueue로 지연 해제 */
static struct work_struct unreg_work;

static void deferred_unregister(struct work_struct *work)
{
    blocking_notifier_chain_unregister(&chain, &my_nb);
}

static int safe_callback(struct notifier_block *nb,
                          unsigned long event, void *data)
{
    if (event == SOME_FINAL_EVENT) {
        schedule_work(&unreg_work);  /* workqueue에서 안전하게 해제 */
        return NOTIFY_STOP;
    }
    return NOTIFY_DONE;
}
⚠️

Blocking notifier 콜백 안에서 동일 체인을 수정(register/unregister)하면 deadlock이 발생합니다. call_chain()down_read()를 잡고 있는 상태에서 unregister()down_write()를 시도하기 때문입니다. 반드시 workqueue나 별도 컨텍스트에서 해제하세요.

ftrace / bpftrace를 이용한 디버깅

# ftrace로 notifier_call_chain 호출 추적
echo 1 > /sys/kernel/debug/tracing/events/notifier/notifier_run/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe

# bpftrace로 특정 notifier 호출 시간 측정
bpftrace -e '
kprobe:blocking_notifier_call_chain {
    @start[tid] = nsecs;
}
kretprobe:blocking_notifier_call_chain /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# 특정 체인에 등록된 notifier 확인 (debugfs)
# CONFIG_DEBUG_NOTIFIERS=y 필요
cat /sys/kernel/debug/notifier_chains
💡

CONFIG_DEBUG_NOTIFIERS: 이 옵션을 활성화하면 notifier 콜백의 반환값을 검증하고, 잘못된 반환값에 대해 WARN()을 출력합니다. 개발 커널에서는 활성화하는 것을 권장합니다. make menuconfig에서 Kernel hacking → Debug Notifiers에서 설정할 수 있습니다.

Block Device 드라이버

#include <linux/blkdev.h>
#include <linux/blk-mq.h>

static struct gendisk *my_disk;
static struct blk_mq_tag_set tag_set;

/* blk-mq 요청 처리 */
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
    const struct blk_mq_queue_data *bd)
{
    struct request *rq = bd->rq;
    struct bio_vec bvec;
    struct req_iterator iter;

    blk_mq_start_request(rq);

    rq_for_each_segment(bvec, rq, iter) {
        sector_t sector = iter.iter.bi_sector;
        void *buf = page_address(bvec.bv_page) + bvec.bv_offset;
        unsigned len = bvec.bv_len;

        if (rq_data_dir(rq) == WRITE)
            memcpy(my_data + sector * 512, buf, len);
        else
            memcpy(buf, my_data + sector * 512, len);
    }

    blk_mq_end_request(rq, BLK_STS_OK);
    return BLK_STS_OK;
}

static const struct blk_mq_ops my_mq_ops = {
    .queue_rq = my_queue_rq,
};

Network Device 드라이버

#include <linux/netdevice.h>
#include <linux/etherdevice.h>

struct my_net_priv {
    struct napi_struct napi;
    spinlock_t tx_lock;
};

static int my_net_open(struct net_device *dev)
{
    struct my_net_priv *priv = netdev_priv(dev);

    napi_enable(&priv->napi);
    netif_tx_start_all_queues(dev);
    return 0;
}

static int my_net_stop(struct net_device *dev)
{
    struct my_net_priv *priv = netdev_priv(dev);

    netif_tx_disable(dev);
    napi_disable(&priv->napi);
    return 0;
}

static netdev_tx_t my_start_xmit(struct sk_buff *skb,
    struct net_device *dev)
{
    struct my_net_priv *priv = netdev_priv(dev);
    unsigned long flags;

    spin_lock_irqsave(&priv->tx_lock, flags);
    if (!my_tx_ring_has_space(priv)) {
        netif_stop_queue(dev);
        spin_unlock_irqrestore(&priv->tx_lock, flags);
        return NETDEV_TX_BUSY;
    }

    my_hw_xmit(priv, skb);
    spin_unlock_irqrestore(&priv->tx_lock, flags);

    dev_kfree_skb(skb);
    return NETDEV_TX_OK;
}

static const struct net_device_ops my_netdev_ops = {
    .ndo_open       = my_net_open,
    .ndo_stop       = my_net_stop,
    .ndo_start_xmit = my_start_xmit,
    .ndo_get_stats64 = my_get_stats64,
};
상세 문서: 물리 NIC + 가상 netdev(TUN/TAP) + XDP/AF_XDP + phylink + ethtool 운영 관점까지 포함한 전체 내용은 net_device 드라이버 (NIC/TUN/TAP) 문서를 참고하세요.
운영 주제드라이버 핵심 포인트확인 명령/지표
멀티큐/RSS큐-IRQ-NAPI 매핑 일관성, NUMA locality 유지ethtool -l/-x, /proc/interrupts
RX 메모리page_pool 재사용, refill 지연 최소화ethtool -S의 rx_nombuf/drop
오프로드 제어ndo_set_features, ndo_setup_tc fallback 정책 명확화ethtool -k, tc filter show
장애 복구ndo_tx_timeout + workqueue reset, RTNL 보호timeout 로그, reset 카운터
가상 netdevTUN/TAP/veth/virtio-net의 공통 netdev 계약 이해TUN/TAP, 상세 문서
실무 기준: 네트워크 드라이버는 단순 송수신 구현보다 큐 병목 제어(RSS/BQL), 제어 경로 안정성(RTNL), 장애 자동 복구(devlink/timeout)의 완성도가 운영 품질을 좌우합니다.
ℹ️

앞서 살펴본 드라이버 등록 방식은 x86/ACPI 환경 기준입니다. ARM, RISC-V 등 임베디드 플랫폼에서는 PCI/USB처럼 자동 열거가 불가능한 SoC 디바이스가 대부분이므로, Device Tree가 하드웨어 기술의 핵심 메커니즘입니다. 다음 섹션에서 Device Tree의 구조와 커널 연동을 상세히 다룹니다.

Device Tree (DTS/DTB)

페이지 분리: Device Tree 심화 내용은 Device Tree 심화 페이지로 이동했습니다.

Managed Device Resources (devm)

devm_* API는 디바이스 해제 시 리소스를 자동으로 정리합니다. 에러 경로에서의 수동 해제가 불필요합니다.

일반 APIManaged API리소스
kmalloc()devm_kmalloc()메모리
ioremap()devm_ioremap()MMIO 매핑
request_irq()devm_request_irq()인터럽트
clk_get()devm_clk_get()클럭
regulator_get()devm_regulator_get()레귤레이터
gpio_request()devm_gpio_request()GPIO
/* devm 사용 예: 에러 시 자동 해제, goto 불필요 */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

    void *data = devm_kzalloc(dev, 4096, GFP_KERNEL);
    if (!data) return -ENOMEM;

    void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base)) return PTR_ERR(base);

    int irq = platform_get_irq(pdev, 0);
    devm_request_irq(dev, irq, my_handler, 0, "mydev", data);
    /* remove 시 모든 리소스 자동 해제 */
    return 0;
}

전원 관리 (PM)

static int my_suspend(struct device *dev)
{
    /* 하드웨어 상태 저장, 클럭 비활성화 */
    clk_disable_unprepare(priv->clk);
    return 0;
}

static int my_resume(struct device *dev)
{
    /* 하드웨어 상태 복원 */
    clk_prepare_enable(priv->clk);
    reinit_hardware(priv);
    return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(my_pm_ops, my_suspend, my_resume);

static struct platform_driver my_driver = {
    .driver = {
        .name = "my-device",
        .pm   = pm_sleep_ptr(&my_pm_ops),
    },
};

DMA 심화 — 주의사항과 고려사항

DMA 매핑 API 유형과 선택 기준

API수명캐시 일관성사용 시나리오
dma_alloc_coherent() 장기 (드라이버 수명) H/W 보장 (uncached/write-combined) 디스크립터 링, 커맨드 큐, 공유 상태
dma_map_single() 단기 (한 번의 전송) S/W sync 필요 패킷 버퍼, 단일 블록 I/O
dma_map_sg() 단기 S/W sync 필요 Scatter-Gather I/O, 대용량 전송
dma_map_page() 단기 S/W sync 필요 highmem 페이지의 DMA 전송
dma_alloc_noncoherent() 장기 S/W sync 필요 대용량 버퍼 (coherent의 성능 오버헤드 회피)
dma_pool_create() 장기 (풀 관리) H/W 보장 소규모 coherent 버퍼를 빈번하게 할당/해제

DMA 방향(Direction)과 캐시 동기화

/* DMA 방향 상수 — 반드시 정확히 지정해야 함 */
DMA_TO_DEVICE       /* CPU → Device: CPU 캐시 → 메모리 flush */
DMA_FROM_DEVICE     /* Device → CPU: 캐시 invalidate */
DMA_BIDIRECTIONAL   /* 양방향: flush + invalidate (비용 큼) */

/* Streaming DMA의 올바른 사용 패턴 */
dma_addr_t dma = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma)) {
    dev_err(dev, "DMA mapping failed\\n");
    return -ENOMEM;
}

/* DMA 전송 시작 (H/W에 dma 주소 전달) */
writel(dma, hw_base + DMA_ADDR_REG);
writel(len, hw_base + DMA_LEN_REG);
writel(DMA_START, hw_base + DMA_CTRL_REG);

/* 전송 완료 후 — CPU가 데이터를 읽기 전에 반드시 sync */
dma_sync_single_for_cpu(dev, dma, len, DMA_FROM_DEVICE);
/* 이제 CPU에서 buf 데이터를 안전하게 읽을 수 있음 */
process_data(buf, len);

/* 다시 디바이스에게 버퍼를 넘기려면 */
dma_sync_single_for_device(dev, dma, len, DMA_FROM_DEVICE);

/* 최종 해제 */
dma_unmap_single(dev, dma, len, DMA_FROM_DEVICE);
DMA 방향 오지정의 위험: DMA_TO_DEVICE로 매핑한 버퍼에서 디바이스가 쓴 데이터를 CPU가 읽으면 stale cache 데이터가 반환될 수 있습니다. 캐시가 invalidate되지 않았기 때문입니다. 이런 버그는 간헐적으로 발생하여 디버깅이 매우 어렵습니다. DMA_BIDIRECTIONAL은 안전하지만 캐시 연산이 두 배이므로 성능 저하가 있습니다.

IOMMU와 DMA 주소 공간

/* IOMMU 존재 시 DMA 주소 변환 흐름:
 *
 *  CPU Virtual Addr → (MMU) → Physical Addr
 *  DMA/Bus Addr     → (IOMMU) → Physical Addr
 *
 *  IOMMU가 없으면: dma_addr_t == phys_addr_t (1:1 매핑)
 *  IOMMU가 있으면: dma_addr_t는 IOMMU가 매핑한 I/O 가상 주소
 */

/* DMA 주소 마스크 설정 — 디바이스의 주소 지정 능력 선언 */
int ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
    /* 64비트 실패 시 32비트로 fallback */
    ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
    if (ret) {
        dev_err(dev, "No suitable DMA available\\n");
        return ret;
    }
}
IOMMU 구현플랫폼커널 드라이버주요 기능
Intel VT-d x86 (Intel) drivers/iommu/intel/ DMA remapping, interrupt remapping, ATS
AMD-Vi x86 (AMD) drivers/iommu/amd/ DMA remapping, interrupt remapping, v2 page table
ARM SMMU ARM/ARM64 drivers/iommu/arm/arm-smmu-v3/ Stage-1/2 변환, PCIe ATS, HTTU
SWIOTLB 모든 플랫폼 kernel/dma/swiotlb.c 소프트웨어 bounce buffer (IOMMU 없을 때 fallback)

DMA 프로그래밍 핵심 주의사항

반드시 지켜야 할 DMA 규칙:
  1. 매핑 후 에러 체크dma_mapping_error()를 항상 호출. IOMMU 공간 소진 시 실패 가능
  2. bounce buffer 인지 — SWIOTLB 사용 시 실제 복사가 발생하여 성능 저하. dma_set_mask()로 64비트 지원 확인
  3. DMA 주소 수명 — map과 unmap 사이에서만 유효. unmap 후 DMA 주소 사용 금지
  4. 캐시 라인 공유 금지 — DMA 버퍼가 다른 데이터와 같은 캐시 라인을 공유하면 false sharing 발생. ____cacheline_aligned 사용
  5. Scatter-Gather 병합 — IOMMU는 물리적으로 불연속인 페이지를 DMA 주소 공간에서 연속으로 매핑 가능. dma_map_sg()sg_dma_len()이 원래 세그먼트와 다를 수 있음
  6. DMA coherent 메모리의 성능 — ARM 등 non-x86에서 coherent 메모리는 uncached로 할당되어 CPU 접근이 느림. 대용량 데이터는 streaming DMA 선호
  7. 64비트 DMA 주소 — 레거시 디바이스는 32비트 DMA만 지원. 4GB 이상 메모리 시스템에서 SWIOTLB bounce buffer 사용됨

DMA 디버깅

# DMA 디버그 활성화 (커널 부트 파라미터)
dma_debug=on

# 또는 CONFIG_DMA_API_DEBUG=y 로 빌드

# DMA 디버그 통계 확인
cat /sys/kernel/debug/dma-api/error_count
cat /sys/kernel/debug/dma-api/all_errors
cat /sys/kernel/debug/dma-api/num_errors

# 일반적으로 검출되는 DMA 오류:
# - DMA-API: device driver frees DMA memory with wrong function
# - DMA-API: device driver maps memory from kernel text
# - DMA-API: device driver tries to sync DMA memory it has not allocated

디바이스 드라이버 주요 버그 패턴

디바이스 드라이버는 커널 코드의 약 70%를 차지하며, 커널 취약점의 가장 큰 원인입니다. 하드웨어와의 상호작용, 비동기 이벤트 처리, DMA 메모리 관리 등에서 반복적으로 발생하는 버그 패턴을 분석합니다.

DMA 매핑 방향 오류

DMA 방향(direction) 불일치 — 데이터 손상/정보 누출:

DMA 매핑 시 dma_map_single()에 전달하는 방향(DMA_TO_DEVICE, DMA_FROM_DEVICE, DMA_BIDIRECTIONAL)이 실제 데이터 흐름과 일치하지 않으면, 캐시 일관성(cache coherency) 문제로 데이터 손상이 발생하거나 초기화되지 않은 커널 메모리가 장치로 전송되어 정보가 누출될 수 있습니다.

/* DMA 방향 오류 패턴 */

/* 취약: 수신 버퍼를 DMA_TO_DEVICE로 매핑 */
dma_addr = dma_map_single(dev, rx_buf, len, DMA_TO_DEVICE);
/* → 캐시가 무효화되지 않아 장치가 쓴 데이터 대신
 *   CPU 캐시의 오래된 데이터를 읽음 (데이터 손상) */

/* 수정: 올바른 방향 지정 */
dma_addr = dma_map_single(dev, rx_buf, len, DMA_FROM_DEVICE);

/* DMA-API 디버깅으로 방향 오류 탐지 */
CONFIG_DMA_API_DEBUG=y
/* 런타임에 dma_map/unmap 쌍의 방향 일치 여부 검증 */
/* dmesg에 "DMA-API: device driver has a bug" 메시지 출력 */

인터럽트 핸들러 경쟁 조건

IRQ 핸들러와 프로세스 컨텍스트 간 경쟁:

인터럽트 핸들러에서 접근하는 공유 데이터를 프로세스 컨텍스트에서도 접근할 때, spin_lock_irqsave() 대신 spin_lock()을 사용하면 데드락이 발생합니다. 프로세스 컨텍스트에서 lock을 보유한 상태에서 인터럽트가 발생하면, IRQ 핸들러가 같은 lock을 획득하려다 영원히 대기합니다.

/* 드라이버 IRQ 데드락 패턴 */

/* 취약: process context에서 spin_lock() 사용 */
spin_lock(&dev->lock);       /* process context */
dev->status = NEW_STATUS;
spin_unlock(&dev->lock);
/* ↑ 이 사이에 IRQ 발생 시 데드락 */

static irqreturn_t my_irq(int irq, void *data) {
    spin_lock(&dev->lock);   /* IRQ context — 이미 잠겨있으면 데드락 */
    process_data(dev);
    spin_unlock(&dev->lock);
    return IRQ_HANDLED;
}

/* 수정: spin_lock_irqsave()로 IRQ 비활성화 */
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags);  /* IRQ 비활성화 + 잠금 */
dev->status = NEW_STATUS;
spin_unlock_irqrestore(&dev->lock, flags);

devm 리소스 해제 순서 문제

devm (managed) 리소스의 역순 해제 함정:

devm_* API로 할당된 리소스는 드라이버 제거 시 등록 역순으로 자동 해제됩니다. 그러나 리소스 간 의존 관계를 고려하지 않으면 이미 해제된 리소스를 참조하는 문제가 발생합니다. 예를 들어, IRQ 핸들러가 접근하는 메모리 버퍼가 IRQ보다 먼저 해제되면 Use-After-Free가 발생합니다.

/* devm 해제 순서 문제 예시 */

static int my_probe(struct platform_device *pdev) {
    /* 등록 순서: 1→버퍼, 2→IRQ */
    buf = devm_kzalloc(&pdev->dev, BUF_SIZE, GFP_KERNEL); /* #1 */
    devm_request_irq(&pdev->dev, irq, handler, ...);     /* #2 */

    /* 해제 순서 (역순): #2→IRQ 해제, #1→버퍼 해제 ✓ */
    /* → IRQ가 먼저 해제되므로 안전 */

    /* 위험한 등록 순서: 1→IRQ, 2→버퍼 */
    devm_request_irq(&pdev->dev, irq, handler, ...);     /* #1 */
    buf = devm_kzalloc(&pdev->dev, BUF_SIZE, GFP_KERNEL); /* #2 */

    /* 해제 순서 (역순): #2→버퍼 해제, #1→IRQ 해제 ✗ */
    /* → 버퍼가 먼저 해제되는데 IRQ 핸들러가 아직 활성 → UAF */
}

/* 교훈: 의존 관계를 고려하여 devm 등록 순서 결정
 * 의존하는 리소스(데이터)를 먼저 등록, 소비자(IRQ/타이머)를 나중에 등록
 * → 해제 시 소비자가 먼저 해제되어 안전 */

USB/PCI 핫플러그 경쟁 조건

장치 제거 중 접근 (disconnect race):

USB나 PCI 핫플러그 장치에서 사용자가 장치를 물리적으로 제거하는 동안 드라이버가 여전히 장치에 접근하면, 잘못된 메모리 접근이나 커널 패닉이 발생합니다. disconnect 콜백에서 진행 중인 I/O를 모두 취소하고, 이후의 I/O 요청을 거부해야 합니다.

/* USB disconnect race 방어 패턴 */

struct my_usb_dev {
    struct usb_device *udev;
    struct mutex io_mutex;
    bool disconnected;    /* disconnect 발생 여부 */
    struct kref kref;     /* 참조 카운트 */
};

/* I/O 수행 전 disconnect 여부 확인 */
static ssize_t my_write(struct file *file, ...) {
    mutex_lock(&dev->io_mutex);
    if (dev->disconnected) {
        mutex_unlock(&dev->io_mutex);
        return -ENODEV;  /* 이미 제거됨 */
    }
    /* 안전하게 I/O 수행 */
    retval = usb_bulk_msg(dev->udev, ...);
    mutex_unlock(&dev->io_mutex);
    return retval;
}

/* disconnect 콜백 */
static void my_disconnect(struct usb_interface *intf) {
    struct my_usb_dev *dev = usb_get_intfdata(intf);
    mutex_lock(&dev->io_mutex);
    dev->disconnected = true;  /* 이후 I/O 거부 */
    mutex_unlock(&dev->io_mutex);
    usb_kill_anchored_urbs(&dev->submitted); /* 진행 중인 URB 취소 */
    kref_put(&dev->kref, my_delete);
}
드라이버 버그 탐지 도구 요약:

CONFIG_DMA_API_DEBUG: DMA 매핑/해제 쌍, 방향 일치 여부 검증
CONFIG_LOCKDEP: IRQ/프로세스 컨텍스트 간 잠금 순서 위반 탐지
CONFIG_KASAN: Use-After-Free, 버퍼 오버플로우 런타임 탐지
CONFIG_KFENCE: 프로덕션 환경용 저오버헤드 메모리 오류 샘플링
CONFIG_PROVE_LOCKING: 잠금 의존성 그래프 검증으로 데드락 사전 탐지
coccinelle (spatch): 커널 코드 정적 분석 도구, 일반적인 드라이버 버그 패턴 탐지

디바이스 드라이버와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.