디바이스 드라이버
디바이스 드라이버는 특정 장치를 다룰 줄 아는 코드입니다. 응용이 보낸 읽기와 쓰기를 받아서 그 장치가 알아듣는 절차로 바꿉니다. 장치마다 다루는 방법이 다릅니다. 그 차이를 이 코드가 혼자 떠맡습니다.
상세
낯선 커피 기계 앞에서는 버튼을 어느 순서로 눌러야 하는지, 예열이 끝났다는 소리를 언제까지 기다려야 하는지 알 수가 없습니다. 그 기계를 매일 쓰는 사람에게 아메리카노 한 잔을 부탁하면 나머지는 그 사람이 압니다. 기계가 바뀌어도 부탁하는 말은 그대로입니다.
장치는 저마다 제어하는 방법이 다릅니다. 어느 자리에 어떤 값을 써야 움직이는지, 일이 끝났다는 것을 어떻게 알리는지가 장치마다 다릅니다. 이 차이를 아는 코드를 한곳에 몰아 둔 것이 디바이스 드라이버입니다. 드라이버가 맞추는 것은 값의 형식만이 아닙니다. 주고받는 순서와 시점도 지킵니다.
그래서 드라이버는 경계를 두 개 갖습니다. 위쪽은 운영체제가 정해 둔 공통 인터페이스입니다. 응용과 운영체제 본체는 이쪽만 보고 부릅니다. 아래쪽은 그 장치가 실제로 요구하는 절차입니다. 드라이버는 위에서 받은 요청을 아래쪽 절차로 옮깁니다.
flowchart TD
A[응용] -->|공통 인터페이스| D[디바이스 드라이버]
D -->|장치가 요구하는 절차| V[장치]
경계가 둘이라 양쪽이 따로 바뀔 수 있습니다. 장치가 바뀌면 드라이버만 갈아 끼웁니다. 응용은 같은 인터페이스를 계속 부릅니다. 반대로 운영체제가 공통 인터페이스를 정해 두면, 장치를 만든 쪽은 그 형식에 맞춰 드라이버만 내놓으면 됩니다.
배경
컴퓨터에 붙는 장치는 종류가 많습니다. 같은 종류라도 제조사와 모델마다 제어 방법이 다릅니다. 이 차이를 응용이 직접 감당하면 장치가 하나 늘 때마다 이미 도는 응용을 고쳐야 합니다. 운영체제 본체가 감당해도 사정은 같습니다. 새 장치가 나올 때마다 본체를 고치고 다시 빌드해야 합니다.
게다가 장치를 어떻게 다루는지는 그 장치를 만든 쪽이 제일 잘 압니다. 그쪽이 남의 운영체제 본체를 고쳐 넣을 수는 없습니다.
그래서 장치마다 달라지는 부분만 따로 떼어, 갈아 끼울 수 있는 조각으로 만들 필요가 생겼습니다. 운영체제는 그 조각이 지켜야 할 형식을 정합니다. 조각 안쪽은 장치를 아는 쪽이 채웁니다. 나머지 코드는 형식만 보고 부르면 됩니다.
이 조각에 붙은 이름이 디바이스 드라이버입니다. 장치를 가리키는 말과 무언가를 몰아 움직인다는 말을 붙여 만든 이름입니다.
갈래
드라이버는 두 축으로 갈립니다. 하나는 응용이 그 장치를 어느 창구로 만나느냐입니다. 다른 하나는 드라이버 코드가 어느 모드에서 도느냐입니다.
문자 디바이스
문자 디바이스 드라이버는 사용자 프로세스와 데이터를 직접 주고받는 드라이버입니다. FreeBSD 공식
문서는 이것이 가장 흔한 종류라고 적습니다. 유닉스 계열에서 대부분의 장치는 장치 노드를 통해
접근하고, 그 파일들은 대개 파일 시스템의 /dev 아래에 놓입니다.
Linux 는 문자 장치와 블록 장치의 번호 공간을 따로 둡니다. 공식 장치 번호 등록표가 문자 쪽 주 번호 1번 아래에 메모리 장치들을, 블록 쪽 주 번호 1번 아래에 램 디스크를 적습니다. 같은 번호라도 창구가 다르면 다른 장치를 가리킵니다.
블록 디바이스
블록 디바이스는 커널이 캐싱을 제공하는 디스크 장치입니다. FreeBSD 에는 이 종류가 없어졌습니다. 다른 유닉스 시스템은 이 두 번째 종류를 지원할 수 있다고 FreeBSD 공식 문서가 적습니다.
그래서 진지한 응용은 블록 디바이스에 의존하지 않습니다. 디스크에 직접 접근하는 거의 모든 응용이 문자 장치를 쓰도록 애써 지정합니다. Linux 쪽은 사정이 달라서, 공식 장치 번호 등록표가 블록 장치 번호 공간을 그대로 유지합니다.
네트워크 인터페이스
네트워크 장치 드라이버는 접근을 위해 장치 노드를 쓰지 않습니다. 어느 장치를 쓸지는 커널 안에서 내려지는 다른 결정으로 정해집니다. 응용은 일반적으로 open 대신 socket 시스템 콜로 네트워크 장치를 쓰기 시작합니다.
Linux 도 이 창구가 따로입니다. Linux 공식 문서는 네트워크 장치 구조체를 alloc_netdev_mqs() 로
할당해야 한다고 적습니다. 등록에 성공한 장치는 마지막 사용이 끝날 때 free_netdev() 로 해제됩니다.
이 구조체는 모듈이 내려간 뒤에도 남아 있어야 합니다. 이 구조체를 등록하는 함수는 두 무리로
나뉩니다. rtnl_lock 이 이미 걸려 있지 않은 보통 문맥에서 쓰는 첫 무리가 register_netdev() 와
unregister_netdev() 입니다.
커널 모드
드라이버 코드가 커널 안에서 도는 쪽입니다. 커널 안에 있으면 커널이 제공하는 자원을 그대로 가져다 쓸 수 있습니다. Linux 공식 문서는 제어 논리를 커널 밖에 두어도 되는 까닭으로 그 장치가 커널이 제공하는 다른 자원을 쓸 필요가 없다는 점을 듭니다. 뒤집으면 그 자원이 있어야 도는 장치는 커널 안에 남습니다. 그래서 이 축은 그 장치가 커널의 무엇을 필요로 하느냐로 갈립니다.
사용자 모드
드라이버 코드가 커널 밖에서 도는 쪽입니다. Linux 공식 문서는 여러 종류의 장치에서 커널 드라이버를 만드는 것이 과하다고 적습니다. 정말 필요한 것은 인터럽트를 처리할 방법과 그 장치의 메모리 공간에 접근할 방법뿐일 때가 있습니다. 장치를 제어하는 논리가 반드시 커널 안에 있어야 하는 것은 아닙니다. 그래서 나온 것이 UIO(Userspace I/O, 사용자 공간 입출력)입니다.
같은 문서는 UIO 가 보편적인 드라이버 인터페이스는 아니라고 못 박습니다. 네트워킹이나 시리얼, USB(Universal Serial Bus, 범용 직렬 버스)처럼 다른 커널 서브시스템이 이미 잘 다루는 장치는 UIO 드라이버 후보가 아닙니다. UIO 에 알맞은 하드웨어는 넷을 모두 만족합니다. 매핑할 수 있는 메모리를 갖고, 그 메모리에 쓰는 것만으로 완전히 제어되며, 대개 인터럽트를 일으키고, 표준 커널 서브시스템 어디에도 안 맞는 장치입니다.
예시
Linux 의 장치 번호
Linux 공식 장치 번호 등록표는 할당된 장치 번호와 /dev 아래 노드의 공식 등록부입니다. 문자 쪽 주
번호 1번 아래에 메모리 장치들이, 블록 쪽 주 번호 1번 아래에 램 디스크가 놓여 있습니다.
1 char memory devices
1 = /dev/mem physical memory access
3 = /dev/null null device
5 = /dev/zero null byte source
1 block ram disk
0 = /dev/ram0 first ram disk
1 = /dev/ram1 second ram disk
250 = /dev/initrd initial ram disk
/dev/null 은 문자 장치이고 주 번호 1, 부 번호 3입니다. 같은 주 번호 1이 블록 쪽에서는 램 디스크를
가리킵니다. 부 번호 250은 /dev/initrd 입니다. 장치 하나가 이 표에서 얻는 것은 이름과 번호 한 쌍입니다.
FreeBSD 의 문자 장치 진입점
FreeBSD 공식 문서가 든 예제는 쓴 값을 기억했다가 그대로 되돌려 주는 가짜 장치입니다. 드라이버는 위쪽 경계가 될 진입점 함수들을 구조체 하나에 채워 넣습니다.
static d_open_t echo_open;
static d_close_t echo_close;
static d_read_t echo_read;
static d_write_t echo_write;
/* character device entry points */
static struct cdevsw echo_cdevsw = {
.d_version = D_VERSION,
.d_open = echo_open,
.d_close = echo_close,
.d_read = echo_read,
.d_write = echo_write,
.d_name = "echo",
};
.d_open 부터 .d_write 까지가 응용이 이 장치를 열고 읽고 쓸 때 불릴 함수입니다. .d_name 은 이
장치에 붙는 이름으로 echo 입니다. 같은 문서는 이 커널 모듈을 kldload 로 올리고 kldunload 로
내리며 kldstat 으로 목록을 본다고 적습니다. 그래서 드라이버를 고칠 때마다 재부팅하지 않아도 됩니다.
Zephyr 의 서브시스템 API
Zephyr 공식 문서는 장치 종류마다 제네릭 타입 API(Application Programming Interface, 응용 프로그램 인터페이스)를 둔다고 적습니다. 드라이버는 초기화할 때 자기 API 함수들의 포인터를 담은 구조체를 가리키는 포인터를 채워 넣습니다. 서브시스템 API 정의는 대개 이렇게 생겼습니다.
typedef int (*subsystem_do_this_t)(const struct device *dev, int foo, int bar);
typedef void (*subsystem_do_that_t)(const struct device *dev, void *baz);
__subsystem struct subsystem_driver_api {
subsystem_do_this_t do_this;
subsystem_do_that_t do_that;
};
특정 서브시스템을 구현하는 드라이버가 이 API 들의 실제 구현을 정의해 채웁니다. 응용은 그 제네릭 API 만 보고 짜면 되고, 응용 코드가 특정 드라이버 구현에 묶이지 않습니다.
Windows 의 드라이버 스택
Microsoft 공식 문서는 응용이 장치에서 데이터를 읽어야 할 때 운영체제가 구현한 함수를 부르고, 운영체제가 다시 드라이버가 구현한 함수를 부른다고 적습니다.
sequenceDiagram
participant A as 앱
participant O as 운영체제
participant D as 드라이버
participant V as 장치
A->>O: 운영체제가 구현한 함수 호출
O->>D: 드라이버가 구현한 함수 호출
D->>V: 장치와 통신
모든 드라이버가 장치와 직접 통신하는 것은 아닙니다. 같은 문서는 여러 드라이버가 드라이버 스택으로 계층을 이뤄 하나의 입출력 요청에 함께 참여하는 일이 잦다고 적습니다. 장치와 직접 통신하는 드라이버를 펑션 드라이버라고 부르고, 보조 처리를 맡는 드라이버를 필터 드라이버라고 부릅니다.
관련 항목
이것을 호출·실행하는 명령
시스템 콜 · open · read · write · ioctl · socket
이것이 식별되는 통로
장치 파일 · 장치 노드 · 주 번호 · 부 번호 · 네트워크
이것이 도는 실행 모드
커널 모드 · 사용자 모드 · UIO · WDF(Windows Driver Frameworks, 윈도우 드라이버 프레임워크) · KMDF(Kernel-Mode Driver Framework, 커널 모드 드라이버 프레임워크) · UMDF(User-Mode Driver Framework, 사용자 모드 드라이버 프레임워크)
이것이 커널 안에서 쓰는 자원
커널 모듈 · 커널 링커 · 커널 서브시스템 · 인터럽트 · 메모리 매핑 · 캐싱
이것을 실은 모듈을 올리고 내리는 명령
kldload · kldunload · kldstat
이것 아래에서 장치를 잇는 버스와 자원
버스 · PCI(Peripheral Component Interconnect, 주변 장치 상호 연결) · ISA(Industry Standard Architecture, 산업 표준 구조) · USB · DMA(Direct Memory Access, 직접 메모리 접근) · 프로브와 어태치 · 버스 자원 · UART(Universal Asynchronous Receiver/Transmitter, 범용 비동기 송수신기) · SPI(Serial Peripheral Interface, 직렬 주변기기 인터페이스) · I2C(Inter-Integrated Circuit, 집적회로 간 통신)
이것이 겹겹이 쌓이는 구조
드라이버 스택 · 펑션 드라이버 · 필터 드라이버
이것이 장치 종류마다 구현하는 API
서브시스템 API · 제네릭 API · API
이것을 구현하는 운영체제
Linux · Windows · FreeBSD · Zephyr · Microsoft
이것이 속하는 상위 분류
다른 이름: device driver · 장치 드라이버