
드라이버 제작 기초의 두 번째 편이다.
이번에 들어가야 하는 내용이 매우매우 많은데
코드를 올릴 수가 없다 보니 일단 정리한 내용을 산더미처럼 가져왔습니다 ( •̀ ω •́ )✧
read() 와 write() 구현 시 고려사항
유저 영역과 커널 영역 간 데이터 이동
커널 메모리 공간에는 직접 접근이 불가
copy_to_user() / copy_from_user() 사용
블록 처리 (Blocking I/O)
읽을 데이터가 없거나 쓰기를 위한 버퍼가 꽉 찼을 때 프로세스 Sleep하는 로직
wait_queue 사용
하드웨어 제어 함수
실제 레지스터 읽기/쓰기
I/O Port 또는 Memory Mapped I/O 사용
동시성 제어 (Concurrency)
여러 프로세스가 동시에 접근할 때의 경쟁 처리
Mutex, Semaphore, Spinlock 등 사용
인터럽트 서비스 함수와의 경쟁 처리
Top Half(ISR)와 프로세스 컨텍스트가 공유 자원에 접근할 때 보호
Read Callback 구조
ssize_t xxx_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
// 1. 잠금 (동시성 제어)
if (mutex_lock_interruptible(&my_mutex))
return -ERESTARTSYS;
// 2. 데이터 확인 및 Blocking 처리
// 데이터가 없으면 루프를 돌며 Sleep (wait_queue 대기)
while (!is_data_ready()) {
mutex_unlock(&my_mutex); // 잠들기 전 락 해제 (중요!)
// 논블로킹 모드(O_NONBLOCK)면 바로 리턴
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
// 프로세스를 재움 (인터럽트 발생 시 깨어날 수 있음)
if (wait_event_interruptible(my_wait_queue, is_data_ready()))
return -ERESTARTSYS; // 시그널에 의해 깨어남
// 깨어난 후 다시 락 획득
if (mutex_lock_interruptible(&my_mutex))
return -ERESTARTSYS;
}
// 3. 데이터 복사 (Kernel -> User)
// copy_to_user(buf, kernel_data, count);
// 4. 잠금 해제
mutex_unlock(&my_mutex);
return copy_size;
}
Memory Mapped I/O의 접근
MMIO를 사용하면 하드웨어 레지스터에 메모리처럼 접근 가능
접근할 때는 전용 접근 함수(매크로)를 사용해야 함
가상 주소로 매핑된 하드웨어 레지스터 영역에 데이터 읽고 쓰기 작업 수행
MMIO 읽기 매크로
특정 가상 주소(addr)에서 값을 읽어 반환하는 함수
readx 형태의 구형 함수
readb(addr); // 8bit data
readw(addr); // 16bit data
readl(addr); // 32bit data
ioread 계열 함수
ioread8(addr); // 8bit data
ioread16(addr); // 16bit data
ioread32(addr); // 32bit data
MMIO 스트림 데이터 읽기
레지스터에서 연속된 데이터를 읽어 버퍼에 저장하며, 루프를 돌며 읽는 것보다 효율적
지정된 I/O 주소 addr 에서 buf 가 가리키는 영역으로 count 만큼 반복해서 읽음
void ioread8_rep(void __iomem *addr, void *buf, unsigned long count);
void ioread16_rep(void __iomem *addr, void *buf, unsigned long count);
void ioread32_rep(void __iomem *addr, void *buf, unsigned long count);
MMIO 쓰기 매크로
특정 가상 주소(addr)에 지정한 값(value)을 쓰는 함수
writex 형태의 구형 함수
writeb(value, addr); // 8bit data
writew(value, addr); // 16bit data
writel(value, addr); // 32bit data
iowrite 계열 함수
iowrite8(value, addr); // 8bit data
iowrite16(value, addr); // 16bit data
iowrite32(value, addr); // 32bit data
MMIO 스트림 데이터 쓰기
버퍼에 있는 데이터를 레지스터로 연속해서 전송
buf 가 가리키는 메모리 영역에서 count만큼의 데이터를 읽어 addr 주소에 반복해서 씀
void iowrite8_rep(void __iomem *addr, const void *buf, unsigned long count);
void iowrite16_rep(void __iomem *addr, const void *buf, unsigned long count);
void iowrite32_rep(void __iomem *addr, const void *buf, unsigned long count);
물리 주소와 가상 주소
MMU : 프로세서가 메모리에 접근하는 가상 주소를 변환 테이블을 참고해 실제 물리 주소로 변환
가상 주소 : 프로세서에서 사용하는 논리적 주소
물리 주소 : 실제 하드웨어적으로 설정된 고정 주소
유저 공간과 커널 공간의 데이터 전송 API
헤더
<asm/uaccess.h> : 구식, 직접 포함하는 방식
<linux/uaccess.h> : 현대 표준, 내부적으로는 아키텍처에 맞춰 asm 파일 가져옴
유효성 검사
unsigned long access_ok(int type, const void *addr, unsigned long size);
주어진 메모리 범위 전체가 유효한 사용자 공간에 속하는지 검사
데이터 접근 전에 유효성 검사 필수
블록 데이터 전송
// 커널 메모리 블록 데이터를 사용자 메모리 블록 데이터에 쓰기
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
// 사용자 메모리 블록 데이터를 커널 메모리 블록 데이터에 쓰기
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
단일 값 전송
// 사용자 공간의 데이터 읽기
int get_user(x, ptr);
// 커널 변수 값을 사용자 공간에 쓰기
int put_user(x, ptr);
x 는 포인터가 아닌 변수 형태이며, ptr 의 타입에 따라 복사할 바이트 크기가 결정됨
디바이스 통신 : I/O 메모리
ARM 등 대부분의 임베디드 아키텍처에서는 Memory Mapped I/O 방식을 사용
주변 장치(Peripheral)에 접근할 때는 SFR(Special Function Register)을 통해 접근 → I/O 메모리
I/O 메모리는 일반 메모리와 동일한 공간(Flat Memory)에 배치되어 주소를 가짐
각 주소별로 Memory Mapped 방식으로 장치들이 매핑되어 있음
주 메모리와 같은 방식으로 접근하지만, 포인터를 사용한 물리 주소 직접 접근은 자제
커널이 제공하는 접근 함수(readl, writel)를 사용할 것
접근 흐름
자원 예약 → 주소 매핑 → 데이터 입출력 → 자원 해제
I/O 메모리 영역 할당(예약)
헤더 : <linux/ioport.h>
struct resource *request_mem_region(resource_size_t start, resource_size_t len, const char *name);
start : 물리 주소의 시작점
len : 예약하려는 메모리 영역의 길이
name : 소유하는 디바이스나 드라이버 식별 이름, /proc/iomem 에 표시됨
반환값 : 성공 시 비어있지 않은 포인터, 실패 시 NULL
예약된 메모리는 /proc/iomem 파일에서 등록된 물리 메모리 맵 확인 가능
# cat /proc/iomem
00000000-3b3fffff : System RAM
00000000-00000fff : reserved
00210000-0113ffff : Kernel code
01140000-0154ffff : reserved
...
I/O 메모리 주소 매핑
헤더 : <asm/io.h> 또는 <linux/io.h>
void __iomem *ioremap(unsigned long phys_addr, unsigned long size);
물리 주소를 커널의 가상 주소 공간에 매핑
반환값 : 접근 가능한 가상 주소 포인터
I/O 자원 해제
// 커널 가상 주소 매핑 해제
void iounmap(volatile void __iomem *addr);
// 메모리 영역 해제
void release_mem_region(unsigned long start, unsigned long len);
프로세스가 사용 중인 메모리 맵
프로세스를 실행하고 pid 확인
pmap <pid> 명령어로 현재 프로세스가 사용 중인 메모리 맵 확인
root@rping:~/work_drivers/exercise/23# ./a.out &
[1] 4582
root@rping:~/work_drivers/exercise/23# pmap 4582
4582: ./a.out
000000557ab90000 4K r-x-- a.out
000000557aba0000 4K r---- a.out
000000557aba1000 4K rw--- a.out
0000007fbe7d0000 1396K r-x-- libc-2.31.so
0000007fbe92d000 60K ----- libc-2.31.so
0000007fbe93c000 16K r---- libc-2.31.so
0000007fbe940000 8K rw--- libc-2.31.so
0000007fbe942000 12K rw--- [ anon ]
...
메모리 매핑 방식 비교
| 구분 | Kernel Space (커널 영역) | User Space (유저 프로세스 영역) |
|---|---|---|
| 사용 주체 | 디바이스 드라이버 | 어플리케이션 |
| 사용 방식 | ioremap() | mmap() |
유저 영역의 메모리 매핑
파일 디스크립터 fd 가 가리키는 디바이스 메모리를 프로세스의 가상 주소 공간에 매핑
헤더 : <sys/mman.h>
// 가상 주소에 매핑
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 매핑 해제
int munmap(void *addr, size_t length);
addr : 매핑을 원하는 시작 주소, 보통 NULL 로 지정하여 커널이 결정하도록 함
length : 매핑할 메모리 영역의 크기로 바이트 단위
prot : 권한 플래그
flags : 매핑의 동작 방식(공유 여부)
fd : 디바이스 파일 디스크립터
offset : 파일의 시작점에서부터 매핑할 오프셋
커널에서의 mmap 처리
사용자가 mmap 을 호출하면 커널은 드라이버 내 xxx_mmap 함수 호출
int xxx_mmap(struct file *filp, struct vm_area_struct *vma);
struct vm_area_struct *vma : 할당받은 정보(주소)가 담긴 구조체
<linux/mm.h> 에 구조체와 매크로 및 상수가 선언되어 있음
remap_pfn_range
remap_pfn_range(vma, addr, pfn, size, prot)
물리 주소를 vma 가 가리키는 유저 가상 주소 범위에 매핑해주는 커널 함수(PAGE_SIZE 단위)
호출 전에 물리 주소 유효성 검사, 매핑 정보 유효성 검사 처리해 두어야 함
3-3부터 3-6까지는 결석 이슈로 생략합니다.
ㅋㅋ 고멘!
기본 모듈과 드라이버 형태
진입점 : module_init() , module_exit()
중요 객체 : cdev (캐릭터 장치 구조체), dev_t (장치 번호)
인터페이스 : file_operations ( .open → dev_open() 등 드라이버 내 콜백 함수 등록)
하드웨어 제어 : isr() (인터럽트 핸들러)
드라이버 모델의 필요성
최신 시스템은 버스와 장치의 복잡한 트리 구조를 가지므로 새로운 모델의 필요성 대두
전원 관리 : 장치 연결 탐색, 부모 장치가 꺼지면 자식 장치도 전원 끄기
유저 영역 접근 : 커널 내부의 장치 정보를 사용자 공간(sysfs)에서 접근 및 제어
핫 플러그-인 : 전원이 켜진 상태에서 장치 연결 및 해제 가능
디바이스 클래스 : 연결 방식에 상관 없이 디바이스를 계층적으로 구분
드라이버의 구성 요소 : 장치 구성 표준화
| 요소 | 설명 | 예시 |
|---|---|---|
| Bus (버스) | 프로세서와 장치 사이의 통신 채널. 장치와 드라이버를 매칭(Binding)하는 역할 | PCI, USB, I2C, SPI, Platform |
| Device (장치) | 버스에 연결된 실제 물리적/논리적 객체 | 마우스, 키보드, 센서 칩 |
| Driver (드라이버) | 특정 장치를 제어하는 소프트웨어 루틴 | usbhid, i2c-dev |
| Class (클래스) | 연결 방식(Bus)과 상관없이 기능(Function)에 따른 분류 | Input(입력 장치), Sound, Net(네트워크) |
드라이버 모델의 장점
전체 디바이스의 연결 트리 구조 : 시스템의 모든 장치 연결 상태를 파악 가능
(연결 장치 열거, 소속 버스 및 상태 확인, 계층적 전원 관리)
코드 중복 최소화 : 참조 카운팅 기능 기본 제공, kobject_get / kobject_put
클래스별 구분 가능 : 사용자의 직관적 장치 구분 가능, 프린터/모니터 등 기능(Class)으로 분류
Sysfs 가상 파일 시스템
통합 드라이버 모델의 내부 구조(kobject 계층)를 사용자 공간에 파일 시스템 형태로 보여줌
/sys 에 마운트되고, 디스크에는 저장되지 않는 가상 파일 시스템
(# mount -t sysfs sysfs /sys → see, /etc/fstab)
커널 내부의 kobject 하나가 sysfs 의 디렉토리 하나에 대응되는 구조
드라이버 모델의 4대 요소
하드웨어의 계층적 관리를 위해 4가지 추상화 객체를 사용
/sys 디렉토리 구조에 반영되는 것을 확인할 수 있음
각 객체는 <linux/device.h> 에 정의된 구조체로 표현
# ls /sys
block bus class dev devices firmware fs kernel module power

버스
프로세서와 디바이스 사이의 통신 채널, 디바이스와 드라이버를 연결하는 관리자
struct bus_type 구조체로 표현됨
드라이버 모델의 최상위 계층에 위치
대부분의 버스는 이미 구현되어 있으므로 직접 구현하는 경우는 거의 없음
버스는 드라이버 모델의 계층 상위단 (연결 상태로 보면 하위단)
버스 등록 및 해제 API
등록 : bus_register(struct bus_type *bus)
해제 : bus_unregister(struct bus_type *bus)
새로운 버스 등록 후에 /sys/bus/ 디렉토리 아래에 생성되는지 확인
디바이스
버스에 연결된 실제 장치 객체
struct device 구조체로 표현됨
커널 객체 관리를 위한 kobject , 연결된 버스 bus , 부모 디바이스의 정보 parent 포함
디바이스 등록 및 해제 API
등록 : device_register(struct device *dev)
제거 : device_unregister(struct device *dev)
호출에 성공하면 /sys/devices 아래에 새로운 디바이스가 보이게 됨
드라이버
특정 디바이스를 제어하는 소프트웨어 루틴의 집합
struct device_driver 구조체로 표현됨
probe 함수는 디바이스 발견 시에 호출되는 초기화 함수
remove 함수는 장치가 제거될 때 자원을 해제
struct device_driver {
char *name; // 드라이버 이름
struct bus_type *bus; // 드라이버가 동작하는 버스
struct kobject kobj; //필수적인 kobject
struct list_head devices;
int (*probe)(struct device *dev); // 시스템 초기화 시 불리는 함수
int (*remove)(struct device *dev); // 시스템에서 제거 시 불리는 함수
void (*shutdown)(struct device *dev); // 전원 끌 때 불리는 함수
}
드라이버 등록 및 해제 API
등록 : driver_register(struct device_driver *drv)
해제 : driver_unregister(struct device_driver *drv)
성공하면 /sys/bus/acme-bus/drivers/ 아래 새로운 드라이버가 생김
클래스
연결 방식이나 내부 구현 방법과 상관없이 기능에 따라 디바이스를 분류
struct class 구조체로 표현됨
디바이스에 마우스, 스피커 등의 기능으로 접근 가능
대부분의 클래스는 /sys/class/ 에 나타나고 블록 디바이스는 /sys/block/ 에 위치
클래스 등록 및 해제 API
등록 : class_register(struct class *cls)
해제 : class_unregister(struct class *cls)
Kobject
<linux>/kobject.h> 내에 정의된 struct kobject 구조체로 표현
모든 디바이스 모델 객체(디바이스, 드라이버, 버스 등)가 공통적으로 포함하는 최상위 추상화 객체
가장 기본적인 요소로서 cdev , device 같은 상위 구조체 내부에 포함되어 사용
하나의 kobject는 /sys 파일 시스템에서 하나의 디렉토리로 나타남
각 attribute는 /sys 파일 시스템에서 하나의 파일로 나타남
kobject 는 부모 포인터 parent 를 통해 다른 kobject 나 kset 과 트리 구조를 형성
Kobject의 기능
참조 카운트 (Reference Counting)
객체의 생명 주기를 관리하는 기능
객체가 참조되고 있을 때는 자원을 해제하지 않음
객체를 사용하는 곳이 없으면 count가 0이 되고, 커널은 그 자원을 해제
초기화
kobject_init() 에 의해 참조 카운트 1로 초기화
증가
struct kobject *kobject_get(struct kobject *kobj)
객체를 사용할 때 호출하여 카운트를 1 증가
감소
void kobject_put(struct kobject *kobj)
객체의 사용이 끝났을 때 호출하여 카운트를 1 감소
카운트가 0이 되면 커널은 연결된 release 콜백을 호출하고 메모리 해제
Sysfs 표현
커널 내부 상태를 사용자 공간에 노출
kobject 는 /sys 의 디렉토리, kobject 의 속성 kobj_attribute 은 디렉토리 내 파일로 표현
Kset
동일하거나 다른 여러 type의 kobject 를 그룹으로 묶어 관리하는 컨테이너
sysfs 상에서 상위 디렉토리를 형성

Subsystem
적절한 이미지를 찾지 못했으니 필요하면 요청 plz.
드라이버의 분류 요약
데이터 단위에 따른 분류
문자 드라이버 (Char) : 바이트 단위로 데이터를 주고받음 (키보드, 센서 등)
블록 드라이버 (Block) : 덩어리(Block) 단위로 데이터를 주고받음 (하드디스크, SSD)
버스 타입에 따른 분류 : USB, I2C, SPI 드라이버 등
기능에 따른 분류 : Input(마우스/키보드), IIO(산업용 센서) 등
플랫폼 드라이버
USB나 PCI처럼 물리적으로 존재하는 버스와 달리 가상으로 운영되는 버스
CPU 내부에 연결된 Timer나 GPIO 컨트롤러 등을 다루며, 이 장치들은 플랫폼 디바이스로 정의됨
디바이스 정보는 Device Tree 또는 platform_device 로 정의되어 커널에 전달됨
드라이버 모델의 구성
struct platform_device : 하드웨어에 대한 정보
struct platform_driver : 장치 연결 시 실행되는 코드
연결 구조
내부 장치
칩 내부 장치의 연결을 담당하는 버스는 플랫폼 버스
내부 타이머, USB Adapter, I2C Adapter 등을 연결하는 컨트롤러
칩마다 다르지만 ARM에서는 AXI, AHB, APB 등을 사용
외부 장치
I2C Adapter 등을 통해 밖으로 연결된 센서나 모듈 관리
구조 : USB/I2C Adapter — 외부 device — USB/I2C Bus
어댑터 자체에 적용되는 것은 Adapter Driver
각 디바이스에 적용되는 것은 Client Driver
platform_driver 구조체
<linux/platform_device.h> 에 정의되어 있으며, 플랫폼 버스에 특화된 기능을 제공
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
};
// prove : 디바이스의 초기화 루틴, 필수
// remove : 디바이스가 제거될 때, 필수
// shutdown : 시스템 종료 시 호출
// suspend : 절전모드로 들어갈 때 호출
// resume : 절전모드에서 빠져나올 때 호출
// driver : 기존 device_driver를 확장하기 위해 상속받은 객체
// id_table : 지원하는 디바이스의 종류(ID) 목록
platform_driver 구조체 정의
모델별로 정의된 디바이스와 드라이버가 존재하고, 각 디바이스에 적합한 드라이버 모델을 정의해서 사용
static struct platform_driver xxx_driver = {
.probe = xxx_probe,
.remove = xxx_remove,
.driver = {
.name = "my_device_name", // 디바이스 이름과 일치해야 매칭됨
.owner = THIS_MODULE,
.of_match_table = xxx_of_match, // Device Tree 매칭용 (최신 커널 필수)
},
};
등록과 해제
등록 : platform_driver_register(&xxx_driver)
해제 : platform_driver_unregister(&xxx_driver)
드라이버는 커널 레벨에서 실행된다.
이 때문에 일반적인 프로그램에 비하면 좀 더 신경써야 할 부분들이 분명히 존재한다.
이걸 이해하고 드라이버 모델의 구조를 숙지한다면
거의 비슷한 형태로 버스, 디바이스, 드라이버, 클래스가 다가올 거라고 생각한다.
중간에 빠진 내용은 여기엔 없으나 필요한 내용이긴 하니까
여러 똑똑이들의 필기를 참고해서 알아 두기를 바랍니다 😏