0️⃣ 들어가며

장치를 연결하는 이유는 무엇일까?
매우 간단함. 사용을 하려고!
리눅스는 장치도 파일처럼 취급한다는 건 모두가 알고 있을 거다. (Everything is 어쩌구)
장치를 사용하기 위해서는 장치 파일을 열고, 읽거나 쓰고, 닫는 작업이 필요하다.
이번에는 이런 과정에 쓰이는 시스템 콜백에 대해 알아보겠다.


1️⃣ 학습 내용

2-3. 시스템 콜백

✅ 시스템 콜백

  • 시스템 콜백 (System Callback)

    드라이버의 핵심 역할은 응용 프로그램의 요청을 받아 하드웨어를 제어하는 것

    시스템 콜백이란 응용 프로그램의 요청이 시스템 콜을 통해 커널을 거쳐 드라이버의 특정 함수로 전달되는 메커니즘

    프로세스 컨텍스트로 동작, 콜백 함수들이 실행될 때 커널은 호출한 프로세스 대신 함수를 실행시켜 줌

    struct file_operations 이 정의된 파일 시스템 <linux/fs.h>와 관련되어 있음

  • 드라이버 실행 컨텍스트

    • 프로세스 컨텍스트 (Process Context)

      방향 : Top (Application) → Bottom (Driver)

      응용 프로그램이 open/read/write 같은 시스템 콜을 호출했을 때 이루어짐

      struct file_operations 에 등록된 함수들이 이 컨텍스트에서 실행됨

    • 인터럽트 컨텍스트 (Interrupt Context)

      방향 : Bottom (Hardware) → Top (Driver)

      하드웨어에서 인터럽트 신호가 발생했을 때 이루어짐

      현재 실행 중인 프로세스와 무관하게 최대한 빨리 실행됨, Sleep(대기) 불가

  • 주요 시스템 콜 함수

    응용 프로그램 함수드라이버 역할설명
    open()장치 초기화장치 사용을 시작할 때 호출. 하드웨어 전원 켜기, 초기화 등 수행
    close()장치 해제장치 사용을 마칠 때 호출. 리소스 반환, 전원 끄기 등 수행 (드라이버 함수명은 release)
    read()데이터 읽기장치에서 데이터를 읽어 사용자 공간으로 복사(copy_to_user)
    write()데이터 쓰기사용자 공간의 데이터를 가져와(copy_from_user) 장치에 씀
    ioctl()제어 명령읽기/쓰기 외의 하드웨어 제어 (예: 통신 속도 설정, LED 점멸 모드 변경)
    mmap()메모리 매핑장치의 메모리(레지스터 등)를 사용자 프로세스 메모리에 직접 매핑하여 고속 접근
    poll()다중 입출력select(), poll(), epoll() 등을 지원. 데이터가 준비될 때까지 기다리거나 상태 확인
  • file_operations 구조체의 주요 구성

    #include <linux/fs.h>
    
    // 함수 프로토타입 선언 (미리 선언해두는 것이 좋음)
    static int xxx_open(struct inode *inode, struct file *filp);
    static int xxx_release(struct inode *inode, struct file *filp);
    static ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
    static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
    
    // file_operations 구조체 초기화
    static struct file_operations xxx_fops = {
        .owner   = THIS_MODULE,  // 모듈의 소유자 (참조 카운트 관리용)
        .open    = xxx_open,     // open() 대응
        .release = xxx_release,  // close() 대응
        .read    = xxx_read,     // read() 대응
        .write   = xxx_write,    // write() 대응
        // .unlocked_ioctl = xxx_ioctl, // ioctl() 대응 (최신 커널은 unlocked_ioctl 사용)
    };

open과 release – 준비/정리 콜백

  • User Space ↔ Kernel Space 매개변수 대응 관계

    // 어플리케이션에서의 사용 예시
    int main()
    {
    	int fd = open("/dev/rpihat", O_RDONLY); // 밑으로 내려가는 구조
    	int c = read(fd, buf, 10);
    	
    	close(fd);
    }
    // User Space
    int fd = open(const char *pathname, int flags);
    int ret = close(int fd);
    
    // Kernel (Driver)
    int xxx_open(struct inode *inode, struct file *filp);
    int xxx_release(struct inode *inode, struct file *filp);

    pathname : 커널에서 해당 경로를 해석하여 struct inode *inode 로 전달

    flags : 커널이 struct file *filp 를 생성하면서 내부에 플래그/모드 정보를 채움

      
  • 장치 준비 작업 : open

    처음으로 디바이스를 사용하는 시점에 디바이스 초기화와 검증 수행

    디바이스가 준비되지 않았거나 이에 준하는 하드웨어 문제가 있는지 디바이스 관련 오류 확인

    필요하다면 드라이버 전용 자료구조를 할당하고 filp->private_data 에 포인터 보관

  • 자원 정리 작업 : release

    open 에서 실행한 내용들을 원상복구하고 리소스 정리

    filp->private_data 로 관리하던 동적 메모리를 해제함

    하드웨어 사용 카운트 감소, 마지막 사용자라면 전원 끄기 작업 등을 수행함

  • 파일의 표현 : <linux/fs.h>

    리눅스에서 파일은 inode와 file, 두 구조체로 나누어 표현됨

    1. struct inode

      파일 자체의 정보를 나타내는 정적 메타데이터 구조체

      파일 타입, 소유자, 접근 권한, 파일 크기, 타임스탬프 등을 포함

    2. struct file

      파일이 어떻게 열려 있는지를 나타내는 동적 핸들 정보 구조체

      open() 호출 시에 커널이 생성하며, 프로세스가 사용하는 fd 는 이 구조체를 가리킴

      현재 파일의 오프셋, 접근 모드, 장치/파일의 file_operations 포인터 등을 포함

데이터 읽기 : read

  • 실행 흐름

    1. User Application

      read(fd, buff, size) 호출

    2. Kernel (VFS)

      해당 디바이스의 file_operation 에 등록된 xxx_read 함수 호출

    3. Driver

      xxx_read 에서는 I/O 제어, 하드웨어 상태를 확인하거나 내부 버퍼의 데이터를 조회

      메모리 복사 함수를 사용해 커널 데이터를 사용자 버퍼(buff) 로 전송

  • 데이터 획득 방식

    1. I/O 제어 함수

      드라이버가 하드웨어 레지스터를 직접 읽어 즉시 반환하는 방식

      예 : DHT11 온습도 센서, 요청 시점에 즉시 값을 읽어 오는 경우

    2. Interrupt Service (Interrupt Context)

      외부 데이터 혹은 비동기식을 사용하여 요청 시 데이터를 전달하지 못할 가능성이 있을 때 사용

      하드웨어 내부 버퍼가 데이터로 채워지면 인터럽트 발생

      ISR(Interrupt Service Routine)이 실행되어 하드웨어 버퍼의 내용을 드라이버 내부 버퍼로 이동

      xxx_read 가 호출되면 드라이버 버퍼에 쌓여 있던 데이터를 사용자에게 전달

  • 데이터 복사 함수 (커널 공간 → 유저 공간)

    put_user(x, ptr) : 단일 데이터를 복사

    copy_to_user(to, from, n) : 블록 단위의 다중 데이터를 복사

  • read System Call 예시

    n = read(fd, buf, size);

    sk_read(file, buf, count, f_ops) {
    		// H/W access code here!
    		copy_to_user(buf, data_read, count);
    }
    request_irq(service_number, irq_service) {
    		irq_service();
    }
    irq_service()
    {
    		H/W access;
    		조건;
    		Wake interruptable;
    }
  • read 시스템 콜 인터페이스

    디바이스의 offp 위치에서 count 바이트만큼 읽어서 사용자 영역인 buff 로 저장

    ssize_t xxx_read(struct file *filp, char *buff, size_t count, loff_t *offp);
    파라미터설명
    filp오픈된 파일 객체
    buff데이터를 저장할 사용자 영역의 버퍼 주소
    count읽기를 요청한 데이터의 크기 (Byte)
    offp현재 파일의 오프셋(위치)
    반환값성공: 읽은 바이트 수 (일반적으로 0 이상).
    실패: -1 반환 (에러 코드는 errno에 설정)
  • 데이터 복사 함수 (커널 → 유저)

    • copy_to_user(to, from, count)

      커널 메모리 from 에서 사용자 메모리 tocount 만큼 블록 복사

      반환값 : 복사에 실패한(남은) 바이트 수, 0이면 전송 성공

    • put_user(x, ptr)

      변수 x 의 값(단일 값)을 사용자 메모리 주소 ptr 에 씀

      반환값 : 성공 시 0, 실패 시 음수 에러 코드

데이터 쓰기 : write

  • 실행 흐름

    1. User Application

      write(fd, buff, size) 호출

    2. Kernel (VFS)

      file_operations에 등록된 xxx_write 함수 호출

    3. Driver

      xxx_write 에서는 메모리 복사 함수를 사용해 사용자 데이터(buff)를 커널 공간으로 이동

      I/O 제어 함수에 연결된 하드웨어에 출력 명령을 수행

  • 데이터 출력 방식 및 버퍼링

    1. I/O 제어 함수

      하드웨어 레지스터에 값을 직접 써서 즉시 제어

    2. Interrupt Service와 버퍼링 (Buffered I/O)

      하드웨어 처리 속도가 느려 즉시 전송이 불가능한 경우 사용

      하드웨어 버퍼가 모두 차서 더 이상 쓸 수 없다면 드라이버는 데이터를 소프트웨어 버퍼(Driver Buffer)에 임시 저장하고 복귀

      하드웨어가 데이터를 모두 처리하여 버퍼가 비워지면 인터럽트 발생

      ISR은 드라이버 버퍼에 대기 중이던 데이터를 하드웨어로 밀어 넣음

  • write System Call 예시

    n = write(fd, buf, size);

    sk_write(file, buf, count, f_ops) {
    		copy_from_user(data, buf, count);
    		// H/W access
    }
    outb(), writeb() // 8bit 단위
    outw(), writew() // 16bit 단위
    outl(), writel() // 32bit 단위
    *addr = data;
         ↓
    ldr r0, =data
    ldr r1, =addr
    str r1, [r0]
  • write 시스템 콜 인터페이스

    사용자 영역인 buff 에서 count 바이트만큼 읽은 후 디바이스의 offp 위치로 저장

    ssize_t xxx_write(struct file *filp, const char *buff, size_t count, loff_t *offp);
    파라미터설명
    filp오픈된 파일 객체 (struct file). private_data 등을 참조할 때 사용
    buff데이터가 담긴 사용자 영역의 버퍼 주소
    count쓰기 요청한 데이터의 크기 (Byte)
    offp현재 파일의 오프셋(위치)
    반환값성공: 기록된 바이트 수 (count와 같거나 적음).
    실패: -1 반환 (실제 에러 코드는 errno에 설정)
  • 데이터 복사 함수 (유저 → 커널)

    • get_user(x, ptr)

      단일 데이터를 가져옴

      사용자 메모리 ptr 에 있는 단일 값을 변수 x로 가져옴

      반환값 : 성공 시 0, 실패 시 음수 에러 코드

    • copy_from_user(to, from, count)

      블록 단위 데이터를 가져옴

      사용자 메모리 from 에서 커널 메모리 tocount 만큼의 블록을 복사

      반환값 : 복사에 실패한(남은) 바이트 수, 0이면 전송 성공

✅ ioctl (Input/Output Control)

  • ioctl 의 개념

    read/write 로는 처리가 어려운, 일반적인 디바이스의 I/O 제어를 위한 시스템 콜

    디바이스의 제어 및 설정 작업을 담당

    정해진 데이터 형식이 없으며, 명령어(Command)와 인자(arg/option)를 통해 동작 수행

    (예 : LED On/Off, 통신 속도 설정, 볼륨 Up/Down)

  • ioctl 시스템 콜 인터페이스

    • User Space (Application)

      #include <sys/ioctl.h>
      int ioctl(int fd, unsigned long request, ...);

      fd : 오픈된 파일 디스크립터

      request (cmd) : 디바이스에 내릴 명령어로, 32bit 정수 형태

      ... (arg) : 명령어에 필요한 추가 데이터

    • Kernel Space (Driver)

      long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
  • ioctl 의 명령어 구조 (Command Encoding)

    +-----------+--------------------------------+----------------+----------------+
    | Direction |              Size              |      Type      |     Number     |
    |  (2 bits) |           (14 bits)            |    (8 bits)    |    (8 bits)    |
    +-----------+--------------------------------+----------------+----------------+
    |  R/W/RW   |        Data Size (bytes)       |  Magic Number  |   Seq Number   |
    +-----------+--------------------------------+----------------+----------------+

    Direction : 데이터 전송 방향(_IOC_NONE , _IOC_READ , _IOC_WRITE)

    Size : 전송할 데이터의 크기를 바이트 단위로 나타냄 (예 : sizeof(int))

    Type : Magic Number, 충돌 방지용 드라이버 식별자 (Documentation/ioctl/ioctl-number.txt 참고)

    Number : 실제 명령, 드라이버 내에서 명령어를 구분하는 일련번호

  • 명령어 생성 매크로

    _IO(type, nr) : 데이터 전송이 없는 단순 명령 (예: Reset)

    _IOR(type, nr, size) : 디바이스에서 데이터를 읽어옴

    _IOW(type, nr, size) : 디바이스에 데이터를 씀

    _IOWR(type, nr, size) : 읽고 쓰기를 모두 수행

  • 명령어 해석 매크로

    _IOC_DIR(cmd) : 데이터 방향 추출

    _IOC_TYPE(cmd) : 매직 넘버(Type) 추출

    _IOC_NR(cmd) : 구분 번호(Number) 추출

    _IOC_SIZE(cmd) : 데이터 크기 추출


2️⃣ 느낀 점

딱 봐도 중요해 보이는 만큼 드라이버를 만들 때 가장 기본이 되는 부분이다.
많이 써보진 않았음에도 그동안 이 코드가 빠지는 드라이버가 없었다. (당연한 얘기임)
이건 숨쉬듯 자연스럽게 써볼 수 있도록 합시다요
다만 ioctl 함수는 최근에 다른 걸 쓴다는 이야기도 있으니 주의!

0개의 댓글