이 코드는 어떤 문제를 푸나요?
파일을 읽으려면 어느 장치의 어느 구간을 어느 메모리에 옮길지 정해야 합니다. bio는 이 정보를 묶어 아래 계층에 전달하는 객체입니다. 제출한 함수가 돌아왔다고 디스크 작업이 끝난 것은 아닙니다. 여기서는 bio_endio 전체를 따라가며 여러 조각의 완료가 합쳐지고 마지막에 원래 요청자에게 알려지는 과정을 설명합니다.
읽을 범위: v6.6 · block/bio.c · bio_endio 1571–1604행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.
먼저 알아둘 개념
bio와 데이터
bio는 데이터 자체를 담은 연속 배열이 아닙니다. 대상 장치, 작업 종류, 섹터 위치와 메모리 구간 목록 등을 설명하며, 실제 데이터는 참조한 메모리에 있습니다.
완료 콜백
bi_end_io는 작업이 끝났을 때 호출할 함수입니다. 성공 여부는 bi_status 등의 상태로 판단하며, 완료됐다는 사실만으로 성공이라고 해석하지 않습니다.
연결한 bio
큰 작업을 나눈 자식 bio는 부모의 완료 횟수와 연결될 수 있습니다. 일부 조각만 끝났을 때 부모에게 전체 완료를 알리면 아직 진행 중인 메모리를 재사용할 위험이 있습니다.
처음 읽을 때
제출 시점과 완료 시점을 나누어 보십시오. 예를 들어 두 조각으로 나뉜 읽기라는 가정에서는 첫 조각 완료와 전체 완료가 다릅니다. 이 숫자는 설명용이며 실제 분할 개수는 요청과 장치 제약에 따라 달라집니다.
더 깊이 살펴볼 때
bio_remaining_done, 무결성 검사, 연결된 bio의 완료 전파와 마지막 콜백 순서를 보십시오. goto again은 연결 깊이만큼 C 호출 스택이 늘어나는 일을 피합니다. 콜백이 bio를 해제할 수 있으므로 호출 뒤에 무심코 bio 필드를 접근하면 안 됩니다.
그림으로 보는 변화

1. 작업을 제출합니다
장치 위치와 메모리 구간을 bio로 전달합니다. CPU는 제출 후 다른 일을 할 수 있습니다.
화살표는 bio가 아래 블록 계층으로 전달되는 방향입니다.
2. 조각의 완료를 모읍니다
남은 완료 횟수와 무결성 처리를 확인합니다. 연결된 bio라면 부모의 완료 처리로 이어집니다.
화살표는 자식 완료가 부모 쪽으로 전달되는 방향이며 데이터 복사를 뜻하지 않습니다.
3. 원래 요청자에게 알립니다
필요한 정리를 마친 뒤 bi_end_io 콜백을 호출합니다. 요청자는 상태를 보고 성공 또는 오류를 처리합니다.
화살표는 완료 통지입니다. 제출 화살표와 시간상 같은 사건이 아닙니다.
bio_endio를 한 줄씩 읽기
줄 번호는 v6.6 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.
void bio_endio(struct bio *bio)
{
again:
if (!bio_remaining_done(bio))
return;
if (!bio_integrity_endio(bio))
return;
rq_qos_done_bio(bio);
if (bio->bi_bdev && bio_flagged(bio, BIO_TRACE_COMPLETION)) {
trace_block_bio_complete(bdev_get_queue(bio->bi_bdev), bio);
bio_clear_flag(bio, BIO_TRACE_COMPLETION);
}
/*
* Need to have a real endio function for chained bios, otherwise
* various corner cases will break (like stacking block devices that
* save/restore bi_end_io) - however, we want to avoid unbounded
* recursion and blowing the stack. Tail call optimization would
* handle this, but compiling with frame pointers also disables
* gcc's sibling call optimization.
*/
if (bio->bi_end_io == bio_chain_endio) {
bio = __bio_chain_endio(bio);
goto again;
}
blk_throtl_bio_endio(bio);
/* release cgroup info */
bio_uninit(bio);
if (bio->bi_end_io)
bio->bi_end_io(bio);
}void bio_endio(struct bio *bio)완료할 bio 포인터를 받습니다. 반환값 없이 bio의 상태와 콜백을 통해 결과를 전달합니다.
again:연결된 부모 bio를 같은 함수 본문에서 다시 처리하기 위한 돌아올 위치입니다.
if (!bio_remaining_done(bio))연결된 bio라면 남은 완료 계수를 하나 줄이고 0이 됐는지 검사합니다. 연결되지 않은 bio는 첫 완료에서 진행하며, 아직 조각이 남았다면 마지막 콜백을 미룹니다.
return;기다릴 완료가 남았으므로 여기서 종료합니다.
if (!bio_integrity_endio(bio))무결성 정보가 있는 bio의 완료 처리를 진행합니다. 이 함수가 아직 후속 처리가 필요하다고 알리면 아래 절차를 미룹니다.
return;무결성 처리가 완료 통지를 이어 맡을 수 있으므로 여기서 돌아갑니다.
rq_qos_done_bio(bio);이 bio를 추적한 I/O 서비스 품질 제어 계층에 완료를 알립니다.
if (bio->bi_bdev && bio_flagged(bio, BIO_TRACE_COMPLETION)) {실제 대상 장치가 있고 완료 추적 플래그도 켜졌을 때만 추적 기록을 남깁니다.
trace_block_bio_complete(bdev_get_queue(bio->bi_bdev), bio);대상 장치의 큐와 bio를 사용해 block_bio_complete 추적 이벤트를 기록합니다.
bio_clear_flag(bio, BIO_TRACE_COMPLETION);동일 bio의 완료 추적을 반복하지 않도록 플래그를 지웁니다.
if (bio->bi_end_io == bio_chain_endio) {콜백이 일반 요청자의 함수인지, 부모로 완료를 전달하는 연결용 함수인지 구분합니다.
bio = __bio_chain_endio(bio);자식의 오류를 필요하면 부모에 전달하고 자식 bio의 참조를 내려놓습니다. 이어 처리할 부모 bio를 받아 현재 대상에 넣습니다.
goto again;C 함수를 재귀 호출하지 않고 부모에 대해 남은 완료 검사부터 반복합니다.
blk_throtl_bio_endio(bio);블록 I/O 속도 제한 계층에 bio 완료를 알립니다. 이 호출은 v6.6 완료 경로에 있으며 요청 데이터의 성공 여부를 새로 결정하는 것은 아닙니다.
bio_uninit(bio);bio_uninit으로 cgroup 참조, 무결성 정보와 암호화 문맥 등 bio에 붙은 보조 자원을 정리합니다. bio 구조체 자체를 곧바로 해제하는 함수와 구분합니다.
if (bio->bi_end_io)완료 함수를 지정한 bio인지 확인합니다.
bio->bi_end_io(bio);원래 요청자에게 완료를 알립니다. 이 호출 안에서 bio가 해제될 수도 있습니다.
함께 생각해 볼 질문
submit 함수에서 돌아오면 버퍼를 바로 재사용해도 되나요?
비동기 작업이라면 안 됩니다. 해당 요청의 완료가 보장된 뒤에 재사용해야 합니다.
왜 부모의 완료를 함수 재귀 호출로 처리하지 않나요?
연결 단계가 길어져도 커널 스택 사용량이 그만큼 늘지 않도록 반복으로 처리합니다.
bio_endio가 호출되면 성공한 것인가요?
완료 절차에 진입한 것입니다. 성공 또는 오류는 bio에 저장된 상태와 각 완료 처리 규칙으로 구분합니다.
출처와 읽은 범위
Linux stable v6.6 · block/bio.c
해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고
