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

새로운 큐 만들기

multiqueue-mode를 mq-mode라고 줄여서 부르겠습니다. mq-mode는 간단히말하면 여러개의 sw-queue (staging-queue라고도 부름)를 만드는 겁니다. 그리고 hw-queue도 여러개를 만듭니다.

sw-queue는 어플에서 전달된 IO의 request를 받습니다. 커널이 프로세서의 갯수만큼 생성합니다. 그리고 IO 스케줄러가 sw-queue에서 스케줄링을합니다. 지금까지 봤던 보통의 request-queue와 유사합니다. 그리고 이 sw-queue에서 hw-queue로  request를 전달합니다. hw-queue는 디스크 장치의 특성에 따라 한개가 될 수도 있고 여러개가 될 수도 있습니다. 하드웨어적으로 멀티 IO를 지원한다면 드라이버에서 여러개의 hw-queue를 만드는 것이고, 기존의 보통 하드디스크라면 하나의 hw-queue를 만듭니다. 그러면 커널이  sw-queue의 request를 hw-queue로 넘깁니다. 그리고 hw-queue에서는 드라이버의 request 처리 함수를 호출해서 request를 장치에 전달하도록 합니다.

말로는 복잡하니까 그냥 한번 만들어보겠습니다. mq-mode 에 대한 논문을 보신 분들은 아시겠지만 리눅스 커널에 null_blk라는 드라이버가 바로 mq-mode를 구현하면서 예제로 만든 드라이버입니다. 실제 장치가 없이 커널과 드라이버가 request를 처리하는 성능만 측정하도록 만들어진 드라이버라 논문을 쓰고 실험하는데 사용된 드라이버입니다. 따라서 null_blk 드라이버에서 핵심적인 코드만 가져와서 멀티큐로 동작하는 램디스크 드라이버를 만들어보겠습니다. 이 강좌를 읽고난 후에는 가장 최신 커널의 null_blk 드라이버 소스를 보는게 최신 커널 소스를 분석하는데 큰 도움이 될 것입니다.

PS. 저도 공부하면서 만든 코드라 미흡한게 많습니다. 제가 빼먹은 코드나 부족한 설명이 있다면 댓글로 알려주세요. 보충하겠습니다.

mybrd_allod() 에 MYBRD_Q_MQ모드 추가하기

이전에 request-mode를 위한 큐를 만들기 위해 MYBRD_Q_RQ 모드를 만들었습니다. 이번에는 MYBRD_Q_MQ 모드를 추가합니다.

blk_mq_tag_set 객체는 커널이 큐를 관리할 때 사용할 데이터를 표현한것입니다. 드라이버버가 하드웨어 장치를 잘 알기때문에 hw-queue를 몇개 만들지를 결정할 수 있습니다. 따라서 blk_mq_tag_set 객체를 초기화할때 몇개의 hw-queue를 만들지 등 드라이버가 결정할 정보들을 전달합니다. 그 외에 어떤 필드들을 초기화해야할지는 blk_mq_tag_set을 인자로받는 blk_mq_alloc_tag_set() 코드를 보면 확인이 될것입니다.

- ops: request-queue의 동작을 위한 함수들

- nr_hw_queues: hw-queue의 갯수

- queue_depth: hw-queue가 최대 가질 수 있는 request의 갯수

- numa_node: hw-queue 등 커널이 필요한 객체들을 할당할 NUMA 노드의 번호

- cmd_size: sw-queue에서 hw-queue로 request를 전달할때 같이 전달할 추가 정보의 크기

- driver_data: hw-queue에게 드라이버가 전달할 데이터

blk_mq_init_allocated_queue()

최종적으로 큐를 생성하는 함수는 blk_mq_init_queue()입니다. 코드를 보면 이미 익숙한 blk_alloc_queue_node() 함수로 큐를 생성합니다. 그리고 blk_mq_init_allocated_queue() 함수로 큐를 초기화합니다. 이건 request-mode에서 큐를 만드는 것과 유사합니다.

blk_mq_init_allocated_queue()는 큐를 초기화하는데, 여기서 큐는 sw-queue와 hw-queue 모두를 말합니다. 모든 큐와 각 큐가 어떻게 매칭이 될지, 각 큐들에 대한 정보 등등을 request_queue 객체를 만들어서 관리합니다.

다음은 request_queue의 필드입니다.

- queue_ctx: sw-queue에 대한 정보

- queue_hctxs: hw-queue에 대한 정보

- mq_map: sw-queue와 hw-queue가 어떻게 매칭될지에 필요한 정보. 예를 들어 2개의 sw-queue와 1개의 hw-queue가 만들어졌으면 모든 sw-queue의 request들이 하나의 hw-queue로 전달되야합니다. 2:2로 매칭될수도 있고, 4:1, 4:2 등등 드라이버가 결정하기 나름입니다. mq_map이 이런 매칭을 결정하는게 아니고 드라이버가 제공한 함수에서 결정하는데, mq_map은 그런 결정에 필요한 정보들을 가지고 있습니다. (디록트로 커널이 제공하는 함수도 있고 우리는 커널 함수를 쓰겠습니다.)

- make_request_fn: blk_queue_make_request() 함수를 써서 hw-queue의 갯수에 따라 bio 처리 함수를 다르게 지정합니다.  

그리고 마지막으로 blk_mq_init_hw_queues() 함수를 호출하는데, 여기에서 mybrd_mq_ops.init_hctx 콜백 함수가 호출됩니다. 각 hw-queue 를 초기화하는 함수입니다.

blk_mq_init_queue() 함수로 sw-queue와 hw-queue를 초기화했으니 disk를 만듭니다. disk 생성은 큐와 상관없이 항상 동일합니다.

blk_sq_make_request()

request-mode에서도 마찬가지로 커널이 request-queue의 bio 처리 함수를 지정했습니다. request-mode에서는 bio 처리 함수에서 스케줄러를 호출한 뒤에 드라이버가 제공한 request 처리 함수를 호출했습니다. mq-mode에서는 어떻게 bio가 처리되는지 blk_sq_make_request()를 통해 간단히 보겠습니다.

큐와 bio를 전달받는 것은 익숙합니다. 그 다음은 blk_mq_map_request()로 request를 만들고, blk_mq_run_hw_queue()로 hw_queue에서 드라이버로 request를 전달합니다.

blk_mq_map_request()

현재 bio는 sw-queue에 있습니다. 그리고 blk_mq_map_request()함수에서 어떤 hw-queue로 넘겨질지를 결정합니다.

blk_mq_map_request()함수는 q->mq_ops->map_queue() 함수를 호출합니다. 여기서 mq_ops는 드라이버가 초기화한 콜백 함수들을 가지고 있습니다. 드라이버 소스를 보면 mybrd_mq_ops.map_queue = blk_mq_map_queue 로 설정하고 있습니다. 드라이버가 직접 sw-queue와 hw-queue의 매칭을 결정하지않고 커널 함수에 위임했습니다.

blk_mq_map_queue를 보겠습니다. 딱 한줄이네요.

/*
 * Default mapping to a software queue, since we use one per CPU.
 */
struct blk_mq_hw_ctx *blk_mq_map_queue(struct request_queue *q, const int cpu)
{
    return q->queue_hw_ctx[q->mq_map[cpu]];
}
EXPORT_SYMBOL(blk_mq_map_queue);

cpu는 현재 코드가 실행중인 cpu입니다. queue_hw_ctx는 드라이버가 요청한 hw-queue 갯수만큼 blk_mq_hw_ctx 객체를 만들 것입니다. 따라서 q->mq_map 배열이 cpu 번호와 hw-queue의 번호를 연결해주는 역할을 하고 있네요. 물론 커널 함수를 안쓰고 드라이버가 직접 만든 함수를 써도 됩니다. 특정 CPU와 특정 hw-queue를 묶고 싶을때는 직접 매칭 함수를 만들면 되겠지요. 커널 함수는 단지 공평한 분산만을 생각합니다.

blk_mq_map_request()를 계속 보면 __blk_mq_alloc_request()를 호출해서 request 객체를 만듭니다.

blk_mq_run_hw_queue

blk_mq_map_request() 함수는 결국 bio를 포함하는 request를 만들고 request가 어떤 hw-queue로 가야할지를 결정하는 함수였습니다. 그럼 이제 request를 hw-queue로 보내야겠지요. blk_mq_run_hw_queue()함수가 sw-queue에 있는 request를 hw-queue로 전달합니다.

blk_mq_run_hw_queue()는 결국 __blk_mq_run_hw_queue()를 호출합니다. hw-queue는 사실상 하는 일이 없습니다. 그냥 드라이버의 mybrd_mq_ops.queue_rq 포인터를 읽어서 함수를 호출하는 것이 대부분입니다. 당연히 mybrd_mq_ops.queue_rq 함수는 request를 받아서 장치를 읽고 쓰는 일을 해야겠지요. 그 일은 request-mode에서와 동일할 것입니다.

댓글

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