디바이스 드라이버 (Device Drivers)
Linux 커널 디바이스 드라이버 개발의 공통 골격을 장치 모델 관점에서 심층 정리합니다. character/block/network 디바이스 분류와 사용자 공간 인터페이스, platform/PCI/USB 기반 프로브-바인딩 생명주기, file_operations와 irq/workqueue/tasklet 분할 전략, DMA 매핑과 캐시 일관성, 전원관리(runtime PM/system suspend) 연계, 에러 경로와 자원 해제 패턴, tracepoint·dynamic debug 기반 디버깅까지 실무 드라이버 품질을 높이는 핵심 원칙을 다룹니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.
단계별 이해
- 장치 수명주기 확인
probe부터 remove까지 흐름을 점검합니다. - 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리합니다. - 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인합니다. - 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현합니다.
디바이스 드라이버 개요
디바이스 드라이버는 커널과 하드웨어 사이의 인터페이스입니다. Linux에서는 세 가지 주요 디바이스 유형으로 분류합니다:
| 유형 | 인터페이스 | 예시 |
|---|---|---|
| Character Device | 바이트 스트림, 순차 접근 | 시리얼, 터미널, /dev/null |
| Block Device | 고정 크기 블록, 랜덤 접근 | 디스크, SSD, USB 스토리지 |
| Network Device | 패킷 기반, 소켓 API | 이더넷, WiFi |
Character Device 드라이버
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int my_open(struct inode *inode, struct file *file)
{
pr_info("device opened\\n");
return 0;
}
static ssize_t my_read(struct file *file,
char __user *buf, size_t len, loff_t *off)
{
char msg[] = "Hello from driver\\n";
if (*off >= sizeof(msg))
return 0;
if (copy_to_user(buf, msg + *off, min(len, sizeof(msg) - *off)))
return -EFAULT;
*off += len;
return len;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
};
static int __init my_init(void)
{
alloc_chrdev_region(&dev_num, 0, 1, "mydev");
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev_num, 1);
my_class = class_create("mydev_class");
device_create(my_class, NULL, dev_num, NULL, "mydev");
return 0;
}
직접 해보기: Hello World 캐릭터 디바이스
간단한 캐릭터 디바이스 드라이버를 작성하고 유저 공간에서 읽기/쓰기를 테스트합니다.
난이도: 중급 ⏱️ 예상 소요 시간: 45분- OS: Ubuntu 22.04+ (커널 헤더 설치됨)
- 권한: sudo 권한 (모듈 로드, 디바이스 노드 생성)
- 사전 지식: 커널 모듈 기초 (module_init/exit)
- 드라이버 소스 작성 (file_operations 구현)
- Makefile 작성 및 빌드
- 모듈 로드 및 디바이스 노드 확인
- 유저 공간에서 read/write 테스트
- 정리 및 언로드
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
결과 검증
- [ ]
/dev/chardev노드가 생성됨 - [ ]
cat /dev/chardev로 데이터 읽기 성공 - [ ]
echo로 데이터 쓰기 성공 - [ ]
dmesg에 open/read/write 로그 출력 - [ ] 언로드 후 디바이스 노드 자동 삭제
다음 단계
Platform Driver
Platform Device 개념
PCI, USB 등 자동 열거(enumeration)가 가능한 버스와 달리, SoC(System-on-Chip) 내장 컨트롤러(UART, SPI, I2C, GPIO, 타이머 등)는 버스 프로토콜로 디바이스를 발견할 수 없습니다. 커널은 이러한 디바이스를 위해 Platform Bus라는 가상 버스를 제공합니다. Platform Bus는 실제 하드웨어 버스가 아니라, device-driver 매칭 인프라를 재사용하기 위한 소프트웨어 추상화입니다.
Platform Device의 하드웨어 정보(레지스터 주소, IRQ 번호 등)는 세 가지 방식으로 커널에 전달됩니다:
| 공급 방식 | 설명 | 대표 환경 |
|---|---|---|
| Device Tree | DTS/DTB 파일에 하드웨어 기술, 부트로더가 전달 | ARM, RISC-V, PowerPC |
| ACPI | ACPI 테이블(DSDT/SSDT)에 디바이스 기술 | x86 서버/데스크톱, ARM 서버 |
| 정적 등록 (레거시) | 보드 파일에서 platform_device_register() 직접 호출 | 구형 ARM 보드 (비권장) |
Device Tree의 상세 문법, 프로퍼티, OF API 등은 Device Tree 심화 섹션을 참고하세요.
struct platform_device 분석
struct platform_device는 platform bus에 연결된 디바이스 하나를 나타냅니다. include/linux/platform_device.h에 정의되어 있습니다.
struct platform_device {
const char *name; /* 디바이스 이름 (name 매칭에 사용) */
int id; /* 인스턴스 번호 (-1이면 단일) */
struct device dev; /* 임베디드 generic device */
struct resource *resource; /* I/O, 메모리, IRQ 리소스 배열 */
u32 num_resources; /* resource 배열 크기 */
const struct platform_device_id *id_entry; /* 매칭된 id_table 항목 */
const char *driver_override; /* 강제 드라이버 바인딩 */
};
핵심 필드별 용도:
| 필드 | 용도 |
|---|---|
dev.platform_data | 보드 레벨 데이터 전달 (레거시). void * 포인터로 드라이버별 구조체 전달 |
dev.of_node | Device Tree에서 생성된 경우, 해당 DT 노드 (struct device_node *) |
dev.fwnode | 통합 firmware 노드. DT와 ACPI를 추상화하는 struct fwnode_handle * |
resource | MMIO 주소 범위, IRQ 번호, DMA 채널 등 하드웨어 리소스 |
driver_override | sysfs에서 수동으로 특정 드라이버를 강제 바인딩할 때 사용 |
struct resource는 디바이스의 하드웨어 리소스를 기술합니다:
struct resource {
resource_size_t start; /* 리소스 시작 주소/번호 */
resource_size_t end; /* 리소스 끝 주소/번호 (inclusive) */
const char *name;
unsigned long flags; /* 리소스 유형 */
};
/* 주요 리소스 유형 플래그 */
IORESOURCE_MEM /* MMIO 메모리 영역 */
IORESOURCE_IRQ /* 인터럽트 번호 */
IORESOURCE_DMA /* DMA 채널 */
IORESOURCE_IO /* I/O 포트 (x86) */
struct platform_driver 분석
struct platform_driver는 platform device를 다루는 드라이버를 나타냅니다:
struct platform_driver {
int (*probe)(struct platform_device *); /* 디바이스 발견 시 호출 */
void (*remove)(struct platform_device *); /* 디바이스 제거 시 호출 */
void (*shutdown)(struct platform_device *); /* reboot/poweroff 시 호출 */
int (*suspend)(struct platform_device *, pm_message_t); /* 레거시 PM */
int (*resume)(struct platform_device *); /* 레거시 PM */
struct device_driver driver; /* 임베디드 generic driver */
const struct platform_device_id *id_table; /* name+data 기반 매칭 */
bool prevent_deferred_probe;
};
driver 필드의 주요 하위 필드:
| 필드 | 용도 |
|---|---|
driver.name | 드라이버 이름. sysfs 경로와 최후순위 매칭에 사용 |
driver.of_match_table | Device Tree compatible 매칭 테이블 |
driver.acpi_match_table | ACPI HID/CID 매칭 테이블 |
driver.pm | struct dev_pm_ops * — 최신 PM 콜백 (전원 관리 참고) |
driver.owner | THIS_MODULE (매크로가 자동 설정) |
id_table은 이름 기반 매칭에 사용되며, 드라이버별 데이터를 전달할 수 있습니다:
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data; /* 드라이버별 private 데이터 */
};
/* 사용 예: 여러 변형 디바이스를 하나의 드라이버로 지원 */
enum { CHIP_V1, CHIP_V2, CHIP_V3 };
static const struct platform_device_id my_ids[] = {
{ "my-chip-v1", CHIP_V1 },
{ "my-chip-v2", CHIP_V2 },
{ "my-chip-v3", CHIP_V3 },
{ } /* sentinel */
};
매칭 메커니즘
Platform Bus의 platform_match() 함수는 다음 4단계 우선순위로 device-driver 매칭을 수행합니다:
| 우선순위 | 매칭 방식 | 비교 대상 |
|---|---|---|
| 1 (최고) | driver_override | sysfs에서 수동 설정된 드라이버 이름과 driver.name 비교 |
| 2 | of_match_table | DT 노드의 compatible과 드라이버의 of_device_id 배열 비교 |
| 3 | acpi_match_table | ACPI 디바이스 HID/CID와 acpi_device_id 배열 비교 |
| 4 | id_table | platform_device.name과 platform_device_id.name 비교 |
| 5 (최저) | driver.name | platform_device.name과 driver.name 문자열 비교 |
커널 내부의 platform_match() 핵심 로직:
/* drivers/base/platform.c */
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* 1단계: driver_override (sysfs 강제 바인딩) */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* 2단계: Device Tree compatible 매칭 */
if (of_driver_match_device(dev, drv))
return 1;
/* 3단계: ACPI 매칭 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* 4단계: id_table 매칭 */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 5단계: driver.name 문자열 매칭 (fallback) */
return (strcmp(pdev->name, drv->name) == 0);
}
각 매칭 방식별 드라이버 정의 예제:
/* Device Tree 매칭 (가장 일반적) */
static const struct of_device_id my_of_ids[] = {
{ .compatible = "vendor,my-ctrl-v2", .data = &chip_v2_data },
{ .compatible = "vendor,my-ctrl-v1", .data = &chip_v1_data },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_ids);
/* ACPI 매칭 */
static const struct acpi_device_id my_acpi_ids[] = {
{ "VNDR0001", (kernel_ulong_t)&chip_v1_data },
{ "VNDR0002", (kernel_ulong_t)&chip_v2_data },
{ }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-ctrl",
.of_match_table = my_of_ids,
.acpi_match_table = ACPI_PTR(my_acpi_ids),
},
};
Platform Device 등록
Platform Device가 커널에 등록되는 세 가지 경로:
1. 정적 등록 (레거시 보드 파일)
DT 이전의 ARM 보드에서 사용하던 방식입니다. 현재는 비권장이지만 레거시 코드에서 여전히 볼 수 있습니다:
/* 레거시 보드 파일 (arch/arm/mach-xxx/) */
static struct resource my_resources[] = {
[0] = {
.start = 0x40000000,
.end = 0x40000FFF,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 42, /* IRQ 번호 */
.end = 42,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device my_pdev = {
.name = "my-ctrl",
.id = -1,
.resource = my_resources,
.num_resources = ARRAY_SIZE(my_resources),
};
/* 보드 초기화 함수에서 */
platform_device_register(&my_pdev);
편의 함수로 간결하게 등록할 수도 있습니다:
/* 간편 등록 함수들 */
platform_device_register_simple("my-ctrl", -1, my_resources, 2);
platform_device_register_data(NULL, "my-ctrl", -1, &pdata, sizeof(pdata));
platform_device_register_resndata(NULL, "my-ctrl", -1,
my_resources, 2, &pdata, sizeof(pdata));
2. Device Tree 기반 자동 생성
현대 임베디드 시스템의 표준 방식입니다. 부팅 시 커널이 DTB를 파싱하여 of_platform_populate()로 platform_device를 자동 생성합니다:
/* 커널 부팅 시 자동 실행 (drivers/of/platform.c) */
of_platform_default_populate(NULL, NULL, NULL);
→ DT의 최상위 "simple-bus" / "simple-mfd" 등의 자식 노드를
순회하며 of_platform_device_create() 호출
→ struct platform_device 자동 생성 + 등록
3. ACPI 기반 자동 생성
x86 및 ARM 서버에서 ACPI 테이블의 디바이스 정보로 platform_device를 자동 생성합니다:
- ACPI 열거 — DSDT/SSDT의 Device() 오브젝트가 platform_device로 변환
- ACPI 네임스페이스 예:
- Device (MYCT) {
- Name (_HID, "VNDR0001")
- Name (_CRS, ResourceTemplate() {
- Memory32Fixed(ReadWrite, 0x40000000, 0x1000)
- Interrupt(ResourceConsumer, ...) { 42 }
- })
- }
- → acpi_default_enumeration()이 platform_device로 변환
probe/remove 생명주기
probe()는 device-driver 매칭이 성공했을 때 커널이 호출하는 진입점입니다. remove()는 디바이스 언바인드 또는 드라이버 언로드 시 역순으로 정리합니다.
probe 호출 조건:
- 디바이스가 등록될 때 이미 드라이버가 로드되어 있으면 즉시 호출
- 드라이버가 등록될 때 이미 디바이스가 존재하면 즉시 호출
- 둘 다 존재하고
platform_match()가 성공해야 함
probe 전형적 순서:
static int my_probe(struct platform_device *pdev)
{
struct my_priv *priv;
void __iomem *base;
int irq, ret;
/* 1. private 데이터 할당 */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 2. 리소스 획득 — MMIO */
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
priv->base = base;
/* 3. 리소스 획득 — IRQ */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq; /* 에러 코드 전파 */
ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
0, dev_name(&pdev->dev), priv);
if (ret)
return ret;
/* 4. 클록, 리셋, 레귤레이터 등 하드웨어 리소스 */
priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
if (IS_ERR(priv->clk))
return dev_err_probe(&pdev->dev, PTR_ERR(priv->clk),
"failed to get clock\\n");
/* 5. 하드웨어 초기화 */
writel(CTRL_ENABLE, base + REG_CTRL);
/* 6. 서브시스템 등록 (misc device, input, net 등) */
ret = misc_register(&priv->miscdev);
if (ret)
return ret;
/* 7. private 데이터를 디바이스에 연결 */
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "probed successfully\\n");
return 0;
}
dev_err_probe()는 -EPROBE_DEFER를 자동으로 감지하여 dev_dbg 수준으로 로깅하고, 그 외 에러는 dev_err로 출력합니다. probe deferral 메시지가 dmesg를 오염시키는 것을 방지하므로 적극 활용하세요.
remove — 역순 정리:
static void my_remove(struct platform_device *pdev)
{
struct my_priv *priv = platform_get_drvdata(pdev);
/* probe의 역순으로 정리 */
misc_deregister(&priv->miscdev);
/* devm_ 계열 리소스는 자동 해제되므로 별도 처리 불필요 */
/* devm_kzalloc, devm_request_irq, devm_ioremap 등 */
}
devm_* (managed) API를 사용하면 remove에서 명시적 해제가 불필요합니다. 상세한 devm API 목록은 Managed Device Resources (devm) 섹션을 참고하세요.
shutdown 콜백:
shutdown()은 시스템 reboot 또는 poweroff 시 호출됩니다. DMA 전송 중단, 인터럽트 비활성화 등 하드웨어를 안전한 상태로 전환합니다:
static void my_shutdown(struct platform_device *pdev)
{
struct my_priv *priv = platform_get_drvdata(pdev);
/* 하드웨어를 안전한 상태로 전환 */
writel(0, priv->base + REG_CTRL); /* 컨트롤러 비활성화 */
disable_irq(priv->irq); /* 인터럽트 차단 */
}
리소스 접근 API
Platform 드라이버에서 디바이스의 하드웨어 리소스에 접근하는 API들:
| API | 용도 |
|---|---|
platform_get_resource(pdev, type, index) | 타입별 N번째 리소스 구조체 반환 |
platform_get_resource_byname(pdev, type, name) | 이름으로 리소스 검색 |
platform_get_irq(pdev, index) | N번째 IRQ 번호 반환 (DT/ACPI 추상화) |
platform_get_irq_byname(pdev, name) | 이름으로 IRQ 검색 |
devm_platform_ioremap_resource(pdev, index) | 리소스 획득 + ioremap 한 번에 수행 |
devm_platform_ioremap_resource_byname(pdev, name) | 이름으로 리소스 획득 + ioremap |
platform_get_drvdata(pdev) | 드라이버 private 데이터 반환 |
platform_set_drvdata(pdev, data) | 드라이버 private 데이터 저장 |
사용 예제:
/* 인덱스 기반 접근 */
struct resource *mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
int irq = platform_get_irq(pdev, 0);
/* 이름 기반 접근 (DT에서 reg-names, interrupt-names 지정 시) */
struct resource *cfg = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config");
int tx_irq = platform_get_irq_byname(pdev, "tx");
int rx_irq = platform_get_irq_byname(pdev, "rx");
/* 리소스 획득 + ioremap 통합 (권장) */
void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
/* private 데이터 저장/조회 */
platform_set_drvdata(pdev, priv); /* probe에서 */
struct my_priv *p = platform_get_drvdata(pdev); /* remove 등에서 */
platform_get_resource()와 devm_ioremap_resource()를 별도로 호출하는 것보다 devm_platform_ioremap_resource() 하나로 통합하는 것이 권장됩니다. NULL 체크와 영역 요청, 매핑을 한 번에 처리하여 코드가 간결해지고 에러 처리 누락을 방지합니다.
편의 매크로
module_platform_driver()는 모듈 init/exit 보일러플레이트를 자동 생성합니다:
/* include/linux/platform_device.h */
#define module_platform_driver(__platform_driver) \
module_driver(__platform_driver, platform_driver_register, \
platform_driver_unregister)
/* 위 매크로는 아래와 동일한 코드를 생성 */
static int __init my_driver_init(void)
{
return platform_driver_register(&my_driver);
}
module_init(my_driver_init);
static void __exit my_driver_exit(void)
{
platform_driver_unregister(&my_driver);
}
module_exit(my_driver_exit);
관련 편의 매크로들:
| 매크로 | 용도 |
|---|---|
module_platform_driver(drv) | 모듈용. init/exit에서 register/unregister 자동 생성 |
builtin_platform_driver(drv) | 빌트인 전용. device_initcall로 자동 등록 |
builtin_platform_driver_probe(drv, probe) | 빌트인 전용. probe를 __init 섹션에 배치하여 메모리 절약 |
MODULE_DEVICE_TABLE(of, ids) | 모듈 자동 로딩용 alias 생성. depmod가 인식 |
MODULE_DEVICE_TABLE(acpi, ids) | ACPI 기반 모듈 자동 로딩용 alias 생성 |
MODULE_DEVICE_TABLE()은 modules.alias 파일에 항목을 추가하여, 디바이스 발견 시 udev가 해당 모듈을 자동 로드할 수 있게 합니다:
/* 드라이버 코드 */
static const struct of_device_id my_of_ids[] = {
{ .compatible = "vendor,my-ctrl" },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_ids);
/* → /lib/modules/.../modules.alias 에 추가됨:
* alias of:N*T*Cvendor,my-ctrlC* my_driver
* → DT에 "vendor,my-ctrl" 노드가 있으면 udev가 my_driver.ko 자동 로드
*/
sysfs 인터페이스
Platform Bus는 sysfs를 통해 사용자 공간에 노출됩니다:
수동 드라이버 바인딩/언바인딩:
# 디바이스를 현재 드라이버에서 분리
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)
#include <linux/dma-mapping.h>
/* Coherent DMA (CPU 캐시와 일관성 보장) */
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, buf, dma_handle);
/* Streaming DMA (방향 지정, 캐시 sync 필요) */
dma_addr_t dma = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
/* ... DMA 전송 ... */
dma_unmap_single(dev, dma, size, DMA_TO_DEVICE);
DMA 매핑 후에는 반드시 dma_mapping_error()로 오류를 확인해야 합니다. IOMMU가 활성화된 시스템에서는 매핑 실패가 가능합니다.
Linux 디바이스 모델 (Device Model)
Linux 디바이스 모델은 커널 2.6에서 도입된 통합 프레임워크로, 모든 디바이스와 드라이버를 bus → device → driver의 계층으로 추상화합니다. 이 모델은 kobject 인프라 위에 구축되며, sysfs(/sys/)를 통해 사용자 공간에 노출됩니다. udev/systemd-udevd가 디바이스 이벤트를 수신하고, 전원 관리(PM)와 핫플러그가 일관된 인터페이스로 동작합니다.
struct device — 모든 디바이스의 기반
struct device는 커널에 등록되는 모든 디바이스의 공통 기반 구조체입니다. PCI, USB, platform, I2C 등 각 버스별 디바이스 구조체(pci_dev, usb_device, platform_device 등)는 이 구조체를 임베디드합니다.
/* include/linux/device.h (핵심 필드 발췌) */
struct device {
struct kobject kobj; /* sysfs 디렉토리 노드 */
struct device *parent; /* 부모 디바이스 (물리적 계층) */
const struct device_type *type; /* 디바이스 타입 (선택적) */
struct bus_type *bus; /* 소속 버스 */
struct device_driver *driver; /* 바인딩된 드라이버 (NULL이면 미바인딩) */
void *platform_data; /* 보드 레벨 데이터 (레거시) */
void *driver_data; /* 드라이버 private 데이터 */
struct device_node *of_node; /* Device Tree 노드 */
struct fwnode_handle *fwnode; /* 통합 FW 노드 (DT/ACPI) */
struct class *class; /* 디바이스 클래스 (net, block 등) */
dev_t devt; /* /dev 노드 번호 (major:minor) */
const struct dev_pm_ops *pm; /* 전원 관리 콜백 */
struct dev_pm_info power; /* PM 런타임 상태 */
struct dma_map_ops *dma_ops; /* DMA 매핑 연산 */
u64 *dma_mask; /* DMA 주소 마스크 */
u64 coherent_dma_mask;
struct list_head devres_head; /* devm 리소스 리스트 */
};
주요 도우미 함수:
/* 디바이스 등록/해제 */
int device_register(struct device *dev); /* device_initialize + device_add */
void device_unregister(struct device *dev); /* device_del + put_device */
/* 참조 카운팅 */
struct device *get_device(struct device *dev); /* kobject_get 래퍼 */
void put_device(struct device *dev); /* kobject_put 래퍼 */
/* driver_data 접근 */
void *dev_get_drvdata(const struct device *dev);
void dev_set_drvdata(struct device *dev, void *data);
/* 로깅 (dev_name 자동 포함) */
dev_err(&dev, "error: %d\\n", ret);
dev_warn(&dev, "warning\\n");
dev_info(&dev, "initialized\\n");
dev_dbg(&dev, "debug message\\n");
dev_err_probe(&dev, ret, "deferred 시 dbg, 그 외 err\\n");
struct device_driver — 드라이버 공통 구조체
struct device_driver는 모든 드라이버의 공통 기반입니다. 버스별 드라이버 구조체(pci_driver, platform_driver 등)가 이를 임베디드합니다.
/* include/linux/device/driver.h */
struct device_driver {
const char *name; /* 드라이버 이름 */
const struct bus_type *bus; /* 소속 버스 */
struct module *owner; /* THIS_MODULE */
const struct of_device_id *of_match_table; /* DT compatible 매칭 */
const struct acpi_device_id *acpi_match_table; /* ACPI HID 매칭 */
int (*probe)(struct device *dev); /* 버스별 probe가 호출 */
void (*remove)(struct device *dev); /* 언바인딩 시 호출 */
void (*shutdown)(struct device *dev); /* reboot/poweroff 시 */
const struct dev_pm_ops *pm; /* PM 콜백 */
const struct attribute_group **dev_groups; /* 드라이버 sysfs 속성 */
bool suppress_bind_attrs; /* bind/unbind sysfs 파일 숨김 */
enum probe_type probe_type; /* PROBE_DEFAULT_STRATEGY / PROBE_FORCE_SYNCHRONOUS */
};
struct bus_type — 버스 추상화
struct bus_type은 PCI, USB, Platform, I2C 등 버스 유형을 추상화합니다. 각 버스는 자신만의 device-driver 매칭 규칙과 probe 메커니즘을 정의합니다.
/* include/linux/device/bus.h */
struct bus_type {
const char *name; /* 버스 이름 (sysfs: /sys/bus/NAME/) */
const char *dev_name; /* 디바이스 이름 생성 접두어 */
/* 핵심 콜백 */
int (*match)(struct device *dev, struct device_driver *drv);
int (*probe)(struct device *dev);
void (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
/* uevent 환경변수 추가 (udev용) */
int (*uevent)(const struct device *dev, struct kobj_uevent_env *env);
/* PM 콜백 */
const struct dev_pm_ops *pm;
/* 내부 데이터 */
struct subsys_private *p; /* klist_devices, klist_drivers, kset 등 */
};
/* 버스 등록/해제 */
int bus_register(const struct bus_type *bus);
void bus_unregister(const struct bus_type *bus);
커널에 등록된 주요 버스:
| bus_type | sysfs 경로 | match 방식 | 디바이스 열거 방식 |
|---|---|---|---|
platform_bus_type | /sys/bus/platform/ | DT compatible, ACPI HID, name | DT/ACPI/정적 등록 |
pci_bus_type | /sys/bus/pci/ | vendor:device:class ID | PCI 설정 공간 스캔 |
usb_bus_type | /sys/bus/usb/ | vendor:product:class ID | USB 열거 프로토콜 |
i2c_bus_type | /sys/bus/i2c/ | DT compatible, i2c_device_id | DT/보드 파일/ACPI |
spi_bus_type | /sys/bus/spi/ | DT compatible, spi_device_id | DT/보드 파일/ACPI |
virtio_bus | /sys/bus/virtio/ | device ID + feature bits | virtio 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);
Device-Driver 바인딩 흐름
디바이스와 드라이버가 연결(bind)되는 과정은 디바이스 모델의 핵심 메커니즘입니다:
/* drivers/base/dd.c — 바인딩 핵심 흐름 (간략화) */
static int really_probe(struct device *dev, struct device_driver *drv)
{
/* 1. dev->driver 설정 */
dev->driver = drv;
/* 2. 핀 설정 (pinctrl default state 적용) */
pinctrl_bind_pins(dev);
/* 3. DMA 설정 */
dma_configure(dev);
/* 4. probe 호출 — 버스 probe가 있으면 우선, 없으면 드라이버 probe */
if (dev->bus->probe)
ret = dev->bus->probe(dev); /* platform_drv_probe() 등 */
else if (drv->probe)
ret = drv->probe(dev);
/* 5. 결과 처리 */
if (ret == -EPROBE_DEFER) {
driver_deferred_probe_add(dev); /* 재시도 큐에 추가 */
} else if (ret == 0) {
driver_bound(dev); /* 바인딩 완료 */
kobject_uevent(&dev->kobj, KOBJ_BIND);
}
return ret;
}
Deferred Probing (지연 프로빙)
커널 부팅 시 디바이스 간 의존성 때문에 probe 순서가 중요합니다. 예를 들어 SPI 컨트롤러 드라이버가 아직 로드되지 않은 상태에서 SPI 디바이스의 probe가 먼저 호출될 수 있습니다. 이때 -EPROBE_DEFER를 반환하면 커널이 해당 디바이스를 지연 큐(deferred probe list)에 넣고, 다른 probe가 성공할 때마다 재시도합니다.
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
/* 의존하는 리소스가 아직 준비되지 않을 수 있음 */
priv->clk = devm_clk_get(dev, NULL);
if (IS_ERR(priv->clk)) {
/* -EPROBE_DEFER이면 자동으로 재시도 큐에 추가됨 */
return dev_err_probe(dev, PTR_ERR(priv->clk),
"failed to get clock\\n");
}
priv->regulator = devm_regulator_get(dev, "vdd");
if (IS_ERR(priv->regulator))
return dev_err_probe(dev, PTR_ERR(priv->regulator),
"failed to get regulator\\n");
/* ... */
}
/* dev_err_probe()의 동작:
* - PTR_ERR가 -EPROBE_DEFER이면 → dev_dbg 수준 로깅 (dmesg 조용히)
* - 그 외 에러이면 → dev_err 수준 로깅
* - 두 경우 모두 에러 코드를 그대로 반환
*/
# 지연 프로빙 상태 확인
$ cat /sys/kernel/debug/devices_deferred
platform soc:spi@40010000: -517 (EPROBE_DEFER)
platform soc:i2c@40020000: -517 (EPROBE_DEFER)
# 부팅 완료 후에도 deferred 상태인 디바이스는 의존성 미해결
# 흔한 원인: 누락된 드라이버 모듈, DT 설정 오류, 순환 의존
Deferred Probe 디버깅: /sys/kernel/debug/devices_deferred에서 대기 중인 디바이스 목록을 확인하세요. 커널 파라미터 driver_deferred_probe_timeout=30을 설정하면 지정된 초 이후 대기를 중단하고 경고를 출력합니다. fw_devlink=on은 많은 최신 메인라인 설정에서 기본 활성화되어 DT/ACPI 의존성을 자동 추적하지만, 실제 기본값은 커널/배포판 설정을 확인해야 합니다.
fwnode — 통합 펌웨어 노드
struct fwnode_handle은 Device Tree와 ACPI를 통합하는 추상화 계층입니다. 하나의 드라이버 코드로 DT 환경(ARM)과 ACPI 환경(x86 서버) 모두를 지원할 수 있습니다.
#include <linux/property.h>
/* fwnode 통합 API — DT/ACPI 구분 없이 프로퍼티 접근 */
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
u32 freq;
const char *label;
/* DT: device_property → of_property_read_u32 */
/* ACPI: device_property → acpi_dev_get_property */
if (device_property_read_u32(dev, "clock-frequency", &freq))
freq = 100000; /* 기본값 */
if (device_property_read_string(dev, "label", &label))
label = "default";
/* 불리언 프로퍼티 (존재 여부만 확인) */
bool wakeup = device_property_read_bool(dev, "wakeup-source");
/* 배열 프로퍼티 */
u32 regs[4];
device_property_read_u32_array(dev, "reg-offsets", regs, 4);
/* 자식 노드 순회 */
struct fwnode_handle *child;
device_for_each_child_node(dev, child) {
u32 addr;
fwnode_property_read_u32(child, "reg", &addr);
/* ... 자식 디바이스 설정 ... */
}
return 0;
}
fwnode 통합 API 주요 함수:
| 통합 API (fwnode/property) | DT 전용 (of_*) | ACPI 전용 |
|---|---|---|
device_property_read_u32() | of_property_read_u32() | acpi_dev_get_property() |
device_property_read_string() | of_property_read_string() | acpi_dev_get_property() |
device_property_read_bool() | of_property_read_bool() | - |
device_get_match_data() | of_device_get_match_data() | acpi_device_get_match_data() |
device_for_each_child_node() | for_each_child_of_node() | - |
새로운 드라이버를 작성할 때는 of_* / acpi_* 전용 API 대신 통합 device_property_* API를 사용하세요. DT와 ACPI를 동시에 지원하는 드라이버를 작성할 수 있으며, 소프트웨어 노드(software_node)를 통한 단위 테스트도 가능해집니다.
struct device_type — 디바이스 세분화
device_type은 같은 버스에 속하지만 유형이 다른 디바이스를 구분합니다. USB 버스에서 디바이스(usb_device)와 인터페이스(usb_interface)가 대표적 예입니다.
struct device_type {
const char *name; /* uevent DEVTYPE= 값 */
const struct attribute_group **groups; /* 추가 sysfs 속성 */
int (*uevent)(const struct device *, struct kobj_uevent_env *);
char *(*devnode)(const struct device *, umode_t *, kuid_t *, kgid_t *);
void (*release)(struct device *);
const struct dev_pm_ops *pm;
};
/* 예: 블록 디바이스에서 disk_type과 partition_type 구분 */
/* net_device에서 이더넷, Wi-Fi, 브리지 등 구분 */
/* udev 규칙에서 DEVTYPE으로 매칭:
* SUBSYSTEM=="net", DEVTYPE=="wlan", ... */
디바이스 모델 전체 계층 구조
커스텀 버스 구현 예제
드라이버 서브시스템 개발자를 위한 커스텀 버스 타입 구현 예제입니다:
/* 커스텀 버스: 간단한 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/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);
콜백 함수의 매개변수:
nb: 현재 호출 중인notifier_block포인터 (container_of로 상위 구조체 접근 가능)action: 이벤트 유형 (예:NETDEV_UP,NETDEV_DOWN)data: 이벤트별 추가 데이터 (예:struct net_device *)
Notifier Chain vs 대안: 직접 함수 호출은 두 서브시스템 간 강한 결합을 만들고, workqueue는 비동기 처리에 적합합니다. Notifier Chain은 동기적이면서 느슨한 결합이 필요할 때 — 즉, 여러 독립 서브시스템이 동일한 이벤트에 반응해야 하는 경우에 이상적입니다.
Notifier Chain 4가지 유형
커널은 사용 컨텍스트에 따라 4가지 유형의 Notifier Chain을 제공합니다. 각 유형은 잠금 메커니즘과 호출 가능한 컨텍스트가 다릅니다.
| 유형 | 잠금 방식 | 콜백에서 sleep | 호출 컨텍스트 | 주요 사용처 |
|---|---|---|---|---|
| Atomic | spinlock + RCU | 불가 | 인터럽트, atomic 컨텍스트 | panic, die, reboot |
| Blocking | rw_semaphore | 가능 | 프로세스 컨텍스트만 | netdevice, inetaddr, PM |
| Raw | 잠금 없음 | 컨텍스트에 따라 | 호출자가 직접 동기화 | 저수준 CPU hotplug |
| SRCU | SRCU (Sleepable RCU) | 가능 | 프로세스 컨텍스트 | PM notifier |
/* 각 유형의 헤드 구조체 (include/linux/notifier.h) */
struct atomic_notifier_head {
spinlock_t lock;
struct notifier_block __rcu *head;
};
struct blocking_notifier_head {
struct rw_semaphore rwsem;
struct notifier_block __rcu *head;
};
struct raw_notifier_head {
struct notifier_block __rcu *head;
};
struct srcu_notifier_head {
struct mutex mutex;
struct srcu_struct srcu;
struct notifier_block __rcu *head;
};
각 유형별 초기화 매크로:
/* 정적 초기화 매크로 */
ATOMIC_NOTIFIER_HEAD(my_atomic_chain);
BLOCKING_NOTIFIER_HEAD(my_blocking_chain);
RAW_NOTIFIER_HEAD(my_raw_chain);
/* SRCU는 동적 초기화 필요 */
struct srcu_notifier_head my_srcu_chain;
srcu_init_notifier_head(&my_srcu_chain);
/* 동적 초기화 함수 (atomic/blocking/raw에도 사용 가능) */
ATOMIC_INIT_NOTIFIER_HEAD(&my_atomic_chain);
BLOCKING_INIT_NOTIFIER_HEAD(&my_blocking_chain);
RAW_INIT_NOTIFIER_HEAD(&my_raw_chain);
유형 선택 기준: 인터럽트/NMI 컨텍스트에서 호출되면 Atomic, 콜백에서 sleep이 필요하면 Blocking 또는 SRCU를 사용하세요. Raw는 호출자가 동기화를 직접 관리해야 하므로 일반적으로 권장하지 않습니다. SRCU는 Blocking과 유사하지만 read-side가 SRCU 보호를 받아 콜백 해제가 더 안전합니다.
Notifier Chain API 상세
모든 유형은 동일한 패턴의 API를 따릅니다. {type}_notifier_chain_register(), {type}_notifier_chain_unregister(), {type}_notifier_call_chain()으로 구성됩니다.
등록 / 해제
/* Atomic Notifier Chain */
int atomic_notifier_chain_register(struct atomic_notifier_head *nh,
struct notifier_block *nb);
int atomic_notifier_chain_unregister(struct atomic_notifier_head *nh,
struct notifier_block *nb);
/* Blocking Notifier Chain */
int blocking_notifier_chain_register(struct blocking_notifier_head *nh,
struct notifier_block *nb);
int blocking_notifier_chain_unregister(struct blocking_notifier_head *nh,
struct notifier_block *nb);
/* Raw Notifier Chain */
int raw_notifier_chain_register(struct raw_notifier_head *nh,
struct notifier_block *nb);
int raw_notifier_chain_unregister(struct raw_notifier_head *nh,
struct notifier_block *nb);
/* SRCU Notifier Chain */
int srcu_notifier_chain_register(struct srcu_notifier_head *nh,
struct notifier_block *nb);
int srcu_notifier_chain_unregister(struct srcu_notifier_head *nh,
struct notifier_block *nb);
모든 등록 함수는 성공 시 0을 반환합니다. 등록 시 notifier_block은 우선순위 내림차순으로 연결 리스트에 삽입됩니다 (높은 priority가 먼저 호출됨).
통지 호출
/* 체인의 모든 콜백을 호출 */
int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
unsigned long val, void *v);
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
unsigned long val, void *v);
int raw_notifier_call_chain(struct raw_notifier_head *nh,
unsigned long val, void *v);
int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
unsigned long val, void *v);
콜백 반환값
콜백 함수는 다음 상수 중 하나를 반환해야 합니다:
| 반환값 | 값 | 의미 |
|---|---|---|
NOTIFY_DONE |
0x0000 | 이벤트에 관심 없음, 다음 콜백 계속 |
NOTIFY_OK |
0x0001 | 정상 처리됨, 다음 콜백 계속 |
NOTIFY_STOP_MASK |
0x8000 | 체인 순회 중단 플래그 |
NOTIFY_BAD |
NOTIFY_STOP_MASK | 0x0002 | 에러 발생, 체인 즉시 중단 |
NOTIFY_STOP |
NOTIFY_OK | NOTIFY_STOP_MASK | 정상이지만 체인 중단 (나머지 콜백 건너뜀) |
/* call_chain()의 반환값에서 errno 추출 */
static inline int notifier_to_errno(int ret)
{
ret &= ~NOTIFY_STOP_MASK;
return ret > NOTIFY_OK ? ret - 1 : 0;
}
/* errno를 notifier 반환값으로 변환 */
static inline int notifier_from_errno(int err)
{
if (err)
return NOTIFY_STOP_MASK | (NOTIFY_OK - err);
return NOTIFY_OK;
}
NOTIFY_BAD의 용도: NOTIFY_BAD를 반환하면 이벤트 발생 주체에게 "거부"를 알릴 수 있습니다. 예를 들어, PM notifier에서 NOTIFY_BAD를 반환하면 suspend가 취소됩니다. call_chain() 호출자는 notifier_to_errno()로 반환값을 검사합니다.
커널 주요 Notifier Chain 목록
다음은 커널에서 가장 자주 사용되는 주요 Notifier Chain입니다:
| Notifier Chain | 유형 | 헤더/소스 | 주요 이벤트 | 등록 API |
|---|---|---|---|---|
| reboot_notifier_list | Blocking | kernel/reboot.c |
SYS_RESTART, SYS_HALT, SYS_POWER_OFF | register_reboot_notifier() |
| panic_notifier_list | Atomic | kernel/panic.c |
커널 패닉 발생 | atomic_notifier_chain_register() |
| netdev_chain | Raw | net/core/dev.c |
NETDEV_UP, NETDEV_DOWN, NETDEV_CHANGE, ... | register_netdevice_notifier() |
| inetaddr_chain | Blocking | net/ipv4/devinet.c |
NETDEV_UP, NETDEV_DOWN (IPv4 주소 변경) | register_inetaddr_notifier() |
| inet6addr_chain | Blocking | net/ipv6/addrconf.c |
NETDEV_UP, NETDEV_DOWN (IPv6 주소 변경) | register_inet6addr_notifier() |
| pm_chain_head | Blocking | kernel/power/main.c |
PM_SUSPEND_PREPARE, PM_POST_SUSPEND, ... | register_pm_notifier() |
| module_notify_list | Blocking | kernel/module/main.c |
MODULE_STATE_LIVE, MODULE_STATE_COMING, ... | register_module_notifier() |
| keyboard_notifier_list | Atomic | drivers/tty/vt/keyboard.c |
KBD_KEYCODE, KBD_KEYSYM, ... | register_keyboard_notifier() |
| die_chain | Atomic | arch/*/kernel/traps.c |
DIE_OOPS, DIE_GPF, DIE_TRAP, ... | register_die_notifier() |
| fb_notifier_list | Blocking | drivers/video/fbdev/core/fbmem.c |
FB_EVENT_MODE_CHANGE, FB_EVENT_BLANK, ... | fb_register_client() |
Notifier Chain 동작 흐름
실전 구현 예제
예제 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,
};
| 운영 주제 | 드라이버 핵심 포인트 | 확인 명령/지표 |
|---|---|---|
| 멀티큐/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 카운터 |
| 가상 netdev | TUN/TAP/veth/virtio-net의 공통 netdev 계약 이해 | TUN/TAP, 상세 문서 |
앞서 살펴본 드라이버 등록 방식은 x86/ACPI 환경 기준입니다. ARM, RISC-V 등 임베디드 플랫폼에서는 PCI/USB처럼 자동 열거가 불가능한 SoC 디바이스가 대부분이므로, Device Tree가 하드웨어 기술의 핵심 메커니즘입니다. 다음 섹션에서 Device Tree의 구조와 커널 연동을 상세히 다룹니다.
Device Tree (DTS/DTB)
Managed Device Resources (devm)
devm_* API는 디바이스 해제 시 리소스를 자동으로 정리합니다. 에러 경로에서의 수동 해제가 불필요합니다.
| 일반 API | Managed API | 리소스 |
|---|---|---|
kmalloc() | devm_kmalloc() | 메모리 |
ioremap() | devm_ioremap() | MMIO 매핑 |
request_irq() | devm_request_irq() | 인터럽트 |
clk_get() | devm_clk_get() | 클럭 |
regulator_get() | devm_regulator_get() | 레귤레이터 |
gpio_request() | devm_gpio_request() | GPIO |
/* devm 사용 예: 에러 시 자동 해제, goto 불필요 */
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
void *data = devm_kzalloc(dev, 4096, GFP_KERNEL);
if (!data) return -ENOMEM;
void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base)) return PTR_ERR(base);
int irq = platform_get_irq(pdev, 0);
devm_request_irq(dev, irq, my_handler, 0, "mydev", data);
/* remove 시 모든 리소스 자동 해제 */
return 0;
}
전원 관리 (PM)
static int my_suspend(struct device *dev)
{
/* 하드웨어 상태 저장, 클럭 비활성화 */
clk_disable_unprepare(priv->clk);
return 0;
}
static int my_resume(struct device *dev)
{
/* 하드웨어 상태 복원 */
clk_prepare_enable(priv->clk);
reinit_hardware(priv);
return 0;
}
static DEFINE_SIMPLE_DEV_PM_OPS(my_pm_ops, my_suspend, my_resume);
static struct platform_driver my_driver = {
.driver = {
.name = "my-device",
.pm = pm_sleep_ptr(&my_pm_ops),
},
};
DMA 심화 — 주의사항과 고려사항
DMA 매핑 API 유형과 선택 기준
| API | 수명 | 캐시 일관성 | 사용 시나리오 |
|---|---|---|---|
dma_alloc_coherent() |
장기 (드라이버 수명) | H/W 보장 (uncached/write-combined) | 디스크립터 링, 커맨드 큐, 공유 상태 |
dma_map_single() |
단기 (한 번의 전송) | S/W sync 필요 | 패킷 버퍼, 단일 블록 I/O |
dma_map_sg() |
단기 | S/W sync 필요 | Scatter-Gather I/O, 대용량 전송 |
dma_map_page() |
단기 | S/W sync 필요 | highmem 페이지의 DMA 전송 |
dma_alloc_noncoherent() |
장기 | S/W sync 필요 | 대용량 버퍼 (coherent의 성능 오버헤드 회피) |
dma_pool_create() |
장기 (풀 관리) | H/W 보장 | 소규모 coherent 버퍼를 빈번하게 할당/해제 |
DMA 방향(Direction)과 캐시 동기화
/* DMA 방향 상수 — 반드시 정확히 지정해야 함 */
DMA_TO_DEVICE /* CPU → Device: CPU 캐시 → 메모리 flush */
DMA_FROM_DEVICE /* Device → CPU: 캐시 invalidate */
DMA_BIDIRECTIONAL /* 양방향: flush + invalidate (비용 큼) */
/* Streaming DMA의 올바른 사용 패턴 */
dma_addr_t dma = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma)) {
dev_err(dev, "DMA mapping failed\\n");
return -ENOMEM;
}
/* DMA 전송 시작 (H/W에 dma 주소 전달) */
writel(dma, hw_base + DMA_ADDR_REG);
writel(len, hw_base + DMA_LEN_REG);
writel(DMA_START, hw_base + DMA_CTRL_REG);
/* 전송 완료 후 — CPU가 데이터를 읽기 전에 반드시 sync */
dma_sync_single_for_cpu(dev, dma, len, DMA_FROM_DEVICE);
/* 이제 CPU에서 buf 데이터를 안전하게 읽을 수 있음 */
process_data(buf, len);
/* 다시 디바이스에게 버퍼를 넘기려면 */
dma_sync_single_for_device(dev, dma, len, DMA_FROM_DEVICE);
/* 최종 해제 */
dma_unmap_single(dev, dma, len, DMA_FROM_DEVICE);
DMA_TO_DEVICE로 매핑한 버퍼에서 디바이스가 쓴 데이터를 CPU가 읽으면
stale cache 데이터가 반환될 수 있습니다. 캐시가 invalidate되지 않았기 때문입니다.
이런 버그는 간헐적으로 발생하여 디버깅이 매우 어렵습니다.
DMA_BIDIRECTIONAL은 안전하지만 캐시 연산이 두 배이므로 성능 저하가 있습니다.
IOMMU와 DMA 주소 공간
/* IOMMU 존재 시 DMA 주소 변환 흐름:
*
* CPU Virtual Addr → (MMU) → Physical Addr
* DMA/Bus Addr → (IOMMU) → Physical Addr
*
* IOMMU가 없으면: dma_addr_t == phys_addr_t (1:1 매핑)
* IOMMU가 있으면: dma_addr_t는 IOMMU가 매핑한 I/O 가상 주소
*/
/* DMA 주소 마스크 설정 — 디바이스의 주소 지정 능력 선언 */
int ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
/* 64비트 실패 시 32비트로 fallback */
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(dev, "No suitable DMA available\\n");
return ret;
}
}
| IOMMU 구현 | 플랫폼 | 커널 드라이버 | 주요 기능 |
|---|---|---|---|
| Intel VT-d | x86 (Intel) | drivers/iommu/intel/ |
DMA remapping, interrupt remapping, ATS |
| AMD-Vi | x86 (AMD) | drivers/iommu/amd/ |
DMA remapping, interrupt remapping, v2 page table |
| ARM SMMU | ARM/ARM64 | drivers/iommu/arm/arm-smmu-v3/ |
Stage-1/2 변환, PCIe ATS, HTTU |
| SWIOTLB | 모든 플랫폼 | kernel/dma/swiotlb.c |
소프트웨어 bounce buffer (IOMMU 없을 때 fallback) |
DMA 프로그래밍 핵심 주의사항
- 매핑 후 에러 체크 —
dma_mapping_error()를 항상 호출. IOMMU 공간 소진 시 실패 가능 - bounce buffer 인지 — SWIOTLB 사용 시 실제 복사가 발생하여 성능 저하.
dma_set_mask()로 64비트 지원 확인 - DMA 주소 수명 — map과 unmap 사이에서만 유효. unmap 후 DMA 주소 사용 금지
- 캐시 라인 공유 금지 — DMA 버퍼가 다른 데이터와 같은 캐시 라인을 공유하면 false sharing 발생.
____cacheline_aligned사용 - Scatter-Gather 병합 — IOMMU는 물리적으로 불연속인 페이지를 DMA 주소 공간에서 연속으로 매핑 가능.
dma_map_sg()후sg_dma_len()이 원래 세그먼트와 다를 수 있음 - DMA coherent 메모리의 성능 — ARM 등 non-x86에서 coherent 메모리는 uncached로 할당되어 CPU 접근이 느림. 대용량 데이터는 streaming DMA 선호
- 64비트 DMA 주소 — 레거시 디바이스는 32비트 DMA만 지원. 4GB 이상 메모리 시스템에서 SWIOTLB bounce buffer 사용됨
DMA 디버깅
# DMA 디버그 활성화 (커널 부트 파라미터)
dma_debug=on
# 또는 CONFIG_DMA_API_DEBUG=y 로 빌드
# DMA 디버그 통계 확인
cat /sys/kernel/debug/dma-api/error_count
cat /sys/kernel/debug/dma-api/all_errors
cat /sys/kernel/debug/dma-api/num_errors
# 일반적으로 검출되는 DMA 오류:
# - DMA-API: device driver frees DMA memory with wrong function
# - DMA-API: device driver maps memory from kernel text
# - DMA-API: device driver tries to sync DMA memory it has not allocated
디바이스 드라이버 주요 버그 패턴
디바이스 드라이버는 커널 코드의 약 70%를 차지하며, 커널 취약점의 가장 큰 원인입니다. 하드웨어와의 상호작용, 비동기 이벤트 처리, DMA 메모리 관리 등에서 반복적으로 발생하는 버그 패턴을 분석합니다.
DMA 매핑 방향 오류
DMA 매핑 시 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" 메시지 출력 */
인터럽트 핸들러 경쟁 조건
인터럽트 핸들러에서 접근하는 공유 데이터를 프로세스 컨텍스트에서도 접근할 때, 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_* 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 핫플러그 경쟁 조건
USB나 PCI 핫플러그 장치에서 사용자가 장치를 물리적으로 제거하는 동안 드라이버가 여전히 장치에 접근하면, 잘못된 메모리 접근이나 커널 패닉이 발생합니다. disconnect 콜백에서 진행 중인 I/O를 모두 취소하고, 이후의 I/O 요청을 거부해야 합니다.
/* USB disconnect race 방어 패턴 */
struct my_usb_dev {
struct usb_device *udev;
struct mutex io_mutex;
bool disconnected; /* disconnect 발생 여부 */
struct kref kref; /* 참조 카운트 */
};
/* I/O 수행 전 disconnect 여부 확인 */
static ssize_t my_write(struct file *file, ...) {
mutex_lock(&dev->io_mutex);
if (dev->disconnected) {
mutex_unlock(&dev->io_mutex);
return -ENODEV; /* 이미 제거됨 */
}
/* 안전하게 I/O 수행 */
retval = usb_bulk_msg(dev->udev, ...);
mutex_unlock(&dev->io_mutex);
return retval;
}
/* disconnect 콜백 */
static void my_disconnect(struct usb_interface *intf) {
struct my_usb_dev *dev = usb_get_intfdata(intf);
mutex_lock(&dev->io_mutex);
dev->disconnected = true; /* 이후 I/O 거부 */
mutex_unlock(&dev->io_mutex);
usb_kill_anchored_urbs(&dev->submitted); /* 진행 중인 URB 취소 */
kref_put(&dev->kref, my_delete);
}
CONFIG_DMA_API_DEBUG: DMA 매핑/해제 쌍, 방향 일치 여부 검증
CONFIG_LOCKDEP: IRQ/프로세스 컨텍스트 간 잠금 순서 위반 탐지
CONFIG_KASAN: Use-After-Free, 버퍼 오버플로우 런타임 탐지
CONFIG_KFENCE: 프로덕션 환경용 저오버헤드 메모리 오류 샘플링
CONFIG_PROVE_LOCKING: 잠금 의존성 그래프 검증으로 데드락 사전 탐지
coccinelle (spatch): 커널 코드 정적 분석 도구, 일반적인 드라이버 버그 패턴 탐지
관련 문서
디바이스 드라이버와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.