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

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

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

핵심 요약

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

단계별 이해

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

디바이스 드라이버 개요

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

유형인터페이스예시
Character Device바이트 스트림, 순차 접근시리얼, 터미널, /dev/null
Block Device고정 크기 블록, 랜덤 접근디스크, SSD, USB 스토리지
Network Device패킷(Packet) 기반, 소켓(Socket) 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 페이지(Page)에서 아래 주제를 상세히 다룹니다.
  • ioctl 핸들러(Handler) — ioctl
  • poll/select/epoll 지원
  • mmap 구현
  • 동시성/잠금(Lock), miscdevice, 다중 인스턴스
  • 인터럽트 처리 — 인터럽트

Platform Driver

Platform Device 개념

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

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

공급 방식설명대표 환경
Device TreeDTS/DTB 파일에 하드웨어 기술, 부트로더(Bootloader)가 전달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; /* 강제 드라이버 바인딩 */
};
코드 설명
  • name, idname은 드라이버 매칭의 최후 수단(fallback)으로 사용되며, id는 동일 이름의 여러 인스턴스를 구분합니다. id-1이면 단일 인스턴스를 의미합니다.
  • dev임베디드 struct device로, 디바이스 모델의 공통 기반 구조체입니다. dev.of_node으로 Device Tree 노드에, dev.platform_data로 보드 레벨 데이터에 접근합니다. include/linux/device.h에 정의되어 있습니다.
  • resource, num_resourcesMMIO 주소, IRQ 번호 등 하드웨어 리소스 배열과 그 크기입니다. Device Tree의 reg, interrupts 속성이 자동으로 이 배열에 채워집니다 (drivers/of/platform.cof_device_alloc()).
  • driver_overridesysfs(/sys/bus/platform/devices/xxx/driver_override)를 통해 특정 드라이버를 강제 바인딩할 때 사용됩니다. platform_match()에서 최우선 순위로 검사합니다.

핵심 필드별 용도:

필드용도
dev.platform_data보드 레벨 데이터 전달 (레거시). void * 포인터로 드라이버별 구조체(Struct) 전달
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;
};
코드 설명
  • probe, remove, shutdown디바이스 생명주기 콜백입니다. probe()는 매칭 성공 시 really_probe()(drivers/base/dd.c)에서 호출되고, remove()는 언바인딩 시, shutdown()은 시스템 reboot/poweroff 시 호출됩니다.
  • driver임베디드 struct device_driver로, 버스 공통 드라이버 인프라를 제공합니다. driver.of_match_table에 Device Tree compatible 테이블을, driver.pm에 전원 관리 콜백을 설정합니다.
  • id_tablestruct platform_device_id 배열로, 이름 기반 매칭과 함께 driver_data를 통해 칩 변형별 데이터를 전달할 수 있습니다. Device Tree 매칭보다 낮은 우선순위입니다.
  • suspend, resume레거시 전원 관리 콜백으로, 현재는 driver.pmstruct dev_pm_ops를 사용하는 것이 권장됩니다.

driver 필드의 주요 하위 필드:

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

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단계 우선순위(Priority)로 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);
}
코드 설명
  • to_platform_device, to_platform_drivercontainer_of() 기반 매크로로, 범용 struct device/struct device_driver 포인터에서 버스별 구조체를 추출합니다. include/linux/platform_device.h에 정의되어 있습니다.
  • 1단계: driver_overridesysfs에서 사용자가 명시적으로 지정한 드라이버를 최우선으로 검사합니다. 디버깅이나 VFIO 바인딩 전환에 활용됩니다.
  • 2단계: of_driver_match_deviceDevice Tree의 compatible 속성과 드라이버의 of_match_table을 비교합니다. 현대 임베디드 시스템에서 가장 일반적인 매칭 방식으로, drivers/of/device.cof_match_device()를 호출합니다.
  • 3단계: acpi_driver_match_deviceACPI 테이블의 HID/CID와 드라이버의 acpi_match_table을 비교합니다. x86 서버/데스크톱에서 주로 사용됩니다.
  • 4~5단계: id_table, nameid_table은 레거시 보드 파일 환경에서 사용되며, name 문자열 비교는 최후 수단(fallback)입니다. DT/ACPI 기반 시스템에서는 거의 도달하지 않습니다.

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

/* 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 네임스페이스(Namespace) 예:
  • 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 매칭이 성공했을 때 커널이 호출하는 진입점(Entry Point)입니다. 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;
}
코드 설명
  • 1. devm_kzalloc디바이스에 연결된(managed) 메모리 할당입니다. remove 시 또는 probe 실패 시 자동으로 kfree()됩니다. drivers/base/devres.c의 devres 스택에 등록됩니다.
  • 2. devm_platform_ioremap_resourceplatform_get_resource() + devm_ioremap_resource()를 합친 편의 함수입니다. MMIO 영역을 가상 주소로 매핑하며, 리소스 충돌 시 -EBUSY를 반환합니다 (drivers/base/platform.c).
  • 3. platform_get_irq + devm_request_irqplatform_get_irq()는 Device Tree의 interrupts 속성에서 IRQ 번호를 추출합니다. devm_request_irq()는 인터럽트 핸들러를 등록하며, remove 시 자동으로 free_irq()가 호출됩니다.
  • 4. devm_clk_get_enableddevm_clk_get() + clk_prepare_enable()을 결합한 함수입니다. -EPROBE_DEFER를 반환할 수 있으므로 dev_err_probe()로 처리하는 것이 좋습니다.
  • 5~6. writel + misc_registerwritel()로 MMIO 레지스터에 기록하여 하드웨어를 초기화하고, misc_register()로 사용자 공간 인터페이스(/dev 노드)를 등록합니다. misc_register()는 devm 버전이 없으므로 remove에서 명시적으로 해제해야 합니다.
  • 7. platform_set_drvdatadev_set_drvdata()의 래퍼로, private 데이터를 디바이스에 연결합니다. remove, suspend 등 다른 콜백에서 platform_get_drvdata()로 회수합니다.
💡

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)와 핫플러그(Hotplug)가 일관된 인터페이스로 동작합니다.

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 리소스 리스트 */
};
코드 설명
  • kobjstruct kobject는 sysfs 디렉토리 노드를 나타내며, 참조 카운팅과 uevent 인프라를 제공합니다. /sys/devices/ 아래 디바이스 계층 구조가 이 객체로 구성됩니다 (lib/kobject.c).
  • parent, bus, driverparent는 물리적 버스 계층(PCI 브리지 등)을 형성하고, bus는 소속 버스 타입, driver는 바인딩된 드라이버를 가리킵니다. driverNULL이면 아직 드라이버가 바인딩되지 않은 상태입니다.
  • of_node, fwnodeof_node은 Device Tree 노드 포인터이고, fwnode은 DT와 ACPI를 통합하는 추상 계층입니다. 최신 드라이버는 fwnode 기반 device_property_* API를 사용하여 두 펌웨어 환경을 동시에 지원합니다.
  • class, devtclass는 기능별 분류(net, block, input 등)이며, devt/dev 노드의 major:minor 번호입니다. device_create() 호출 시 udev가 이 정보로 디바이스 파일을 생성합니다.
  • devres_headdevm_* API로 할당된 리소스들의 LIFO 스택입니다. devres_release_all()(drivers/base/devres.c)이 드라이버 언바인딩 시 등록 역순으로 모든 리소스를 자동 해제합니다.

주요 도우미 함수:

/* 디바이스 등록/해제 */
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 */
};
코드 설명
  • name, bus, ownername은 sysfs에 표시되는 드라이버 이름이며, bus는 소속 버스를 지정합니다. ownerTHIS_MODULE로 설정되어 드라이버 사용 중 모듈이 언로드되는 것을 방지합니다.
  • of_match_table, acpi_match_tableDevice Tree compatible 문자열과 ACPI HID/CID를 매칭하는 테이블입니다. 버스의 match() 콜백이 이 테이블을 참조하여 디바이스-드라이버 쌍을 결정합니다.
  • probe, remove, shutdowninclude/linux/device/driver.h에 정의된 범용 콜백입니다. 버스별 구조체(예: platform_driver)가 자체 probe를 제공하면, 버스의 probe 래퍼가 이를 호출합니다. 호출 체인: really_probe()bus->probe() → 버스별 래퍼 → drv->probe().
  • dev_groupsstruct attribute_group 배열로, 드라이버 바인딩 시 sysfs 속성 파일을 자동 생성합니다. driver_bound()(drivers/base/dd.c)에서 device_add_groups()를 호출합니다.
  • probe_typePROBE_FORCE_SYNCHRONOUS로 설정하면 비동기 probe를 금지합니다. 기본값은 PROBE_DEFAULT_STRATEGY로, 커널 커맨드 라인이나 드라이버 설정에 따라 비동기 실행이 가능합니다.

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);
코드 설명
  • match디바이스-드라이버 매칭을 결정하는 핵심 콜백입니다. 새 디바이스가 등록되면 __device_attach()가, 새 드라이버가 등록되면 __driver_attach()가 이 콜백을 호출합니다 (drivers/base/dd.c). 반환값 1이면 매칭 성공입니다.
  • probe, remove버스 레벨 probe/remove 래퍼입니다. really_probe()bus->probe()를 우선 호출하고, 없으면 drv->probe()를 직접 호출합니다. Platform Bus의 경우 platform_probe()struct devicestruct platform_device로 변환 후 드라이버의 probe를 호출합니다.
  • uevent디바이스 이벤트 발생 시 udev에 전달할 환경변수를 추가합니다. PCI Bus는 PCI_ID, USB Bus는 PRODUCT 등 버스별 식별 정보를 설정하여 udev 규칙 매칭과 모듈 자동 로딩(modules.alias)을 지원합니다.
  • p (subsys_private)버스에 등록된 디바이스 리스트(klist_devices)와 드라이버 리스트(klist_drivers)를 관리하는 내부 구조체입니다. bus_register() 호출 시 drivers/base/bus.c에서 할당되며, /sys/bus/NAME/ 디렉토리를 생성합니다.
  • bus_register, bus_unregisterbus_register()는 sysfs 디렉토리 생성, devices/drivers/ 서브디렉토리 생성, kset 초기화를 수행합니다 (drivers/base/bus.c). bus_unregister()는 역순으로 정리합니다.

커널에 등록된 주요 버스:

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;
}
코드 설명
  • dev->driver = drv매칭된 드라이버를 디바이스에 연결합니다. probe 실패 시 really_probe()가 이를 NULL로 되돌립니다.
  • pinctrl_bind_pinsDevice Tree의 pinctrl-0/pinctrl-names 속성에 따라 핀 멀티플렉싱을 "default" 상태로 설정합니다 (drivers/pinctrl/core.c). SoC에서 GPIO/UART/SPI 등 핀 기능 선택에 필수적입니다.
  • dma_configure디바이스의 DMA 설정을 초기화합니다. IOMMU가 있으면 DMA 도메인을 연결하고, Device Tree의 dma-ranges 속성으로 DMA 주소 오프셋을 설정합니다 (drivers/base/dma.c).
  • bus->probe vs drv->probe버스의 probe 래퍼가 있으면 이를 우선 호출합니다. Platform Bus의 경우 platform_probe()(drivers/base/platform.c)가 struct devicestruct platform_device로 변환 후 드라이버의 probe를 호출하는 래퍼 역할을 합니다.
  • -EPROBE_DEFER의존 리소스가 아직 준비되지 않았음을 나타냅니다. driver_deferred_probe_add()가 디바이스를 재시도 리스트에 추가하고, 다른 probe 성공 시 driver_deferred_probe_trigger()로 재시도합니다.
  • driver_bound + KOBJ_BINDprobe 성공 시 driver_bound()가 디바이스를 드라이버의 klist에 등록하고, KOBJ_BIND uevent를 발생시켜 udev에 바인딩 완료를 알립니다.

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 — 통합 펌웨어(Firmware) 노드

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 가능 프로세스(Process) 컨텍스트만 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우선순위 내림차순으로 연결 리스트(Linked List)에 삽입됩니다 (높은 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 커널 패닉(Kernel Panic) 발생 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, 상세 문서
실무 기준: 네트워크 드라이버는 단순 송수신 구현보다 큐 병목(Bottleneck) 제어(RSS/BQL), 제어 경로 안정성(RTNL), 장애 자동 복구(devlink/timeout)의 완성도가 운영 품질을 좌우합니다.
ℹ️

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

Device Tree (DTS/DTB)

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

probe() 내부 동작과 에러 처리 패턴

probe() 함수는 드라이버의 진입점으로, 하드웨어 초기화와 리소스 할당을 담당합니다. probe 내부의 실행 순서와 에러 처리 패턴을 정확히 이해하는 것이 안정적인 드라이버의 핵심입니다.

probe 실행 순서

really_probe()가 드라이버의 probe()를 호출하기 전에 커널이 수행하는 사전 작업이 있습니다:

probe() 실행 전체 흐름 really_probe(dev, drv) dev->driver = drv (드라이버 연결) pinctrl_bind_pins() (핀 설정) dma_configure() (DMA 설정) bus->probe(dev) / drv->probe(dev) return 0 (성공) driver_bound(dev) kobject_uevent(KOBJ_BIND) dev->driver 유지, sysfs 링크 생성 -EPROBE_DEFER driver_deferred_probe_add(dev) 다음 성공 probe 후 재시도 dev->driver = NULL 복원 기타 에러 (-ENOMEM 등) dev_err() 로깅 dev->driver = NULL, 정리
really_probe()가 드라이버 probe 전후로 수행하는 전체 단계

probe 에러 처리 패턴

probe에서 에러 처리는 두 가지 접근법이 있습니다. 전통적인 goto 체인과 devm_* API를 활용한 자동 해제입니다:

/* 패턴 1: 전통적 goto 체인 (non-devm 리소스 사용 시) */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    int ret;

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

    priv->base = ioremap(res->start, resource_size(res));
    if (!priv->base) {
        ret = -ENOMEM;
        goto err_free;
    }

    ret = request_irq(irq, my_handler, 0, "mydev", priv);
    if (ret)
        goto err_unmap;

    return 0;

err_unmap:
    iounmap(priv->base);
err_free:
    kfree(priv);
    return ret;
}

/* 패턴 2: devm 기반 (권장) — goto 불필요 */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

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

    ret = devm_request_irq(dev, irq, my_handler, 0, "mydev", priv);
    if (ret)
        return ret;  /* 실패 시 이전 devm 리소스 자동 해제 */

    return 0;
}
코드 설명
  • 패턴 1: goto 체인전통적 에러 처리 방식으로, 각 리소스 할당 실패 시 이전에 성공한 할당을 역순으로 해제합니다. kzalloc()/ioremap()/request_irq()는 non-managed API이므로, 해제 책임이 드라이버에 있습니다. 레이블 순서(err_unmaperr_free)가 할당 역순이어야 합니다.
  • 패턴 2: devm 기반devm_kzalloc(), devm_platform_ioremap_resource(), devm_request_irq()struct devicedevres_head 리스트에 해제 콜백을 등록합니다. probe 중간에 실패하면 devres_release_all()(drivers/base/devres.c)이 등록 역순(LIFO)으로 모든 리소스를 자동 해제하므로 goto가 불필요합니다.
  • PTR_ERR / IS_ERRdevm_platform_ioremap_resource()는 실패 시 ERR_PTR(-errno)를 반환합니다. IS_ERR()로 에러를 확인하고 PTR_ERR()로 에러 코드를 추출하는 것은 커널의 표준 에러 전달 패턴입니다 (include/linux/err.h).

dev_err_probe() 활용

커널 5.9에서 도입된 dev_err_probe()-EPROBE_DEFER를 특별히 처리합니다:

/* dev_err_probe 동작 원리 */
int dev_err_probe(const struct device *dev, int err,
                  const char *fmt, ...)
{
    if (err == -EPROBE_DEFER) {
        device_set_deferred_probe_reason(dev, &vaf);
        dev_dbg(dev, ...);  /* 조용히 (dmesg 안 나옴) */
    } else {
        dev_err(dev, ...);  /* 에러 로깅 */
    }
    return err;
}

/* 사용 예: 한 줄로 에러 로깅 + 반환 */
priv->clk = devm_clk_get(dev, NULL);
if (IS_ERR(priv->clk))
    return dev_err_probe(dev, PTR_ERR(priv->clk),
                          "clock 획득 실패\n");

Device Tree vs ACPI 바인딩 비교

현대 리눅스 드라이버는 Device Tree(DT)와 ACPI 두 가지 펌웨어 인터페이스를 통해 하드웨어를 기술받습니다. 동일한 드라이버가 두 환경을 모두 지원하려면 각각의 매칭 메커니즘과 프로퍼티 접근 방식을 이해해야 합니다.

항목Device TreeACPI
매칭 키 compatible 문자열 HID (Hardware ID), CID (Compatible ID)
매칭 테이블 of_match_table (struct of_device_id[]) acpi_match_table (struct acpi_device_id[])
프로퍼티 접근 of_property_read_*() acpi_dev_get_property(), _DSD 메서드
통합 API device_property_read_*() — DT/ACPI 모두 지원
리소스 기술 reg, interrupts 프로퍼티 _CRS (Current Resource Settings) 메서드
전원 관리 DT 프로퍼티 (power-domains 등) _PS0/_PS3, _PRx 메서드
주요 플랫폼 ARM, RISC-V, PowerPC x86, ARM 서버 (SBSA)
수정 용이성 DT overlay로 동적 수정 가능 SSDT 오버레이 (제한적)
/* DT + ACPI 동시 지원 드라이버 패턴 */
static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-controller",
      .data = &variant_a },
    { .compatible = "vendor,my-controller-v2",
      .data = &variant_b },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static const struct acpi_device_id my_acpi_match[] = {
    { "VNDR0001", (kernel_ulong_t)&variant_a },
    { "VNDR0002", (kernel_ulong_t)&variant_b },
    { }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_match);

static int my_probe(struct platform_device *pdev)
{
    const struct my_variant *variant;

    /* 통합 API: DT든 ACPI든 동일하게 동작 */
    variant = device_get_match_data(&pdev->dev);
    if (!variant)
        return -ENODEV;

    /* 통합 프로퍼티 접근 */
    device_property_read_u32(&pdev->dev, "clock-frequency", &freq);
    /* ... */
}

static struct platform_driver my_driver = {
    .probe = my_probe,
    .driver = {
        .name            = "my-controller",
        .of_match_table  = my_of_match,
        .acpi_match_table = ACPI_PTR(my_acpi_match),
        .pm              = &my_pm_ops,
    },
};
DT vs ACPI 매칭 흐름 비교 Device Tree 경로 DTS: compatible = "vendor,my-ctrl" of_match_device(drv->of_match_table, dev) of_property_read_u32(dev->of_node, ...) ACPI 경로 DSDT: Device(MYCT) { _HID "VNDR0001" } acpi_match_device(drv->acpi_match_table, dev) acpi_dev_get_property(adev, ...) 통합 API: device_property_read_*() fwnode_handle 추상화를 통해 DT/ACPI 구분 없이 동일 코드 사용
DT와 ACPI가 서로 다른 매칭 경로를 거치지만 device_property API로 통합됨

플랫폼 디바이스 vs 버스 디바이스 비교

리눅스 커널에서 디바이스는 크게 자동 열거 가능한 버스 디바이스(PCI, USB)와 수동 기술이 필요한 플랫폼 디바이스로 나뉩니다. 이 구분은 드라이버 설계에 직접적인 영향을 미칩니다.

항목Platform DevicePCI DeviceUSB Device
열거 방식 DT/ACPI/정적 등록 PCI 설정 공간 스캔 USB 열거 프로토콜
버스 구조체 platform_bus_type pci_bus_type usb_bus_type
디바이스 구조체 platform_device pci_dev usb_device
드라이버 구조체 platform_driver pci_driver usb_driver
매칭 기준 DT compatible, ACPI HID, name vendor:device:subvendor:subdevice vendor:product:class
리소스 획득 platform_get_resource() pci_resource_start() usb_rcvbulkpipe()
핫플러그 일반적으로 불가 가능 (PCIe hot-plug) 기본 지원
DMA 설정 DT/ACPI에서 자동 PCI BAR + IOMMU HCD가 관리
등록 매크로 module_platform_driver() module_pci_driver() module_usb_driver()
/* 세 가지 버스의 probe 시그니처 비교 */

/* Platform: struct platform_device 전달 */
static int plat_probe(struct platform_device *pdev) { ... }

/* PCI: struct pci_dev + pci_device_id 전달 */
static int pci_probe(struct pci_dev *pdev,
                     const struct pci_device_id *id) { ... }

/* USB: struct usb_interface + usb_device_id 전달 */
static int usb_probe(struct usb_interface *intf,
                     const struct usb_device_id *id) { ... }

Managed Device Resources (devm)

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

devm API 매핑 테이블

일반 APIManaged API리소스
kmalloc()devm_kmalloc()메모리
kzalloc()devm_kzalloc()0 초기화 메모리
ioremap()devm_ioremap()MMIO 매핑
request_irq()devm_request_irq()인터럽트
request_threaded_irq()devm_request_threaded_irq()스레드(Thread) IRQ
clk_get()devm_clk_get()클럭
clk_get_enabled()devm_clk_get_enabled()활성화된 클럭 (6.0+)
regulator_get()devm_regulator_get()레귤레이터
gpio_request()devm_gpio_request()GPIO
reset_control_get()devm_reset_control_get()리셋 컨트롤러
platform_get_resource() + ioremap()devm_platform_ioremap_resource()플랫폼 리소스 매핑

devm 내부 동작 원리

devm_* API는 내부적으로 struct devicedevres_head 리스트에 리소스를 LIFO(후입선출) 순서로 저장합니다. 드라이버가 제거되면 devres_release_all()이 등록 역순으로 모든 리소스를 해제합니다.

devm 리소스 스택 (LIFO 해제) probe() 등록 순서 1. devm_kzalloc (메모리) 2. devm_ioremap (MMIO) 3. devm_clk_get (클럭) 4. devm_request_irq (IRQ) 등록 devres_head 리스트 TOP: IRQ (#4) 클럭 (#3) MMIO (#2) 메모리 (#1) LIFO: 위에서 아래로 해제 remove() 해제 순서 1. free_irq (IRQ) 2. clk_put (클럭) 3. iounmap (MMIO) 4. kfree (메모리) 해제 devm 등록 순서 핵심 규칙 1. 데이터(메모리, MMIO, 클럭)를 먼저 등록, 소비자(IRQ, 타이머, 워크큐)를 나중에 등록 2. 해제 시 소비자가 먼저 해제 → 데이터를 참조하는 활성 핸들러가 없어 안전 3. 순서가 뒤바뀌면 Use-After-Free 발생 (IRQ가 이미 해제된 메모리 참조)
devm 리소스는 등록 역순(LIFO)으로 해제되므로 등록 순서가 중요
/* 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;
}

커스텀 devm 액션

표준 devm API가 없는 리소스도 devm_add_action_or_reset()으로 관리할 수 있습니다:

/* 커스텀 정리 함수를 devm 스택에 등록 */
static void my_cleanup(void *data)
{
    struct my_hw *hw = data;
    my_hw_shutdown(hw);
    my_hw_power_off(hw);
}

static int my_probe(struct platform_device *pdev)
{
    struct my_hw *hw = devm_kzalloc(&pdev->dev, sizeof(*hw), GFP_KERNEL);
    my_hw_power_on(hw);
    my_hw_init(hw);

    /* 에러/remove 시 my_cleanup 자동 호출 */
    int ret = devm_add_action_or_reset(&pdev->dev, my_cleanup, hw);
    if (ret)
        return ret;  /* 등록 실패 시 cleanup 즉시 호출됨 */

    /* ... 나머지 초기화 ... */
    return 0;
}

전원 관리 (PM)

시스템 슬립(Sleep) 콜백

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

Runtime PM (런타임 전원 관리(Runtime PM))

Runtime PM은 시스템이 활성 상태에서도 개별 디바이스의 전원을 동적으로 관리합니다. 사용하지 않는 디바이스의 클럭/전원을 자동으로 차단하여 전력 소비를 줄입니다.

#include <linux/pm_runtime.h>

static int my_runtime_suspend(struct device *dev)
{
    struct my_priv *priv = dev_get_drvdata(dev);
    clk_disable_unprepare(priv->clk);
    return 0;
}

static int my_runtime_resume(struct device *dev)
{
    struct my_priv *priv = dev_get_drvdata(dev);
    return clk_prepare_enable(priv->clk);
}

static const struct dev_pm_ops my_pm_ops = {
    SYSTEM_SLEEP_PM_OPS(my_suspend, my_resume)
    RUNTIME_PM_OPS(my_runtime_suspend, my_runtime_resume, NULL)
};

/* probe에서 Runtime PM 활성화 */
static int my_probe(struct platform_device *pdev)
{
    /* ... 초기화 ... */

    pm_runtime_set_active(&pdev->dev);
    pm_runtime_enable(&pdev->dev);
    pm_runtime_set_autosuspend_delay(&pdev->dev, 200); /* 200ms */
    pm_runtime_use_autosuspend(&pdev->dev);

    return 0;
}

/* I/O 수행 시 get/put으로 활성 상태 보장 */
static ssize_t my_read(struct file *f, ...)
{
    pm_runtime_get_sync(dev);    /* 디바이스 활성화 */
    /* ... 하드웨어 접근 ... */
    pm_runtime_mark_last_busy(dev);
    pm_runtime_put_autosuspend(dev); /* autosuspend 타이머 시작 */
}

PM 상태 머신

디바이스 PM 상태 머신 Active (RPM_ACTIVE) 클럭/전원 ON, I/O 가능 Suspending runtime_suspend() 실행 중 Suspended (RPM_SUSPENDED) 클럭/전원 OFF, I/O 불가 Resuming runtime_resume() 실행 중 pm_runtime_put() (usage_count=0) 완료 (return 0) pm_runtime_get() 완료 (return 0) System Sleep: suspend() → [S3/S2idle] → resume() | 시스템 전체 suspend 시 모든 디바이스 순차 처리 Runtime PM이 이미 suspended이면 system suspend에서 콜백 생략 가능 (pm_runtime_suspended() 확인)
Runtime PM은 개별 디바이스 단위, System Sleep은 시스템 전체 단위로 전원 관리
PM 콜백호출 시점컨텍스트주요 작업
runtime_suspendusage_count 0, autosuspend 만료프로세스 (워크큐)클럭 차단, 저전력 모드
runtime_resumepm_runtime_get*() 호출프로세스 또는 IRQ클럭 활성화, 레지스터 복원
suspend시스템 sleep 진입프로세스HW 상태 저장, 전원 차단
resume시스템 wake-up프로세스HW 상태 복원, 재초기화
suspend_latesuspend 후, noirq 전프로세스공유 리소스 최종 정리
resume_earlynoirq 후, resume 전프로세스공유 리소스 사전 복원
freeze/thawhibernate snapshot 전/후프로세스DMA 중지/재개

sysfs 속성 노출

sysfs는 커널 객체를 사용자 공간에 파일로 노출하는 가상 파일시스템(Virtual Filesystem)입니다. 디바이스 드라이버는 sysfs 속성(Attribute)을 통해 상태 조회와 제어 인터페이스를 제공합니다. DEVICE_ATTR 매크로 계열을 사용하면 일관된 패턴으로 속성을 정의할 수 있습니다.

DEVICE_ATTR 매크로

매크로생성 함수권한용도
DEVICE_ATTR_RO(name)name_show()0444읽기 전용 상태
DEVICE_ATTR_WO(name)name_store()0200쓰기 전용 제어
DEVICE_ATTR_RW(name)name_show() + name_store()0644읽기/쓰기
/* sysfs 속성 노출 — DEVICE_ATTR 패턴 */
static ssize_t status_show(struct device *dev,
                           struct device_attribute *attr, char *buf)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    return sysfs_emit(buf, "%s\n",
                      mydev->running ? "running" : "stopped");
}

static ssize_t command_store(struct device *dev,
                             struct device_attribute *attr,
                             const char *buf, size_t count)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    if (sysfs_streq(buf, "start"))
        mydev->running = true;
    else if (sysfs_streq(buf, "stop"))
        mydev->running = false;
    else
        return -EINVAL;

    return count;
}

static DEVICE_ATTR_RO(status);
static DEVICE_ATTR_WO(command);

/* 속성 그룹 등록 (권장) */
static struct attribute *my_attrs[] = {
    &dev_attr_status.attr,
    &dev_attr_command.attr,
    NULL,
};
ATTRIBUTE_GROUPS(my);

static struct platform_driver my_driver = {
    .driver = {
        .name = "my_device",
        .dev_groups = my_groups,  /* probe 시 자동 생성, remove 시 자동 제거 */
    },
    .probe  = my_probe,
    .remove = my_remove,
};
코드 설명
  • sysfs_emit()sprintf() 대신 반드시 사용해야 합니다. 내부적으로 PAGE_SIZE 경계를 검사하여 버퍼 오버플로를 방지합니다. include/linux/sysfs.h에 정의되어 있습니다.
  • sysfs_streq()사용자 공간에서 전달되는 문자열에 포함된 줄바꿈(\n)을 무시하고 비교합니다. echo start > /sys/.../command 시 trailing newline을 자동 처리합니다.
  • dev_groupsstruct device_driverdev_groups 필드에 등록하면, probe() 성공 시 sysfs 파일이 자동 생성되고 remove() 시 자동 제거됩니다. 수동으로 sysfs_create_group()을 호출할 필요가 없습니다.
  • ATTRIBUTE_GROUPS(my)my_attrs[] 배열로부터 my_groupmy_groups[]를 자동 생성하는 매크로입니다. include/linux/sysfs.h에 정의되어 있습니다.
⚠️

sysfs ABI 규칙 — 한번 노출하면 삭제/변경 불가:

  • sysfs 파일은 안정 ABI로 간주됩니다. 릴리스 후 삭제하거나 의미를 변경하면 사용자 공간 프로그램이 깨집니다.
  • one-value-per-file 원칙: 하나의 파일에 하나의 값만 노출하세요. 여러 값을 한 파일에 넣으면 파싱 호환성이 깨집니다.
  • store() 콜백에서 입력 검증은 필수입니다. 잘못된 값에 대해 -EINVAL을 반환하세요.
  • show() 콜백에서 sprintf()/snprintf() 대신 반드시 sysfs_emit()을 사용하세요. 커널 5.10 이후 checkpatch 경고 대상입니다.

바이너리 속성 (BIN_ATTR)

레지스터 덤프, EEPROM 내용, 펌웨어 버전 등 바이너리 데이터를 노출할 때는 텍스트 기반 DEVICE_ATTR 대신 BIN_ATTR 매크로를 사용합니다. 바이너리 속성은 read()/write() 시스템 콜 인터페이스를 제공하며, 오프셋(Offset) 기반 접근을 지원합니다.

static ssize_t regs_read(struct file *filp, struct kobject *kobj,
                         struct bin_attribute *attr,
                         char *buf, loff_t off, size_t count)
{
    struct device *dev = kobj_to_dev(kobj);
    struct my_device *mydev = dev_get_drvdata(dev);

    if (off + count > REGS_SIZE)
        count = REGS_SIZE - off;
    memcpy_fromio(buf, mydev->regs + off, count);
    return count;
}
static BIN_ATTR_RO(regs, REGS_SIZE);
코드 설명
  • bin_attributestruct attribute를 확장한 구조체로, size 필드와 read()/write() 콜백을 포함합니다. include/linux/sysfs.h에 정의되어 있습니다.
  • kobj_to_dev()kobject 포인터에서 struct device를 추출하는 매크로입니다. 바이너리 속성 콜백은 struct device 대신 struct kobject를 받으므로 변환이 필요합니다.
  • off, count사용자 공간에서 pread()/read()로 전달되는 오프셋과 크기입니다. 범위 검사를 반드시 수행해야 합니다.
  • BIN_ATTR_RO(regs, REGS_SIZE)읽기 전용 바이너리 속성을 선언합니다. 두 번째 인자는 파일 크기로, 사용자 공간에서 stat()으로 확인할 수 있습니다.

DMA — 주의사항과 고려사항

DMA 매핑 API 유형과 선택 기준

API수명캐시 일관성사용 시나리오
dma_alloc_coherent() 장기 (드라이버 수명) H/W 보장 (uncached/write-combined) 디스크립터 링, 커맨드 큐, 공유 상태
dma_map_single() 단기 (한 번의 전송) S/W sync 필요 패킷 버퍼(Buffer), 단일 블록 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의 성능 오버헤드(Overhead) 회피)
dma_pool_create() 장기 (풀 관리) H/W 보장 소규모 coherent 버퍼를 빈번하게 할당/해제

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

/* 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 주소 공간(Address Space)

/* 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. 캐시 라인(Cache Line) 공유 금지 — 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

펌웨어 로딩

많은 하드웨어 장치는 동작 전에 펌웨어(Firmware)를 로드해야 합니다. 리눅스 커널은 request_firmware() API를 통해 /lib/firmware/ 디렉터리에서 펌웨어 바이너리를 로드하고 드라이버에 전달하는 메커니즘(Mechanism)을 제공합니다.

펌웨어 요청 API

API동작사용 시나리오
request_firmware()동기 로드, sleep 가능probe() 등 프로세스 컨텍스트
request_firmware_nowait()비동기 로드, 콜백초기화 지연 방지
request_firmware_direct()userspace helper 우회펌웨어 경로 확정 시
firmware_request_nowarn()실패 시 경고 미출력옵션 펌웨어
release_firmware()펌웨어 메모리 해제반드시 호출
/* 펌웨어 로딩 — request_firmware() 패턴 */
#include <linux/firmware.h>

static int my_load_firmware(struct device *dev)
{
    const struct firmware *fw;
    int ret;

    ret = request_firmware(&fw, "my_device/fw.bin", dev);
    if (ret) {
        dev_err(dev, "firmware load failed: %d\n", ret);
        return ret;
    }

    dev_info(dev, "firmware loaded: %zu bytes\n", fw->size);

    /* 펌웨어 데이터 사용 */
    ret = my_upload_firmware(dev, fw->data, fw->size);

    release_firmware(fw);  /* 반드시 해제 */
    return ret;
}

MODULE_FIRMWARE("my_device/fw.bin");
코드 설명
  • request_firmware()/lib/firmware/ 경로에서 지정된 파일을 검색하여 커널 메모리에 로드합니다. 프로세스 컨텍스트에서만 호출 가능하며(sleep 가능), 파일을 찾지 못하면 음수 에러 코드를 반환합니다. include/linux/firmware.h에 선언되어 있습니다.
  • fw->data, fw->sizestruct firmware의 핵심 필드로, data는 펌웨어 바이너리의 메모리 포인터이고 size는 바이트 단위 크기입니다. 이 포인터는 release_firmware() 호출 후 무효화됩니다.
  • release_firmware()펌웨어 데이터가 차지하는 커널 메모리를 해제합니다. 호출하지 않으면 메모리 누수(Memory Leak)가 발생합니다. 펌웨어를 장치에 업로드한 후 즉시 해제하는 것이 일반적입니다.
  • MODULE_FIRMWARE()모듈 메타데이터에 필요한 펌웨어 파일명을 선언합니다. modinfo 명령으로 확인 가능하며, dracut이나 mkinitramfs 같은 initramfs 생성 도구가 이 정보를 참조하여 필요한 펌웨어를 자동 포함합니다.
💡

펌웨어 로딩 모범 사례:

  • request_firmware() + release_firmware() 조합을 사용하세요. 펌웨어는 장치에 업로드한 후 즉시 해제하는 것이 메모리 효율적입니다. devm_ 변형은 드라이버 전체 수명 동안 메모리를 점유하므로 대부분의 경우 불필요합니다.
  • MODULE_FIRMWARE("my_device/fw.bin") 선언은 필수입니다. initramfs 생성 도구(dracut, mkinitramfs)가 이 매크로를 참조하여 부팅에 필요한 펌웨어를 자동으로 포함합니다.
  • 펌웨어 검색 경로: 커널은 /lib/firmware/를 기본으로 검색하며, CONFIG_EXTRA_FIRMWARE 설정으로 커널 이미지에 펌웨어를 직접 내장할 수도 있습니다.

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

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

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" 메시지 출력 */

인터럽트 핸들러 경쟁 조건(Race Condition)

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, 버퍼 오버플로(Buffer Overflow)우 런타임 탐지
CONFIG_KFENCE: 프로덕션 환경용 저오버헤드 메모리 오류 샘플링
CONFIG_PROVE_LOCKING: 잠금 의존성 그래프 검증으로 데드락 사전 탐지
coccinelle (spatch): 커널 코드 정적 분석 도구, 일반적인 드라이버 버그 패턴 탐지

드라이버 바인딩/언바인딩 메커니즘

디바이스와 드라이버의 연결(bind)과 분리(unbind)는 sysfs를 통해 런타임에 수동으로도 제어할 수 있습니다. 이는 디버깅, 드라이버 교체, 핫플러그 테스트에 필수적인 메커니즘입니다.

sysfs bind/unbind 인터페이스

# 현재 바인딩 상태 확인
$ ls /sys/bus/platform/drivers/my-driver/
bind  module  uevent  unbind  40000000.my-device

# 수동 언바인딩: 디바이스를 드라이버에서 분리
$ echo "40000000.my-device" > /sys/bus/platform/drivers/my-driver/unbind

# 수동 바인딩: 디바이스를 드라이버에 다시 연결
$ echo "40000000.my-device" > /sys/bus/platform/drivers/my-driver/bind

# 드라이버 오버라이드: 다른 드라이버에 강제 바인딩
$ echo "other-driver" > /sys/bus/platform/devices/40000000.my-device/driver_override
$ echo "40000000.my-device" > /sys/bus/platform/drivers/other-driver/bind

# 오버라이드 해제
$ echo "" > /sys/bus/platform/devices/40000000.my-device/driver_override

언바인딩 내부 흐름

/* drivers/base/dd.c — 언바인딩 핵심 흐름 */
static void __device_release_driver(struct device *dev,
                                     struct device *parent)
{
    struct device_driver *drv = dev->driver;

    /* 1. KOBJ_UNBIND uevent → udev에 통지 */
    kobject_uevent(&dev->kobj, KOBJ_UNBIND);

    /* 2. bus->remove() 또는 drv->remove() 호출 */
    if (dev->bus && dev->bus->remove)
        dev->bus->remove(dev);
    else if (drv->remove)
        drv->remove(dev);

    /* 3. devres 리소스 전체 해제 */
    devres_release_all(dev);

    /* 4. DMA 설정 정리 */
    dma_deconfigure(dev);

    /* 5. dev->driver = NULL */
    dev->driver = NULL;

    /* 6. 디바이스 참조 해제 */
    device_links_driver_cleanup(dev);
}
언바인딩 시 주의: remove() 콜백에서는 진행 중인 I/O를 모두 완료하거나 취소해야 합니다. 특히 DMA 전송, 타이머, 워크큐가 활성 상태이면 cancel_work_sync(), del_timer_sync()를 반드시 호출하세요. devm이 아닌 리소스는 수동으로 해제해야 합니다.

드라이버 보안

디바이스 드라이버는 커널 공간(Kernel Space)에서 실행되며 하드웨어에 직접 접근하므로, 보안 취약점의 영향이 크고 공격 표면이 넓습니다. 드라이버 개발 시 반드시 고려해야 할 보안 원칙을 정리합니다.

권한 검사와 CAP_SYS_RAWIO

/* 위험한 하드웨어 접근 전 capability 검사 */
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
    case MY_IOC_READ_REG:
        /* 일반 읽기는 모든 사용자 허용 */
        return my_read_reg(f, arg);

    case MY_IOC_WRITE_REG:
        /* 레지스터 직접 쓰기는 CAP_SYS_RAWIO 필요 */
        if (!capable(CAP_SYS_RAWIO))
            return -EPERM;
        return my_write_reg(f, arg);

    case MY_IOC_DMA_SETUP:
        /* DMA 설정 변경은 CAP_SYS_ADMIN 필요 */
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        return my_setup_dma(f, arg);

    case MY_IOC_FW_UPDATE:
        /* 펌웨어 업데이트는 추가 네임스페이스 검사 */
        if (!ns_capable(current_user_ns(), CAP_SYS_ADMIN))
            return -EPERM;
        return my_fw_update(f, arg);
    }
    return -ENOTTY;
}

사용자 입력 검증

검증 항목위험방어 코드
ioctl 명령 번호 잘못된 cmd로 의도하지 않은 동작 _IOC_TYPE(cmd), _IOC_NR(cmd) 범위 검사
버퍼 크기 커널 스택/힙 오버플로우 _IOC_SIZE(cmd) 확인, 최대값 제한
사용자 포인터 커널 메모리 읽기/쓰기 copy_from_user()/copy_to_user() 필수
레지스터 오프셋(Offset) 범위 밖 MMIO 접근 허용 범위 화이트리스트 검사
DMA 주소/크기 임의 메모리 읽기/쓰기 IOMMU 보호, 주소 범위 검증
/* 사용자 입력 검증 패턴 */
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    struct my_ioctl_data data;

    /* 1. ioctl 매직 넘버 검증 */
    if (_IOC_TYPE(cmd) != MY_IOC_MAGIC)
        return -ENOTTY;

    /* 2. 사용자 데이터 안전하게 복사 */
    if (copy_from_user(&data, (void __user *)arg, sizeof(data)))
        return -EFAULT;

    /* 3. 범위 검증 */
    if (data.offset >= MAX_REG_OFFSET)
        return -EINVAL;
    if (data.size > MAX_BUF_SIZE || data.size == 0)
        return -EINVAL;

    /* 4. 오버플로우 검사 */
    if (check_add_overflow(data.offset, data.size, &end))
        return -EOVERFLOW;
    /* ... */
}

드라이버 디버깅 도구

디바이스 드라이버 개발에서 디버깅은 핵심 역량입니다. 커널이 제공하는 다양한 디버깅 인프라를 효과적으로 활용하는 방법을 정리합니다.

Dynamic Debug (동적 디버그)

# 특정 드라이버의 dev_dbg 메시지 활성화
$ echo "module my_driver +p" > /sys/kernel/debug/dynamic_debug/control

# 특정 파일의 특정 라인 활성화
$ echo "file my_driver.c line 100-200 +p" > /sys/kernel/debug/dynamic_debug/control

# 함수 단위 활성화
$ echo "func my_probe +p" > /sys/kernel/debug/dynamic_debug/control

# 플래그: +p (출력), +f (함수명), +l (라인번호), +t (스레드ID)
$ echo "module my_driver +pflt" > /sys/kernel/debug/dynamic_debug/control

# 현재 활성화된 디버그 메시지 확인
$ grep "=p" /sys/kernel/debug/dynamic_debug/control

devcoredump와 devlink

# devlink를 통한 드라이버 상태 확인
$ devlink dev show
$ devlink dev info pci/0000:03:00.0

# 디바이스 헬스 리포트 (지원하는 드라이버)
$ devlink health show pci/0000:03:00.0

# devcoredump: 디바이스 크래시 덤프 수집
$ ls /sys/class/devcoredump/devcd*/
$ cat /sys/class/devcoredump/devcd0/data > crash_dump.bin

# ftrace로 드라이버 probe 추적
$ echo "p:probe_entry platform_drv_probe pdev=%di" > \
    /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/probe_entry/enable
$ cat /sys/kernel/debug/tracing/trace

디버깅용 Kconfig 옵션

Kconfig용도오버헤드
CONFIG_DEBUG_DRIVER드라이버 코어 디버그 메시지 활성화낮음
CONFIG_DMA_API_DEBUGDMA 매핑/해제 쌍 검증중간
CONFIG_LOCKDEP잠금 순서 위반 탐지높음
CONFIG_KASANUse-After-Free, 버퍼 오버플로우 탐지높음
CONFIG_KFENCE프로덕션용 저오버헤드 메모리 오류 샘플링매우 낮음
CONFIG_PROVE_LOCKING잠금 의존성 그래프 검증높음
CONFIG_DEBUG_DEVRESdevm 리소스 할당/해제 추적낮음
CONFIG_PM_DEBUGPM 전환 디버그 메시지낮음
CONFIG_DYNAMIC_DEBUG런타임 dev_dbg 활성화/비활성화매우 낮음
# devm 리소스 추적 (CONFIG_DEBUG_DEVRES=y)
$ echo 1 > /sys/module/devres/parameters/log

# dmesg에서 devm 할당/해제 로그 확인
$ dmesg | grep devres
[  1.234] my-device 40000000.ctrl: DEVRES ADD devm_kzalloc (4096 bytes)
[  1.235] my-device 40000000.ctrl: DEVRES ADD devm_ioremap (0x1000 bytes)
[  1.236] my-device 40000000.ctrl: DEVRES ADD devm_request_irq (IRQ 45)

드라이버 성능 최적화

부팅 시간 단축과 런타임 성능 향상을 위한 드라이버 최적화 기법을 다룹니다.

비동기 프로브 (Async Probe)

기본적으로 드라이버 프로브는 순차적으로 실행되어 부팅 시간을 늘립니다. 비동기 프로브를 사용하면 여러 드라이버가 병렬로 초기화됩니다.

/* 방법 1: 드라이버 코드에서 비동기 프로브 선언 */
static struct platform_driver my_driver = {
    .probe = my_probe,
    .driver = {
        .name = "my-device",
        .probe_type = PROBE_PREFER_ASYNCHRONOUS,
    },
};

/* 방법 2: 커널 부트 파라미터로 전체 비동기 활성화 */
/* driver_async_probe=* (모든 드라이버) */
/* driver_async_probe=my_driver,other_driver (특정 드라이버만) */
# 부팅 시 probe 소요 시간 측정
$ dmesg | grep "probe of"
[  0.523] my-device 40000000.ctrl: probe of 40000000.ctrl took 45ms

# initcall 디버그로 전체 드라이버 초기화 시간 확인
# 커널 부트 파라미터: initcall_debug
$ dmesg | grep initcall | sort -t= -k2 -n

지연 프로브 최적화

지연 프로브(deferred probe)는 의존성 미해결 시 자동 재시도하지만, 과도한 재시도는 부팅 시간을 늘립니다. 커널 6.x에서 도입된 fw_devlink이 이 문제를 개선합니다.

# 지연 프로브 대기 디바이스 확인
$ cat /sys/kernel/debug/devices_deferred

# fw_devlink 상태 확인 (기본: on)
$ cat /proc/cmdline | grep fw_devlink

# fw_devlink 모드
# fw_devlink=on     — DT/ACPI 의존성 자동 추적 (기본)
# fw_devlink=strict — 의존성 미해결 시 probe 차단 (디버깅용)
# fw_devlink=off    — 비활성화 (디버깅용)

# 지연 프로브 타임아웃 설정
# driver_deferred_probe_timeout=30  (30초 후 포기)
최적화 기법효과적용 방법
비동기 프로브부팅 시간 단축 (병렬 초기화)PROBE_PREFER_ASYNCHRONOUS
fw_devlink=on불필요한 deferred 재시도 감소커널 부트 파라미터 (기본 활성)
devm API 사용에러 경로 단순화, 리소스 누수 방지코드 레벨
Runtime PM유휴 디바이스 전력 절약pm_runtime_* API
빠른 probe 실패불필요한 초기화 시간 제거dev_err_probe()로 즉시 반환
모듈 빌드커널 이미지 크기 감소, 필요 시 로드CONFIG_*=m

커널 6.x 디바이스 드라이버 변경사항

커널 6.0 이후 디바이스 드라이버 서브시스템에 도입된 주요 변경사항을 정리합니다.

커널 버전변경사항영향
6.0 devm_clk_get_enabled() 도입 clk_get + clk_prepare_enable을 한 번에 처리, 에러 경로 단순화
6.1 class_create() 시그니처 변경 THIS_MODULE 인자 제거, class_create("name")으로 간소화
6.2 remove() 콜백 반환형 void 전환 platform_driver 등의 remove가 int에서 void로 변경, 에러 반환 불가
6.3 driver_override sysfs 보안 강화 driver_override에 대한 race condition 수정, 안전한 바인딩 제어
6.4 fw_devlink 기능 확장 ACPI 의존성 추적 개선, 부팅 시간 단축
6.5 struct class const 선언 권장 기존 동적 할당에서 정적 const 구조체로 전환 패턴
6.6 LTS DEFINE_SIMPLE_DEV_PM_OPS() 확산 이전 SIMPLE_DEV_PM_OPS 대체, CONFIG_PM 없을 때 데드코드 제거
6.8 device_property API 확장 소프트웨어 노드(swnode) 기반 테스트 지원 강화
6.12 LTS 드라이버 코어 Rust 바인딩 Rust로 작성된 드라이버 프레임워크 초기 지원
/* 6.2+ remove 콜백: void 반환 */
/* 이전 (deprecated) */
static int old_remove(struct platform_device *pdev)
{
    /* 정리 작업 */
    return 0;  /* 반환값이 무시되었음 */
}

/* 6.2+ (권장) */
static void new_remove(struct platform_device *pdev)
{
    /* 정리 작업 — 실패 시 경고만 출력 */
}

/* 6.1+ class_create: THIS_MODULE 제거 */
/* 이전: my_class = class_create(THIS_MODULE, "mydev"); */
/* 6.1+: */
my_class = class_create("mydev");

/* 6.5+ const struct class 패턴 */
static const struct class my_class = {
    .name = "mydev",
};
/* class_register(&my_class) / class_unregister(&my_class) */

/* 6.0+ devm_clk_get_enabled: get + prepare + enable 한 번에 */
priv->clk = devm_clk_get_enabled(dev, NULL);
if (IS_ERR(priv->clk))
    return dev_err_probe(dev, PTR_ERR(priv->clk),
                          "clock 획득/활성화 실패\n");
/* remove 시 자동으로 disable + unprepare + put */
마이그레이션 팁: 기존 드라이버를 최신 API로 업데이트할 때는 coccinelle(spatch)의 scripts/coccinelle/api/ 디렉토리에 있는 시맨틱 패치(Patch)를 활용하세요. 예를 들어 devm_platform_ioremap_resource.cocciplatform_get_resource() + devm_ioremap_resource() 패턴을 자동으로 devm_platform_ioremap_resource()로 변환합니다.

참고자료

커널 공식 문서

커널 소스 코드

외부 자료

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