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

null_blk 드라이버에서 wait-queue 사용 예제

wait-queue 사용법은 간단한 것이니까 내부부터 설명하기보다는 예제를 보면서 생각해보는게 좋을것 같습니다.

mybrd를 만들때 소개한대로 mybrd에서 참고한 드라이버 소스가 있습니다. 램디스크 드라이버 brd와 null_blk라는 커널의 블럭 레이어 테스트 드라이버입니다. 이 두 드라이버를 합쳐서 램디스크가 멀티큐를 처리할 수 있도록 만든게 mybrd 드라이버입니다. 사실 램디스크가 멀티큐를 지원할 필요는 없지만 멀티큐의 구현을 알아보려는 생각으로 시도해본 것이지요.

그래서 null_blk.c 파일을 보면 mybrd 코드와 거의 동일합니다. 그런데 한가지만 다른게 있습니다. null_blk 드라이버는 struct nullb_cmd라는 구조체를 만들어서 bio-mode일때와 request-queue-mode일때 모두 동일하게 IO를 처리합니다.

null_queue_bio함수는 mybrd의 mybrd_make_request_fn 함수와 같은 일을 하는 함수입니다. bio-mode일때 bio를 처리하는 함수입니다. null_add_dev에서 blk_queue_make_request함수의 인자로 전달됩니다. mybrd에서 mybrd_make_request_fn 함수도 마찬가지로 blk_queue_make_request함수의 인자로 전달되서 bio를 처리할 때 호출되었습니다.

null_rq_prep_fn함수는 mybrd_request_fn에 해당하는 함수입니다. blk_queue_prep_rq 함수가 호출될때 인자로 전달되서 request를 처리할 때 호출됩니다.

null_blk 드라이버 소스에서는 null_queue_bio와 null_rq_prep_fn함수에서 alloc_cmd함수를 통해 nullb_cmd 객체를 만듭니다. 그리고 bio-mode일때는 nullb_cmd의 bio필드에 bio 포인터를 복사하고, request-mode일때는 rq필드에 request의 포인터를 복사합니다. 그래서 하위 처리 함수에서는 nullb_cmd의 객체만 전달받아서 처리하는 것이지요.

null_blk 드라이버의 구현과 별도로 제가 말씀드리고싶은 것은 바로 alloc_cmd에서 nullb_cmd 객체를 사용할때 바로 wait-queue를 사용한다는 것입니다.

가장 먼저 봐야할 코드는 nullb_queue 객체의 cmds 필드와 tag_map 필드입니다.

static int setup_commands(struct nullb_queue *nq)
{
    struct nullb_cmd *cmd;
	int i, tag_size;

	nq->cmds = kzalloc(nq->queue_depth * sizeof(*cmd), GFP_KERNEL);
	if (!nq->cmds)
		return -ENOMEM;

	tag_size = ALIGN(nq->queue_depth, BITS_PER_LONG) / BITS_PER_LONG;
	nq->tag_map = kzalloc(tag_size * sizeof(unsigned long), GFP_KERNEL);
	if (!nq->tag_map) {
		kfree(nq->cmds);
		return -ENOMEM;
	}

	for (i = 0; i < nq->queue_depth; i++) {
		cmd = &nq->cmds[i];
		INIT_LIST_HEAD(&cmd->list);
		cmd->ll_list.next = NULL;
		cmd->tag = -1U;
	}

	return 0;
}

cmds필드에는 nullb_cmd 객체를 미리 request-queue의 depth만큼 할당해놓습니다. 그리고 tag_map필드에 비트맵을 만듭니다. 이 비트맵은 각 비트가 하나의 nullb_cmd 객체의 사용중인지 가용한지 상태를 보여주는 것입니다. 비트의 값이 0이면 가용한 것이고, 1이면 이미 사용중인 것입니다.


static unsigned int get_tag(struct nullb_queue *nq)
{
    unsigned int tag;

	do {
		tag = find_first_zero_bit(nq->tag_map, nq->queue_depth);
		if (tag >= nq->queue_depth)
			return -1U;
	} while (test_and_set_bit_lock(tag, nq->tag_map));

	return tag;
}

static struct nullb_cmd *__alloc_cmd(struct nullb_queue *nq)
{
    struct nullb_cmd *cmd;
	unsigned int tag;

	tag = get_tag(nq);
	if (tag != -1U) {
		cmd = &nq->cmds[tag];
		cmd->tag = tag;
		cmd->nq = nq;
		if (irqmode == NULL_IRQ_TIMER) {
			hrtimer_init(&cmd->timer, CLOCK_MONOTONIC,
				     HRTIMER_MODE_REL);
			cmd->timer.function = null_cmd_timer_expired;
		}
		return cmd;
	}

	return NULL;
}

get_tag함수는 find_first_zero_bit함수를 이용해서 비트맵에서 0인 비트를 찾은 후, test_and_set_bit_lock함수를 이용해서 해당 비트를 1로 바꿉니다. test_and_set 계열의 함수들이 다 그렇듯이 값을 바꾸기 이전 값을 반환합니다. 만약 1을 반환하면 값을 1로 바꾸기 전에 다른 쓰레드에서 1로 바꾼 것이므로, 다시 0인 비트를 찾습니다.

__alloc_cmd에서는 만약 가용한 nullb_cmd를 찾지못할경우 NULL을 반환합니다.

static struct nullb_cmd *alloc_cmd(struct nullb_queue *nq, int can_wait)
{
    struct nullb_cmd *cmd;
	DEFINE_WAIT(wait);

	cmd = __alloc_cmd(nq);
	if (cmd || !can_wait)
		return cmd;

	do {
		prepare_to_wait(&nq->wait, &wait, TASK_UNINTERRUPTIBLE);
		cmd = __alloc_cmd(nq);
		if (cmd)
			break;

		io_schedule();
	} while (1);

	finish_wait(&nq->wait, &wait);
	return cmd;
}

alloc_cmd 함수는 __alloc_cmd를 호출해서 nullb_cmd 객체를 찾습니다. 이상없이 객체를 찾으면 당연히 IO처리를 계속하면 됩니다만 만약 가용한 nullb_cmd객체가 없다면 어떻게 해야할지를 alloc_cmd에서 결정하는 것입니다.

가용한 nullb_cmd 객체가 없다면 가장 먼저 prepare_to_wait을 호출합니다. 함수 인자나 내부 구현등은 나중에 prepare_to_wait의 코드를 볼때 생각하기로하고, 일단 지금은 어떻게 사용하는지만 생각해보겠습니다. 이름만봐도 지금은 나중에 잠들경우를 준비한다는걸 알 수 있습니다. 그리고 __alloc_cmd를 다시한번 호출해봅니다. 만약 또다시 실패한다면 이젠 정말 잠들어야합니다. 그래서 결국 io_schedule함수를 호출해서 프로세스를 강제로 잠재웁니다.

잠든 프로세스가 언제 깨어날까요. 그건 찾기를 실패한 자원이 다시 가용해질때겠지요. 그러므로 nullb_cmd 객체를 반환하는 함수를 찾아야합니다. alloc_cmd가 있으니 free_cmd가 있겠지요. 그리고 free_cmd에서는 put_tag를 호출합니다.

static void put_tag(struct nullb_queue *nq, unsigned int tag)
{
    clear_bit_unlock(tag, nq->tag_map);

	if (waitqueue_active(&nq->wait))
		wake_up(&nq->wait);
}

static void free_cmd(struct nullb_cmd *cmd)
{
    put_tag(cmd->nq, cmd->tag);
}

put_tag는 waitqueue_active를 호출해서 현재 wait-queue에서 잠든 프로세스가 있는지 확인하고 wake_up함수로 프로세스를 깨웁니다.

그러면 alloc_cmd의 io_schedule에서 잠든 프로세스는 깨어나고 다시 prepare_to_wait함수와 __alloc_cmd함수를 호출합니다. 이 루프를 nullb_cmd 객체를 찾을 때까지 반복합니다. 사용자 어플은 커널 레벨에서 순간순간 깨어나지만, 사용자 레벨로는 되돌아오지 않습니다. 그리고 nullb_cmd객체를 찾게되면 루프를 빠져나와서 finish_wait을 호출하고 종료합니다.

다시한번 정리하면 필요한 자원을 못찾았을때

  • prepare_to_wait -> io_schedule (or schedule) -> finish_wait

자원을 해지하고, 자원을 기다리며 잠든 프로세스를 깨울때

  • waitqueue_active -> wake_up

댓글

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