
심화 실습이 한바탕 휩쓸고 지나간 11주차는 디바이스 드라이버에 대해 학습을 했습니다. 중요한 내용처럼 느껴졌지만 이해를 한 것은 ..zero!! 그러니 블로그를 작성하면서 복습을 한 번 해봅시다.!! 이 글만 읽어도 디바이스 드라이버 완전 정복 ? 이라는 말이 나오도록 한번 가보즈악 .🛫
우선 현재 사용하는 리눅스 커널의 드라이버 소스코드를 확인해봅시다.
https://mirrors.edge.kernel.org/pub/linux/kernel/v6.x/
해당 링크에서 라즈베리 파이 및 우분투에 설치되어 있는 커널 버전을 다운로드 받을 수 있습니다.
다운로드 이후에는 폴더 내에서 드라이버의 소스코드를 확인할 수 있습니다 .
| 디렉토리 | 설명 | 주요 역할 및 특징 |
|---|---|---|
arch/ | Architecture | CPU 아키텍처별 종속적인 코드 (x86, ARM, RISC-V 등). 부팅 시 하드웨어 초기화 코드가 포함됨. |
block/ | Block Layer | HDD, SSD와 같은 블록 장치 입출력 스케줄링 및 제어 로직. |
certs/ | Certificates | 커널 모듈 서명 및 보안 인증 관련 인증서와 키 처리. |
crypto/ | Cryptography | 암호화 알고리즘 (AES, SHA 등) API 제공. |
drivers/ | Device Drivers | 각종 하드웨어용 드라이버 (GPU, USB, Network 등). 커널 소스의 약 60% 이상을 차지함. |
fs/ | File Systems | 파일 시스템 구현체 (EXT4, VFS, NTFS, NFS 등). |
include/ | Header Files | 커널 전반에서 사용되는 공통 헤더 파일 (.h). |
init/ | Initialization | 커널 부팅 초기화 코드 (main.c 포함). start_kernel() 함수가 여기서 실행됨. |
ipc/ | Inter-Process Comm. | 프로세스 간 통신 (Semaphore, Message Queue, Shared Memory 등). |
kernel/ | Core Kernel | 커널의 핵심 로직 (스케줄러, 프로세스 관리, 시그널 처리 등). |
lib/ | Library routines | 커널에서 공통으로 사용하는 유틸리티 함수 (문자열 처리, 압축 등). |
mm/ | Memory Mgmt. | 메모리 관리 코드 (가상 메모리, 페이징, 할당/해제 알고리즘). |
net/ | Networking | 네트워크 프로토콜 스택 (TCP/IP, Bluetooth, IPv6 등). |
scripts/ | Scripts | 커널 빌드(Compile) 및 관리에 필요한 스크립트 도구들. |
tools/ | Tools | 커널 개발 및 테스트에 필요한 사용자 공간(User space) 도구. |
Documentation/ | Documentation | 커널 기능 및 개발 가이드 문서. |
/init 폴더 내에는 부팅 시 실행되는 main func를 확인 할 수 있습니다. Bootloader (U-Boot 등): 하드웨어를 최소한으로 깨우고 커널 이미지를 메모리에 로드
Architecture Setup (arch/ 폴더 내 코드):
start_kernel()의 역할
위의 하드웨어 종속적인 준비가 끝나면, 드디어 우리가 찾은 init/main.c에 있는 start_kernel()로 점프
이처럼 리눅스 커널의 코드를 확인했다면 본격적으로 리눅스 드라이버로 들어가봅시다 .
드라이버를 작성하기 위해서는 고려해야할 몇가지 사항들이 있습니다 .
드라이버의 실행 환경은 kernel space이다 .
-> 사용자 프로그램과 커널은 메모리 주소 체계가 완전히 분리되어 있다.
-> 전용 커널 함수를 사용한다 .
커널에서의 오류는 시스템에 치명적인 영향을 미칠 수 있으니 주의해서 사용 해야 합니다 .
효율적인 하드웨어 제어
하드웨어는 CPU보다 처리 속도가 느립니다. 따라서 드라이버가 디바이스의 응답을 어떻게 기다리느냐에 따라 시스템 전체의 성능이 결정됩니다.
| 구분 | In-tree (커널 트리 내부) | Out-of-tree (외부/독립) |
|---|---|---|
| 소스 위치 | 커널 소스 트리 내부 (drivers/ 아래에 폴더 생성) | 커널 소스와 완전히 별도의 디렉토리 |
| 대표 경로 | ~/linux-6.8/drivers/char/my_driver/ | ~/my_driver_project/ |
| 빌드 방식 | 커널 전체 빌드 시 같이 컴파일 | make -C /lib/modules/$(uname -r)/build M=$PWD |
| 장점 |
|
|
| 단점 |
|
|
| 주 사용 시기 | upstream 제출 예정, 안정화 단계 | 초기 개발, 프로토타이핑, 회사 내부 드라이버 |
장점
├─ 동적 로드/언로드 가능 (insmod / rmmod)
├─ 커널 재컴파일 & 재부팅 없이 교체 가능
└─ 메모리 효율적 (필요할 때만 메모리 사용)
단점
├─ 커널 API/심볼을 엄격히 따라야 함
├─ C언어로만 작성 가능 (Rust는 아직 제한적)
└─ 커널 오염(taint) 발생 (out-of-tree 모듈은 기본적으로 taint됨)
# 1. 빌드
make
# 2. 생성된 파일 확인
ls
# hello.ko hello.mod.c hello.o Module.symvers modules.order
# 3. 모듈 로드
sudo insmod hello.ko
# 4. 메시지 확인 (여러 방법)
dmesg | tail -n5
# 또는
sudo journalctl -k --since "5 minutes ago" | grep hello
# 5. 모듈 목록에서 확인
lsmod | grep hello
# 6. 모듈 제거
sudo rmmod hello
# 7. 제거 후 메시지 다시 확인
dmesg | tail
-> 이 때 문자 장치와 블록장치는 독립적으로 사용된다.
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; // 커널 객체 (내부 관리용)
struct module *owner; // 보통 THIS_MODULE 대입
const struct file_operations *ops; // 장치 호출 시 실행될 함수 모음 (read/write 등)
struct list_head list; // 커널 내 문자 장치 리스트 관리용
dev_t dev; // 메이저/마이너 번호 (dev_t 타입)
unsigned int count; // 할당받은 마이너 번호의 개수
};
장치 번호 할당 (dev_t)
메이저/마이너 번호를 먼저 예약합니다.
alloc_chrdev_region(&dev, 0, 1, "my_device"); (동적 할당)cdev 초기화
cdev 구조체와 file_operations(함수 포인터 모음)를 연결합니다.
cdev_init(&my_cdev, &my_fops);my_cdev.owner = THIS_MODULE;커널에 등록
준비된 장치를 커널 시스템에 활성화합니다.
cdev_add(&my_cdev, dev, 1); #make menuconfig : 메뉴 파일 kconfig를 불러들여 메뉴를 표시하는데 사용된다.

해당 이미지처럼 설정을 정할 수 있는 CMD 창으로 접근할 수 있습니다.
3가지 조건
1. <*> : built-in : vmlinux
2. <M> : module = > /lib/modules/.../drivers/iio/humidity/dht11.ko
3. < > : excluded
전체 프로세스
make menuconfig
<> or <*> or <M> --> .config
~~
ie) CONFIG_DHT11=m
Makefile
# cat drivers/iio/humidity/Makefile | grep dht11
obj-$(CONFIG_DHT11) += dht11.o
obj-m += xxx.o // ---> xxx.ko //<M>
obj-y += yyy.o // ---> vmlinux -> zImage //<*>
obj- +=zzz.o // no compile 사실 만들어 지지 않음 //<>
===== > 각 옵션에 따른 커널 모듈 진행 과정
이처럼 Kconfig 파일은 커널 소스트리 전반에 퍼져 있으며 특정 기능을 커널에 포함할지 말지를 결정하는 옵션들의 구조를 정의한 설계도 입니다.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
pr_info("Module loaded! Hello kernel world\n");
return 0;
}
static void __exit my_module_exit(void)
{
pr_info("Module unloaded. Goodbye~\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL"); // ← 절대 빠지면 안 되는 1순위
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Basic kernel module example");

이처럼 커널 메세지에는 printk를 사용합니다(이때 실수 연산은 불가능 )
Console에서 지정한 log level 보다 높게 설정해 원하는 메세지만 출력할 수 있습니다.
| 레벨 (숫자) | 매크로 이름 (String) | 의미 | 사용 예시 |
|---|---|---|---|
| 0 | KERN_EMERG | Emergency: 시스템을 사용할 수 없는 상태 | 시스템 충돌(Crash) 직전 |
| 1 | KERN_ALERT | Alert: 즉각적인 조치가 필요한 상태 | 하드웨어 데이터 손상 위험 |
| 2 | KERN_CRIT | Critical: 위험한 상태 | 심각한 하드웨어 에러 |
| 3 | KERN_ERR | Error: 에러 발생 | 드라이버 로드 실패, 장치 인식 불능 |
| 4 | KERN_WARNING | Warning: 주의 요망 | 비정상적이지만 동작은 가능한 경우 |
| 5 | KERN_NOTICE | Notice: 정상적이지만 중요한 상태 | 보안 관련 알림, 설정 변경 |
| 6 | KERN_INFO | Informational: 일반적인 정보 | [기본값] 모듈 로드 성공 메시지 |
| 7 | KERN_DEBUG | Debugging: 디버깅용 정보 | 개발 중 변수 값 확인용 |
콘솔 로그 지정
→ 현재 콘솔 로그 레벨 확인
cat /proc/sys/kernel/printk
3 4 1 3
맨 앞의 숫자 3보다 높아야 콘솔에 표시된다. (높다는 게 숫자가 아니라 level , 숫자로는 더 낮음)
echo 3 >> /proc/sys/kernel/printk실시간 확인 방법
dmesg -n 7 # 콘솔에 출력될 로그 레벨 범위를 확장 (7까지 출력)
dmesg -w # 실시간 모니터링
# modinfo 02_dev.ko
filename: /root/exercise_A05.251102/03/02_dev.ko
license: GPL
srcversion: F10301C449E9A0AC5976BC5
depends:
name: 02_dev
vermagic: 6.1.21-v8+ SMP preempt mod_unload modversions aarch64| 매크로 이름 | 의미 | 상세 설명 | 필수 여부 |
|---|---|---|---|
MODULE_LICENSE() | 라이선스 | 모듈의 배포 라이선스를 지정합니다. (예: "GPL", "GPL v2", "Proprietary") | 필수 |
MODULE_AUTHOR() | 작성자 | 모듈을 만든 사람의 이름과 이메일 주소를 기입합니다. | 권장 |
MODULE_DESCRIPTION() | 모듈 설명 | 이 모듈이 어떤 기능을 수행하는지 짧게 요약합니다. | 권장 |
MODULE_VERSION() | 버전 | 모듈의 버전 번호를 관리합니다. (예: "1.0", "v1.2-alpha") | 선택 |
MODULE_ALIAS() | 별칭 | 모듈의 별명을 지정하여 modprobe 시 다른 이름으로도 호출 가능하게 합니다. | 선택 |
드라이버 로딩 시 상황에 맞는 매개변수를 지정할 수 있다.
모듈 매개변수를 사용하여 가변 값을 지정할 수 있다.
# insmod dev.ko one=0x34 two="Hello!"
전달하기
변수 타입
⇒ 배열은 ,를 사용하여 구분
매개 변수 전달 example
// 04_dev.c
#include <linux/moduleparam.h>
#include <linux/stat.h>
.
.
/*TODO:
module parameter myintArray as int array.
myintArray, int
*/
module_param_array( myintArray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintArray, "An array of integers");
.
.
# insmod 04_dev.ko myint=4
root@rpi:~/exercise_A05.251102/03# dmesg
[609931.438013] myint is an integer: 4
[609931.438045] mystring is a string : blah
[609931.438058] myintArray[0] = 0
[609931.438069] myintArray[1] = 0
[609931.438080] got 0 arguments for myintArray.
[609931.438090] (mod)init 3가지 방법
1. shell에서 노드 생성
장치 파일은 user process와 device driver를 연결해주는 매개체 역할
sudo mknod [파일명] [타입] [Major] [Minor]
mknod() 시스템 콜 (User 레벨 호출) -> 반드시 root 권한이 필요 linux/device.h (커널 헤더)insmod)되자마자 커널이 알아서 /dev에 파일을 짠! 하고 나타나게 할 때 사용합니다.struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...);device_create를 호출함/dev 아래에 파일을 자동으로 생성함.응용 프로그램에서 드라이버로 전달되어지는 메커니즘
→ 드라이버의 서비스를 받기 위한 방법
To driver context
어떤 경로로 찾아왔나?
1. Top(application) --> driver
process context
=> system call 에 의해
2. Bottom( hardware) --> driver
interrupt context
=> interrupt 에 의해
// 1. 드라이버 내부 함수 구현
static int my_open(struct inode *inode, struct file *file) {
pr_info("장치가 열렸습니다!\n");
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) {
pr_info("장치로부터 데이터를 읽습니다.\n");
return 0; // 읽은 바이트 수 반환
}
static int my_release(struct inode *inode, struct file *file) {
pr_info("장치가 닫혔습니다!\n");
return 0;
}
// 2. 구조체 변수 선언 및 함수 매핑 (중요!)
static struct file_operations my_fops = {
.owner = THIS_MODULE, // 모듈 참조 카운트 관리용
.open = my_open, // 앱의 open() -> my_open() 실행
.read = my_read, // 앱의 read() -> my_read() 실행
.release = my_release, // 앱의 close() -> my_release() 실행
};
.open = my_open 처럼 쓰는 방식 ⇒ Designated Initializer
💡구조체 안의 멤버 순서가 바뀌어도 상관 없고 필요한 것만 골라서 초기화 가능
// To file operations
====================
int main(){
int fd = open("/dev/rpihat",O_WONLY);
int c = read(fd , buf,10);
return 0;
}
/*
open(const char* pathname , int flags)
path name : inode
flags -> flip
*/

단위로 할당
외부 단편화 💡: 총 여유 공간은 충분하지만 연속된 덩어리가 없어서 할당하지 못하는 상태
→ 크기가 다른 메모리를 반복적으로 할당하고 해제하다 보면 하나하나의 크기가 작아 프로세스가 들어갈 수 없게 된다.
이를 막기 위해 버디 할당자가 재구성
HOW ❔ : 병합 (Coalescing): 메모리를 해제할 때, 옆에 붙어 있는 같은 크기의 빈 블록(버디)이 있다면 하나로 합쳐서 더 큰 블록으로 만듭니다. 이를 통해 잘게 쪼개진 파편들을 다시 큰 덩어리로 복원
→ 더 단위가 작음
slab : 하나 이상의 연속된 페이지 프레임 더 작은 단위로 쪼갠 공간 → 이를 할당함
kmem-cache에 정의한 각 캐쉬 단위로 할당
# cat /proc/slabinfo | grep task_
task_struct 227 248 7872 4 8 : tunables 0 0 0 : slabdata 62 62 0
task_group 100 100 640 25 4 : tunables 0 0 0 : slabdata 4 4 0
이렇게 사이즈가 정해진 가판대를 만들어 놓고 장사
장점
kmalloc() : 물리적으로 연속된 공간의 메모리 할당
kfree()
kernel 버전 마다 upper limit가 다르나 slab.h에서 확인 가능
kmalloc option ⭐
kmalloc(size , flag)
- GFP_KERNEL : process context
app -> driver -> kmalloc --> out of memory? [blocked 됨 ]
- GFP_ATOMiC : interrupt context
device --> interrupt -> driver -> kmalloc --> out of memory? => wait(x)
Hardware는 기다려주지 않음 -> fail
둘 중 하나를 자주 쓴다
- GFP_KERNEL : 할당이 여의치 않으면 잠들 수 있다
그냥 할당만 , 초기화는 x
- GFP_ATOMIC : 메모리 할당이 여의치 않으면 빈 손으로 돌아감
- GFP_ZERO : 깨끗한 동적 메모리 할당
devm_kzalloc : 메모리 할당 후 자동 해제⭐ ⭐⭐⭐
vmalloc() , vfree() : 커다란 동적 메모리 할당
→ 물리적으로 비연속적인 공간의 메모리 할당
get_free_pages() , free_pages()
인터럽트란? 하드웨어(NVIC)에 연결된 신호
irq에서 controller가 우선 순위가 높은 것들을 먼저 처리한다 .

프로세스에서 인터럽트 발생 -> 인터럽트 벡터 테이블에서 벡터값 확인
인터럽트 서비스 루틴 호출
인터럽트 금지
프로세스상태 저장
인터럽트 처리
프로세서 상태 복구
인터럽트 허용
다시 프로세서로 복귀

GPIO Descriptor + Device Tree + devm_ API 를 활용한 스위치 인터럽트 예제
// ... (생략)
struct key_gpio_dev { // 구조체 선언
struct gpio_desc *key_gpio; // GPIO 디스크립터 (현대적인 GPIO 핸들)
int irq; // 해당 GPIO에 매핑된 IRQ 번호
};
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct key_gpio_dev *key_dev = dev_id;
int value = gpiod_get_value(key_dev->key_gpio); // ← 버튼 상태 읽기 (0 or 1)
pr_info("GPIO KEY: State changed, value = %d\n", value);
// 여기서 입력 이벤트 발생 시키거나, LED 켜기/끄기 등 추가 작업 가능
return IRQ_HANDLED;
}
static int key_gpio_probe(struct platform_device *pdev)
{
// 1. 메모리 할당 ===> 자동 해제
key_dev = devm_kzalloc(...);
// 2. Device Tree에서 GPIO 정보 가져오기 (가장 중요한 부분)
key_dev->key_gpio = devm_gpiod_get(&pdev->dev, NULL, GPIOD_IN);
// 내부적으로 Device Tree 파싱:
// 1. "gpios" 프로퍼티 읽기 → [phandle=7, line=22, flags=1]
// 2. phandle=7 → &gpio 컨트롤러 획득
// 3. line=22 → GPIO22 요청
// 4. flags=1 → ACTIVE_LOW 설정 (gpiod_get_value()=0이 눌린 상태)
// 3. GPIO → IRQ 번호 변환
key_dev->irq = gpiod_to_irq(key_dev->key_gpio);
// 4. 인터럽트 등록 (Falling edge = 버튼 누를 때)
devm_request_irq(&pdev->dev, key_dev->irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "gpio_key_irq", key_dev);
}
static const struct of_device_id key_gpio_of_match[] = {
{ .compatible = "rpi, key_K1 ", },
{}, // 테이블 종료를 나타내는 sentinel
}; // 디바이스 트리 노드를 나열하는 매칭 테이블
static struct platform_driver key_gpio_driver = {
.driver = {
.name = "gpio_key_driver", //드라이버 이름(for debugging)
.of_match_table = key_gpio_of_match, // 위에서 만든 매칭 테이블
},
.probe = key_gpio_probe, // 장치 발견 -> 호출 되는 함수
.remove = key_gpio_remove, // 장치 제거시 호출
//.shutdown , .suspend , .resume 등은 생략 가능
};
Device Tree Overlay code
/dts-v1/;
/plugin/;
compatible = "brcm,bcm2835";
fragment@0 {
target-path = "/";
__overlay__ {
key_K1: key_K1 {
compatible = "rpi,key_K1";
gpios = <&gpio 22 GPIO_ACTIVE_LOW>; // ← GPIO22, 눌렀을 때 Low
//physical 15 -> gpio22
};
key_K2: key_K2 {
compatible = "rpi,key_K2";
gpios = <&gpio 23 GPIO_ACTIVE_LOW>;
};
};
};
};
compatible = "rpi,key_K1"; → 이 문자열이 커널 드라이버의 of_match_table과 매칭gpio-keys.c (커널 기본 드라이버)gpios = <&gpio 22 GPIO_ACTIVE_LOW>; #insmod 01_dev.ko
# dtoverlay 01_dev.dtbo
# dmesg
[24591.222865] (dev)GPIO KEY: State changed, value = 0
[24591.352453] (dev)GPIO KEY: State changed, value = 1
=========> 스위치가 눌러졌을 때 값이 바뀌는 것을 확인할 수 있다
# rmmod 01_dev
root@rpi:~/exercise_A05.251102/22# demesg -c
bash: demesg: command not found
root@rpi:~/exercise_A05.251102/22# dmesg -c
[27090.580446] gpio_key_driver key_K1: GPIO KEY driver removed
root@rpi:~/exercise_A05.251102/22# dtoverlay -l
Overlays (in load order):
0: 01_dev
======> 드라이버를 없애도 dtoverlay는 남아있음
# dtoverlay -r 01_dev
root@rpi:~/exercise_A05.251102/22# dtoverlay -l
No overlays loaded
===> 오버레이 삭제 확인 일찍 오니 get 한 아몬드 모찌 단팥빵 아 슈웃~~~
쫀득한 팥빵 처음 느껴보는 교육장에서의 이 식감 한번 더 나와다오 ..
옆에는 800여 페이지의 라면 받침대 .. 내용은 알차 보이지만 아직 한번도 펼쳐본적 없는 무자비한 녀석 ( 언제 공부 하냐. . )
2026 내 손으로 한 살 더 먹기 .. 조금 푸짐하게 한살 먹기 .
얘는 2026에도 여전하구나 나의 애착인형 귭해쿤 ...... 제발 프로젝트만은 피하게다오 ... ^^ JOKE