0️⃣ 들어가며

드라이버 제작 기초의 두 번째 편이다.
이번에 들어가야 하는 내용이 매우매우 많은데
코드를 올릴 수가 없다 보니 일단 정리한 내용을 산더미처럼 가져왔습니다 ( •̀ ω •́ )✧


1️⃣ 학습 내용

3-2. 하드웨어 통신

✅ 하드웨어 통신과 시스템 콜

  • read()write() 구현 시 고려사항

    1. 유저 영역과 커널 영역 간 데이터 이동

      커널 메모리 공간에는 직접 접근이 불가

      copy_to_user() / copy_from_user() 사용

    2. 블록 처리 (Blocking I/O)

      읽을 데이터가 없거나 쓰기를 위한 버퍼가 꽉 찼을 때 프로세스 Sleep하는 로직

      wait_queue 사용

    3. 하드웨어 제어 함수

      실제 레지스터 읽기/쓰기

      I/O Port 또는 Memory Mapped I/O 사용

    4. 동시성 제어 (Concurrency)

      여러 프로세스가 동시에 접근할 때의 경쟁 처리

      Mutex, Semaphore, Spinlock 등 사용

    5. 인터럽트 서비스 함수와의 경쟁 처리

      Top Half(ISR)와 프로세스 컨텍스트가 공유 자원에 접근할 때 보호

  • Read Callback 구조

    ssize_t xxx_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
    {
        // 1. 잠금 (동시성 제어)
        if (mutex_lock_interruptible(&my_mutex))
            return -ERESTARTSYS;
    
        // 2. 데이터 확인 및 Blocking 처리
        // 데이터가 없으면 루프를 돌며 Sleep (wait_queue 대기)
        while (!is_data_ready()) { 
            mutex_unlock(&my_mutex); // 잠들기 전 락 해제 (중요!)
    
            // 논블로킹 모드(O_NONBLOCK)면 바로 리턴
            if (filp->f_flags & O_NONBLOCK)
                return -EAGAIN;
    
            // 프로세스를 재움 (인터럽트 발생 시 깨어날 수 있음)
            if (wait_event_interruptible(my_wait_queue, is_data_ready()))
                return -ERESTARTSYS; // 시그널에 의해 깨어남
    
            // 깨어난 후 다시 락 획득
            if (mutex_lock_interruptible(&my_mutex))
                return -ERESTARTSYS;
        }
    
        // 3. 데이터 복사 (Kernel -> User)
        // copy_to_user(buf, kernel_data, count);
        
        // 4. 잠금 해제
        mutex_unlock(&my_mutex);
        
        return copy_size;
    }

✅ Memory Mapped I/O 접근 제어

  • Memory Mapped I/O의 접근

    MMIO를 사용하면 하드웨어 레지스터에 메모리처럼 접근 가능

    접근할 때는 전용 접근 함수(매크로)를 사용해야 함

    가상 주소로 매핑된 하드웨어 레지스터 영역에 데이터 읽고 쓰기 작업 수행

  • MMIO 읽기 매크로

    특정 가상 주소(addr)에서 값을 읽어 반환하는 함수

    • readx 형태의 구형 함수

      readb(addr); // 8bit data
      readw(addr); // 16bit data
      readl(addr); // 32bit data
    • ioread 계열 함수

      ioread8(addr);  // 8bit data
      ioread16(addr); // 16bit data
      ioread32(addr); // 32bit data
  • MMIO 스트림 데이터 읽기

    레지스터에서 연속된 데이터를 읽어 버퍼에 저장하며, 루프를 돌며 읽는 것보다 효율적

    지정된 I/O 주소 addr 에서 buf 가 가리키는 영역으로 count 만큼 반복해서 읽음

    void ioread8_rep(void __iomem *addr, void *buf, unsigned long count);
    void ioread16_rep(void __iomem *addr, void *buf, unsigned long count);
    void ioread32_rep(void __iomem *addr, void *buf, unsigned long count);
  • MMIO 쓰기 매크로

    특정 가상 주소(addr)에 지정한 값(value)을 쓰는 함수

    • writex 형태의 구형 함수

      writeb(value, addr); // 8bit data
      writew(value, addr); // 16bit data
      writel(value, addr); // 32bit data
    • iowrite 계열 함수

      iowrite8(value, addr);  // 8bit data
      iowrite16(value, addr); // 16bit data
      iowrite32(value, addr); // 32bit data
  • MMIO 스트림 데이터 쓰기

    버퍼에 있는 데이터를 레지스터로 연속해서 전송

    buf 가 가리키는 메모리 영역에서 count만큼의 데이터를 읽어 addr 주소에 반복해서 씀

    void iowrite8_rep(void __iomem *addr, const void *buf, unsigned long count);
    void iowrite16_rep(void __iomem *addr, const void *buf, unsigned long count);
    void iowrite32_rep(void __iomem *addr, const void *buf, unsigned long count);

✅ 주소 체계와 주소 변환

  • 물리 주소와 가상 주소

    MMU : 프로세서가 메모리에 접근하는 가상 주소를 변환 테이블을 참고해 실제 물리 주소로 변환

    가상 주소 : 프로세서에서 사용하는 논리적 주소

    물리 주소 : 실제 하드웨어적으로 설정된 고정 주소

  • 유저 공간과 커널 공간의 데이터 전송 API

    • 헤더

      <asm/uaccess.h> : 구식, 직접 포함하는 방식

      <linux/uaccess.h> : 현대 표준, 내부적으로는 아키텍처에 맞춰 asm 파일 가져옴

    • 유효성 검사

      unsigned long access_ok(int type, const void *addr, unsigned long size);

      주어진 메모리 범위 전체가 유효한 사용자 공간에 속하는지 검사

      데이터 접근 전에 유효성 검사 필수

    • 블록 데이터 전송

      // 커널 메모리 블록 데이터를 사용자 메모리 블록 데이터에 쓰기
      unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
      
      // 사용자 메모리 블록 데이터를 커널 메모리 블록 데이터에 쓰기
      unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
    • 단일 값 전송

      // 사용자 공간의 데이터 읽기
      int get_user(x, ptr);
      
      // 커널 변수 값을 사용자 공간에 쓰기
      int put_user(x, ptr);

      x 는 포인터가 아닌 변수 형태이며, ptr 의 타입에 따라 복사할 바이트 크기가 결정됨

✅ I/O 메모리 할당 및 가상 주소 매핑

  • 디바이스 통신 : I/O 메모리

    ARM 등 대부분의 임베디드 아키텍처에서는 Memory Mapped I/O 방식을 사용

    주변 장치(Peripheral)에 접근할 때는 SFR(Special Function Register)을 통해 접근 → I/O 메모리

    I/O 메모리는 일반 메모리와 동일한 공간(Flat Memory)에 배치되어 주소를 가짐

    각 주소별로 Memory Mapped 방식으로 장치들이 매핑되어 있음

    주 메모리와 같은 방식으로 접근하지만, 포인터를 사용한 물리 주소 직접 접근은 자제

    커널이 제공하는 접근 함수(readl, writel)를 사용할 것

  • 접근 흐름

    자원 예약 → 주소 매핑 → 데이터 입출력 → 자원 해제

    1. I/O 메모리 영역 할당(예약)

      헤더 : <linux/ioport.h>

      struct resource *request_mem_region(resource_size_t start, resource_size_t len, const char *name);

      start : 물리 주소의 시작점

      len : 예약하려는 메모리 영역의 길이

      name : 소유하는 디바이스나 드라이버 식별 이름, /proc/iomem 에 표시됨

      반환값 : 성공 시 비어있지 않은 포인터, 실패 시 NULL

      예약된 메모리는 /proc/iomem 파일에서 등록된 물리 메모리 맵 확인 가능

      # cat /proc/iomem
      00000000-3b3fffff : System RAM
      00000000-00000fff : reserved
      00210000-0113ffff : Kernel code
      01140000-0154ffff : reserved
      ...
    2. I/O 메모리 주소 매핑

      헤더 : <asm/io.h> 또는 <linux/io.h>

      void __iomem *ioremap(unsigned long phys_addr, unsigned long size);

      물리 주소를 커널의 가상 주소 공간에 매핑

      반환값 : 접근 가능한 가상 주소 포인터

    3. I/O 자원 해제

      // 커널 가상 주소 매핑 해제
      void iounmap(volatile void __iomem *addr);
      
      // 메모리 영역 해제
      void release_mem_region(unsigned long start, unsigned long len);
  • 프로세스가 사용 중인 메모리 맵

    프로세스를 실행하고 pid 확인

    pmap <pid> 명령어로 현재 프로세스가 사용 중인 메모리 맵 확인

    root@rping:~/work_drivers/exercise/23# ./a.out &
    [1] 4582
    root@rping:~/work_drivers/exercise/23# pmap 4582
    4582:   ./a.out
    000000557ab90000      4K r-x-- a.out
    000000557aba0000      4K r---- a.out
    000000557aba1000      4K rw--- a.out
    0000007fbe7d0000   1396K r-x-- libc-2.31.so
    0000007fbe92d000     60K ----- libc-2.31.so
    0000007fbe93c000     16K r---- libc-2.31.so
    0000007fbe940000      8K rw--- libc-2.31.so
    0000007fbe942000     12K rw---   [ anon ]
    ...
  • 메모리 매핑 방식 비교

    구분Kernel Space (커널 영역)User Space (유저 프로세스 영역)
    사용 주체디바이스 드라이버어플리케이션
    사용 방식ioremap()mmap()

✅ 유저 영역 메모리 매핑

  • 유저 영역의 메모리 매핑

    파일 디스크립터 fd 가 가리키는 디바이스 메모리를 프로세스의 가상 주소 공간에 매핑

    헤더 : <sys/mman.h>

    // 가상 주소에 매핑
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    
    // 매핑 해제
    int munmap(void *addr, size_t length);

    addr : 매핑을 원하는 시작 주소, 보통 NULL 로 지정하여 커널이 결정하도록 함

    length : 매핑할 메모리 영역의 크기로 바이트 단위

    prot : 권한 플래그

    flags : 매핑의 동작 방식(공유 여부)

    fd : 디바이스 파일 디스크립터

    offset : 파일의 시작점에서부터 매핑할 오프셋

  • 커널에서의 mmap 처리

    사용자가 mmap 을 호출하면 커널은 드라이버 내 xxx_mmap 함수 호출

    int xxx_mmap(struct file *filp, struct vm_area_struct *vma);

    struct vm_area_struct *vma : 할당받은 정보(주소)가 담긴 구조체

    <linux/mm.h> 에 구조체와 매크로 및 상수가 선언되어 있음

    • remap_pfn_range

      remap_pfn_range(vma, addr, pfn, size, prot)

      물리 주소를 vma 가 가리키는 유저 가상 주소 범위에 매핑해주는 커널 함수(PAGE_SIZE 단위)

      호출 전에 물리 주소 유효성 검사, 매핑 정보 유효성 검사 처리해 두어야 함

3-3. 커널의 스레드

3-3부터 3-6까지는 결석 이슈로 생략합니다.
ㅋㅋ 고멘!

3-4. 세마포어 경쟁

3-5. 블로킹 입출력

3-6. 작업의 딜레이

4. 디바이스 드라이버 모델

4-1. New Driver Model

✅ 드라이버의 기본 구조

  • 기본 모듈과 드라이버 형태

    진입점 : module_init() , module_exit()

    중요 객체 : cdev (캐릭터 장치 구조체), dev_t (장치 번호)

    인터페이스 : file_operations ( .opendev_open() 등 드라이버 내 콜백 함수 등록)

    하드웨어 제어 : isr() (인터럽트 핸들러)

✅ 통합 드라이버 모델과 sysfs

  • 드라이버 모델의 필요성

    최신 시스템은 버스와 장치의 복잡한 트리 구조를 가지므로 새로운 모델의 필요성 대두

    전원 관리 : 장치 연결 탐색, 부모 장치가 꺼지면 자식 장치도 전원 끄기

    유저 영역 접근 : 커널 내부의 장치 정보를 사용자 공간(sysfs)에서 접근 및 제어

    핫 플러그-인 : 전원이 켜진 상태에서 장치 연결 및 해제 가능

    디바이스 클래스 : 연결 방식에 상관 없이 디바이스를 계층적으로 구분

  • 드라이버의 구성 요소 : 장치 구성 표준화

    요소설명예시
    Bus (버스)프로세서와 장치 사이의 통신 채널. 장치와 드라이버를 매칭(Binding)하는 역할PCI, USB, I2C, SPI, Platform
    Device (장치)버스에 연결된 실제 물리적/논리적 객체마우스, 키보드, 센서 칩
    Driver (드라이버)특정 장치를 제어하는 소프트웨어 루틴usbhid, i2c-dev
    Class (클래스)연결 방식(Bus)과 상관없이 기능(Function)에 따른 분류Input(입력 장치), Sound, Net(네트워크)
  • 드라이버 모델의 장점

    전체 디바이스의 연결 트리 구조 : 시스템의 모든 장치 연결 상태를 파악 가능

    (연결 장치 열거, 소속 버스 및 상태 확인, 계층적 전원 관리)

    코드 중복 최소화 : 참조 카운팅 기능 기본 제공, kobject_get / kobject_put

    클래스별 구분 가능 : 사용자의 직관적 장치 구분 가능, 프린터/모니터 등 기능(Class)으로 분류

  • Sysfs 가상 파일 시스템

    통합 드라이버 모델의 내부 구조(kobject 계층)를 사용자 공간에 파일 시스템 형태로 보여줌

    /sys 에 마운트되고, 디스크에는 저장되지 않는 가상 파일 시스템

    (# mount -t sysfs sysfs /sys → see, /etc/fstab)

    커널 내부의 kobject 하나가 sysfs 의 디렉토리 하나에 대응되는 구조

4-2. Buses, Devices, Drivers and Classes

✅ 버스, 디바이스, 드라이버, 클래스의 구성

  • 드라이버 모델의 4대 요소

    하드웨어의 계층적 관리를 위해 4가지 추상화 객체를 사용

    /sys 디렉토리 구조에 반영되는 것을 확인할 수 있음

    각 객체는 <linux/device.h> 에 정의된 구조체로 표현

    # ls /sys
    block  bus  class  dev  devices  firmware  fs  kernel  module  power

✅ 버스 (Bus)

  • 버스

    프로세서와 디바이스 사이의 통신 채널, 디바이스와 드라이버를 연결하는 관리자

    struct bus_type 구조체로 표현됨

    드라이버 모델의 최상위 계층에 위치

    대부분의 버스는 이미 구현되어 있으므로 직접 구현하는 경우는 거의 없음

    버스는 드라이버 모델의 계층 상위단 (연결 상태로 보면 하위단)

  • 버스 등록 및 해제 API

    등록 : bus_register(struct bus_type *bus)

    해제 : bus_unregister(struct bus_type *bus)

    새로운 버스 등록 후에 /sys/bus/ 디렉토리 아래에 생성되는지 확인

✅ 디바이스 (Device)

  • 디바이스

    버스에 연결된 실제 장치 객체

    struct device 구조체로 표현됨

    커널 객체 관리를 위한 kobject , 연결된 버스 bus , 부모 디바이스의 정보 parent 포함

  • 디바이스 등록 및 해제 API

    등록 : device_register(struct device *dev)

    제거 : device_unregister(struct device *dev)

    호출에 성공하면 /sys/devices 아래에 새로운 디바이스가 보이게 됨

✅ 드라이버 (Driver)

  • 드라이버

    특정 디바이스를 제어하는 소프트웨어 루틴의 집합

    struct device_driver 구조체로 표현됨

    probe 함수는 디바이스 발견 시에 호출되는 초기화 함수

    remove 함수는 장치가 제거될 때 자원을 해제

    struct device_driver {
    		char *name;            // 드라이버 이름
    		struct bus_type *bus;  // 드라이버가 동작하는 버스
    		struct kobject kobj;   //필수적인 kobject
    		struct list_head devices;
    		int (*probe)(struct device *dev);     // 시스템 초기화 시 불리는 함수
    		int (*remove)(struct device *dev);    // 시스템에서 제거 시 불리는 함수
    		void (*shutdown)(struct device *dev); // 전원 끌 때 불리는 함수
    }
  • 드라이버 등록 및 해제 API

    등록 : driver_register(struct device_driver *drv)

    해제 : driver_unregister(struct device_driver *drv)

    성공하면 /sys/bus/acme-bus/drivers/ 아래 새로운 드라이버가 생김

✅ 클래스 (Class)

  • 클래스

    연결 방식이나 내부 구현 방법과 상관없이 기능에 따라 디바이스를 분류

    struct class 구조체로 표현됨

    디바이스에 마우스, 스피커 등의 기능으로 접근 가능

    대부분의 클래스는 /sys/class/ 에 나타나고 블록 디바이스는 /sys/block/ 에 위치

  • 클래스 등록 및 해제 API

    등록 : class_register(struct class *cls)

    해제 : class_unregister(struct class *cls)

4-3. Kobjects, Ksets and Subsystems

✅ Kobject (Kernel Object)

  • Kobject

    <linux>/kobject.h> 내에 정의된 struct kobject 구조체로 표현

    모든 디바이스 모델 객체(디바이스, 드라이버, 버스 등)가 공통적으로 포함하는 최상위 추상화 객체

    가장 기본적인 요소로서 cdev , device 같은 상위 구조체 내부에 포함되어 사용

    하나의 kobject/sys 파일 시스템에서 하나의 디렉토리로 나타남

    각 attribute는 /sys 파일 시스템에서 하나의 파일로 나타남

    kobject 는 부모 포인터 parent 를 통해 다른 kobjectkset 과 트리 구조를 형성

  • Kobject의 기능

    • 참조 카운트 (Reference Counting)

      객체의 생명 주기를 관리하는 기능

      객체가 참조되고 있을 때는 자원을 해제하지 않음

      객체를 사용하는 곳이 없으면 count가 0이 되고, 커널은 그 자원을 해제

      1. 초기화

        kobject_init() 에 의해 참조 카운트 1로 초기화

      2. 증가

        struct kobject *kobject_get(struct kobject *kobj)

        객체를 사용할 때 호출하여 카운트를 1 증가

      3. 감소

        void kobject_put(struct kobject *kobj)

        객체의 사용이 끝났을 때 호출하여 카운트를 1 감소

        카운트가 0이 되면 커널은 연결된 release 콜백을 호출하고 메모리 해제

    • Sysfs 표현

      커널 내부 상태를 사용자 공간에 노출

      kobject/sys 의 디렉토리, kobject 의 속성 kobj_attribute 은 디렉토리 내 파일로 표현

✅ Kset과 Subsystem

  • Kset

    동일하거나 다른 여러 type의 kobject 를 그룹으로 묶어 관리하는 컨테이너

    sysfs 상에서 상위 디렉토리를 형성

  • Subsystem

    적절한 이미지를 찾지 못했으니 필요하면 요청 plz.

4-4. 플랫폼 드라이버

✅ 플랫폼 드라이버 (Platform Driver)

  • 드라이버의 분류 요약

    1. 데이터 단위에 따른 분류

      문자 드라이버 (Char) : 바이트 단위로 데이터를 주고받음 (키보드, 센서 등)

      블록 드라이버 (Block) : 덩어리(Block) 단위로 데이터를 주고받음 (하드디스크, SSD)

    2. 버스 타입에 따른 분류 : USB, I2C, SPI 드라이버 등

    3. 기능에 따른 분류 : Input(마우스/키보드), IIO(산업용 센서) 등

  • 플랫폼 드라이버

    USB나 PCI처럼 물리적으로 존재하는 버스와 달리 가상으로 운영되는 버스

    CPU 내부에 연결된 Timer나 GPIO 컨트롤러 등을 다루며, 이 장치들은 플랫폼 디바이스로 정의됨

    디바이스 정보는 Device Tree 또는 platform_device 로 정의되어 커널에 전달됨

  • 드라이버 모델의 구성

    struct platform_device : 하드웨어에 대한 정보

    struct platform_driver : 장치 연결 시 실행되는 코드

  • 연결 구조

    • 내부 장치

      칩 내부 장치의 연결을 담당하는 버스는 플랫폼 버스

      내부 타이머, USB Adapter, I2C Adapter 등을 연결하는 컨트롤러

      칩마다 다르지만 ARM에서는 AXI, AHB, APB 등을 사용

    • 외부 장치

      I2C Adapter 등을 통해 밖으로 연결된 센서나 모듈 관리

      구조 : USB/I2C Adapter — 외부 device — USB/I2C Bus

      어댑터 자체에 적용되는 것은 Adapter Driver

      각 디바이스에 적용되는 것은 Client Driver

  • platform_driver 구조체

    <linux/platform_device.h> 에 정의되어 있으며, 플랫폼 버스에 특화된 기능을 제공

    struct platform_driver {
    		int (*probe)(struct platform_device *);
    		int (*remove)(struct platform_device *);
    		void (*shutdown)(struct platform_device *);
    		int (*suspend)(struct platform_device *, pm_message_t state);
    		int (*resume)(struct platform_device *);
    		struct device_driver driver;
    		const struct platform_device_id *id_table;
    };
    // prove : 디바이스의 초기화 루틴, 필수
    // remove : 디바이스가 제거될 때, 필수
    // shutdown : 시스템 종료 시 호출
    // suspend : 절전모드로 들어갈 때 호출
    // resume : 절전모드에서 빠져나올 때 호출
    // driver : 기존 device_driver를 확장하기 위해 상속받은 객체
    // id_table : 지원하는 디바이스의 종류(ID) 목록

✅ 플랫폼 드라이버 구현

  • platform_driver 구조체 정의

    모델별로 정의된 디바이스와 드라이버가 존재하고, 각 디바이스에 적합한 드라이버 모델을 정의해서 사용

    static struct platform_driver xxx_driver = {
        .probe      = xxx_probe,
        .remove     = xxx_remove,
        .driver     = {
            .name   = "my_device_name", // 디바이스 이름과 일치해야 매칭됨
            .owner  = THIS_MODULE,
            .of_match_table = xxx_of_match, // Device Tree 매칭용 (최신 커널 필수)
        },
    };
  • 등록과 해제

    등록 : platform_driver_register(&xxx_driver)

    해제 : platform_driver_unregister(&xxx_driver)


2️⃣ 느낀 점

드라이버는 커널 레벨에서 실행된다.
이 때문에 일반적인 프로그램에 비하면 좀 더 신경써야 할 부분들이 분명히 존재한다.
이걸 이해하고 드라이버 모델의 구조를 숙지한다면
거의 비슷한 형태로 버스, 디바이스, 드라이버, 클래스가 다가올 거라고 생각한다.
중간에 빠진 내용은 여기엔 없으나 필요한 내용이긴 하니까
여러 똑똑이들의 필기를 참고해서 알아 두기를 바랍니다 😏

0개의 댓글