포스트

임베디드 스터디 - 시스템 콜과 디바이스 드라이버 접근

임베디드 스터디 - 시스템 콜과 디바이스 드라이버 접근

file_operations 구조체

  • 리눅스에서 사용자 공간은 어떤 디바이스든 동일하게 open()read()write()로 접근함
  • 디바이스마다 실제 구현이 다르기 때문에, 커널은 vtable 방식으로 드라이버별 함수를 연결함
    • C++의 vtable과 동일한 개념
  • 이 함수 포인터 집합을 file_operations 구조체라고 함
1
2
3
4
5
6
7
8
struct file_operations {
    int     (*open)   (struct inode *, struct file *);
    ssize_t (*read)   (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)  (struct file *, const char __user *, size_t, loff_t *);
    long    (*ioctl)  (struct file *, unsigned int, unsigned long);
    loff_t  (*llseek) (struct file *, loff_t, int);
    int     (*release)(struct inode *, struct file *);
};
  • 드라이버 개발자는 이 구조체에 자신의 함수를 채워 넣고 커널에 등록함
  • 사용자가 read()를 호출하면 커널이 해당 디바이스의 함수 포인터를 찾아 실행함

주요 멤버 함수

open / read / write / release

  • open : 디바이스 파일을 열 때 호출
  • read : 디바이스에서 데이터를 읽을 때 호출
  • write : 디바이스로 데이터를 쓸 때 호출
  • release : 디바이스 파일을 닫을 때 호출 (close()에 대응)

ioctl

  • read/write로 처리하기 애매한 제어 명령을 전달할 때 사용함
    • 데이터 입출력이 아닌 디바이스 설정·제어가 목적
    • 보레이트(baud rate) 변경, LED 밝기 설정, 모터 속도 제어 등
1
2
ioctl(fd, UART_SET_BAUDRATE, 115200);
ioctl(fd, LED_SET_BRIGHTNESS, 80);
  • 커맨드 번호(두 번째 인자)로 어떤 제어를 수행할지 구분함

llseek

  • lseek() 시스템 콜이 커널로 들어오면 file_operations.llseek이 호출됨
  • 드라이버가 내부 파일의 오프셋 위치를 갱신하고, 이후 read()는 그 위치부터 데이터를 읽음
1
2
3
4
5
6
7
lseek(fd, 1024, SEEK_SET)
        ↓
file_operations.llseek 호출
        ↓
드라이버가 내부 오프셋을 1024로 갱신
        ↓
다음 read() → 1024번지부터 읽기
  • lllong long의 약자 — 대용량 스토리지의 큰 오프셋 값을 처리하기 위해 64비트 타입 사용

register_chrdev() / unregister_chrdev()

  • file_operations 구조체를 커널 드라이버 테이블에 등록/해제하는 함수
1
2
3
4
5
6
7
8
9
10
static int __init my_driver_init(void) {
    register_chrdev(4, "tty", &my_fops);
    //              │    │       │
    //         Major번호  이름   file_operations 구조체
    return 0;
}

static void __exit my_driver_exit(void) {
    unregister_chrdev(4, "tty");
}
  • register_chrdev() : module_init() 안에서 호출 — 모듈 로드 시 드라이버 테이블에 등록
  • unregister_chrdev() : module_exit() 안에서 호출 — 모듈 제거 시 드라이버 테이블에서 해제
    • 해제하지 않으면 모듈 코드는 사라졌는데 테이블에 함수 포인터가 남아 댕글링 포인터 문제 발생, 커널 패닉으로 이어진다.

전체 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
드라이버 개발자
    ↓
file_operations 구조체에 함수 포인터 등록
    ↓
module_init() → register_chrdev()로 커널 드라이버 테이블에 등록
    ↓
사용자 공간: open("/dev/ttyS0") → 시스템 콜
    ↓
커널: Major number로 드라이버 테이블 조회
    → file_operations.open 호출
    ↓
사용자 공간: read() → 시스템 콜
    ↓
커널: file_operations.read 호출 → UART 레지스터 접근 → 데이터 반환
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.