Block / Storage · Linux v6.6 · source + diagram note

request queue

Linux v6.6의 bio 제출·request/tag 준비, hardware queue dispatch, freeze depth와 완료 경계를 고정 원문 그림과 표로 읽습니다.

전체 흐름

block/blk-mq.c v6.6에서 bio 제출은 기존 request와 병합되거나 새 request를 준비한다. 그 뒤 plug 보관, scheduler 삽입, 직접 issue 시도로 갈라진다. blk_mq_run_hw_queue()는 이미 쌓인 작업을 실행할지를 판단하며 request/tag 할당 함수가 아니다. driver 완료는 별도 blk_mq_end_request() 경계다.

그림 1. bio 병합과 request 전달 출구
bio입력 · 준비제한/무결성 검사조건부병합 시도plug / scheduler병합 성공기존 request새 rq 할당 없음병합 실패cached / new rqtag 확보 가능성전달 출구분기flush · plugscheduler · direct병합 성공은 새 request를 만들지 않는다. flush/plug 보관도 device 완료가 아니다.

점선은 병합 실패 후 새 request가 필요한 경로다. 모든 제출이 곧바로 driver로 넘어가지는 않는다.

시점주요 함수/조건결과를 읽는 기준
bio 준비제한·분할·integrity 검사; v6.18.37에는 zone write plug 재진입여기서 끝난 bio에는 request가 없을 수 있음
병합blk_mq_get_cached_request() / blk_mq_get_new_requests()성공하면 기존 request에 붙고 새 tag를 얻지 않음
새 request__blk_mq_alloc_requests()의 tag 획득요청을 가졌어도 아직 issue/완료가 아님
전달flush / plug / scheduler / direct issue 조건보관·삽입·issue 시도는 서로 다른 출구
완료blk_mq_end_request()driver 완료와 status 처리 경계

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: block/blk-mq.c
Linux v6.6: block/blk-core.c
Linux v6.6: include/linux/blk-mq.h

설명: blk-mq.c에서 submit·tag 할당·hardware queue run·freeze·완료 함수의 실제 경계를 분리한다. blk-core.c는 bio 제출 진입 및 공통 block 처리를 함께 확인할 때 참고한다.

대표 코드

고정 버전의 함수 경계를 기준으로 발췌했다. 원래 run_hw_queue 제목 아래의 freeze 코드는 실제 dispatch 함수가 아니다.

blk_mq_submit_bio() 시작 (앞 함수 말미 포함)

고정 원본: block/blk-mq.c:2946-2985
		bio->bi_ioprio = get_current_ioprio();
	blkcg_set_ioprio(bio);
}

/**
 * blk_mq_submit_bio - Create and send a request to block device.
 * @bio: Bio pointer.
 *
 * Builds up a request structure from @q and @bio and send to the device. The
 * request may not be queued directly to hardware if:
 * * This request can be merged with another one
 * * We want to place request at plug queue for possible future merging
 * * There is an IO scheduler active at this queue
 *
 * It will not queue the request if there is an error with the bio, or at the
 * request creation.
 */
void blk_mq_submit_bio(struct bio *bio)
{
	struct request_queue *q = bdev_get_queue(bio->bi_bdev);
	struct blk_plug *plug = blk_mq_plug(bio);
	const int is_sync = op_is_sync(bio->bi_opf);
	struct blk_mq_hw_ctx *hctx;
	struct request *rq;
	unsigned int nr_segs = 1;
	blk_status_t ret;

	bio = blk_queue_bounce(bio, q);
	if (bio_may_exceed_limits(bio, &q->limits)) {
		bio = __bio_split_to_limits(bio, &q->limits, &nr_segs);
		if (!bio)
			return;
	}

	if (!bio_integrity_prep(bio))
		return;

	bio_set_ioprio(bio);

	rq = blk_mq_get_cached_request(q, plug, &bio, nr_segs);

발췌의 첫 줄은 bio_set_ioprio()의 끝이다. blk_mq_submit_bio() 정의는 L2963부터이며 cached/new request helper에 들어간다.

blk_mq_submit_bio()의 request·출구 분기

고정 원본: block/blk-mq.c:2986-3024
	if (!rq) {
		if (!bio)
			return;
		rq = blk_mq_get_new_requests(q, plug, bio, nr_segs);
		if (unlikely(!rq))
			return;
	}

	trace_block_getrq(bio);

	rq_qos_track(q, rq, bio);

	blk_mq_bio_to_request(rq, bio, nr_segs);

	ret = blk_crypto_rq_get_keyslot(rq);
	if (ret != BLK_STS_OK) {
		bio->bi_status = ret;
		bio_endio(bio);
		blk_mq_free_request(rq);
		return;
	}

	if (op_is_flush(bio->bi_opf) && blk_insert_flush(rq))
		return;

	if (plug) {
		blk_add_rq_to_plug(plug, rq);
		return;
	}

	hctx = rq->mq_hctx;
	if ((rq->rq_flags & RQF_USE_SCHED) ||
	    (hctx->dispatch_busy && (q->nr_hw_queues == 1 || !is_sync))) {
		blk_mq_insert_request(rq, 0);
		blk_mq_run_hw_queue(hctx, true);
	} else {
		blk_mq_run_dispatch_ops(q, blk_mq_try_issue_directly(hctx, rq));
	}
}

병합·cached/new request·flush/plug/scheduler/direct 분기는 조건에 따라 갈라진다. 이 반환은 device 완료를 뜻하지 않는다.

__blk_mq_alloc_requests()의 tag 획득

고정 원본: block/blk-mq.c:447-503
	if (data->cmd_flags & REQ_NOWAIT)
		data->flags |= BLK_MQ_REQ_NOWAIT;

	if (q->elevator) {
		/*
		 * All requests use scheduler tags when an I/O scheduler is
		 * enabled for the queue.
		 */
		data->rq_flags |= RQF_SCHED_TAGS;

		/*
		 * Flush/passthrough requests are special and go directly to the
		 * dispatch list.
		 */
		if ((data->cmd_flags & REQ_OP_MASK) != REQ_OP_FLUSH &&
		    !blk_op_is_passthrough(data->cmd_flags)) {
			struct elevator_mq_ops *ops = &q->elevator->type->ops;

			WARN_ON_ONCE(data->flags & BLK_MQ_REQ_RESERVED);

			data->rq_flags |= RQF_USE_SCHED;
			if (ops->limit_depth)
				ops->limit_depth(data->cmd_flags, data);
		}
	}

retry:
	data->ctx = blk_mq_get_ctx(q);
	data->hctx = blk_mq_map_queue(q, data->cmd_flags, data->ctx);
	if (!(data->rq_flags & RQF_SCHED_TAGS))
		blk_mq_tag_busy(data->hctx);

	if (data->flags & BLK_MQ_REQ_RESERVED)
		data->rq_flags |= RQF_RESV;

	/*
	 * Try batched alloc if we want more than 1 tag.
	 */
	if (data->nr_tags > 1) {
		rq = __blk_mq_alloc_requests_batch(data);
		if (rq) {
			blk_mq_rq_time_init(rq, alloc_time_ns);
			return rq;
		}
		data->nr_tags = 1;
	}

	/*
	 * Waiting allocations only fail because of an inactive hctx.  In that
	 * case just retry the hctx assignment and tag allocation as CPU hotplug
	 * should have migrated us to an online CPU by now.
	 */
	tag = blk_mq_get_tag(data);
	if (tag == BLK_MQ_NO_TAG) {
		if (data->flags & BLK_MQ_REQ_NOWAIT)
			return NULL;
		/*

scheduler가 있으면 scheduler tag 조건을 설정한다. BLK_MQ_NO_TAG에서 NOWAIT은 NULL을 반환할 수 있고, 대기형은 재시도한다.

blk_mq_run_hw_queue() 실제 정의

고정 원본: block/blk-mq.c:2233-2266
void blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx, bool async)
{
	bool need_run;

	/*
	 * We can't run the queue inline with interrupts disabled.
	 */
	WARN_ON_ONCE(!async && in_interrupt());

	might_sleep_if(!async && hctx->flags & BLK_MQ_F_BLOCKING);

	/*
	 * When queue is quiesced, we may be switching io scheduler, or
	 * updating nr_hw_queues, or other things, and we can't run queue
	 * any more, even __blk_mq_hctx_has_pending() can't be called safely.
	 *
	 * And queue will be rerun in blk_mq_unquiesce_queue() if it is
	 * quiesced.
	 */
	__blk_mq_run_dispatch_ops(hctx->queue, false,
		need_run = !blk_queue_quiesced(hctx->queue) &&
		blk_mq_hctx_has_pending(hctx));

	if (!need_run)
		return;

	if (async || !cpumask_test_cpu(raw_smp_processor_id(), hctx->cpumask)) {
		blk_mq_delay_run_hw_queue(hctx, 0);
		return;
	}

	blk_mq_run_dispatch_ops(hctx->queue,
				blk_mq_sched_dispatch_requests(hctx));
}

quiesced/empty이면 반환할 수 있고, async/CPU 조건에서 지연 실행한다. 이 함수는 새 request를 할당하지 않는다.

blk_freeze_queue_start() 본문 끝·freeze wait·blk_freeze_queue() 시작

고정 원본: block/blk-mq.c:126-165
	mutex_lock(&q->mq_freeze_lock);
	if (++q->mq_freeze_depth == 1) {
		percpu_ref_kill(&q->q_usage_counter);
		mutex_unlock(&q->mq_freeze_lock);
		if (queue_is_mq(q))
			blk_mq_run_hw_queues(q, false);
	} else {
		mutex_unlock(&q->mq_freeze_lock);
	}
}
EXPORT_SYMBOL_GPL(blk_freeze_queue_start);

void blk_mq_freeze_queue_wait(struct request_queue *q)
{
	wait_event(q->mq_freeze_wq, percpu_ref_is_zero(&q->q_usage_counter));
}
EXPORT_SYMBOL_GPL(blk_mq_freeze_queue_wait);

int blk_mq_freeze_queue_wait_timeout(struct request_queue *q,
				     unsigned long timeout)
{
	return wait_event_timeout(q->mq_freeze_wq,
					percpu_ref_is_zero(&q->q_usage_counter),
					timeout);
}
EXPORT_SYMBOL_GPL(blk_mq_freeze_queue_wait_timeout);

/*
 * Guarantee no request is in use, so we can change any data structure of
 * the queue afterward.
 */
void blk_freeze_queue(struct request_queue *q)
{
	/*
	 * In the !blk_mq case we are only calling this to kill the
	 * q_usage_counter, otherwise this increases the freeze depth
	 * and waits for it to return to zero.  For this reason there is
	 * no blk_unfreeze_queue(), and blk_freeze_queue() is not
	 * exported to drivers as the only user for unfreeze is blk_mq.
	 */

보존한 원본의 시작은 blk_freeze_queue_start() 함수 몸통 L126이다. 첫 freeze에서 percpu_ref를 kill하고 wait 함수가 0을 기다린다.

blk_mq_end_request() — 완료 경계

고정 원본: block/blk-mq.c:1049-1054
void blk_mq_end_request(struct request *rq, blk_status_t error)
{
	if (blk_update_request(rq, error, blk_rq_bytes(rq)))
		BUG();
	__blk_mq_end_request(rq, error);
}

driver가 완료를 알리는 별도 경로다. blk_update_request()와 __blk_mq_end_request()가 status를 처리한다.

함수별 분석

blk_mq_submit_bio()에서 blk_mq_get_cached_request()는 plug의 cached request를 검사하고 병합하면 bio를 비운다. 없으면 blk_mq_get_new_requests()가 bio_queue_enter() 뒤 병합, QoS, 새 request 할당을 수행한다. blk_crypto_rq_get_keyslot() 실패는 bio_endio() 후 request를 해제한다. 정상 준비 뒤에도 flush와 plug가 먼저 받을 수 있고, scheduler를 쓰거나 dispatch가 바쁘면 삽입 후 hardware queue를 실행한다. 그 외에는 직접 issue를 시도한다.

그림 2. 준비 오류·tag 실패·driver 완료의 경계
bio제출 시작검사 · 병합준비 오류bio 종료no rq / bio_endio할당 오류NOWAIT 실패tag/rq 없음requestqueued/issued완료 전 단계driver 완료end_requeststatus 전달제출 함수 반환이나 queue 실행 호출은 완료 callback이 아니다. 오류 위치별 소유권을 구분한다.

오류는 발생 위치마다 bio와 request의 소유 상태가 다르다. 그림은 대표 경로만 보인다.

경계호출자가 알 수 있는 것동일시하면 안 되는 것
준비 검사bio가 이른 오류로 종료될 수 있음request 할당 실패와 동일하지 않음
병합 성공bio가 기존 request에 합쳐짐새 tag 생성이나 별도 issue 아님
tag 미획득NOWAIT에서 BLK_MQ_NO_TAG면 NULL 가능device latency나 완료 오류 아님
제출 출구flush/plug/scheduler/direct issue 갈림모든 분기가 즉시 driver dispatch 아님
완료 함수request 상태와 blk_status_t 처리run_hw_queue() 호출 자체가 완료 아님

구조체와 필드

request_queue.q_usage_counter는 queue 사용 중인 경로의 참조를 세고 freeze가 신규 진입을 막는 기준이다. blk_plug에 cached request가 있으면 그 request가 가진 참조·tag를 재사용할 수 있다. blk_mq_alloc_data는 새 request의 ctx/hctx와 tag 선택 맥락을 전달한다. hctx는 dispatch 대상이지만 그 자체가 한 request의 완료 상태는 아니다.

그림 3. queue 참조와 cached/new request의 갈림
queue사용 참조freeze 중 진입 제한재사용cached rq기존 queue 참조 유지새 요청get_new_requestsqueue 진입 후 할당할당기blk_mq_get_tagscheduler/driver tag결과requestdispatch 전 상태cached rq는 기존 queue 참조·tag를 재사용한다. 새 rq의 tag 획득도 driver issue·완료는 아니다.

상단 cached 경로는 tag allocator를 지나지 않는다. 하단 새 request 경로에서 scheduler tag와 driver tag 조건을 확인한다.

객체/필드의미주의할 점
request_queue.q_usage_counterbio/request가 queue를 사용하는 동안의 참조freeze depth와 별개; drain 대기는 별도
blk_plug.cached_rq(s)plug에 보관된 재사용 request새 tag를 매번 받는다고 보면 안 됨
blk_mq_alloc_data.rq_flagsscheduler tag 및 scheduler 사용 여부scheduler tag와 driver tag를 혼동하지 않음
blk_mq_hw_ctxCPU/type에 매핑된 hardware queue 문맥quiesce·pending 조건은 run에서 다시 확인
requestbio를 묶고 dispatch·완료로 이어지는 객체allocation, issue, completion은 각기 다른 시점

실행 단계와 상태 변화

v6.6은 blk_mq_hw_queue_need_run() 결과가 거짓이면 바로 반환한다. 그 뒤 async 또는 현재 CPU가 hctx cpumask 밖이면 지연 실행하고, 그 외에는 scheduler dispatch를 호출한다. freeze는 별도 경로다. v6.6은 blk_freeze_queue_start()가 freeze depth를 늘리고 첫 진입에서 사용 참조를 kill한다. 첫 freeze는 percpu_ref_kill() 뒤 밀린 hardware queue도 실행해 참조가 빠져나가게 한다. blk_mq_freeze_queue_wait()가 참조 수 0을 기다리는 것은 신규 진입 차단과 다른 단계다.

그림 4. hardware queue 실행 조건과 freeze depth·drain
RUN: pending과 quiesce를 검사한 뒤 dispatch 선택FREEZE: 새 queue 참조 차단과 기존 참조 drainhctxrun_hw_queuepending · QUIESCED불가returnquiesced/emptyasync/CPU 외delay_run나중에 실행그 외dispatchdriver issue 시도queuefreeze_depth첫 freeze인지 검사depth 1percpu_ref_kill신규 진입 차단별도 waitref == 0진행 중 참조 drain경계변경 가능완료와는 별개freeze는 dispatch 중단 플래그와 다르다. run_hw_queue는 request를 할당하지 않는다.

윗줄의 반환·지연·inline dispatch는 조건부 대안이다. 아랫줄의 freeze는 신규 사용 제한과 기존 참조 drain을 나눠 보인다.

조건/호출변화 또는 다음 단계잘못된 해석
blk_mq_hw_queue_need_run() falsequiesced 또는 pending 없음 → 반환; 버전에 따라 재검사request 할당 실패/완료와 같지 않음
async 또는 CPU mask 밖blk_mq_delay_run_hw_queue() 예약이 함수 호출 시점에 driver 완료 아님
동기·허용 CPUblk_mq_sched_dispatch_requests() 시도모든 request가 driver에 수락된다는 보장 아님
첫 freeze depthpercpu_ref_kill(q_usage_counter)기존 사용자가 즉시 사라지는 것은 아님
blk_mq_freeze_queue_wait()참조 0까지 대기quiesce flag 설정이나 완료 callback과 동일하지 않음

불변 조건과 실패 사례

queue 참조

cached request는 기존 사용 참조를 보유한다. 새 request 경로의 bio_queue_enter()와 중복해 세지 않는다.

tag와 issue

tag 확보는 request 준비 단계다. scheduler tag 여부와 driver issue, 완료를 분리한다.

freeze와 quiesce

freeze 첫 깊이는 신규 queue 참조를 차단하고 별도 wait로 기존 참조를 drain한다. quiesce는 dispatch 선택에 영향을 준다.

버전별 run

v6.6은 blk_mq_hw_queue_need_run() 결과가 거짓이면 바로 반환한다.

계측과 검증

공유 tracefs 설정을 바꾸지 않고 현재 장치 통계와 queue 구성을 먼저 읽는다. block_rq_issue/block_rq_complete 이벤트가 보이더라도 그것만으로 tag 대기 시간, freeze depth 또는 병합되지 않은 bio 전체를 역산할 수 없다. 내부 함수 순서는 고정 소스와 대조해야 한다.

읽기 전용 관찰확인 가능한 것한계
cat /proc/diskstats장치별 누적 I/O·시간 카운터bio 병합·tag 획득·freeze depth를 분리 못함
cat /sys/block/<dev>/queue/scheduler현재 scheduler 선택개별 request의 scheduler 진입을 증명하지 않음
ls /sys/block/<dev>/mq/노출된 hardware queue 디렉터리run 호출 횟수나 dispatch 성공 횟수 아님
ls /sys/kernel/tracing/events/block/tracepoint 존재 여부공유 계측 설정을 바꾸지 않음
읽기 전용 예:
cat /proc/diskstats
cat /sys/block/<dev>/queue/scheduler
ls /sys/block/<dev>/mq/

추가 확인 사항

  • 이 bio는 기존 request에 병합되었는가, cached request를 썼는가, 새 tag를 얻었는가?
  • scheduler tag 조건과 flush/passthrough 예외는 어느 분기에서 갈리는가?
  • blk_mq_run_hw_queue()의 반환은 pending 없음, quiesce, 지연 예약 중 어느 상태인가?
  • freeze depth가 처음 1이 된 시점과 q_usage_counter가 0이 된 시점은 언제인가?
  • 제출 오류와 driver가 알린 blk_status_t를 어떻게 구분할 수 있는가?