I/O scheduler의 네 가지 계약
bio 병합 후보 검색, queue별 elevator 선택, mq-deadline의 request 삽입·선택, 완료 통계는 서로 다른 호출 시점이다. 아래 세 경로를 한 줄의 필수 호출 체인으로 읽지 않는다.
none이거나 scheduler가 비활성인 queue도 있다.elv_merge(q, &req, bio)는 기존 request 후보와 병합 종류를 찾는다. 병합 성공이나 request 삽입 자체가 아니다.장치가 scheduler를 지원하지 않거나 none을 쓰는 경우 mq-deadline 경로는 실행되지 않는다.
소스 코드 위치
먼저 확인할 Linux v6.6 소스 파일
Linux v6.6: block/elevator.c
Linux v6.6: block/mq-deadline.c
Linux v6.6: block/kyber-iosched.c
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
고정 태그의 대표 코드
두 발췌 모두 함수 정의부터 닫는 중괄호까지 연속된 원문이다. 기존 상자의 앞 함수 꼬리와 v6.18.37에 섞인 v6.6 초기화 코드를 제거했다. 원본 링크와 별도로 정확한 줄 범위를 표시한다.
elv_merge
enum elv_merge elv_merge(struct request_queue *q, struct request **req,
struct bio *bio)
{
struct elevator_queue *e = q->elevator;
struct request *__rq;
/*
* Levels of merges:
* nomerges: No merges at all attempted
* noxmerges: Only simple one-hit cache try
* merges: All merge tries attempted
*/
if (blk_queue_nomerges(q) || !bio_mergeable(bio))
return ELEVATOR_NO_MERGE;
/*
* First try one-hit cache.
*/
if (q->last_merge && elv_bio_merge_ok(q->last_merge, bio)) {
enum elv_merge ret = blk_try_merge(q->last_merge, bio);
if (ret != ELEVATOR_NO_MERGE) {
*req = q->last_merge;
return ret;
}
}
if (blk_queue_noxmerges(q))
return ELEVATOR_NO_MERGE;
/*
* See if our hash lookup can find a potential backmerge.
*/
__rq = elv_rqhash_find(q, bio->bi_iter.bi_sector);
if (__rq && elv_bio_merge_ok(__rq, bio)) {
*req = __rq;
if (blk_discard_mergable(__rq))
return ELEVATOR_DISCARD_MERGE;
return ELEVATOR_BACK_MERGE;
}
if (e->type->ops.request_merge)
return e->type->ops.request_merge(q, req, bio);
return ELEVATOR_NO_MERGE;
}
읽기 기준: NO_MERGE 단락, last_merge, hash, scheduler request_merge callback의 순서와 반환값을 본다. 이 함수가 bio를 실제로 합치거나 request를 삽입한다고 읽지 않는다.
dd_init_sched
/*
* initialize elevator private data (deadline_data).
*/
static int dd_init_sched(struct request_queue *q, struct elevator_type *e)
{
struct deadline_data *dd;
struct elevator_queue *eq;
enum dd_prio prio;
int ret = -ENOMEM;
eq = elevator_alloc(q, e);
if (!eq)
return ret;
dd = kzalloc_node(sizeof(*dd), GFP_KERNEL, q->node);
if (!dd)
goto put_eq;
eq->elevator_data = dd;
for (prio = 0; prio <= DD_PRIO_MAX; prio++) {
struct dd_per_prio *per_prio = &dd->per_prio[prio];
INIT_LIST_HEAD(&per_prio->dispatch);
INIT_LIST_HEAD(&per_prio->fifo_list[DD_READ]);
INIT_LIST_HEAD(&per_prio->fifo_list[DD_WRITE]);
per_prio->sort_list[DD_READ] = RB_ROOT;
per_prio->sort_list[DD_WRITE] = RB_ROOT;
}
dd->fifo_expire[DD_READ] = read_expire;
dd->fifo_expire[DD_WRITE] = write_expire;
dd->writes_starved = writes_starved;
dd->front_merges = 1;
dd->last_dir = DD_WRITE;
dd->fifo_batch = fifo_batch;
dd->prio_aging_expire = prio_aging_expire;
spin_lock_init(&dd->lock);
spin_lock_init(&dd->zone_lock);
/* We dispatch from request queue wide instead of hw queue */
blk_queue_flag_set(QUEUE_FLAG_SQ_SCHED, q);
q->elevator = eq;
return 0;
put_eq:
kobject_put(&eq->kobj);
return ret;
}
읽기 기준: elevator_queue와 deadline_data를 이 함수에서 할당하고 priority별 dispatch/FIFO/RB tree 및 zone_lock을 초기화한다.
함수별 계약과 호출 경계
초기화는 I/O마다 실행되지 않는다. elv_merge()는 bio가 기존 request에 합쳐질 수 있는지 탐색하고, dd_init_sched()는 scheduler의 private 자료구조를 준비한다. request 삽입·dispatch는 그 뒤의 별도 callback이다.
화살표는 기능상 의존 관계다. elv_merge()의 반환이 dd_init_sched()를 호출한다는 뜻이 아니다.
| 경계 | 입력 | 결과 / 비결과 |
|---|---|---|
elv_merge | queue, request 후보 포인터, bio | 병합 종류와 후보 request 반환. 삽입/완료를 수행하지 않음. |
dd_init_sched | queue, elevator_type | deadline_data와 queue elevator 상태 준비. bio 병합을 시도하지 않음. |
dd_insert_requests | hctx, request list, flags | mq-deadline의 정렬/FIFO에 요청 삽입·병합 시도. |
dd_dispatch_request | hctx | 정책에 따라 다음 request를 선택해 반환. 장치 완료는 아님. |
mq-deadline 객체와 자료구조
v6.6은 priority별 per_prio->dispatch와 zone_lock을 초기화한다.
| 객체 | 수명 또는 역할 | 버전 고정 근거 |
|---|---|---|
| request_queue.elevator | 선택된 elevator instance, none이면 NULL 가능 | elevator_init_mq L612–650 |
| elevator_data | deadline_data private 상태 | dd_init_sched L692–737 |
| sort_list / fifo_list | sector 순서와 만료 순서를 위한 별도 구조 | mq-deadline.c L709–717 |
| dispatch 목록 | priority별 dd_per_prio 목록 | mq-deadline.c L712, L603–616 |
요청 수명에서 관찰할 상태
bio 병합이 없거나 request가 scheduler를 우회해도 완료 경로는 존재할 수 있다. dispatch 선택을 I/O 완료로 간주하지 않는다.
| 관찰 시점 | 알 수 있는 것 | 아직 알 수 없는 것 |
|---|---|---|
| scheduler sysfs | 해당 queue의 현재 선택된 elevator | 모든 bio가 실제 병합되었는지 |
| block_rq_insert | request가 블록 계층 삽입 지점에 도달 | 이미 driver에 제출·완료되었는지 |
| block_rq_issue | driver 쪽 issue 경계 도달 | 장치 service 완료 여부 |
| block_rq_complete | request 완료 event | 지연 전체가 scheduler 때문인지 |
불변 조건과 실패 사례
모든 queue에 mq-deadline이 붙는다고 가정하지 않는다. none, hardware queue 수, tag flags와 지원 기능을 먼저 본다.
elv_merge()는 NO_MERGE 또는 후보와 병합 종류를 반환한다. 실제 bio/request 상태 변경은 호출자와 후속 merge 경로에서 확인한다.
private data 할당 실패 시 elevator 설치가 성공한 것으로 다루지 않는다. 사용 중 scheduler 변경도 queue freeze/quiesce와 오류 반환 경계를 가진다.
dd_dispatch_request의 반환은 driver issue 전 단계다. 완료 통계는 scheduler 경유 요청과 우회 요청을 구별한다.
읽기 전용 계측과 해석
운영 중인 전역 tracefs 설정을 바꾸지 않고 현재 queue 상태와 이미 기록된 자료를 확인한다. 장치 이름은 대상 시스템에 맞게 바꾼다.
| 읽기 전용 관찰 | 의미 | 한계 |
|---|---|---|
cat /sys/block/<disk>/queue/scheduler | 대괄호로 현재 queue의 선택값 표시 | scheduler 사용 불가 queue는 none만 보일 수 있음 |
cat /sys/kernel/tracing/available_events | grep '^block:block_rq_' | 지원되는 block event 이름 확인 | event가 있다는 사실은 기록이 켜졌다는 뜻이 아님 |
cat /sys/kernel/tracing/trace | 이미 켜진 tracing의 현재 스냅샷 읽기 | 빈 기록이면 설정을 바꾸지 않고 기존 로그·계측으로 보완 |
insert→issue 시간에는 큐 대기와 다른 블록 계층 요인이 함께 들어간다. issue→complete 시간에도 장치·드라이버 시간이 섞인다. 이 간격 하나만으로 scheduler latency를 확정하지 않는다.
추가 확인 사항
- 대상 queue에 scheduler가 실제 설치됐는가? 설정 조건과 sysfs의 현재값을 대조한다.
elv_merge()의 반환이 후보 탐색에서 끝났는가, 호출자가 실제 bio merge까지 진행했는가?- mq-deadline의 insert/dispatch와 driver issue/completion 사이에 어느 구간의 지연이 생겼는가?
- 스케줄러를 우회한 request가 완료 통계에 포함되지 않는 경계를 확인했는가?