Linux kernel

Device Driver

sjoonb 2023. 4. 14. 18:03

Device Driver

리눅스 시스템에선 모든 것을 파일로 취급한다고 해도 무방하다. 

파일을 크게 정규파일, 장치파일로 나눌 수 있다. (이외에도 디렉토리, Symbolic link, Named Pipes, Socket 등의 파일 종류도 존재한다. 여기선, Devcie Driver 에 집중하기 위해 생략했다)

유저 프로그램 관점에서 정규파일과 장치파일에 접근하는 것은 다음과 같은 차이가 존재한다.

  • 정규 파일 접근: 시스템 콜을 호출하여 파일에 관한 작업을 요청 할 때 이는 VFS 를 통해 파일 시스템 계층으로 전달되어 실질적인 동작은 파일 시스템에서 이뤄지게 된다. 이 경우 유저프로그램과 디바이스 드라이버와의 상호 작용은 간접적으로 발생한다.
  • 장치파일 접근: 마찬가지로 시스템 콜을 호출한다. 요청은 VFS 를 통해 직접 디바이스 드라이버에 전달되고, 디바이스 드라이브는 요청을 해석하여 필요한 작업을 수행한 후 결과를 유저 프로그램에게 반환한다. 이 경우 사용자 작업은 디바이스 드라이버와 직접 상호작용한다고 볼 수 있다.

이전 포스팅에서 VFS 에 대하여 알아보았을 때 유저 프로그램에선 file strucutre 를 관리하고 그 안에 file_operations 구조체가 존재하며 VFS를 통해 파일과 관련한 동작을 처리할 수 있다는 것을 배웠다. 디바이스 드라이버는 해당 file_operations 구조체에서 참조하여 사용될 함수를 정의한 소프트웨어라고 볼 수 있다.

Device Driver 는 재사용성과 모듈성을 위해 wrapper 와 core 로 분리된다. 즉, 여러 디바이스 코어에서 같은 wrapper 를 재사용할 수 있는 것이다.

위 이미지에서 tty 는 teletypewritter 의 약자로, UNIX 계열 OS 에서 텍스티 기반 터미널 장치를 지칭한다. tty는 하드웨어 터미널(serial console)또는 가상 터미널을 의미할 수 있는데, 위 경우에는 하드웨어 serial 통신장치를 의미한다.

위 Device driver core 의 함수들은 serial 통신 장치에 사용된다. 간단하게, 데이터를 주고 받는데 사용되는 함수라고 보면된다. 디바이스 종류에 따라 device driver core 의 구현은 다를 것이다.

디바이스 드라이버는 또 크게 3가지 종류로 구분된다. 주로 사용되는 종류로는 크게 character driver, block driver, network driver 가 있다. character driver 는 byte 단위로 stream 되는 장치를 핸들링하며 여기에는 앞서 살펴본 tty 외에도 키보드, 마우스 등이 포함된다. block driver 는 블록단위의 데이터를 교환하며 HDD, SSD가 있다. network driver 는 주로 패킷과 관련한 데이터(약 1500B)를 처리하며 Ethernet, Wi-Fi, 블루트스의 어댑터가 이 타입에 속한다.

chrdevs 테이블은 등록된 문자 드라이버 관련 file_operations 을 추적할 수 있도록 도와주는 자료구조이다. major number 는 각 유형의 문자 드리이버에 할당되는 고유 식별자로 이는 여러 유형의 문자 드라이버를 구분하고 해당 드라이버와 연결하는데 사용된다.

드라이버와 커널 인터페이스의 구조는 다음과 같다.

blkdevs, chrdevs 는 각각 분리되어 자신만의 major number 를 관리한다.

다음으로 VFS와 Device driver 가 어떤식으로 연동되는지 살펴보자.

디바이스 드라이버를 커널에 등록하는 과정은 다음과 같다.

좌측은 블록 드라이버, 우측은 문자 드라이버의 등록 과정이고 약간 다른 방식으로 진행된다. 우측 문자 드라이버의 등록 과정을 중점으로 살펴보겠다.

먼저, 사전에 디바이스 드라이버가 등록되는 과정이 필요하다. 디바이스를 식별하기 위해 major number 를 할당해야 하는데 디바이스 드라이버 내에서 alloc_chrdev_region() 함수를 통해 동적으로  할당하거나, register_chrdev_region() 함수로 직접 major number 를 지정해 줄 수 있다. (이미 있는 major number 라면 적절히 에러처리가 될 것이다) 

다음으로, 문자 드라이버와 관련한 정보를 관리하는 cdev 구조체를 초기화 하며 커널에 등록한다.  cdev_init() 함수를 통해 드라이버의 file operations 을 문자 장치와 연결하며, cdev_add() 를 통해 시스템에 추가한다.

이제 cdev_map 에서는 해당 cdev 구조체에 접근할 수 있다. 해당 구조체는 각 디바이스 번호(major number, minor number) 를 각각의 문자 디바이스 구조체(cdev structure)에 매핑한다. 사용자 프로그램이 문자 디바이스에 접근하려고 할 때 커널은 이 mapping 을 활용해 요청을 처리할 드라이버를 결정한다. 

여기서  chrdevs 테이블과 cdev_map 의 역할이 조금 헷갈린다. minor number 에 대하여 파악한다면 이 두가지를 구분하기가 좀 더 수월해 질 것 같다. 

 


major number 는 특정 장치 인스턴스를 고유하게 식별하기 위해 minor number 와 짝을 이룬다. major number 는 드라이버 유형을 식별하는 반면, minor number 는 동일한 유형의 장치에 대한 여러 인스턴스를 구분한다. 예를 들어 여러 tty 장치가 동일한 major number 를 공유 하는 경우, minor number 를 통해 각 장치를 구분할 수 있는 것이다. 

여기서 cdev_map 에서 접근 가능한 cdev 구조체는 major number 와 minor number 로 구분되는 반면, chrdevs 는 major number 만으로 구분된다. 

chrdev 테이블의 각 항목은 file_operations 구조체를 가리키고 있는 반면, cdev_map 의 각 항목은 해당 문자 장치에 대한 정보(메이저 및 마이너 번호 및 해당 file_operations 구조체에 대한 참조 포함)를 포함하는 구조체 cdev에 해당된다. 

cdev_map 은 드라이버 뿐만 아니라 개별 문자장치에 대한 정보도 저장하므로, 적어도 chrdevs 테이블과 동일한 수의 항목을 갖게 된다. 단일 드라이버가 여러 장치를 처리하는 경우 (즉 동일한 major number 에 대하여 여러개의 minor number 가 있는 경우) cdev_map은 chrdev 테이블 보다 더 많은 엔트리를 갖게 된다. 

여전히 헷갈린다. 유저 프로그램 관점에서 chrdev 와 cdev_map 의 관계를 파악해 보자.

유저 프로그램에서 문자 디바이스에 접근할 때 커널은 요청 처리를 담당하는 드라이버를 결정해야 한다. 이를 위해 커널은 장치와 관련된 메이저 및 마이너 번호에 의존한다. cdev_map 의 주요 목적은 장치 번호(메이저 및 마이너 번호)와 해당 문자 장치 구조체 cdev 간의 매핑을 제공하는 것이다. 커널은 cdev_map 에서 디바이스 번호를 조회하여 해당 구조체 cdev 를 찾는다.

반면에 chrdev 테이블은 주로 문자 장치 드라이버를 major number 로 정리하고 관리하기 위한 수단으로 사용된다. chrdev 테이블은 드라이버 등록 및 관리에 사용되지만, 유저 프로그램에서 요청을 처리하는데엔 직접 관여하진 않는다.

새로운 문자 디바이스가 인스턴스가 생성되어 등록되어야 하는 경우에 cdev 구조체가 생성된다고 볼 수 있을 것 같다.

즉, chrdevs와 cdev 모두 문자 디바이스 드라이버가 커널에 로드될 때 생성 및 등록되는데, 둘 사이에는 몇가지 차이점이 존재한다는 것이다. 디바이스 드라이버를 처음 만들 때 chrdevs를 사용하여 등록하는 것이 맞고, 향후 동일한 장치의 여러 인스턴스 생성시 사용되며 그 외에도 모든 문자 디바이스 드라이버를 관리하는데 지속적으로 사용된다. 새 문자 장치 드라이버가 로드될 때 해당 드라이버의 major number 와 연산이 chrdevs 에 등록될 것이다.

반면에, 단일 인스턴스든 여러 인스턴스든 드라이버에 의해 새 장치가 초기화 될 때마다 cdev 인스턴스가 생성된다. 커널은 cdev 인스턴스를 통해 드라이버와 연결된 개별 장치를 관리하고 추적할 수 있으며, chrdevs 는 각 major number 와 관련된 드라이버 작업을 관리하는 데 사용된다.

이를 기억하며 디바이스 드라이버 동작의 개요를 살펴보자. 다음은 디바이스 드라이버 코드의 예시이다.

// sample_driver.c

// It's pseudo code

struct file_operations mycrdv_fops
{
	.read = mycdrv_read();
	.write = mycdrv_write();
    .open = mycdrv_open();
	.release = mycdrv_release();
}

mycdrv_open();
mycdrv_read();
mycdrv_write();

my_init();
my_exit();

 

  1. 드라이버가 커널에 insert 되면, my_init 함수가 수행되며 장치 파일의 major number 가 할당된다. 이 때 major number 는 유저 프로그램에서 직접 명시하여 요청할 수도 있고, 커널에게 자동으로 할당하도록 요철할 수도 있다. major number 는 chrdevs 테이블에 등록되며, 이어서 cdev 구조체를 생성한다. 이후 cdev_map table 에 해당 cdev 구조체를 등록한다. 이 과정을 통해 드라이버가 등록되었다고 볼 수 있다. 
  2. 장치 파일을 생성한다(/dev/mycdrv). 직접 mknod 를 호출하는 법과, 간접적으로 device_create() 함수를 호출하는 방법이 있다.
  3. 사용자 프로그램에서 open 을 수행하면 커널은 inode 를 통해 해당 디바이스가 문자형 장치임을 인식하고 mycdrv_open() 을 호출한다.
  4.  사용자프로그램에서 write 시스템 콜을 호출하면 VFS 가 파일 객체에서 f_ops 필드 정보를 이용하여 mycdrv_write() 을 호출한다.

위 과정 중 1, 2번 과정을 수행하는 my_init() 의 동작은 다음과 같다.

  1. register_chrdev_region() or alloc_chrdev_region() 을 호출해 직접 / 간접적으로 해당 장치의 major, minor number 를 결정한다. 
  2. cdev_alloc(), cdev_init(), cdev_add() 를 순차적으로 호출하여 cdev  구조체를 생성하며, cdev map 에 이를 등록한다.
  3. class_create & device_create() 를 사용하거나, mknod 명령어를 사용하여 device file 을 생성한다.

해당 드라이버를 더 이상 사용햘 필요가 없어지면 my_exit() 을 호출하게 될 것이다. my_exit() 의 동작은 다음과 같다.

  1. 만약 class_create() 나 device_create() 를 사용하였다면, device_destroy() & class_destory() 를 수행하거나, mknod 명령어를 사용하였다면 rmnod 명령어를 사용하여 defice file 을 삭제한다.
  2. cdev_del() 를 호출하여 cdev 구조체를 삭제한다.
  3. unreigister_chrdev_region() 을 호출하여 crdevs 에서도 해당 장치에 관한 정보를 삭제한다.

리눅스에서 디바이스 파일에 대한 정보를 다음과 같이 출력해 볼 수 있다.

tty 의 major number 는 4이며, ttys0 의 minor number 는 64, tty1 은 minor number 1로 구분되는 것을 확인할 수 있다. 

이는 유저 프로그램에서 문자 디바이스를 읽고 쓰는 과정을 보여준다. 

디바이스 드라이버에 file_operatoins 가 구현되어 있는데, 문자 드라이버를 위한 주요 operations 은 다음과 같다.

open 이 완료된 후에, 커널과 유저프로그램은 read, write operations 을 통해 데이터를 주고 받을 것이다. 여기서 커널은 유저프로그램을 절대 신뢰하지 않기 때문에 (유저 프로그램의 오동작으로  커널이 멈추거나 해선 안되기 때문) memcpy() 나 직접적인 포인터 역참조 등을 통해선 데이터를 주고받을 수 없다. 따라서, 드라이버는 반드시 kernel 의 특별한 함수를 활용하여 user space 와 데이터를 주고받아야 한다. 그 관련 함수는 다음과 같다.

copy_from_user() 및 copy_to_user() 모두 사용자 공간과 커널 공간 메모리 간의 안전한 데이터 전송을 보장하고 무단 액세스 또는 메모리 손상을 방지하는 동작을 포함한다. 두 함수 모두 유저 프로세스가 해당 메모리 영역에 접근하는데 필요한 권한을 가지고 있는지 확인하는 동작이 포함되어 있다.

위에서 설명한 주요 operations 중에 비교적 생소한 중요한 unlocked_ioctl 의 동작에 대하여 살펴보려 한다.

이는 시리얼 포트를 초기화 하는 용도로 사용된다.

디바이스 드라이버에서 write 로 데이터를 보내면 이를 마더보드의 serial port 를 통해 output buffer 로 전달되고 이를 다시 프린트와 같은 디바이스로 전달하는 과정이 수행된다. 이 때 serial port 에 status (in, out buffer 의 상태), command 레지스터에 bps (전송 속도) 등의 초기화 작업이 필요한데, 이 때 호출하는 것이 해당 함수라고 생각하면 된다. 

 다음으론 디바이스 모델에 대하여 알아보자

Device Model

리눅스 커널에서 디바이스 모델은 시스템에 존재하는 하드웨어 디바이스를 계층적 관계 및 속성과 함께 표현한 것이다. 디바이스 모델은 다음과 같은 몇 가지 주요 구성 요소를 중심으로 구축된다.

디바이스: USB 드라이브, 네트워크 카드 또는 하드 드라이브와 같은 실제 하드웨어 구성 요소로, 커널에서 struct device 구조체로 관리된다.

드라이버: 드라이버는 하드웨어 장치와 상호 작용하는 소프트웨어 구성 요소로 나머지 커널에 균일한 인터페이스를 제공한다. 드라이버는 struct device_driver 구조체로 표시된다.

버스: 버스는 장치가 서로 통신하고 시스템의 나머지 부분과 통신하는 데 사용되는 통신 채널이다. 버스는 struct bus_type 데이터 구조체로 표시된다.

클래스: 네트워크 장치, 저장 장치 또는 입력 장치와 같이 유사한 기능을 수행하거나 유사한 특성을 가진 장치 그룹이다. 클래스는 struct class 데이터 구조체로 표시된다. 

디바이스 모델은 리눅스 커널이 장치를 검색, 열거 및 해당 드라이버와 일치시키고 전원 상태 및 기타 속성을 관리하는 데 도움이 된다. 또한 디바이스 모델은 udev와 같은 유저 프로그램이 디바이스 hotplugging 및 동적 디바이스 관리를 처리할 수 있게 해준다.

보다 구체적인 예시를 살펴보자

Linux 커널에서 USB 마우스는 장치 모델을 따르는 장치의 예시이다. 디바이스 모델이 USB 마우스를 어떻게 나타내는지에 대한 간단한 개요는 다음과 같다. 

  • 장치: USB 마우스 자체는 하드웨어 장치입니다. 커널의 구조체 장치 데이터 구조체로 표시됩니다. 이 장치에는 연결된 USB 포트인 상위 장치에 대한 정보와 장치별 속성이 포함되어 있습니다.
  • 드라이버: USB 마우스는 Linux 커널과 인터페이스하기 위해 드라이버를 사용합니다. 드라이버는 버튼 누르기 및 움직임과 같은 마우스의 입력 이벤트를 처리합니다. USB 마우스의 경우 일반적으로 HID(휴먼 인터페이스 장치) 드라이버, 특히 usbhid 드라이버가 사용됩니다. 이 드라이버는 device_driver 데이터 구조체로 표현됩니다.
  • 버스: USB 마우스는 호스트 컨트롤러와 USB 장치 간의 통신 채널인 USB 버스를 사용하여 컴퓨터와 통신합니다. USB 버스는 커널에서 구조체 bus_type 데이터 구조체로 표시됩니다.
  • 클래스: USB 마우스는 키보드, 마우스, 터치스크린 등 시스템에 입력을 제공하는 장치를 그룹화하는 "입력" 클래스에 속합니다. 이 클래스는 커널에서 구조체 class 데이터 구조체로 표시됩니다.

Linux 커널은 디바이스 모델을 사용하여 USB 마우스가 연결되면 이를 검색하고 적절한 HID 드라이버와 일치시킨 다음 /dev/input/ 디렉터리에 장치 노드를 생성한다(예: /dev/input/mouse0 또는 /dev/input/eventX). 그런 다음 이 장치 노드를 사용자 공간 응용 프로그램에서 사용하여 마우스 입력 이벤트를 처리할 수 있다.

Kernel Frameworks

디바이스 모델, sysfs, VFS 등 다양한 하위 시스템 및 장치를 개발, 관리 및 상호 작용할 수 있는 구조화된 방법을 제공하는 사전 빌드된 소프트웨어를 커널 프레임워크라고 부른다. 재사용 가능한 인프라를 제공하여 디바이스 드라이버 및 기타 커널 모듈을 작성하고 유지 관리하는 프로세스를 간소화한다. 

한 그룹에 속하는 유사한 driver 가 여러개 있을 때 공통 부분을 추려서 만든게 framework 라고 볼 수 있다. 기존엔  Core, Driver (초록과 파랑) 이 하나의 디바이스 드라이버 였다면, 현재는 재사용성을 위해 core 가 분리되어 있다.

유저 프로그램에서부터 하드웨어 장비까지의 요청과 응답은 위와 같은 과정을 통해 일어난다.

이 중 Bus infrastructure 에선 usb 마우스를 예시로 plug 했을 때 이를 감지하고 그 mouse 의 id 를 읽어내는 역할을 한다. 

다음은 USB Bus 의 예시이다.

USB bus

위 이미지에서 디바이스 모델 외부는 하드웨어와 관련한 부분이며, 빨간색 박스는 커널 소프트웨어라고 파악할 수 있다. 

각각의 디바이스 드라이버, DEV 는 여러 장치들을 나타낸다.

예를들어, 핑크색을 usb mouse, 노란색을 HDD, 주황색을 USB 스피커라고 생각할 수 있다. 

usb 마우스를 플러그했다고 가정하면, 회색 상자안의 USB controller 가 동작할 것이다. 이를 adapter 에서 핸들링 하며, 해당 하드웨어의 id를 읽어 낸 후 이를 USB core 에 전달한다.

디바이스 드라이버는 미리 등록되어 있는 상황이며, 해당 하드웨어에 대한 초기화 작업을 실행한다. 디바이스 드라이버가 등록될 때 구조체는 미리 등록되어 있다. 이제 막 연결된 DEV1 을 초기화 한다.

USB bus 는 다음과 같은 요소로 이루어져 있다.

adaptor driver 보다 자세히 알아보자. adaptor driver 를 host controller driver 라고도 부르는데, 이는 컴퓨터 토는 기타 전자 장치의 USB host controller 하드웨어를 관리하는 역할을 한다. 

즉 별도의 하드웨어를 관리하는 특별한 드라이버인 것인데, 이는 일반적인 USB 디바이스 드라이버를 연결할 때 꼭 필요하다. USB를 플러그인 했을 때 이를 관리하는 하드웨어와 그를 위한 특별한 드라이버가 필요하다고 생각하면 될 것 같다. 

다음은 USB 하위 시스템에서 호스트 컨트롤러 드라이버와 장치 드라이버가 함께 작동하는 방식에 대한 개요이다.

  1. USB 장치가 시스템에 연결되면 호스트 컨트롤러가 장치를 감지하고 장치에 고유 주소를 할당하고 기능에 대한 정보를 수집하는 열거 프로세스를 시작합니다.
  2. 호스트 컨트롤러 드라이버는 이 정보를 Linux 커널의 USB 코어 서브시스템에 전달합니다.
  3. USB 코어 서브시스템은 장치의 공급업체 ID, 제품 ID 및 기타 속성을 기반으로 새로 연결된 USB 장치를 처리할 일치하는 장치 드라이버를 검색합니다.
  4. 일치하는 드라이버가 발견되면 USB 코어 서브시스템은 해당 드라이버를 장치에 바인딩하여 드라이버가 USB 버스를 통해 장치를 제어하고 통신할 수 있도록 합니다.
  5. 장치 드라이버와 호스트 컨트롤러 드라이버는 USB 제어, 벌크, 인터럽트 및 등시 전송과 같은 표준 USB API 및 추상화를 사용하여 USB 코어 서브시스템을 통해 상호 작용합니다.
  6. 사용자 공간 애플리케이션은 /dev 디렉터리의 장치 노드를 사용하는 장치 드라이버를 통해 또는 운영 체제에서 제공하는 상위 수준 API를 통해 USB 장치와 통신할 수 있습니다.

요약하면 호스트 컨트롤러 드라이버는 USB 호스트 컨트롤러 하드웨어를 관리하고, 장치 드라이버는 USB 장치의 특정 기능을 처리한다. 이 두 드라이버는 Linux 커널의 USB 코어 서브시스템을 통해 함께 작동하여 USB 장치의 통신 및 제어를 가능하게 한다.

adaptor driver 역시 초기화 과정이 필요하고, 이는 다음과 같다. 

좌측 초록색 박스는 adaptor driver 이며, 우측 rtl8150 의 경우 usb 디바이스 드라이버이다. 두가지 드라이버 모드 USB 코어에 등록될 것이다. 

디바이스 드라이버의 Device Identifiers 는 드라이버가 관리할 수 있는 장치들의 set 을 정의하여 USB core 가 해당 디바이스 드라이버가 어떤 디바이스를 관리하는데 쓰여야 하는지 알도록 해준다.

위 이미지는 mouse 와 관련한 device id 테이블로 첫번째 열은 REALTEK 회사의 RTL8150 을 가르킨다고 볼 수 있다. 

USB core 에 의하여 usb_driver 라는 구조체가 초기화 된다. 각 USB 장치는 반드시 이를 초기화 해야하며, 이를 USB core 에 등록해야 한다.

여기서 눈여겨 볼 부분은 probe 메서드로, 새로 감지된 장치를 초기화하고 구성하기 위해 드라이버가 구현하는 콜백 함수이다. probe 메서드는 커널이 장치 식별자(예: 공급업체 ID, 제품 ID 또는 장치 클래스)를 기반으로 특정 드라이버와 일치하는 장치를 찾을 때 호출된다. 이 메서드 내에서 메모리 또는 데이터 구조와 같이 장치에 필요한 리소스를 할당하고 초기화된다. 또한, dev 디렉터리에 장치 노드를 생성하거나 sysfs 파일 시스템에 장치를 등록하는 등 적절한 커널 서브시스템에 장치를 등록하는 역할도 수행한다.

모든 드라이버가 등록된 상태에서, 디바이스가 감지되었을 때의 동작은 다음과 같다.

USB 버스를 포함하는 커널프레임워크, 디바이스 모델의 전반적인 구조는 다음과 같다.

Device Tree

usb 장치처럼 동적으로 추가 삭제되는 장치들도 있지만, SoC 안의 디바이스와 같이 칩안에 내장된 작은 디바이스 또한 존재한다. 그러한 장치들은 마음대로 추가하고 삭제할 수 없기 때문에 정적으로 만들어져 커널에 포함된다. 

그리고 이와 관련된 동작들을 관리하는 특별한 bus 가 있는데, 이를 platform bus 라고 칭한다. platform bus 는 hotplug 가 불가능한 디바이스들을 관장하는 버스라고 생각하면 된다.

device tree 에 다양한 node 들이 들어가 있는데 그 중 하나의 예시는 다음과 같다.

여기서 UART(Universal Asynchronous Receiver-Transmitter)는 두 장치 간의 직렬 데이터 전송에 사용되는 하드웨어 통신 프로토콜이다.