전체 흐름
bio는 장치·섹터·메모리 세그먼트·완료 통지를 묶는 I/O 기술자다. bio_alloc_bioset()은 BIO와 벡터 저장공간을 준비하지만 데이터 페이지를 자동으로 붙이지 않는다. 호출자가 작업 유형에 맞게 섹터·세그먼트·완료 계약을 준비한다. 벡터가 0개인 BIO도 할당할 수 있다.
REQ_ALLOC_CACHE가 적합하면 cache를 시도하고, 그 밖에는 mempool. 실패하면 NULL.bi_iter의 위치·길이, 필요한 페이지 세그먼트와 완료 콜백을 작업에 맞게 설정.submit_bio()는 계수 후 전달. 검증 실패는 조기 bio_endio() 가능.제출 함수의 반환은 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
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(¤t->bio_list[0]) ||
!bio_list_empty(¤t->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
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를 하위 경로에 전달한다.
bio_split()은 앞 구간 clone을 만들고 원래 BIO의 iterator를 뒤 구간으로 진행시킨다.bi_io_vec를 공유할 수 있다. 수명을 함께 관리해야 한다.bio_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은 별도 필드로 관리한다.
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_status | BIO 완료 상태 | chain 자식의 오류는 부모에 전파될 수 있다. |
bi_end_io / __bi_remaining | 완료 callback / chain 완료 수 | callback은 remaining·integrity gate 뒤에만 호출된다. |
실행 단계와 상태 변화
분할과 chain은 선택 경로이며, 제출 직후에도 검증 오류로 바로 완료될 수 있다. bio_endio() 호출 한 번이 언제나 최종 callback 한 번을 뜻하지는 않는다.
bio_init()이 iterator·status·callback을 초기화; 호출자가 작업별 값을 채운다.bi_status와 조기 endio; 통과하면 blk-mq 또는 드라이버 경로.bio_remaining_done() 또는 integrity 처리가 미완료면 endio가 일단 반환.bi_end_io가 있으면 호출.request 완료와 BIO callback은 같은 객체 사건이 아니다. bio_endio와 remaining
| 조건 | 결과 | 확인할 점 |
|---|---|---|
nr_vecs > BIO_INLINE_VECS | 외부 bvec 할당 후 init | bvec 할당 실패 시 확보한 BIO를 해제하고 NULL. |
0 < nr_vecs <= BIO_INLINE_VECS | inline vec로 초기화: bio_init(..., bi_inline_vecs, BIO_INLINE_VECS, ...) | vec 용량이지 데이터 페이지 부착 완료가 아니다. |
nr_vecs == 0 | bio_init(..., NULL, 0, ...) | 0-vector BIO를 누락하지 않는다. |
| split을 호출 | 앞 구간 clone + 원래 iterator 전진 | v6.6: 잘못된 섹터 수는 BUG_ON, zone append·복제 할당 실패는 NULL. |
| chain된 BIO endio | remaining 수가 0이 될 때까지 최종 callback 지연 | 자식 오류를 부모 status로 전파하고 자식을 put한다. |
v6.6의 bio_split()은 범위 위반을 BUG_ON으로 검사하고, zone append 또는 복제 할당 실패에는 NULL을 돌려준다. 버전별 split 오류 계약.
불변 조건과 실패 사례
BIO 완료 상태는 request 완료 이벤트와 별도로 관찰한다. 특히 bi_iter는 분할·진행 중 바뀌며, 제출 함수 반환으로 버퍼 재사용이나 영속성을 단정하지 않는다.
계측과 검증
먼저 커널에서 사용할 수 있는 block 이벤트 이름과 필드를 확인한다. 아래 명령은 tracefs를 마운트하거나 이벤트를 켜지 않으며 공유 trace buffer를 소비하지 않는다.
| 관찰 수단 | 확인 가능한 것 | 단독으로 단정하지 말 것 |
|---|---|---|
block:block_bio_queue | BIO가 queue 계측 지점을 지났는지 | 모든 BIO가 새 request로 할당되었다는 뜻 |
block:block_bio_complete | 완료 계측이 기록된 BIO | request 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
doneevent의 format 파일은 현재 실행 커널이 제공하는 필드 정의다. 제출 경로 · 완료 경로 · Linux Event Tracing 문서.
추가 확인 사항
- 해당 호출자가 0-vector 작업인지, 세그먼트를 채워야 하는 데이터 작업인지?
- split clone이 원래 BIO의 vec를 공유할 때, 두 객체와 bioset의 수명을 누가 보유하는지?
- chain을 쓴다면 자식 완료·부모 자체 완료가 각각 remaining에 어떻게 반영되는지?
- queue와 completion tracepoint가 모두 보이더라도 같은 request 한 개로 단정하지 않은 근거는 무엇인지?