드라이버 작성을 위한 커널 모듈 탑재 그리고 각 장치를 어떻게 리눅스에서 인식하고 사용하는지를 알아보았습니다 .
12주차에는 드라이버의 커널에서 인터럽트를 어떻게 처리하는지와 여러 가지 장치에서의 드라이버 구현 방법에 대해서 알아보았습니다.
그렇다면. 복습도 할 겸 기록을 시작해보도록 하겠습니다!!! 😍
인터럽트의 처리와 금지는 ISR에서 이루어집니다 .
request_irq() 에서 플래그를 통해 CPU 전체 및 일부에서 인터럽트를 허용하거나 금지할 수 있습니다.
인터럽트의 처리 방식에는 두가지가 있습니다 .
"deferred interrupt handling"
리눅스 커널에서 인터럽트를 효율적으로 처리하기 위해서 인터럽트 처리 로직을
즉시 실행해야하는 짧은 코드와 나중에 실행해도 되는 무거운 코드로 구분 | 구분 | Softirq | Tasklet | Workqueue |
|---|---|---|---|
| 실행 컨텍스트 | 인터럽트 컨텍스트 | 인터럽트 컨텍스트 | 프로세스 컨텍스트 |
| 병렬 처리 | 여러 CPU에서 동시 실행 가능 | 동일 Tasklet은 한 CPU만 실행 | 커널 스레드에 의해 병렬 실행 |
| Sleep(잠자기) | 불가능 | 불가능 | 가능 (Blocking OK) |
| 용도 | 네트워크 블록 디바이스 (고성능) | 일반적인 드라이버 작업 | 입출력 등 시간이 긴 작업 |
| 특징 | - 정적 생성 -성능이 우수 -커널 소스 수정 요구 | 동적 생성 | 우리가 필요한 BH는 프로세스적인 특징들이 필요하다 |
리눅스 커널의 모든 디바이스 드라이버가 하나의 인터럽트 서비스 루틴 내에서 작업을 둘로 쪼개어 진행합니다. 그러면 예시를 통해 Top Half와 Bottom Half를 이해해봅시다 !
NIC(네트워크 인터페이스 카드 )
- 커널 스레드를 생성하여 BH를 구현한다 .
- 프로세스 컨텍스트의 특징을 모두 닺는다
- 선점 가능
- 인터럽트 허용
- 많은 메모리 할당 , 세마포어 획득 , 블로킹 I/O
그렇다면 Workqueue를 사용하여 커널 서비스에서 인터럽트를 어떻게 처리하는지 예제를 보며 확인해보도록 하겠습니다 .
리눅스에는 KWorker라는 일꾼들이 존재합니다 .
이는 지연 처리를 담당하는 일꾼 커널 스레드들로 워크큐에 등록된 task를 하나씩 꺼내어 비동기적으로 처리하는 역할을 담당합니다. 이를 통해 인터럽트 핸들러나 기타 주요 커널 스레드가 오랜 시간 블록되는 것을 방지하고 시스템 응답성을 높일 수 있습니다.
이를 구현하기 위해서는 Global, Custom 의 두가지 워크큐를 활용할 수 있습니다 .
linux/workqueue.hWQ_UNBOUND : 특정 CPU에 종속되지 않고, 스케줄러가 비어 있는 CPU를 찾아 유연하게 할당WQ_FREEABLE : 시스템이 Suspend(절전 모드) 상태로 진입할 때, 해당 큐에 쌓인 작업들을 일시 중단(Freeze)WQ_HIGPRI : 높은 우선순위를 부여합니다. 일반 워크큐보다 먼저 실행되어야 하는 긴급한 지연 처리WQ_MEM_RECLAIM : 메모리 할당이 부족한 상황(OOM 등)에서도 워커 스레드가 반드시 실행되도록 보장 → 메모리 해제 필수Sample
#include <linux/workqueue.h>
// 1. 실행될 핸들러 함수 정의 (함수명이 매크로보다 먼저 정의되거나 선언되어야 함)
void my_work_handler(struct work_struct *work) {
// Bottom Half 로직 수행
pr_info("Custom Workqueue is processing...\n");
}
// 2. DECLARE_WORK 매크로로 선언 및 초기화 (동시에 수행)
// 구조체 변수명(my_work)과 실행할 핸들러(my_work_handler)를 인자로 전달합니다.
DECLARE_WORK(my_work, my_work_handler);
// 3. 워크큐 포인터는 그대로 유지 (워크큐 생성은 런타임에 해야 함)
struct workqueue_struct *my_wq;
/* --- 모듈 초기화 함수 내에서 --- */
int __init my_module_init(void) {
// 워크큐 생성
my_wq = alloc_workqueue("my_custom_queue", WQ_HIGPRI | WQ_UNBOUND, 0);
if (!my_wq)
return -ENOMEM;
return 0;
}

해당 이미지는 workqueue를 이용한 라즈베리 파이에서 인터럽트 처리를 도식화 한 내용입니다 .
isr_func()schedule_work() 호출schedule_work()
인터럽트 → workqueue로 전달
• work_struct(&gdetect)를 시스템 workqueue에 등록
• 나중에 여유 있을 때 실행 , 예약 매우 빠름
dev_callback()
• 실제 "느린 작업" 수행 가능
• [process context]
request_threaded_irq() 함수 사용ret = devm_request_threaded_irq(&pdev->dev,
key_dev->irq, // irq no.
isr_func, // top-half
isr_func_bh, // bot-half
IRQF_TRIGGER_FALLING, // falling trigger
"gpio_key_irq",
key_dev);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
platform_set_drvdata(pdev, key_dev);
dev_info(&pdev->dev, "GPIO KEY driver probed\n");
-> "이 인터럽트 처리가 너무 길어서 아예 전용 스레드를 하나 만들어서 처리하겠다"는 인터럽트 중심의 사고방식으로 복잡한 I2C/SPI 통신이 필요한 센서 인터럽트에 주로 사용됩니다.
리눅스 커널에서는 유저(User)와 커널(Kernel)의 메모리 공간이 분리되어 있으며 포인터를 직접 전달해 읽고 쓸 수 없고, 전용 함수를 사용해야 합니다.
access_ok : 사용자 메모리 공간의 유효성 검사copy_to_user : 커널 메모리 블록의 데이터를 사용자 메모리 블록 데이터에 쓰기copy_from_user : 시용자 메모리 블록 데이터를 커널 메모리 블록에 쓰기get_user: 사용자 공간의 데이터 읽기 . 사이즈는 변수의 바이트 수 만큼(자동)put_user(x,ptr) : 커널 변수의 값을 사용자 공간에 쓰기ARM 아키텍처(라즈베리 파이 등)는 Memory Mapped I/O 방식을 사용합니다. 장치의 레지스터(SFR)가 일반 시스템 RAM처럼 주소 공간에 배치되어 있어, 특정 주소에 값을 쓰고 읽음으로써 하드웨어를 제어하고 통신할 수 있습니다.
이 때 하드웨어의 실제 주소(Physical Address)에 접근하려면, 포인터로 접근기 보다는 프로세스가 이해할 수 있는 가상 주소(Virtual Address)로 매핑해야 합니다 .
ioremap)디바이스 드라이버 내부에서 하드웨어 레지스터에 접근할 때 사용합니다.
// 예시: 라즈베리 파이 4 GPIO 베이스 주소 매핑
#define GPIO_BASE_PHYS 0xFE200000
#define GPIO_SIZE 0x100
static void __iomem *gpio_base;
gpio_base = ioremap(GPIO_BASE_PHYS, GPIO_SIZE); // 물리 주소를 커널 가상 주소로 매핑
set_reg = gpio_base + GPIO_CLR_OFFSET; // LED turn ONN
writel(1 << gpio, set_reg); ====> 이처럼 base 주소에 값을 writel를 통해 제어가 가능하다.
// 매핑 해제
iounmap(gpio_base);
mmap)/dev/mem 활용: 유저 공간에서 커널 도움 없이(직접적으로는 아니지만) 물리 주소에 접근할 수 있게 해주는 특수 파일입니다.
작동 원리: mmap() 호출 시 커널은 vm_area_struct를 통해 가상 메모리 영역(VMA)을 할당하고, 물리 주소와 연결합니다.
/* GPIO와 mmap */
gpio_map = mmap(NULL, /* 0 -> 커널이 알아서 할당해서 반환*/
GPIO_SIZE, //매핑 영역의 크기 지정 page_size 단위
PROT_READ | PROT_WRITE,
MAP_SHARED,
mem_fd,
GPIO_BASE);
if (gpio_map == MAP_FAILED) {
printf("[Error] mmap() : %d\n", (int)gpio_map);
perror -1;
}
gpio = (volatile unsigned *)gpio_map; /* 메모리 맵에 대한 포인터 */
GPIO_OUT(gno); /* 해당 GPIO 핀을 출력으로 설정 */
GPIO_SET(gno); /* 해당 GPIO 핀에 값 설정 */
munmap(gpio_map, GPIO_SIZE); /* 앞에서 mmap 부분 해제 */

하드웨어 통신 예제
SSD1306 LCD 제어
I2C통신을 활용하여 간단한 라즈베리 파이 모니터 출력 예제를 진행했습니다.
우선 SSD1306의 내부 동작 원리는 해당 블로그에 기록해놓았으니 여기서는 실습 과정 위주로 기록하겠습니다.
$ tail -c +63 ./<image_name>.bmp > image.bin
#헤더 파일을 제외하고 실제 픽셀 값만 추출하기 위해서 사용
$ cat <src_code>.c
.
.
.
unsigned char reverse_bits(unsigned char byte) {
//바이트를 받아서 endian 변환 후 반환
byte = ((byte & 0xF0) >> 4) | ((byte & 0x0F) << 4);
byte = ((byte & 0xCC) >> 2) | ((byte & 0x33) << 2);
byte = ((byte & 0xAA) >> 1) | ((byte & 0x55) << 1);
return byte;
}
① 비트 순서(MSB vs LSB)의 차이
② 픽셀 데이터의 상하 반전
Byte Swapping & Bit ReversalSample code
printf("mapped addres : %p\n", ptrdata );
// seek data from mono bmp file
lseek(img, BMP_MONO_DATA_OFFSET, SEEK_SET ); // from offset 62, pixel data begins...
read (img, buf, SSD1306_PIX_BYTECOUNT );
for(i=0;i<SSD1306_PIX_BYTECOUNT ; i++)
buf[i] = change_endian( buf[i] ); // .bmp file has different byte order with SSD1306 so flip them all.
write(dev, buf , SSD1306_PIX_BYTECOUNT );
munmap(ptrdata, MMAP_SIZE);
close(dev);
close(img);
return 0;
}
result
root@rpi:~/../.# ./app ./<image_name>.bmp
mapped addres : 0x7fa7c52000
==> 프로세스의 가상 메모리 공간에 할당된 시작 주소
HZ : 1초당 발생하는 jiffies ⇒ 100일 때 1초에 100번 Tick 발생
jiffies : 1Tick당 소요되는 시간
Tick : 시스템 타이머 인터럽트가 발생하는 최소 시간 단위 : Tick(sec)=1/HZ
Tickless Kernel (NO_HZ) : 최근의 리눅스 커널로 할 일이 없을 때는 타이머 인터럽트를 멈추고 예약된 작업이 있을 때만 깨어나는 방식
┌─────────────────────────────────────────────────────────────┐
│ 하드웨어 타이머 인터럽트 (주기적)
│ (HZ = 250Hz → 4ms마다)
├─────────────────────────────────────────────────────────────┤
│ 1. 하드웨어 타이머 만료 → IRQ 발생
│ ↓
│ 2. do_timer_irq() 또는 timer_interrupt() 호출
│ ↓
│ 3. update_jiffies() 또는 jiffies_64 업데이트
│ ↓
│ 4. jiffies 변수 원자적 증가:
│ jiffies = jiffies + 1
│ ↓
│ 5. 관련 구조체 업데이트:
│ - xtime (wall clock 시간)
│ - tick_sched (next tick 정보)
│ - timekeeper (ntp, skew 보정)
│ ↓
│ 6. 타이머 휠 회전 & 재정렬
│ ↓
│ 7. timer softirq 예약 → ksoftirqd 실행
│ ↓
│ 8. 하드웨어 타이머 다음 값으로 리로드 (4ms 후)
└─────────────────────────────────────────────────────────────┘
mdelay() : ms
udelay() : us
ndelay() : ns(나노초)
⇒ busy waiting이라 과한 시간 사용은 지양
High Resolution Timer
틱 단위 보다 세밀한 제어가 필요 + 다음 틱 간의 오버 헤드 발생
.
#include <linux/hrtimer.h>
#include <linux/ktime.h>
static struct hrtimer my_hrtimer;
static ktime_t kt_period;
// static enum hrtimer_NOrestart // => 이것도 존재
// 타이머 만료 시 호출되는 콜백 함수
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
// [ 정밀 작업 수행 ]
pr_info("hrtimer Callback at jiffies %lu\n", jiffies);
// 주기적인 반복을 위해 다음 만료 시간 설정
hrtimer_forward_now(timer, kt_period);
return HRTIMER_RESTART; // 타이머 재시작
}
static int __init hrtimer_example_init(void)
{
// 100 마이크로초 단위 설정 (0초, 100,000나노초)
kt_period = ktime_set(0, 100000);
// 타이머 초기화: 모노토닉 클럭(절대 시간), 상대 시간 모드
hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = &my_hrtimer_callback;
// 타이머 시작
hrtimer_start(&my_hrtimer, kt_period, HRTIMER_MODE_REL);
return 0;
}
이전까지의 디바이스 드라이버는 커널 모듈 로드를 통한 간단한 예제였습니다.
그렇다면 디바이스 드라이버 모델과 각 장치에 따른 드라이버 구현에 대해서 알아보겠습니다.
시스템 전체 디바이스들의 연결 트리 구조를 파악 가능
+ 계층적 전원 관리 가능

Bus (버스): 하드웨어 장치와 드라이버가 만나는 통로
acme-bus는 자신에게 연결된 devices와 이를 제어할 drivers 목록을 관리Driver (드라이버):장치를 어떻게 구동할지 알고 있는 소프트웨어
acme-keypad-driver는 버스에 등록되어 자신과 매칭 되는 장치를 기다린다Device (장치):실제 하드웨어 인스턴스
Class (클래스):장치를 기능 단위로 묶은 추상화 계층


| 구분 | kobject | kset | subsystem |
|---|---|---|---|
| 무엇인가 | 가장 기본 객체 (하나의 디렉토리) | kobject들의 집합 (디렉토리 + 목록) | kset + 등록/제거 로직을 가진 큰 단위 |
| 주요 역할 | sysfs에 디렉토리 하나 만듦 | 같은 종류의 kobject들을 관리·정렬 | 전체 서브시스템(예: block, net)을 관리 |
| kset 포함 | 포함 안 함 | 자기 자신도 kobject임 | 내부에 kset을 가짐 |
| 대표 예시 | 하나의 디바이스, 하나의 드라이버 | /sys/class/net/ 아래 모든 net_device | block_subsys, net_subsys, power_subsys |
| 사용 예 | struct device, struct kobj_example | class kset, bus kset | subsys_initcall()로 등록되는 큰 단위 |
리눅스 드라이버를 개발할 때, 커널 내부의 데이터를 유저(User Space)에게 어떻게 보여줄 것인가는 매우 중요한 설계 결정입니다. 각 방식의 차이를 비교해보겠습니다.
sysfs는 하드웨어 장치와 드라이버 정보를 사용자 공간으로 보여주기 위한 가상 파일 시스템(Virtual File System)입니다.
구조화: 커널 내부의 복잡한 장치 계층 구조(Device Topology)를 사용자가 읽고 쓸 수 있는 파일과 디렉터리 형태로 시각화하여 제공합니다.
통합 관리: 단순히 정보를 보여주는 것을 넘어, kobject라는 객체 지향적 구조를 통해 장치의 상태를 관리하고 제어하는 통합 인터페이스 역할을 합니다.
① 단순 커널 모듈 (Simple Module)
특징: printk로 로그만 남길 뿐, 유저가 직접 접근할 통로가 없습니다.
한계: open(), read(), write() 같은 표준 함수를 쓸 수 없어 실시간 제어가 불가능합니다.
② Procfs 방식 (/proc)
특징: 시스템 정보(CPU, 메모리 통계) 전달이 주 목적
장점: 구현이 매우 빠르고 간단 (Major/Minor 번호 등록이 필요 없음)
용도: 디버깅용 데이터 확인, 일회성 테스트 코드 작성 시 유리
③ Sysfs & Dev 방식 (/dev, 표준)
특징: 실제 하드웨어를 추상화한 표준 디바이스 파일을 생성
장점: udev와 통합되어 장치를 꽂으면 자동으로 /dev/에 파일이 생기며, 권한 설정 및 표준 입출력이 완벽히 지원됩니다
실제 배포되는 프로덕션 환경의 드라이버 개발 시 필수