일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 커널 모듈
- 라즈베리파이
- yocto
- 캐릭터 디바이스
- 가계부 개발
- cdev
- 개발자 취준
- rtsp
- 디바이스 트리 작성법
- qt widget
- systemd
- 데몬 프로세스
- 디바이스 트리
- SQLite
- /dev
- 리눅스 커널 드라이버
- 임베디드 리눅스
- IOT
- raspberry pi 4
- udev
- 금융권 취준
- 라즈베리파이 카메라
- 노션
- 멀티클라이언트
- ioctl
- 바이브 코딩
- .so 라이브러리
- wiringpi
- pthreads
- 취준
- Today
- Total
이로또
임베디드 리눅스 드라이버의 이해 2편: 캐릭터 디바이스와 장치 파일, ioctl 본문
이 글에서 캐릭터 디바이스 드라이버를 중심으로, 리눅스 커널과 사용자 공간 간의 연결 구조를 정리했습니다. 장치 번호의 개념과 등록 방식, /dev 파일 생성 방법, cdev 구조체를 통한 커널 등록 방식, 그리고 read, write, ioctl 함수의 동작 흐름과 역할까지 실제 코드 흐름을 따라 확인 할 수 있습니다.
목차
- 캐릭터 디바이스란?
- 장치 번호의 개념 (Major / Minor)
- 장치 번호 등록 방법 (register_chrdev_region, alloc_chrdev_region)
- cdev 구조체와 커널 등록 흐름
- /dev 장치 파일 생성 방식 (자동 vs 수동)
- /proc, /sys 가상 파일 시스템
- file_operations 구조체와 함수 연결
- 사용자 공간 ↔ 커널 공간 데이터 흐름 (copy_from_user, copy_to_user)
- 인터럽트 기반 read/write 흐름
- ioctl 시스템 콜과 커맨드 매크로
- 전체 흐름
- 실습 코드
1. 캐릭터 디바이스란?
캐릭터 디바이스는 1바이트씩 데이터를 처리하는 장치로, 대표적인 예로 키보드, 마우스등이 있습니다.
캐릭터 디바이스는 블록 디바이스와 달리 내부 버퍼 없이 실시간으로 데이터를 전달하며, read, write 함수를 통해 제어됩니다.
항목 | 캐릭터 디바이스 | 블록 디바이스 |
처리 단위 | 1바이트 | 512바이트 이상 블록 |
예시 | 키보드, 마우스, UART | HDD, SSD |
함수 | read, write, open, close 등 | request, submit_bio |
1-1. 캐릭터 드라이버 개발 흐름
- init 함수: 모듈이 로드될 때 실행됨
- file_operations 구조체 정의: read, write 함수 등록
- register_chrdev_region() / alloc_chrdev_region(): 장치 번호 등록
- cdev_init() / cdev_add(): 커널에 디바이스 등록
- device_create(): /dev/mydevice 파일 생성
- read(), write(), open(): 유저 공간과 데이터 송수신 처리
1-2. 사용자 → 드라이버로 접근하는 흐름
- 사용자 코드에서 open("/dev/mydevice") 실행
- 커널의 캐릭터 드라이버로 전달
- file_operations 구조체에 등록된 open() 실행
- read(), write()도 마찬가지로 연결됨
2. 장치 번호란?
리눅스에서 디바이스 파일과 드라이버를 연결하기 위한 고유한 번호입니다.
dev_t라는 자료형에 주 번호(Major)와 부 번호(Minor)가 합쳐져 들어갑니다.
- 주 번호: 어떤 드라이버인지 식별, 어떤 드라이버에 연결할 지 알려주는 번호
- 부 번호: 같은 드라이버 안에서 여러 장치를 구분
MAJOR(dev), MINOR(dev), MKDEV(major, minor)
예시)
디바이스 파일 | 주 번호 | 부 번호 | 의미 |
/dev/tty0 | 4 | 0 | 첫 번째 터미널 |
/dev/tty1 | 4 | 1 | 두 번째 터미널 |
/dev/led0 | 240 | 0 | 첫 번째 LED |
/dev/led1 | 240 | 1 | 두 번째 LED |
3. 장치 번호 등록 방법
장치 파일이 있으면, 이게 실제로 어떤 드라이버랑 연결되는 지 리눅스가 알아야 합니다.
그 연결을 해 주는 게 장치 번호이고, 장치 번호 등록 방법은 아래와 같습니다.
방식 | 함수 | 설명 |
수동 등록 | register_chrdev_region() | 원하는 주번호를 지정하여 등록합니다 |
자동 할당 | alloc_chrdev_region() | 커널이 비어 있는 번호를 자동으로 할당합니다 (실무에서 많이 사용됨) |
등록 해제 | unregister_chrdev_region() | 모듈 제거 시 반드시 반납해야 합니다 |
3-1. register_chrdev_region() 함수란?
디바이스 드라이버가 사용할 장치 번호를 커널에 "등록"하는 함수입니다.
이걸 해야 /dev 아래에서 장치 파일로 사용할 수 있습니다.
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
매개변수 | 설명 |
first | 주 번호 + 부 번호를 합친 dev_t 타입. 등록할 장치 번호의 시작점. |
count | 등록할 부 번호 개수 (장치 개수). 예: 부 번호 0번부터 3번까지 쓰고 싶으면 count = 4 |
name | 디바이스 이름. /proc/devices, /sys에 표시됨 보통 "mydevice"처럼 간단한 문자열 |
3-2. alloc_chrdev_region() 함수란?
커널이 비어 있는 장치 번호를 자동으로 찾아서 할당해주는 함수
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
매개변수 | 설명 | 쉽게 말하면 |
dev | 할당된 장치 번호가 저장되는 곳 (dev_t 포인터) | 커널이 정해준 장치 번호 결과값을 담는 변수 |
firstminor | 부 번호 시작값 (보통 0) | 몇 번부터 쓸 건지. 보통은 0부터 시작 |
count | 부 번호 개수 | 장치를 몇 개 등록할지, 예: 3개면 0~2 |
name | 디바이스 이름 | /proc/devices나 /sys에 나오는 드라이버 이름 |
3-3. 장치 번호의 타입: dev_t
dev_t는 장치 번호를 저장하는 자료형입니다. linux/types.h 에 정의 되어 있습니다.
구성 | 비트 수 | 설명 |
Major | 12 bits | 어떤 드라이버인지 구분 (ex. LED, UART, GPIO) |
Minor | 20 bits | 그 드라이버 안에서 어떤 개별 장치인지 구분 (ex. led0, led1) |
장치 번호 관련 매크로는 아래와 같습니다.
매크로 | 설명 |
MAJOR(dev) | dev_t에서 주 번호 추출 |
MINOR(dev) | dev_t에서 부 번호 추출 |
MKDEV(major, minor) | 주 번호 + 부 번호를 하나의 dev_t로 결합 |
dev_t dev;
int major, minor;
dev = MKDEV(240, 0); // 주 번호 240, 부 번호 0
major = MAJOR(dev); // 240
minor = MINOR(dev); // 0
4. 캐릭터 디바이스 등록 흐름 (struct cdev 사용)
- cdev_init()으로 구조체를 초기화합니다
- cdev_add()로 커널에 등록합니다
- cdev_del()로 등록을 해제합니다
struct cdev my_cdev;
dev_t dev; // 장치 번호
cdev_init(&my_cdev, &fops); // fops는 file_operations 구조체
my_cdev.owner = THIS_MODULE; // 소유 모듈 설정
cdev_add(&my_cdev, dev, 1); // 커널에 장치 등록
cdev_del(&my_cdev); // 해제
file_operations 구조체를 통해 open, read, write, ioctl 함수들을 연결합니다.
4-1. struct cdev 란?
리눅스 커널에서 캐릭터 디바이스의 실제 객체를 표현하는 구조체입니다.
이걸 통해 read, write, open 같은 작업을 커널이 연결해 줍니다.
필드 | 설명 |
struct kobject kobj; | 커널 오브젝트. /sys 파일 시스템에 장치를 노출할 때 사용 |
struct module *owner; | 이 디바이스를 소유한 커널 모듈을 가리킴 → 보통 THIS_MODULE |
const struct file_operations *ops; | read, write, open, ioctl 등의 함수가 들어 있는 구조체 포인터 |
struct list_head list; | 내부 관리용 리스트. 커널이 장치들을 리스트로 연결해 관리할 때 사용 |
dev_t dev; | 이 장치가 사용하는 장치 번호 (MKDEV()로 만든 것) |
unsigned int count; | 부 번호의 개수 (이 디바이스가 담당하는 장치 개수) |
4-2. cdev_init() 함수란?
캐릭터 디바이스 구조체인 struct cdev를 초기화해주는 함수
void cdev_init(struct cdev *cdev, struct file_operations *fops);
// 예제 코드
struct cdev my_cdev;
struct file_operations fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
.open = my_open,
.release = my_release,
};
cdev_init(&my_cdev, &fops); // 구조체 초기화
매개변수 | 설명 |
*cdev | 초기화할 cdev 구조체 (이미 메모리에 선언되어 있어야 함) |
*fops | 장치가 사용할 file_operations 구조체 포인터 → read(), write(), open() 같은 함수 포인터들이 들어 있음 |
4-3. cdev_add() 함수란?
초기화된 struct cdev 구조체를 커널에 등록해서, 해당 장치 번호로 read, write, open 등이 동작하도록 만드는 함수
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
//예제 코드
dev_t dev; // 장치 번호
struct cdev my_cdev;
alloc_chrdev_region(&dev, 0, 1, "mydev"); // 장치 번호 자동 할당
cdev_init(&my_cdev, &fops); // cdev 구조체 초기화
cdev_add(&my_cdev, dev, 1); // 커널에 장치 등록
매개변수 | 설명 |
*cdev | 등록할 struct cdev 구조체 포인터 |
num | 등록할 장치 번호 (MKDEV(240, 0) 같은 dev_t) |
count | 등록할 부 번호 개수 (대부분 1개면 1) |
구분 | 등록 전 | 등록 후 |
chrdevs[] | name = null, fops = null | name = "sk_fops", fops = 함수포인터 |
유저 read() | 실패 (no such device) | sk_read() 함수 호출됨 |
디바이스 드라이버 동작 | X | O |
5. /dev 디렉토리
리눅스에서 모든 장치를 파일처럼 다루기 위해 만든 디바이스 파일들이 모여있는 디렉토리입니다.
실제 드라이버와 연결된 인터페이스이며, 사용자가 read(), write() 등 시스템 호출을 하면 /dev/장치파일을 통해 커널 드라이버로 전달됩니다.
디바이스 파일은 Major 번호와 Minor 번호를 기반으로 커널과 연결되어있습니다.
5-1. /dev 장치 파일 자동 생성 방법
// 드라이버에서
device_create(class, parent, devt, drvdata, "mydev"); // 생성
device_destroy(class, devt); // 제거
// 실습 예제
struct class *my_class;
struct device *my_device;
// 1. 클래스 생성
my_class = class_create(THIS_MODULE, "my_class");
// 2. 장치 생성 (→ /dev/mydev 자동 생성됨)
my_device = device_create(my_class, NULL, dev, NULL, "mydev");
// 3. 해제 시
device_destroy(my_class, dev);
class_destroy(my_class);
- class: class_create()로 만든 클래스
- devt: 장치 번호 (MKDEV(240, 0) 같은 것)
- "mydev": /dev/mydev로 생성됨
5-2. /dev 장치 파일 수동 생성 방법
관리자 권한이 필요합니다.
mknod()는 사용자 영역의 시스템 콜입니다.
mknod /dev/name type major minor
- Minor number
- Major number
- b(block) or c(character)
- Device Name
6. /proc과 /sys 가상 파일 시스템
6-1. /proc — 커널 정보 (by kernel)
- 가상 파일 시스템으로, 커널이 제공하는 시스템 상태, 설정, 프로세스 정보를 담고 있습니다.
- 모든 정보는 커널 메모리에서 동적으로 생성되며, 시스템 상태를 조회하거나 디버깅할 때 주로 사용합니다.
경로 | 내용 |
/proc/cpuinfo | CPU 정보 |
/proc/meminfo | 메모리 정보 |
/proc/modules | 로드된 커널 모듈 목록 |
/proc/interrupts | 인터럽트 사용 현황 |
/proc/devices | 등록된 장치의 주 번호 목록 |
6-2. /sys — 드라이버/장치 정보 (by driver)
- sysfs 가상 파일 시스템으로, 커널이 로딩한 드라이버와 디바이스를 구조적으로 보여줍니다.
- 장치 트리(Device Tree)처럼 계층 구조로 구성되어 있으며, 드라이버 개발자나 디바이스 관리자가 디버깅할 때 주로 사용합니다.
경로 | 설명 |
/sys/class | LED, input, tty 등 논리적 장치 |
/sys/block | 블록 장치 (e.g., sda) |
/sys/bus | 장치 버스 (USB, I2C, SPI 등) |
/sys/devices | 실제 장치 노드 (디바이스 트리 구조) |
/sys/module | 로드된 모듈 상세 정보 (파라미터 포함) |
6-3. 정리 비교
항목 | /proc | /sys |
목적 | 시스템 상태 정보 제공 | 디바이스/드라이버 구조 표현 |
구성 주체 | 커널 | 커널 + 드라이버 |
사용 목적 | 시스템 전반 정보 확인 (CPU, 메모리, 프로세스 등) | 디바이스 트리 구조 확인, 디바이스 관리 |
대표 예시 | /proc/devices, /proc/interrupts | /sys/class/gpio, /sys/bus/usb/devices |
7. open/close 시스템 콜과 드라이버 함수의 매개변수 관계
7-1. open
[사용자 영역]
int fd = open(const char *pathname, int flags);
- pathname: 예) "/dev/mydev"
- flags: 열기 옵션 (읽기/쓰기 등)
- 리턴값: fd (파일 디스크립터)
[커널 영역]
int xxx_open(struct inode *inode, struct file *filp);
- 커널이 open("/dev/mydev")을 호출하면, 내부에서 이 함수를 호출함
- pathname → inode로 변환됨 (디바이스 정보 포함)
- flags → filp 구조체 내부로 전달됨 (접근 모드, private_data 등)
유저 공간 매개변수 | 커널 내부 함수로 변환 |
pathname | struct inode *inode |
flags | struct file *filp |
7-2. close
[사용자 영역]
int ret = close(int fd);
[커널 영역]
int xxx_release(struct inode *inode, struct file *filp);
8. Interrupt 기반 Read/Write 흐름
유저 프로그램
↓
read(fd, buf, size) 또는 write(fd, buf, size)
↓
file_operations 구조체의 read/write 함수 호출
↓
버퍼 처리 / 메모리 복사 / 하드웨어 제어
8-1. Read
1. Interrupt Service Routine (ISR)
- [하드웨어]:
- 하드웨어 내부에 데이터가 어느 정도 쌓이면 → 인터럽트를 발생시킴
- [커널]:→ 데이터를 커널 내부 Buffer에 저장
- 커널은 등록된 ISR(Interrupt Service Routine)을 호출해서
2. Read 함수
- 유저 공간에서 read(fd, buf, size) 호출 시,
- 커널은 이미 ISR이 쌓아둔 Buffer에서 데이터를 꺼내고
- copy_to_user()로 유저 공간으로 복사해줌
→ 이때 read는 비동기(asynchronous) 입출력처럼 동작
8-2. Write
1. [HW] 데이터 전송 상황 감지
- 하드웨어(예: UART 송신 버퍼)는 내부 버퍼가 비어갈 때 → “이제 새로운 데이터를 채워줘!”라고 인터럽트 발생
- 보통 Transmit FIFO가 비어가는 시점 또는 전송 완료 시점에 인터럽트가 걸림
2. [Buffer] → [HW]
- 커널은 write 함수 호출 당시 유저 공간에서 받아온 데이터를 → 커널 내부 Buffer에 저장해 둠
- 인터럽트가 발생하면, → 커널의 ISR이 Buffer에서 데이터를 꺼내서 하드웨어 레지스터에 씀
8. file_operations 구조체
struct file_operations sk_fops = {
.open = sk_open,
.release = sk_release,
.read = sk_read,
.write = sk_write,
.unlocked_ioctl = sk_ioctl,
};
사용자 공간에서의 open(), read() 등 함수 호출은 이 구조체를 통해 커널 내부 함수로 연결됩니다.
9. 사용자 ↔ 커널 데이터 복사 흐름
- 사용자 → 커널: copy_from_user()
- 커널 → 사용자: copy_to_user()
ssize_t sk_read(...) {
copy_to_user(user_buf, kernel_buf, size);
}
사용자가 호출한 read/write 함수는 내부적으로 위 함수들을 통해 데이터를 주고받습니다.
10. ioctl 시스템 콜
read/write 이외의 명령 전달이나 설정 제어에 사용되는 시스템 콜입니다.
예를 들어, LED ON/OFF, 모드 설정 등의 작업을 처리할 수 있습니다.
#define MY_MAGIC 'L'
#define LED_ON _IO(MY_MAGIC, 1)
#define LED_OFF _IO(MY_MAGIC, 2)
드라이버에서는 switch(cmd) 문을 통해 명령을 처리하며, 매직 넘버와 명령 번호를 분석하기 위해 아래 매크로들을 활용합니다.
분석 매크로 | 설명 |
_IOC_TYPE(cmd) | 매직 넘버 추출 |
_IOC_NR(cmd) | 명령 번호 추출 |
_IOC_DIR(cmd) | 데이터 방향 (읽기/쓰기) |
int ioctl(int fd, int cmd, ...);
인자 | 설명 |
fd | 대상 디바이스 파일 (open()으로 얻은 파일 디스크립터) |
cmd | 어떤 제어 명령을 보낼지 (매크로 상수), 정수로 보낸다 |
... | 필요하면 추가 인자 (옵션, 포인터 등) |
10-1. 흐름 단계별 설명
1. User 영역
ioctl(fd, LED_ON, NULL);
- 유저 앱에서 ioctl 시스템 콜 호출
- fd는 열어둔 디바이스 파일
- LED_ON은 _IO() 같은 매크로로 정의된 명령
2. Device File (디바이스 파일을 통해 커널 진입)
- file_operations.unlocked_ioctl을 통해
- 커널 드라이버의 xxx_ioctl() 함수로 연결됨
3. Kernel 영역
int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
- 여기서 cmd를 해석해서 어떤 동작을 수행할지 결정
4. cmd 유효성 검사
switch (_IOC_NR(cmd)) {
case 1: // LED_ON
case 2: // LED_OFF
}
- _IOC_NR(), _IOC_TYPE(), _IOC_DIR() 등을 사용해
- 명령 번호, 타입, 방향 검사
- 만약 유효하지 않으면:
- return -EINVAL;
5. 메모리 접근 및 처리
- 만약 추가 인자(argp)가 있다면:
copy_from_user(&kbuf, (void __user *)arg, sizeof(kbuf));
또는
copy_to_user((void __user *)arg, &kbuf, sizeof(kbuf));
[User]
ioctl(fd, request, argp)
↓
[Device File]
↓
[Kernel: ioctl(inode, file, cmd, arg)]
↓
- 명령 유효성 검사 (_IOC_TYPE, _IOC_NR 등)
- switch(cmd)
- 메모리 복사 (copy_from_user, copy_to_user)
10-2. Magic Number란?
- 각 드라이버마다 고유한 식별자로 쓰는 1바이트 값
- 예: 'L' → LED, 'M' → Motor 등
- Documentation/ioctl/ioctl-number.rst 참고해서 충돌 없도록 선정
#define MY_MAGIC 'L'
#define LED_ON _IO(MY_MAGIC, 1)
#define LED_OFF _IO(MY_MAGIC, 2)
11. 전체 흐름 정리
드라이버 등록
insmod my_driver.ko
→ module_init() 실행
→ alloc_chrdev_region + cdev_add + device_create
사용자 프로그램 동작
fd = open("/dev/rpihat", O_RDWR);
write(fd, data, size);
ioctl(fd, LED_ON);
read(fd, buf, size);
close(fd);
드라이버 제거
rmmod my_driver
→ module_exit() 호출 → cdev_del + unregister_chrdev_region
11. 실습코드 (sk_driver.c)
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/device.h>
/*TODO:
implement license
GPL
*/
MODULE_LICENSE("GPL");
static int sk_major=0, sk_minor=0;
static int result;
static dev_t sk_dev;
static struct cdev sk_cdev;
static struct file_operations sk_fops;
static struct class *dev_class;
static int sk_open(struct inode *inode, struct file *filp)
{
printk("Device has been opened\n");
return 0;
}
static int sk_release(struct inode *inode, struct file *filp)
{
printk("Device has been closed \n");
return 0;
}
static ssize_t sk_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
char data[50];
size_t copied = copy_from_user(data, buf, count);
if ( copied )
pr_err("(dev) error copy from user");
printk("(dev) data >>>>> = %s : %lu : %lu\n", data, count, copied );
return count;
}
static ssize_t sk_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
char data[20] = "this is read func...";
size_t copied = copy_to_user(buf, data, count);
if ( copied )
pr_err("(dev) error copy to user");
return 0;
}
static ssize_t sk_ioctl(struct file *filp, unsigned int cmd, unsigned long arg )
{
/*TODO: implement
cmd
*/
switch(cmd){
case '0': { printk("\ncmd = 0\n");break;}
case '1': { printk("\ncmd = 1\n");break;}
case '2': { printk("\ncmd = 2\n");break;}
case '3': { printk("\ncmd = 3\n");break;}
default: return 0;
}
return 0;
}
/*TODO: implement file operations
open
release
write
read
unlocked_ioctl
*/
static struct file_operations sk_fops = {
.open = sk_open,
.release = sk_release,
.write = sk_write,
.read = sk_read,
.unlocked_ioctl = sk_ioctl,
};
static int sk_register_cdev(void)
{
int error;
if(sk_major){
/*TODO:
implement: make up device number
MKDEV
*/
sk_dev = MKDEV (sk_major, sk_minor);
error = register_chrdev_region(sk_dev, 1, "sk");
}else{
/*TODO:
implement: allocate device number
alloc_chrdev_region
*/
error = alloc_chrdev_region(&sk_dev, sk_minor, 1 , "sk");
/*TODO:
implement: extract major number from device number
MAJOR
*/
sk_major = MAJOR(sk_dev);
}
if(error<0){
printk(KERN_WARNING "sk: can't get major %d\n", sk_major);
return result;
}
printk("(dev) major number = %d\n", sk_major);
/*TODO:
implement: initialize cdev struct
init
*/
cdev_init(&sk_cdev, &sk_fops);
sk_cdev.owner = THIS_MODULE;
sk_cdev.ops = &sk_fops;
/*TODO:
implement: register cdev to kernel
add
*/
error = cdev_add(&sk_cdev, sk_dev, 1);
if(error){
printk(KERN_NOTICE "sk Register Error %d\n", error);
unregister_chrdev_region(sk_dev, 1);
return error;
}
// 클래스 생성
dev_class = class_create(THIS_MODULE, "rpihat");
if (IS_ERR(dev_class)){
printk(KERN_ERR "Failed to create device class\n");
error = PTR_ERR(dev_class);
goto r_class;
}
// 장치 노드 생성
if (IS_ERR(device_create(dev_class, NULL, sk_dev, NULL, "rpihat"))){
printk(KERN_ERR "Failed to create device file\n");
error = -EINVAL;
goto r_device;
}
printk(KERN_INFO "Device /dev/rpihat created successfully\n");
return 0;
// 에러 처리
r_device:
class_destroy(dev_class);
r_class:
unregister_chrdev_region(sk_dev, 1);
return error;
}
/*
* TODO: create device class
*/
static int __init sk_init(void)
{
printk("(mod) The module is up... \n");
if((result = sk_register_cdev())<0)
{
return result;
}
return 0;
}
static void __exit sk_exit(void)
{
printk("(mod) The module is down... \n");
device_destroy(dev_class, sk_dev);
class_destroy(dev_class);
cdev_del(&sk_cdev);
unregister_chrdev_region(sk_dev, 1);
}
module_init(sk_init);
module_exit(sk_exit);
1. 헤더 포함 및 라이선스 명시
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/device.h>
MODULE_LICENSE("GPL");
- 필수 커널 헤더 포함
- GPL 라이선스를 명시해야 커널 심볼을 사용할 수 있음
2. 전역 변수 및 구조체 정의
static int sk_major=0, sk_minor=0;
static int result;
static dev_t sk_dev;
static struct cdev sk_cdev;
static struct class *dev_class;
- sk_dev: 장치 번호 저장
- sk_cdev: 커널에 등록할 캐릭터 디바이스 객체
- dev_class: /dev 하위 장치 생성을 위한 클래스
3. 주요 파일 연산 함수 구현
static int sk_open(...) { printk("Device has been opened\n"); return 0; }
static int sk_release(...) { printk("Device has been closed\n"); return 0; }
static ssize_t sk_write(...) { ... copy_from_user ... }
static ssize_t sk_read(...) { ... copy_to_user ... }
static ssize_t sk_ioctl(...) { ... switch(cmd) ... }
- 사용자 앱의 open(), read(), write(), ioctl() 호출을 처리
- copy_from_user(), copy_to_user()로 사용자 공간과 데이터 전달
- ioctl()은 단순 숫자 비교 기반으로 커맨드를 처리함 (향후 매크로로 확장 가능)
4. file_operations 연결
static struct file_operations sk_fops = {
.open = sk_open,
.release = sk_release,
.write = sk_write,
.read = sk_read,
.unlocked_ioctl = sk_ioctl,
};
- 커널은 /dev/rpihat을 통해 사용자 요청이 들어오면 이 구조체의 함수로 연결함
5. 장치 번호 등록 및 cdev 등록
error = alloc_chrdev_region(&sk_dev, sk_minor, 1 , "sk");
sk_major = MAJOR(sk_dev);
cdev_init(&sk_cdev, &sk_fops);
cdev_add(&sk_cdev, sk_dev, 1);
- alloc_chrdev_region()으로 자동 장치 번호 할당
- cdev_init() + cdev_add()로 커널에 등록
6. /dev 장치 노드 자동 생성
dev_class = class_create(THIS_MODULE, "rpihat");
device_create(dev_class, NULL, sk_dev, NULL, "rpihat");
- udev를 통해 /dev/rpihat 자동 생성
- 사용자 프로그램은 해당 파일을 통해 디바이스 접근 가능
7. 모듈 초기화 및 정리
static int __init sk_init(void) { return sk_register_cdev(); }
static void __exit sk_exit(void) {
device_destroy(dev_class, sk_dev);
class_destroy(dev_class);
cdev_del(&sk_cdev);
unregister_chrdev_region(sk_dev, 1);
}
- insmod 시 sk_init() 실행
- rmmod 시 sk_exit() 실행하여 자원 해제
'임베디드' 카테고리의 다른 글
임베디드 리눅스 드라이버의 이해 5편: 커널과 하드웨어의 연결 (0) | 2025.06.04 |
---|---|
임베디드 리눅스 드라이버의 이해 4편: 인터럽트 처리의 원리 (1) | 2025.06.04 |
임베디드 리눅스 드라이버의 이해 3편: 커널에서의 메모리 할당 (0) | 2025.06.04 |
임베디드 리눅스 커널 구조와 드라이버의 이해 1편 (0) | 2025.05.26 |
임베디드 시스템의 이해 (1) | 2025.04.18 |