이로또

임베디드 리눅스 드라이버의 이해 2편: 캐릭터 디바이스와 장치 파일, ioctl 본문

임베디드

임베디드 리눅스 드라이버의 이해 2편: 캐릭터 디바이스와 장치 파일, ioctl

이로또 2025. 5. 27. 19:57

이 글에서 캐릭터 디바이스 드라이버를 중심으로, 리눅스 커널과 사용자 공간 간의 연결 구조를 정리했습니다. 장치 번호의 개념과 등록 방식, /dev 파일 생성 방법, cdev 구조체를 통한 커널 등록 방식, 그리고 read, write, ioctl 함수의 동작 흐름과 역할까지 실제 코드 흐름을 따라 확인 할 수 있습니다.

목차

  1. 캐릭터 디바이스란?
  2. 장치 번호의 개념 (Major / Minor)
  3. 장치 번호 등록 방법 (register_chrdev_region, alloc_chrdev_region)
  4. cdev 구조체와 커널 등록 흐름
  5. /dev 장치 파일 생성 방식 (자동 vs 수동)
  6. /proc, /sys 가상 파일 시스템
  7. file_operations 구조체와 함수 연결
  8. 사용자 공간 ↔ 커널 공간 데이터 흐름 (copy_from_user, copy_to_user)
  9. 인터럽트 기반 read/write 흐름
  10. ioctl 시스템 콜과 커맨드 매크로
  11. 전체 흐름
  12. 실습 코드
  13.  

1. 캐릭터 디바이스란?

캐릭터 디바이스는 1바이트씩 데이터를 처리하는 장치로, 대표적인 예로 키보드, 마우스등이 있습니다.

캐릭터 디바이스는 블록 디바이스와 달리 내부 버퍼 없이 실시간으로 데이터를 전달하며, read, write 함수를 통해 제어됩니다.

항목 캐릭터 디바이스 블록 디바이스
처리 단위 1바이트 512바이트 이상 블록
예시 키보드, 마우스, UART HDD, SSD
함수 read, write, open, close 등 request, submit_bio

 

1-1. 캐릭터 드라이버 개발 흐름

  1. init 함수: 모듈이 로드될 때 실행됨
  2. file_operations 구조체 정의: read, write 함수 등록
  3. register_chrdev_region() / alloc_chrdev_region(): 장치 번호 등록
  4. cdev_init() / cdev_add(): 커널에 디바이스 등록
  5. device_create(): /dev/mydevice 파일 생성
  6. read(), write(), open(): 유저 공간과 데이터 송수신 처리

1-2. 사용자 → 드라이버로 접근하는 흐름

  1. 사용자 코드에서 open("/dev/mydevice") 실행 
  2. 커널의 캐릭터 드라이버로 전달
  3. file_operations 구조체에 등록된 open() 실행
  4. 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() 실행하여 자원 해제