디바이스 드라이버(device driver)

Jin Hur·2021년 8월 7일
0
post-custom-banner

reference: "리눅스 커널 내부구조" / 백승제, 최종무

유닉스 계열 시스템에서 모든 것은 '파일'로 취급.
모니터 디바이스도 파일, 키보드 디바이스도 파일로 취급된다.
물론 파일슷템을 통해 디스크에 저장되어 있는 '파일'도 파일이다. 디스크에 저장되어 있는 파일을 '정규파일(regular file)'이라 부른다.

이에 따라 태스크는 모니터이든 키보드이든, 정규파일이든 상관없이 파일이라 생각하여 접근하고, open(), read(), write(), close() 등 일관된 함수 인터페이스를 통해 접근한다.

장치를 가리키는 파일을 '정규파일'과 구분하여 '장치파일(device file)'이라 부른다.

파일 유형:
정규파일, 디렉터리, 문자 장치 파일, 블록 장치 파일, 링크 파일, 파이프, 소켓

디바이스 드라이버 사전적 정의

장치 드라이버/제어기 또는 디바이스 드라이버는 특정 하드웨어나 장치를 제어하기 위한 커널의 일부분으로 동작하는 프로그램이다. 컴퓨터를 구성하는 다양한 입출력 장치마다 각각 장치드라이버가 프로그램 되어 커널에 통합되어 실행된다.
...
장치 드라이버는 흔히 장치 칩의 레지스터에 접근하여 하드웨어를 제어하며 하드웨어와 주변 기기를 사용하는 프로그램의 중간 다리 역할을 한다.
...
커널은 부팅 시에 시작되어 컴퓨터 종료시 커널이 끝난다. 장치 드라이버는 하드웨어와 밀접하게 연관되고 해당 장치를 제어하는 프로그램이다.

source: (위키백과)https://ko.wikipedia.org/wiki/%EC%9E%A5%EC%B9%98_%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B2%84


사용자(태스크) 입장에서의 디바이스 드라이버

사용자 태스크가 접근하는 장치 파일이라는 개념은 VFS가 제공하는 파일 객체를 의미한다. 파일 객체에 사용자 태스크가 행할 수 있는 연산은 struct fileoperations라는 이름으로 정의되어 있다.

source: https://jiming.tistory.com/127_


사용자 태스크는 위 file_operations의 open(), read(), write(), release() 등의 함수를 이용하여 파일 객체를 접근할 수 있다. 참고로 release() 함수는 사용자 태스크가 호출하는 close() 함수에 대응된다.

만약 사용자 태스크가 정규파일에 접근한다면 파일시스템에서 제공하는 함수를 호출하게 된다.
ex) f_op->read() // VFS
=> generic_file_read_iter() // Specific File Layer
=> 특정 파일시스템의 read() 호출 // Specific FS Layer

사용자 태스크가 file_operations 구조체(f_op는 이 구조체에 대한 포인터)에 정의되어 있는 함수를 통해 장치파일에 접근할 때 호출할 함수를 정의하고 구현해 주는 것이 바로 디바이스 드라이버이다. 사용자 태스크는 디바이스 드라비어 개발자가 작성한 여러 가지 함수들을 일일이 알 필요 없이 파일 객체에 정의되어 있는 함수를 호출함으로써 장치에 접근할 수 있다는 장점이 있는 것이다.

시스템에 존재하는 장치는 한두 가지가 아니므로 여러 개의 디바이스 드라이버를 구분하기 위해 각 디바이스 드라이버마다 고유한 번호를 정해준다. 이때 각 디바이스 드라이버에게 정해준 고유한 번호를 주 번호(Major num)라 부흔다. 리눅스는 4096개의 주 번호를 지원한다.

리눅스에서 각 장치는 자신을 나타내는 장치 파일을 가지며, 아 장치 파일을 관리하는 inode 객체에 주 번호가 기록되어 있다. 구체적으로 inode 객체의 i_rdev 필드에 주 번호와 부 번호를 저장한다

주 번호: 12bit, 부 번호: 20bit

사용자 태스크가 특정 장치파일에 접근하면, 이 장치파일에 적절한 디바이스 드라이버의 주 번호를 알게 되는 것이다. 그리고 주 번호를 알게 되면 장치파일에 등록된 디바이스 드라이버 내부 함수를 호출할 수 있게 된다.

부 번호: 만약 시스템에 4개의 모니터가 장착되어 있다면, 이들은 물리적으로 동일한 특성을 가지기 떄문에 같은 디바이스 드라이버를 사용해도 될 것이다. 따라서 동일한 주 번호를 공유해도 된다. 하지만 특정 모니터의 출력을 위해서는 이들간의 식별이 필요하고 이 때 부 번호가 사용된다.

리눅스에서 장치파일은 일반적으로 /dev 디렉터리 밑에 존재한다. 모니터 즉, 터미널을 나타내는 장치파일의 이름은 전통적으로 tty란 이름으로 존재하며 복수개로 존재할 때는 /dev/tty0, /dev/tty1, /dev/tty2 등과 같이 뒤쪽에 번호를 붙여 구분한다.
(장치파일 이름 / 주 번호 / 부 번호)
/dev/tty0 4, 0
/dev/tty1 4, 1
/dev/tty2 4, 2
/dev/sda 8, 0
/dev/sda1 8, 1
/dev/sda2 8, 2
/dev/sdb 8, 16
/dev/sdb1 8, 17 ...
=> 주 번호가 같다는 것은 같은 디바이스 드라이버에 의해 구동될 수 있다는 의미가 됨.

장치파일은 결국 사용자 태스크에게 디바이스 드라이버 내부 함수를 호출할 수 있는 진입점(entry point)을 제공하는 것이다. 이때 디바이스 드라이버를 선택하는 것은 주 번호를 통해서 이루어진다.

장치파일 생성 방법 (mknod)

커널에서는 udev를 통한 동적인 장치파일 생성/제거를 지원할 수도 있다. 추후 설명

기본적인 장치파일 생성/제거에 대해 살펴보면,

$mknod /dev/mydrv [c|b] 주번호 부번호
mknod 명령어 인자: 장치파일 이름, 장치 유형, 어떤 디바이스 드라이버를 사용할 지 나타내는 주 번호와 식별을 위한 부 번호
장치 파일 유형에 따라 c인 경우 파일 아이노드의 i_mode에 S_IFCHR, b인 경우 S_LFBLK라는 값으로 저장.

장치 파일 유형
리눅스는 디바이스 드라이버를 크게 문자 디바이스 드라이버, 블록 디바이스 드라이버, 그리고 네트워크 디바이스 드라이버로 구분한다.

  • 문자 디바이스 드라이버: 순차 접근이 가능하고, 임의의 크기로 데이터 전송이 가능한 드라이버. ex) 터미널 드라이버
  • 블록 디바이스 드라이버: 임의 접근이 가능하고 고정된 크기의 블록 단위로 데이터를 전송하는 드라이버로. ex) 디스크 드라이버

    최근에는 디바이스의 기능이 발전하고 종류도 많아짐에 따라 이러한 구분이 모호해짐. 현재는 단지 커널의 '페이지캐시'와 큐를 통해 데이터를 주고받는가의 여부를 통해 문자와 블록 디바이스 드라이버를 구분함.
    - 문자 디바이스 드라이버: read(), write() 함수와 1:1 매칭되어 디바이스 드라이버 함수가 호출됨.
    - 블록 디바이스 드라이버: read(), wrte() 함수에 대응되는 함수가 존재하지 않으며, 큐를 통해 '페이지캐시'와 통신. (block_device_operations 구조체에 선언된 함수 호출)



개발자 입장에서 디바이스 드라이버

디바이스 드라이버라는 말 자체는 하드웨어로써 존재하는 디바이스를 구동시키기 위한 소프트웨어를 부르는 말이다. 디바이스 드라이버는 어느 운영체제이건, 혹은 운영체제가 없는 환경에서도 동작되도록 제작되어야 한다.
하지만 운영체제 마다 디바이스 드라이버를 관리하는 방식이 다르기에 디바이스 드라이버를 작성할 때는 디바이스를 구동시키기 위해 필요한 '하드웨어 밀접 코드'와 '운영체제 관련 코드'를 분리하는것이 바람직.

리눅스의 디바이스 드라이버는 특정 하드웨어를 위한 디바이스 드라이버 코어와, 코어를 리눅스에서 사용가능한 형태로 만들어 주기 위한 일종의 래퍼로 구성된다. 디바이스 드라이버 코어는 하드웨어 메뉴얼을 참조하여 해당 하드웨어의 특성에 맞도록 작성하게 된다.

디바이스 드라이버 코어를 리눅스 커널이 알 수 있도록 커널에 등록시키고, 사용자 태스크가 장치파일을 통해 접근 할 수 있게 해주려면 사용자 태스크가 호출할 함수들과 코어의 함수들을 매핑시켜주어야 하는데, 이것이 바로 래퍼(wrapper)이다. 이 래퍼에 대해서는 개발자가 고민할 필요없이 리눅스 커널이 드라이버의 래퍼가 제공해야할 함수들을 정의해놓았다. 즉, 디바이스 드라이버 개발자는 어떤 함수를 사용자 태스크에게 제공할 지 고민할 필요 없이 file_operations 자료구조에 정의되어 있는 함수들에 대해서만 제공하면 된다.

사용자 태스크는 file_operations(f_op가 가리키는)에 정의되어 있는 함수를 통해 일관되게 장치에 접근할 수 있고, 개발자 또한 일관된 인터페이스를 고려하기만 하면 된다. 이것이 바로 리눅스가 채택하고 있는 디바이스 드라이버 관리 구조이다.


사용자 태스크는 일관된 구조를 갖출 수 있게 된다. 거꾸로 디바이스 드라이버 개발자는 DDI(Device Driver Interface), 즉 file_operations 구조체에 정의되어 있는 함수를 디바이스 드라이버 내에 구현해 줌으로써 간단히 개발을 완료.



리눅스의 디바이스 드라이버 관리

커널 내부에는 장치파일의 유형과 주 번호를 이용해 디바이스 드라이버를(조금 더 구체적으로는 적절한 file_operations 구조체를) 찾아올 수 있는 자료구조가 유지되고 있어야 할 것이다.

리눅스 커널은 문자 디바이스 드라이버와 블록 디바이스 드라이버를 위해 각각 cdev_map과 bdev_map이라는 이름의 자료구조를 유지한다. 이들 자료구조는 문자 디바이스 드라이브인 경우 cdev 구조체를, 블록 디바이스 드라이브인 경우 gendisk 구조체를 각각 255개씩 저장할 수 있는 배열형태로 구현되어 있다. 객 배열은 디바이스 드라이버의 주 번호를 255로 나누었을 때 나머지 값(해시값)을 인덱스로 하여 접근되며, 동일한 해시값을 가지는 드라이버는 *next 포인터를 이용해 연결한다.

이를 통해 문자/블록 디바이스 드라이버는 각각 2^12=4096개씩, 최대 8192개의 디바이스 드라이버가 이들 자료구조를 통해 관리된다.

cdev_map, bdev_map: 커널이 디바이스 드라이버를 관리하기 위해 유지하는 자료구조의 이름.

cdev 자료구조의 ops 필드에는 문자 디바이스 드라이버가 제공하는 file_operations 구조체가 저장되어 있으며, gendisk 자료구조의 fops 필드에는 블록 디바이스 드라이버가 제공하는 block_device_operations 구조체가 저장되어 있다.

위에서 블록 디바이스 드라이버는 read(), wrte() 함수에 대응되는 함수가 존재하지 않으며, 큐를 통해 '페이지캐시'와 통신하고 block_device_operations 구조체에 선언된 함수 호출헌더 하였음.

결국 사용자 태스크가 장치파일에 접근하는 경우, 장치파일의 inode 구조체에 저장되어 있는 i_mode필드(장치 유형 저장)와 i_rdev필드(디바이스 드라이버 주번호 / 부번호) 값을 이용하여 적절한 file_operations 구조체(또는 block_device_operations 구조체)를 찾아서 호출함으로써 디바이스 드라이버가 제공하는 함수를 사용할 수 있게 되는 것이다.



디바이스 드라이버 등록

문자 디바이스 드라이버 등록

문자 디바이스 드라이버인 경우 cdev_init() 함수를 호출하여 cdev 구조체를 할당받고, 구조체의 필드를 적절히 초기화한다. 그런 뒤 cdev_add() 함수를 호출함으로써 cdev 구조체를 cdev_map에 등록한다.
뿐만 아니라 디바이스 드라이버를 위한 주번호가 있어야 이 배열에 등록하는 것이 가능할 것인데, 커널은 chrdevs라는 이름의 자료구조를 사용하여 이는 주 번호 할당/관리한다.

미리 정해진 주 번호를 사용하는 경우 register_chrdev_region() 함수 호출 -> 디바이스 드라이버의 주 번호를 chrdevs 배열에 등록.
비 사용중인 주 번호를 할당하여 사용하고 싶다면 alloc_chrdev_region() 함수를 사용하여 비사용중인 주 번호를 리턴받아 사용 .

장치 디바이스 드라이버 등록

블록 디바이스 드라이버인 경우 주 번호는 major_names라는 커널 내 자료구조를 통해 관리되며, register_blkdev() 함수를 통해 특정 주 번호를 등록할 수 있다. 동적으로 주 번호를 할당받고 싶은 경우에는 이 함수의 첫 번째 인자로 0을 넘기면 사용가능한 주 번호를 할당받아 리턴해 준다.

alloc_disk() 함수를 통해 gendisk 구조체를 할당받고 add_disk() 함수를 통해 bdev_map에 등록시킨다.
그런데 블록 디바이스 드라이버는 앞서 언급한 대로 커널의 '페이지캐시'와 큐를 통해 데이터를 주고받는다. 따라서 블록 디바이스 드라이버는 커널과 데이터를 주고 받을 를 생성해야 하며, 커널이 큐에 데이터 읽기/쓰기 요청을 넣고 호출할 함수를 지정해 주어야 한다. 이는 blk_alloc_queue()와 blk_queue_make_request() 함수를 통해 이루어진다.

정리

새로운 디바이스 드라이버를 리눅스에 추가하는 과정(기본적으로 4단계).

1. 디바이스 드라이버 코어 함수 구현. 하드웨어 메뉴얼을 통해 작성된 코드들
2. 작성한 코어 함수를 리눅스에 등록시키기 위한 래퍼를 작성. 즉 file_operations의 인터페이스를 위한 함수를 구현
3. 바로 앞서의 설명된 일련의 함수를 이용하여 디바이스 드라이버를 커널에 등록
4. 디바이스 드라이버를 호출하기 위한 진입점(entry point)에 해당하는 장치파일을 생성

post-custom-banner

0개의 댓글