Character Device Drivers
저번 시간에 말한 Character Device Drivers를 만나려면 3가지 요소가 필요하다
- File operations
- Adding a character device
- Device files
여러 Device Drivers 가 있다.
Sockets interface를 통해 network protocol stack과 소통하는 Network Device Driver,
open,close,read and write call들을 통해 Block단위인 Block Device Driver,
마지막으로 우리가 만들 Character Device Drivers 가 있다.
Registering and Initializing Drivers
Network Devices
- 구조체 : net_device 와 net_device_ops
- 등록 함수 : register_netdev()
Block Devices
- 구조체 : gendisk 와 block_device_operations
- 등록 함수 : add_disk()
Character Devices
- 구조체 : cdev 와 file_operations -> 이 장치의 이름은 무엇이고, 메모리 주소는 어디인가?
- 등록 함수 : cdev_add() -> open 혹은 read 시에 어떤 함수를 실행할 것인가?
그래서 character devices 는 구조체 2개를 초기화하고 등록 함수로 추가하면 된다.
File Operations

위에서 말했던 cdev 구조체 안에 file_operations가 넣어져 있고 그 안에 open(), read(), write()함수를 각각 다 초기화 시켜서 구현해주면 된다.
cdev Structure
리눅스 커널 관점에서 이 장치는 character device 다 라는 것을 정의하는 본체이며 "include/linux/cdev.h" 에서 정의가 되어 있다.
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
여기서 주목할 것은 바로 ops이다.
장치가 수행할 수 있는 read, write, open 같은 동작(Helper functions)들의 리스트를 가리키는 포인터이며 이 포인터를 통해 유저의 시스템 콜이 실제 드라이버 함수와 연결이 되기 때문이다.
ops를 초기화하는 방식은 2가지 정도가 있는데,
- 직접 대입 방식: my_cdev->ops = &my_ops; 처럼 포인터를 직접 연결한다.
- 함수 활용 방식: cdev_init(my_cdev, &my_ops); 함수를 호출하여 내부 필드들을 기본값으로 초기화하고 ops를 연결한다.
file_operations Structure
driver의 동작을 정의하는 구조체로 "include/linux/fs.h"에 정의가 되어 있다.
이 구조체는 또한 함수 포인터들의 집합(Collection of function pointers)이다.
- 개발자가 모든 기능을 다 구현할 필요는 없으며, 지원하지 않는 기능은 NULL로 남겨두면 커널이 알아서 처리한다.
많은 함수들이 있지만 우리는 read, write, open, release, unlocked_ioctl 만 살펴보도록 하겠다.
open()
device file에서 첫번째로 수행되는 함수이다.
주의사항 : 만약 entry가 null이라면, 그것은 항상 성공한다.(그냥 return한다는 의미)
release()
애플리케이션이 열려 있던 장치 파일을 닫을 때(close()) 호출되는 함수입니다.
read()
device로부터 user buffer에 데이터를 전달할 때 사용된다.
- 성공 시: 읽어온 데이터의 바이트 수를 양수(0 포함)로 반환한다.
- 실패 시: 에러를 나타내는 -EINVAL 값을 반환한다.
write()
user buffer로부터 device에 데이터를 보낼 때 사용된다.
- 성공 시: 성공적으로 보낸 데이터의 바이트 수를 반환한다.
- 실패 시: 에러를 나타내는 -EINVAL 값을 반환한다.
unlocked_ioctl()
일반적인 읽기/쓰기(read/write) 외에, 장치만이 가진 특수한 기능이나 설정을 제어하기 위해 사용한다.
Parameters - (*file, unsigned int cmd, unsigned long arg )
- Flag/Cmd (unsigned int): 드라이버가 수행해야 할 명령의 종류를 지정합니다. (예: "장치 초기화해라", "속도를 조절해라" 등)
- Parameter (unsigned long): 명령 수행에 필요한 추가 데이터입니다. 보통 유저 공간의 데이터 주소(포인터)를 전달할 때 사용합니다.
cmd가 그러면 어떻게 명령의 종류를 지정하지 라는 의문이 들 수 있다.
cmd는 단순히 숫자가 아니라 매직 넘버(Type), 순서 번호(Ordinal), 데이터 전송 방향, 인자의 크기 등이 조합된 복합적인 비트 값이다.
예전에는 "Documentation/ioctl/ioctl-number.txt" 에 있었지만
요즘에는 "Documentation/userspace-api/ioctl/ioctl-number.rst" 에 존재한다.
저 파일들에는 kernel을 통해 사용되는 magic number들이 리스트업 되어 있지만
일부 ioctl 명령은 커널이 file_operations(fops) 테이블을 확인하지 않고도 자체적으로 알아서 처리한다.
-> 그래서 저 문서를 참고하여 기존에 사용 중인 번호와 겹치지 않는 자신만의 매직 넘버를 골라야 한다.
드라이버 내부에서는 보통 cmd를 통해 switch문을 사용해서 실행된다.
Registration
이제 그러면 등록하는 과정에 대해 알아보자. 아까 그냥 주소로 등록하거나 아니면 cdev_add()를 사용해서 등록하라고 했었다.
cdev_add(struct cdev *p, dev_t dev, unsigned count)
dev
dev는 커널이 특정 장치를 식별하기 위해 사용하는 32비트 숫자다. 이 숫자는 하나처럼 보이지만 실제로는 major, minor 숫자로 계산된다.
Count
이 드라이버가 제어할 연속된 minor 번호의 개수이다.
보통 0부터 시작하며 이 때, count가 3이라면 0,1,2의 minor number를 가진 device를 관리하는 것
Major number
커널 내에 특정한 device driver를 식별하는 숫자다. 숫자를 직접 정하기보다는 커널이 남는 번호를 알아서 주는 동적 할당(Dynamic Allocation) 방식이 권장된다.
- dynamic allocation 방식에는 커널이 안쓰는 major number를 던져주는 alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 을 사용하면 된다.
Minor number
device driver 가 관장하는 device 중에 정확히 어떤 디바이스인지를 식별하는 숫자이다.
이러한 Major, Minor number를 확인할 수 있는 방법은 "ls -l /dev" 명령어를 사용하는 것이다.

이 명령어는 터미널에서 Device files들을 볼 수 있는데 여기서 두 숫자를 확인 가능하다.
가끔씩은 open이나 read 함수 내부에서 지금 들어온 요청이 어디 device driver에서 온건지를 알아야할 때가 있는데,
- 이 때 open 함수의 인자로 들어오는 inode 구조체에 Major, Minor number들이 있어서 거기서 얻을 수 있고
- 혹은 imajor() 나 iminor() 함수를 쓰면 내부적으로 MAJOR(), MINOR() 매크로 호출되서 간단하게 얻을 수도 있다.
Initialization of Device Drivers
- cdev와 file_operations 구조체를 초기화 한다.
- 사용하지 않는 major, minor number 를 얻는다 (alloc_chrdev_regioin()활용)
- cdev_add() 등을 사용해 등록 한다.
module_init() 함수 안에 구현해야함!! 그래야 컴파일해서 insmod 하면 드라이버가 커널에 정식으로 등록된다는 것을 참고
이렇게 커널에 등록된 함수는 후에 유저가 사용할 수 있는 Device file로도 만들어줘야함
그래서 이제 우리는 Device file을 만들것이다.
Device Files
리눅스에서 device를 포함한 모든 하드웨어들은 file system의 file로 접근이 된다.
그건 Character devices들도 마찬가지인데, 관례적으로 "/dev"에 위치가 되어 있으며 device nodes라고도 부른다.
이러한 device files은 "mknod" 라는 command로 별도로 만들 수 있다.
Auto-detection in Makefile
하지만 매번 mknod를 쳐서 device file을 만든다는 것은 상당히 번거롭기 때문에 보통 스크립트를 짜서 만든다.
- "alloc_chrdev_region()"을 통해 할당받은 major number는 "/proc/devices"에서 확인할 수 있다.
- awk 명령어를 사용해 "/proc/devices"에서 우리 드라이버 이름에 해당하는 major number만 뽑아낸다.
- 추출한 번호를 이용해 mknod를 실행하여 /dev에 파일을 만듭니다.

#!/bin/sh
MODULE=“mymod”
major=$(awk “\$2==\“$MODULE\” {print \$1}” /proc/devices)
mknod /dev/${MODULE}0 c $major 0
- MODULE="mymod"
- 우리가 만든 드라이버(모듈)의 이름을 변수로 설정함
- major=$(awk "\$2==\"$MODULE\" {print \$1}" /proc/devices)
- /proc/devices: 현재 커널에 등록된 모든 장치와 주 번호가 적혀 있는 텍스트 파일.
- awk: 파일에서 특정 조건을 만족하는 행을 찾아 원하는 값을 뽑아내는 도구이다.
- 의미: /proc/devices 파일의 두 번째 열($2)이 드라이버 이름("mymod")과 일치하는 줄을 찾아서, 그 줄의 첫 번째 열($1, 즉 주 번호)을 추출해 major라는 변수에 저장한다.
- mknod /dev/${MODULE}0 c $major 0
- mknod: 실제 장치 파일을 생성하는 명령어.
- /dev/${MODULE}0: /dev/mymod0이라는 이름의 파일을 만든다.
- c: 문자 장치(Character Device) 타입으로 생성한다는 뜻
- $major 0: 위에서 알아낸 주 번호와 부 번호 0번을 부여하여 파일을 완성한다

Device Driver를 생성하려고 함
- 1-1. alloc_chrdev_region()을 통해 /proc/devices에 안쓰는 Major number를 요청하면
- 1-2. Major number를 받아온다.
- 1-3. cdev_add(struct cdev *p, dev_t dev, unsigned count)로 Device Drivers를 등록한다.
Makefile에 Device file 자동 생성 스트립트를 만듬
- 2-1. 자동 스크립트에 설정한 대로 일단 Major number를 요청한다.
- 2-2. Major number를 받아온다.
- 2-3. mknod로 User가 쉽게 이용할 수 있게 Device file을 만든다.
이렇게 만들고 나서는 User program이 실제로 Driver를 사용하는 것이다.
- 3-1. 커널에게 나 이 device file을 사용하고 싶다고 open() 시스템 콜을 보낸다.
- 3-2. 커널이 그 device file을 열어 파일에 접근할 수 있도록 fd(file description)을 건네준다.
- 3-2. 그렇게 받은 fd로 파일들을 사용하며 read를 요청하면 device file안의 ops 구조체를 확인해서 driver 안에 설정되어 있는 read 함수로 연결해 준다.
Parameter Passing
커널 모듈을 로드할 때(실행 시점) 외부에서 변수 값을 동적으로 주입하는 기능이다.
미리 하드코딩으로 고정시켜놓는다는 단점을 조금이나마 타파할 수 있는 기능으로 유연함이 장점이다.
예를 들어 insmod mymod count=10 으로 count를 insertion time에 정해 test등에도 훨씬 유연하게 할 수 있다.
이 기능은 "moduleparam.h" 에 정의되어 있으며 이 기능을 사용하려면 함수 외부에 매크로를 설정해야한다.
static int count = 0;
module_param(count, int, S_IRUGO);
count를 먼저 초기화해주고(static이 붙는 이유는 scope를 이 파일 내로 한정짓기 위해서) 그 count를 module_param에 넣어준다
module_param()
- 첫 번째 인자 : 값을 전달받을 변수 이름을 넣고
- 두 번째 인자 : 그 변수의 자료형을 넣은 뒤
- 세 번째 인자 : S_IRUGO, S_IRUGO|S_IWUSR 등의 접근 권한을 넣어놓는다.
- S_IRUGO: 누구나 읽을 수 있지만 수정은 불가능한 권한.
- S_IWUSR: 루트(root) 사용자가 값을 수정할 수 있게 허용.
이 매크로는 module_init처럼 코드 블록 밖에 있어야함!!
Kernel Dependency
리눅스의 커널은 계속해서 업데이트 되고 있다. 그에 따라 내부의 struct 나 function 이나 계속해서 바뀌어 가고 있는데, 이 말은 커널의 버전에 따라 구조체와 함수가 다르다는 소리가 된다. 특히 모듈은 커널 내부 자원을 이용하는 경우가 많기 때문에 특히 취약한데
- LINUX_VERSION_CODE: 현재 시스템의 커널 버전을 정수 값으로 나타낸다.
- KERNEL_VERSION(a, b, c): 특정 버전 번호를 비교 가능한 정수 값으로 변환해준다.
그래서 이 2개의 함수를 이용해서 현재 시스템의 커널 버전을 가져와 버전에 따라 최신 커널용 코드, 구식 커널용 코드를 구비한다.
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
/* 최신 커널용 코드 */
#else
/* 구형 커널용 코드 */
#endif
'수업 정리 > 임베디드시스템소프트웨어' 카테고리의 다른 글
| [embedded system software/9주차] 이론 (0) | 2026.04.29 |
|---|---|
| [embedded system software/6주차] 이론 (0) | 2026.04.15 |
| [embedded system software/5주차] 수업정리 (0) | 2026.04.01 |
| [embedded system software/4주차] 수업 정리 (0) | 2026.03.25 |
| [embedded system software/2주차] Loadable Kernel Modules (1) | 2026.03.16 |