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

bio

block I/O 요청의 기본 단위.

전체 흐름

bio는 장치·섹터·메모리 세그먼트·완료 통지를 묶는 I/O 기술자다. bio_alloc_bioset()은 BIO와 벡터 저장공간을 준비하지만 데이터 페이지를 자동으로 붙이지 않는다. 호출자가 작업 유형에 맞게 섹터·세그먼트·완료 계약을 준비한다. 벡터가 0개인 BIO도 할당할 수 있다.

그림 1. 할당·호출자 준비·제출의 경계 (v6.6)
① 할당REQ_ALLOC_CACHE가 적합하면 cache를 시도하고, 그 밖에는 mempool. 실패하면 NULL.
② 호출자가 준비bi_iter의 위치·길이, 필요한 페이지 세그먼트와 완료 콜백을 작업에 맞게 설정.
③ 제출submit_bio()는 계수 후 전달. 검증 실패는 조기 bio_endio() 가능.
조건부 갈림길
벡터 0개flush 등 작업에 따라 데이터 페이지 없이 제출하는 BIO도 있다.
split / chain분할과 부모 완료 연결은 해당 호출자가 선택하는 별도 경로다.
blk-mq / 드라이버제출된 BIO는 장치 경로에 따라 request 생성·병합 또는 드라이버 submit으로 간다.

BIO의 이후 경로는 작업 유형과 하위 장치에 따라 달라진다. 할당 · 제출 계약

제출 함수의 반환은 I/O 완료나 저장장치 영속성 보증이 아니다. 완료 상태는 bi_end_io 계약으로 전달된다.

소스 코드 위치

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

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

설명: block/bio.c는 BIO 할당·분할·완료, block/blk-core.c는 제출 경로를 다룬다. struct bio 정의는 include/linux/blk_types.h, 보조 인터페이스는 include/linux/bio.h에 있다.

대표 코드

bio_alloc_bioset

원본 코드: block/bio.c:492-580 · v6.6 · 로컬 원문
bio_alloc_bioset 함수 전체 (89줄)
struct bio *bio_alloc_bioset(struct block_device *bdev, unsigned short nr_vecs,
			     blk_opf_t opf, gfp_t gfp_mask,
			     struct bio_set *bs)
{
	gfp_t saved_gfp = gfp_mask;
	struct bio *bio;
	void *p;

	/* should not use nobvec bioset for nr_vecs > 0 */
	if (WARN_ON_ONCE(!mempool_initialized(&bs->bvec_pool) && nr_vecs > 0))
		return NULL;

	if (opf & REQ_ALLOC_CACHE) {
		if (bs->cache && nr_vecs <= BIO_INLINE_VECS) {
			bio = bio_alloc_percpu_cache(bdev, nr_vecs, opf,
						     gfp_mask, bs);
			if (bio)
				return bio;
			/*
			 * No cached bio available, bio returned below marked with
			 * REQ_ALLOC_CACHE to particpate in per-cpu alloc cache.
			 */
		} else {
			opf &= ~REQ_ALLOC_CACHE;
		}
	}

	/*
	 * submit_bio_noacct() converts recursion to iteration; this means if
	 * we're running beneath it, any bios we allocate and submit will not be
	 * submitted (and thus freed) until after we return.
	 *
	 * This exposes us to a potential deadlock if we allocate multiple bios
	 * from the same bio_set() while running underneath submit_bio_noacct().
	 * If we were to allocate multiple bios (say a stacking block driver
	 * that was splitting bios), we would deadlock if we exhausted the
	 * mempool's reserve.
	 *
	 * We solve this, and guarantee forward progress, with a rescuer
	 * workqueue per bio_set. If we go to allocate and there are bios on
	 * current->bio_list, we first try the allocation without
	 * __GFP_DIRECT_RECLAIM; if that fails, we punt those bios we would be
	 * blocking to the rescuer workqueue before we retry with the original
	 * gfp_flags.
	 */
	if (current->bio_list &&
	    (!bio_list_empty(&current->bio_list[0]) ||
	     !bio_list_empty(&current->bio_list[1])) &&
	    bs->rescue_workqueue)
		gfp_mask &= ~__GFP_DIRECT_RECLAIM;

	p = mempool_alloc(&bs->bio_pool, gfp_mask);
	if (!p && gfp_mask != saved_gfp) {
		punt_bios_to_rescuer(bs);
		gfp_mask = saved_gfp;
		p = mempool_alloc(&bs->bio_pool, gfp_mask);
	}
	if (unlikely(!p))
		return NULL;
	if (!mempool_is_saturated(&bs->bio_pool))
		opf &= ~REQ_ALLOC_CACHE;

	bio = p + bs->front_pad;
	if (nr_vecs > BIO_INLINE_VECS) {
		struct bio_vec *bvl = NULL;

		bvl = bvec_alloc(&bs->bvec_pool, &nr_vecs, gfp_mask);
		if (!bvl && gfp_mask != saved_gfp) {
			punt_bios_to_rescuer(bs);
			gfp_mask = saved_gfp;
			bvl = bvec_alloc(&bs->bvec_pool, &nr_vecs, gfp_mask);
		}
		if (unlikely(!bvl))
			goto err_free;

		bio_init(bio, bdev, bvl, nr_vecs, opf);
	} else if (nr_vecs) {
		bio_init(bio, bdev, bio->bi_inline_vecs, BIO_INLINE_VECS, opf);
	} else {
		bio_init(bio, bdev, NULL, 0, opf);
	}

	bio->bi_pool = bs;
	return bio;

err_free:
	mempool_free(p, &bs->bio_pool);
	return NULL;
}

확인 사항: 이 코드에서는 반환값보다 상태 변경을 먼저 확인한다. 어느 잠금을 획득한 뒤 어떤 필드를 바꾸는지, 실패 시 어느 레이블로 분기하는지, 변경된 상태를 다음 호출자가 어떤 전제로 사용하는지 추적한다.

submit_bio

원본 코드: block/blk-core.c:825-835 · v6.6 · 로컬 원문
void submit_bio(struct bio *bio)
{
	if (bio_op(bio) == REQ_OP_READ) {
		task_io_account_read(bio->bi_iter.bi_size);
		count_vm_events(PGPGIN, bio_sectors(bio));
	} else if (bio_op(bio) == REQ_OP_WRITE) {
		count_vm_events(PGPGOUT, bio_sectors(bio));
	}

	submit_bio_noacct(bio);
}

submit_bio()의 정의는 block/bio.c가 아니라 block/blk-core.c에 있다. bio를 제출하는 것과 I/O가 끝나는 것은 별도 사건이다. 제출 뒤 completion callback과 bio 수명 규약을 지켜야 하며, 함수 반환만을 데이터 영속성 보장으로 해석하지 않는다.

함수별 분석

할당, 페이지 부착, 분할, 부모 완료 연결, 제출은 서로 다른 함수의 책임이다. submit_bio()는 bio_vec를 붙이지 않고 준비된 BIO를 하위 경로에 전달한다.

그림 2. 선택적 split·chain과 완료 관계 (v6.6)
원래 BIObio_split()은 앞 구간 clone을 만들고 원래 BIO의 iterator를 뒤 구간으로 진행시킨다.
분할 BIO일반 데이터 작업에서는 원래 BIO의 bi_io_vec를 공유할 수 있다. 수명을 함께 관리해야 한다.
부모 BIObio_chain(child, parent)을 쓰면 부모 remaining을 늘리고 자식 종료를 부모 쪽 완료에 합친다.

split 자체가 자동으로 chain을 수행하거나 모든 BIO가 반드시 split되는 것은 아니다. bio_split · bio_chain

함수입력·수행실패·수명 경계
bio_alloc_bioset()bdev, nr_vecs, opf, gfp_mask, bs를 받아 BIO와 vec 용량을 초기화한다.cache 조건 불충족이면 mempool, 할당 실패면 NULL. 데이터 페이지는 붙이지 않는다.
bio_split()앞 구간 clone을 만들고 원래 BIO의 bi_iter를 진행시킨다.v6.6: 잘못된 섹터 수는 BUG_ON, zone append·복제 할당 실패는 NULL.
bio_chain()자식의 bi_private/bi_end_io를 부모 연결에 쓰고 부모 remaining을 증가시킨다.자식에 기존 private/end_io가 있으면 BUG_ON; 자식 완료가 부모 완료 수명에 반영된다.
submit_bio()읽기/쓰기 계수 후 submit_bio_noacct()로 전달.호출 뒤 완료 callback 전에는 호출자가 BIO를 만지지 않는 계약이다.

근거: block/bio.c:492-580 · block/bio.c:1607-1648 · block/bio.c:312-345 · block/blk-core.c:721-835.

구조체와 필드

이 필드는 시간 순서가 아니라 한 BIO의 서로 다른 역할이다. bi_iter는 전송 위치와 남은 크기의 cursor이고, bi_io_vec는 페이지 세그먼트를 가리킨다. 완료 결과와 callback은 별도 필드로 관리한다.

그림 3. BIO 필드의 역할을 축으로 구분
위치·길이bi_iter.bi_sector, bi_iter.bi_size: 시작 위치와 남은 범위.
페이지 세그먼트bi_io_vec/bi_vcnt: 데이터 작업의 vec; 0개일 수 있다.
완료 계약bi_end_io, bi_private, bi_status: 호출자 통지와 상태.

카드 배치는 함수 호출 순서가 아니다. 초기화 직후에는 iter size와 status가 0이고 callback은 NULL이다. bio_init

필드뜻주의
bi_iter섹터·남은 바이트·현재 vec 위치split·advance로 변할 수 있어 원본 길이와 혼동하지 않는다.
bi_io_vec데이터 segment 배열분할 clone과 원본이 공유할 수 있어 원본/bs 수명을 확인한다.
bi_statusBIO 완료 상태chain 자식의 오류는 부모에 전파될 수 있다.
bi_end_io / __bi_remaining완료 callback / chain 완료 수callback은 remaining·integrity gate 뒤에만 호출된다.

실행 단계와 상태 변화

분할과 chain은 선택 경로이며, 제출 직후에도 검증 오류로 바로 완료될 수 있다. bio_endio() 호출 한 번이 언제나 최종 callback 한 번을 뜻하지는 않는다.

그림 4. 제출 이후 완료가 열리는 조건 (v6.6)
제출 전bio_init()이 iterator·status·callback을 초기화; 호출자가 작업별 값을 채운다.
제출 분기검증 실패는 bi_status와 조기 endio; 통과하면 blk-mq 또는 드라이버 경로.
완료 게이트bio_remaining_done() 또는 integrity 처리가 미완료면 endio가 일단 반환.
게이트를 통과한 뒤
chained 자식자식 상태를 부모에 전파하고 자식을 put한 뒤 부모의 remaining을 다시 검사.
일반 BIO완료 처리를 마친 뒤 등록된 bi_end_io가 있으면 호출.

request 완료와 BIO callback은 같은 객체 사건이 아니다. bio_endio와 remaining

조건결과확인할 점
nr_vecs > BIO_INLINE_VECS외부 bvec 할당 후 initbvec 할당 실패 시 확보한 BIO를 해제하고 NULL.
0 < nr_vecs <= BIO_INLINE_VECSinline vec로 초기화: bio_init(..., bi_inline_vecs, BIO_INLINE_VECS, ...)vec 용량이지 데이터 페이지 부착 완료가 아니다.
nr_vecs == 0bio_init(..., NULL, 0, ...)0-vector BIO를 누락하지 않는다.
split을 호출앞 구간 clone + 원래 iterator 전진v6.6: 잘못된 섹터 수는 BUG_ON, zone append·복제 할당 실패는 NULL.
chain된 BIO endioremaining 수가 0이 될 때까지 최종 callback 지연자식 오류를 부모 status로 전파하고 자식을 put한다.

v6.6의 bio_split()은 범위 위반을 BUG_ON으로 검사하고, zone append 또는 복제 할당 실패에는 NULL을 돌려준다. 버전별 split 오류 계약.

불변 조건과 실패 사례

할당 ≠ 데이터 준비할당은 BIO와 vec 저장공간을 준비한다. 세그먼트·sector·callback 설정은 호출자와 작업 유형에 달렸다.
split과 공유 수명일반 split clone은 원본 vec를 공유할 수 있으므로 split이 끝날 때까지 원본 BIO·bioset을 먼저 해제하지 않는다.
chain 완료 수bio_chain은 부모 remaining을 올린다. 자식 완료만으로 부모 callback이 곧바로 실행되는 것은 아니다.

BIO 완료 상태는 request 완료 이벤트와 별도로 관찰한다. 특히 bi_iter는 분할·진행 중 바뀌며, 제출 함수 반환으로 버퍼 재사용이나 영속성을 단정하지 않는다.

계측과 검증

먼저 커널에서 사용할 수 있는 block 이벤트 이름과 필드를 확인한다. 아래 명령은 tracefs를 마운트하거나 이벤트를 켜지 않으며 공유 trace buffer를 소비하지 않는다.

관찰 수단확인 가능한 것단독으로 단정하지 말 것
block:block_bio_queueBIO가 queue 계측 지점을 지났는지모든 BIO가 새 request로 할당되었다는 뜻
block:block_bio_complete완료 계측이 기록된 BIOrequest completion과 항상 1:1 대응한다는 뜻
events/block/*/format현재 커널의 event 이름·필드·출력 형식이 파일을 읽는 것만으로 실제 I/O가 trace되지는 않음
읽기 전용 이벤트 확인:
# tracefs가 이미 마운트되어 있을 때, 지원되는 이벤트만 확인
tr=/sys/kernel/tracing
for ev in block_bio_queue block_bio_complete; do
  f="$tr/events/block/$ev/format"
  if [ -r "$f" ]; then
    printf "%s\n" "$ev"
    sed -n "1,20p" "$f"
  fi
done

event의 format 파일은 현재 실행 커널이 제공하는 필드 정의다. 제출 경로 · 완료 경로 · Linux Event Tracing 문서.

추가 확인 사항

  • 해당 호출자가 0-vector 작업인지, 세그먼트를 채워야 하는 데이터 작업인지?
  • split clone이 원래 BIO의 vec를 공유할 때, 두 객체와 bioset의 수명을 누가 보유하는지?
  • chain을 쓴다면 자식 완료·부모 자체 완료가 각각 remaining에 어떻게 반영되는지?
  • queue와 completion tracepoint가 모두 보이더라도 같은 request 한 개로 단정하지 않은 근거는 무엇인지?