전체 흐름
block/blk-mq.c v6.6에서 bio 제출은 기존 request와 병합되거나 새 request를 준비한다. 그 뒤 plug 보관, scheduler 삽입, 직접 issue 시도로 갈라진다. blk_mq_run_hw_queue()는 이미 쌓인 작업을 실행할지를 판단하며 request/tag 할당 함수가 아니다. driver 완료는 별도 blk_mq_end_request() 경계다.
점선은 병합 실패 후 새 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 처리를 함께 확인할 때 참고한다.
GitHub 원본:
대표 코드
고정 버전의 함수 경계를 기준으로 발췌했다. 원래 run_hw_queue 제목 아래의 freeze 코드는 실제 dispatch 함수가 아니다.
blk_mq_submit_bio() 시작 (앞 함수 말미 포함)
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·출구 분기
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 획득
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() 실제 정의
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() 시작
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() — 완료 경계
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를 시도한다.
오류는 발생 위치마다 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의 완료 상태는 아니다.
상단 cached 경로는 tag allocator를 지나지 않는다. 하단 새 request 경로에서 scheduler tag와 driver tag 조건을 확인한다.
| 객체/필드 | 의미 | 주의할 점 |
|---|---|---|
request_queue.q_usage_counter | bio/request가 queue를 사용하는 동안의 참조 | freeze depth와 별개; drain 대기는 별도 |
blk_plug.cached_rq(s) | plug에 보관된 재사용 request | 새 tag를 매번 받는다고 보면 안 됨 |
blk_mq_alloc_data.rq_flags | scheduler tag 및 scheduler 사용 여부 | scheduler tag와 driver tag를 혼동하지 않음 |
blk_mq_hw_ctx | CPU/type에 매핑된 hardware queue 문맥 | quiesce·pending 조건은 run에서 다시 확인 |
request | bio를 묶고 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을 기다리는 것은 신규 진입 차단과 다른 단계다.
윗줄의 반환·지연·inline dispatch는 조건부 대안이다. 아랫줄의 freeze는 신규 사용 제한과 기존 참조 drain을 나눠 보인다.
| 조건/호출 | 변화 또는 다음 단계 | 잘못된 해석 |
|---|---|---|
blk_mq_hw_queue_need_run() false | quiesced 또는 pending 없음 → 반환; 버전에 따라 재검사 | request 할당 실패/완료와 같지 않음 |
async 또는 CPU mask 밖 | blk_mq_delay_run_hw_queue() 예약 | 이 함수 호출 시점에 driver 완료 아님 |
| 동기·허용 CPU | blk_mq_sched_dispatch_requests() 시도 | 모든 request가 driver에 수락된다는 보장 아님 |
| 첫 freeze depth | percpu_ref_kill(q_usage_counter) | 기존 사용자가 즉시 사라지는 것은 아님 |
blk_mq_freeze_queue_wait() | 참조 0까지 대기 | quiesce flag 설정이나 완료 callback과 동일하지 않음 |
불변 조건과 실패 사례
cached request는 기존 사용 참조를 보유한다. 새 request 경로의 bio_queue_enter()와 중복해 세지 않는다.
tag 확보는 request 준비 단계다. scheduler tag 여부와 driver issue, 완료를 분리한다.
freeze 첫 깊이는 신규 queue 참조를 차단하고 별도 wait로 기존 참조를 drain한다. quiesce는 dispatch 선택에 영향을 준다.
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를 어떻게 구분할 수 있는가?