Linux kernel v4.4에서 간단한 블록 장치 드라이버 만들어보기

request_queue와 gendisk 객체 만들기

소스를 보면서 강좌를 읽어주세요.

struct mybrd_device

mybrd 드라이버 전체를 관리할 데이터를 모아놨습니다. mybrd_lock은 당연히 동기화를 위해 만들었고 나머지 mybrd_queue와 mybrd_disk는 각 객체를 생성하는 코드에서 설명하겠습니다.

어쨌든 블럭 장치의 드라이버에서 가장 중요한 데이터 구조가 바로 struct request_queue와 struct gendisk라는걸 기억해야합니다. 이번 장 전체가 바로 이 두개의 데이터 구조를 어떻게 만들고 사용하느냐를 설명하는 것입니다.

mybrd_init()

mybrd_init에서 바뀐건 mybrd_alloc()을 호출하는 것입니다. 이름 그대로 mybrd_device 구조체의 객체를 만듭니다. 당연히 struct request_queue와 struct gendisk 구조체의 객체로 만들겠지요.

mybrd_alloc()

mybrd_alloc은 총 3가지 단계로 나눠집니다.

mybrd_device object

객체가 저장될 메모리를 할당하고 spin-lock을 초기화합니다. 별게 없지요.

request_queue object

request-queue라는걸 만듭니다. 이것이 뭐냐면 request가 저장되는 queue입니다. 큐는 특정한 데이터 객체들이 한쪽으로 저장되고 한쪽으로 빠져나가는 걸 말하지요. 결국 request라는게 저장되고 빠져나가는 큐입니다. request는 다음 mybrd_make_request_fn() 함수에서 처리하게됩니다.

일단 여기에서는 request-queue라는 객체를 만들고, 그 객체의 각 필드를 초기화한다는걸 생각하면 됩니다. 각 세부 필드를 하나씩 설명하려면 한꺼번에 외울게 너무 많아지니 생략합니다.

하나만 알고 넘어가면 됩니다. blk_queue_make_request()가 request-queue라는 객체와 mybrd_make_request_fn()함수를 연결한다는 것만 생각하면 됩니다. 큐가 있으면 큐에 데이터를 넣는 코드가 있고 빼는 코드가 있고, 그리고 큐에서 빼낸 데이터를 처리하는 코드가 있겠지요. 커널은 이 request-queue에 request라는걸 넣고 빼주는걸 알아서 해줍니다. 왜냐면 모든 드라이버에 공통적인 코드이기 때문입니다. 커널은 결국 드라이버를 위한 프레임워크 역할을 합니다. 만약 모든 드라이버에 공통적으로 사용될만한 코드가 있으면 커널 개발자들은 반드시 커널 함수로 구현해놓습니다. 그리고 드라이버 개발자가 이용하도록 합니다.  드라이버 개발이 편하라는 이유도 있지만 더 큰 이유는 버그를 줄이기 위해서입니다. 드라이버 개발자들이 해야할 일이 많을 수록 드라이버는 더 불안정해지겠지요. 커널 코드는 전세계 커널 개발자들이 리뷰하고 테스트합니다. 또 전세계 수많은 스마트폰, 서버 등의 머신에서 돌아가면서 자동으로 테스트되고 버그 리포팅이 됩니다. 하지만 드라이버는 특정 회사에서 개발하게 되고, 결국 커널의 약점은 대부분 드라이버가 됩니다. 따라서 최대한 드라이버 코드가 커널 함수를 많이 쓸수록 더 안정적인 드라이버가 되고, 그게 결국 운영체제 전체의 안정성을 높여줍니다.

request-queue객체를 만들고 해지하는 커널 함수가 따로있는 것도 메모리 할당만 하면 되는게 아니라, 그 외에 request-queue 객체의 많은 필드들을 초기화해야하는데, 그런 초기화들이 드라이버마다 다른게 아니라 공통적이기 때문입니다. 많은 커널 객체들이 할당 함수를 따로 가지고 있습니다. 그럴때는 반드시 할당 함수를 사용하고 kmalloc등을 써서 직접 객체를 생성하고 초기화하지 않도록 주의해야합니다.

커널이 큐를 관리해주므로 우리가 만들건 결국 큐에서 빠져나온 request를 분석해서 뭘 할지를 결정하는 것입니다. 만약 우리가 하드디스크 드라이버의 개발자라면 request를 분석해서 메모리 어디에 있는 데이터를 읽거나 쓰면 되는지 확인해서, 메모리의 데이터를 하드디스크로 보내거나 디스크의 데이터를 메모리로 가져오기면 하면 됩니다. request 객체에 대한 설명도 자연스럽게 나왔습니다. request 객체는 결국 메모리 어디에 얼마만큼의 데이터를 읽거나 쓰라는 정보를 가지는 객체입니다. 직접 mybrd_make_request_fn()함수를 만들어보면 이해가 될 것입니다.

gendisk object

request-queue다음에는 gendisk라는 객체를 만듭니다. gendisk는 이름 그대로 디스크를 표현하는 객체입니다. 마찬가지로 전용 할당 함수 alloc_disk()가 있습니다. alloc_disk()의 인자는 1인데 1개의 디스크를 만든다는게 아니라 디스크에 최대 1개의 파티션이 있다는 것입니다. 그냥 디스크를 파티션으로 나누는게 아닐때 1로 지정합니다.

코드를 보면 디스크의 major number, minor number를 설정합니다. 왜냐면 이 디스크를 위한 장치 파일이 생성되기 때문입니다. first_minor를 111로 설정했습니다. 나중에 실험해보면 바로 장치 파일의 minor number가 111인걸 확인할 수 있습니다. 

이 디스크에서 사용할 request-queue가 방금 우리가 생성한 rq라는 것을 설정합니다. 그림고 디스크의 이름과 크기를 지정합니다. 디스크의 크기는 섹터 단위입니다. 한 섹터는 512바이트이므로 바이트 단위 숫자를 512로 나눠서 지정합니다.

fops필드가 있습니다. 바로 디스크의 장치 파일을 열고 사용할 때 호출될 함수입니다. 커널에서 struct block_device_operations의 정의를 찾아보면 read, write, open, close, ioctl 등 익숙한 이름들이 있습니다. 바로 어플리케이션에서 장치 파일을 가지고 read(), write(), open(), close(), ioctl() 등의 시스템 콜을 호출할 때 드라이버가 등록한 함수들이 호출되도록 만든 것입니다. mybrd는 /dev/mybrd 장치 파일을 만듭니다. 우리는 ioctl() 시스템 콜에 mybrd_ioctl()함수가 호출되도록 등록했으니, 나중에 장치 파일을 열고 ioctl()을 호출하는 어플을 만들어서 실험해보시기 바랍니다.

커널의 콜 스택을 분석할때 dump_stack() 함수가 편리합니다. 현재까지의 콜 스택을 커널 메세지로 출력하는 함수입니다. mybrd_ioctl()함수에서 dump_stack()을 호출하면 어플의 시스템 콜이 어떻게 커널에서 처리되는지 파악하기 편하겠지요.

gendisk객체를 만들었으면 커널에 새로운 디스크를 생성하라고 알려줘야합니다. 그게 바로 add_disk()함수입니다. 사실 request-queue를 만들긴 했지만, 커널에 드라이버가 만든 request-queue를 알려주는 함수는 없습니다. 바로 gendisk 객체에 request-queue를 등록하면 커널은 gendisk에 접근할 때마다 이 디스크가 사용할 request-queue가 뭔지 알게되는 것이지요. 따라서 블럭 장치의 가장 핵심 객체가 바로 gendisk이고, 이 핵심 객체를 커널이 사용하도록 등록하는게 add_disk()함수입니다.

add_disk()가 호출된 후에는 /dev/mybrd 파일이 생깁니다. /sys/block/mybrd 디렉토리도 생깁니다. 그리고 커널은 새로 디스크가 연결된걸 알고, 디스크를 읽어봅니다. add_disk()가 호출된 순간부터 디스크로 IO가 발생합니다. 그러니 add_disk()를 호출하기 전에 IO를 처리할 모든 준비가 끝나야겠지요.

댓글

댓글 본문
작성자
비밀번호
버전 관리
gurugio
현재 버전
선택 버전
graphittie 자세히 보기