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

I/O scheduler

queue별 선택과 bio 병합 후보, mq-deadline의 삽입·dispatch·완료 경계를 비교합니다.

I/O scheduler의 네 가지 계약

bio 병합 후보 검색, queue별 elevator 선택, mq-deadline의 request 삽입·선택, 완료 통계는 서로 다른 호출 시점이다. 아래 세 경로를 한 줄의 필수 호출 체인으로 읽지 않는다.

그림 1. queue 설정, bio 병합, request 처리의 별도 경로
선택·설치queue의 elevator 선택은 초기화/변경 경로다. none이거나 scheduler가 비활성인 queue도 있다.
bio 병합 후보elv_merge(q, &req, bio)는 기존 request 후보와 병합 종류를 찾는다. 병합 성공이나 request 삽입 자체가 아니다.
요청 처리선택된 mq-deadline의 insert/dispatch callback이 request를 정렬·선택한다. 이후 driver issue와 completion은 별도 경계다.

장치가 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

원본 코드: block/elevator.c L276–322 · torvalds/[email protected]
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

원본 코드: block/mq-deadline.c L689–737 · torvalds/[email protected]
/*
 * 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이다.

그림 2. mq-deadline이 선택된 queue의 설정·요청 경로
queue 선택elevator_init_mq()는 queue 지원 여부·required features·기본 선택을 검사한다. 기본 mq-deadline은 단일 hardware queue 또는 shared tags 등 조건에서 선택한다(L558–650).
scheduler private datadd_init_sched(q, e)는 elevator_alloc()으로 eq를 만든 뒤 deadline_data를 붙인다(L692–737).
request 삽입dd_insert_requests()가 요청별 dd_insert_request()를 호출한다.
dispatch 후보dd_dispatch_request()는 aged request를 먼저 검사하고, 없으면 priority 순서로 고른다(L596–622).

화살표는 기능상 의존 관계다. elv_merge()의 반환이 dd_init_sched()를 호출한다는 뜻이 아니다.

경계입력결과 / 비결과
elv_mergequeue, request 후보 포인터, bio병합 종류와 후보 request 반환. 삽입/완료를 수행하지 않음.
dd_init_schedqueue, elevator_typedeadline_data와 queue elevator 상태 준비. bio 병합을 시도하지 않음.
dd_insert_requestshctx, request list, flagsmq-deadline의 정렬/FIFO에 요청 삽입·병합 시도.
dd_dispatch_requesthctx정책에 따라 다음 request를 선택해 반환. 장치 완료는 아님.

mq-deadline 객체와 자료구조

그림 3. request_queue에서 deadline_data와 request까지
QUEUEq->elevator선택된 elevator_queue. none이면 비어 있을 수 있다.
PRIVATEdeadline_dataeq->elevator_data. priority별 dispatch/FIFO/RB tree와 zone_lock.
PRIORITYdd_per_priodispatch 목록, 읽기/쓰기 FIFO 및 sector 정렬 RB tree.
REQUESTrq->elv.priv[0]스케줄러 경유 request인지 완료 경로에서 판별한다.

v6.6은 priority별 per_prio->dispatch와 zone_lock을 초기화한다.

객체수명 또는 역할버전 고정 근거
request_queue.elevator선택된 elevator instance, none이면 NULL 가능elevator_init_mq L612–650
elevator_datadeadline_data private 상태dd_init_sched L692–737
sort_list / fifo_listsector 순서와 만료 순서를 위한 별도 구조mq-deadline.c L709–717
dispatch 목록priority별 dd_per_prio 목록mq-deadline.c L712, L603–616

요청 수명에서 관찰할 상태

그림 4. 설정·병합·삽입·dispatch·완료
elevator 설정mq-deadline이 실제 선택된 경우만 private data 초기화.
bio merge 후보elv_merge: NO_MERGE 또는 후보·병합 종류. 실제 merge는 호출자 경계.
request 삽입·선택dd_insert_requests → dd_dispatch_request. 선택 후 driver로 issue.
완료 accountingdd_finish_request()는 스케줄러를 우회한 request를 건너뛰고, zoned write는 zone lock·재시작까지 다룬다(L919 이후).

bio 병합이 없거나 request가 scheduler를 우회해도 완료 경로는 존재할 수 있다. dispatch 선택을 I/O 완료로 간주하지 않는다.

관찰 시점알 수 있는 것아직 알 수 없는 것
scheduler sysfs해당 queue의 현재 선택된 elevator모든 bio가 실제 병합되었는지
block_rq_insertrequest가 블록 계층 삽입 지점에 도달이미 driver에 제출·완료되었는지
block_rq_issuedriver 쪽 issue 경계 도달장치 service 완료 여부
block_rq_completerequest 완료 event지연 전체가 scheduler 때문인지

불변 조건과 실패 사례

선택은 queue별

모든 queue에 mq-deadline이 붙는다고 가정하지 않는다. none, hardware queue 수, tag flags와 지원 기능을 먼저 본다.

merge 후보 ≠ 실제 결합

elv_merge()는 NO_MERGE 또는 후보와 병합 종류를 반환한다. 실제 bio/request 상태 변경은 호출자와 후속 merge 경로에서 확인한다.

초기화 실패

private data 할당 실패 시 elevator 설치가 성공한 것으로 다루지 않는다. 사용 중 scheduler 변경도 queue freeze/quiesce와 오류 반환 경계를 가진다.

dispatch ≠ 완료

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가 완료 통계에 포함되지 않는 경계를 확인했는가?