전체 흐름
파티션 검색은 파일시스템 I/O의 bio → blk-mq → 완료 경로 자체가 아닙니다. 디스크를 등록하거나 다시 검색할 때 장치의 파티션 표를 읽어 각 구간을 block_device로 노출하는 절차입니다. 일반 데이터 I/O는 그 뒤 해당 장치를 사용할 수 있습니다.
- 등록 또는 재검색 요청
device_add_disk()뒤 용량이 있으면 검색을 요청할 수 있고, 명시적 재검색도 같은 하위 경로에 도달합니다. - bdev_disk_changed()
open_mutex아래 기존 파티션을 정리하고 필요하면blk_add_partitions()를 호출합니다. - check_partition()파티션 형식 파서를 시도해 NULL·오류 포인터·유효한
parsed_partitions중 하나를 반환합니다. - blk_add_partition()유효한 항목의 시작·길이를 확인한 뒤
add_partition()으로 각 장치를 등록합니다.
화살표 대신 번호는 대표 호출·처리 순서입니다. 등록 시점과 명시적 재검색 시점은 구분해야 합니다.
초기 디스크 등록의 상위 호출은 block/genhd.c의 device_add_disk()·disk_scan_partitions() 쪽에서, 아래의 변경·해석·등록은 고정 block/partitions/core.c에서 대조합니다. 변경 함수 · 해석·등록 함수.
소스 코드 위치
먼저 확인할 Linux 6.18.37 LTS 소스 파일
Linux 6.18.37 LTS: block/genhd.c
Linux 6.18.37 LTS: block/partitions/core.c
Linux 6.18.37 LTS: include/linux/blkdev.h
설명: block/genhd.c에서 검색 요청, block/partitions/core.c에서 변경·파싱·등록, include/linux/blkdev.h에서 gendisk와 파티션 검색 판정을 각각 확인합니다. 아래 대표 코드는 사이트에 고정된 버전의 core.c에서 다시 뽑았습니다.
대표 코드
기존 발췌는 앞 함수의 마지막 줄에서 시작하고 두 함수 모두 중간에서 끝났습니다. 아래는 각 함수의 선언부터 닫는 중괄호까지 해당 버전의 고정 원문을 실었습니다. 코드의 모든 갈림길이 아래 도표와 대응합니다.
blk_add_partitions()
static int blk_add_partitions(struct gendisk *disk)
{
struct parsed_partitions *state;
int ret = -EAGAIN, p;
if (!disk_has_partscan(disk))
return 0;
state = check_partition(disk);
if (!state)
return 0;
if (IS_ERR(state)) {
/*
* I/O error reading the partition table. If we tried to read
* beyond EOD, retry after unlocking the native capacity.
*/
if (PTR_ERR(state) == -ENOSPC) {
printk(KERN_WARNING "%s: partition table beyond EOD, ",
disk->disk_name);
if (disk_unlock_native_capacity(disk))
return -EAGAIN;
}
return -EIO;
}
/*
* Partitions are not supported on host managed zoned block devices.
*/
if (bdev_is_zoned(disk->part0)) {
pr_warn("%s: ignoring partition table on host managed zoned block device\n",
disk->disk_name);
ret = 0;
goto out_free_state;
}
/*
* If we read beyond EOD, try unlocking native capacity even if the
* partition table was successfully read as we could be missing some
* partitions.
*/
if (state->access_beyond_eod) {
printk(KERN_WARNING
"%s: partition table partially beyond EOD, ",
disk->disk_name);
if (disk_unlock_native_capacity(disk))
goto out_free_state;
}
/* tell userspace that the media / partition table may have changed */
kobject_uevent(&disk_to_dev(disk)->kobj, KOBJ_CHANGE);
for (p = 1; p < state->limit; p++)
if (!blk_add_partition(disk, state, p))
goto out_free_state;
ret = 0;
out_free_state:
free_partitions(state);
return ret;
}읽는 법: 검색 억제 판정 → 파서 결과 → 용량 초과 재시도 → 디스크 KOBJ_CHANGE → 항목별 등록 → 임시 결과 해제 순서입니다. 해당 버전 원문.
bdev_disk_changed()
int bdev_disk_changed(struct gendisk *disk, bool invalidate)
{
struct block_device *part;
unsigned long idx;
int ret = 0;
lockdep_assert_held(&disk->open_mutex);
if (!disk_live(disk))
return -ENXIO;
rescan:
if (disk->open_partitions)
return -EBUSY;
sync_blockdev(disk->part0);
invalidate_bdev(disk->part0);
xa_for_each_start(&disk->part_tbl, idx, part, 1) {
/*
* Remove the block device from the inode hash, so that
* it cannot be looked up any more even when openers
* still hold references.
*/
bdev_unhash(part);
/*
* If @disk->open_partitions isn't elevated but there's
* still an active holder of that block device things
* are broken.
*/
WARN_ON_ONCE(atomic_read(&part->bd_openers));
invalidate_bdev(part);
drop_partition(part);
}
clear_bit(GD_NEED_PART_SCAN, &disk->state);
/*
* Historically we only set the capacity to zero for devices that
* support partitions (independ of actually having partitions created).
* Doing that is rather inconsistent, but changing it broke legacy
* udisks polling for legacy ide-cdrom devices. Use the crude check
* below to get the sane behavior for most device while not breaking
* userspace for this particular setup.
*/
if (invalidate) {
if (!(disk->flags & GENHD_FL_NO_PART) ||
!(disk->flags & GENHD_FL_REMOVABLE))
set_capacity(disk, 0);
}
if (get_capacity(disk)) {
ret = blk_add_partitions(disk);
if (ret == -EAGAIN)
goto rescan;
} else if (invalidate) {
/*
* Tell userspace that the media / partition table may have
* changed.
*/
kobject_uevent(&disk_to_dev(disk)->kobj, KOBJ_CHANGE);
}
return ret;
}
/*
* Only exported for loop and dasd for historic reasons. Don't use in new
* code!
*/
EXPORT_SYMBOL_GPL(bdev_disk_changed);읽는 법: 디스크 생존·열린 파티션 검사 → 기존 파티션 정리 → 용량 검사 → blk_add_partitions() 호출 순서입니다. -EAGAIN이면 rescan 레이블로 돌아갑니다. 해당 버전 원문.
함수별 분석
bdev_disk_changed()가 blk_add_partitions()를 호출합니다. 기존 그림의 반대 순서를 바로잡았습니다. blk_add_partitions()는 디스크의 capacity와 queue를 공개하는 함수가 아니라 파티션 표를 해석해 항목을 등록하는 함수입니다.
!disk_has_partscan(disk)이면 파서를 호출하지 않고 0을 반환합니다.check_partition()가 NULL이면 0, 오류 포인터이면 주로 -EIO입니다. NULL은 표를 인식하지 못한 경우뿐 아니라 상태 할당 실패에서도 나올 수 있습니다. EOD 초과를 풀 수 있으면 -EAGAIN을 돌려 재검색합니다.parsed_partitions를 얻으면 zoned 제약과 EOD를 확인하고, 1번부터 항목을 순회해 장치를 만들고 임시 결과를 해제합니다.세 칸은 순차 단계가 아닌 배타적인 결과 범주입니다. 유효한 표에서도 zoned·EOD 분기가 다시 있습니다.
| 판정 | Linux 6.18.37 LTS 고정 코드 | 결과 |
|---|---|---|
| 검색 대상인가 | !disk_has_partscan(disk)이면 | 이 생략 조건이 참이면 파서 호출 없이 0 |
| 파서 반환값 | NULL / ERR_PTR / 유효한 parsed_partitions | NULL은 미인식 또는 할당 실패 / 읽기 오류·재시도 / 항목 검사 |
| zoned 장치 | bdev_is_zoned(disk->part0) | 표를 읽어도 파티션 등록을 건너뛰고 임시 상태 해제 |
| EOD 초과 | disk_unlock_native_capacity()가 한 번 용량 재설정을 시도 | 가능하면 -EAGAIN으로 상위 rescan; 불가능하면 오류 또는 경계 제한 |
bdev_disk_changed()는 disk->open_mutex를 가진 상태를 요구하고, 열린 파티션이 있으면 -EBUSY로 검색을 막습니다. 기존 파티션을 정리한 뒤에야 현재 capacity가 0이 아닌 경우 파서를 호출합니다.
구조체와 필드
기존 그림은 gendisk → block_device → parsed_partitions → bd_meta_info를 하나의 수명 체인처럼 보였습니다. 실제로는 파서의 임시 결과에서 영속 파티션 객체를 만들며, 메타데이터는 선택적입니다.
part_tbl을 소유합니다. open_mutex가 갱신을 직렬화합니다.check_partition()의 임시 해석 결과입니다. 각 항목의 from, size, info를 담고 등록 후 해제됩니다.add_partition()이 시작 섹터·길이·장치 번호와 선택적 메타데이터를 채우고 part_tbl에 넣습니다.가운데 임시 객체는 등록 후 해제됩니다. 오른쪽 파티션 장치는 번호별로 0개 이상 존재할 수 있습니다.
| 객체·필드 | 수명 또는 값 | 고정 코드에서 확인할 지점 |
|---|---|---|
gendisk->part0 | 전체 디스크를 표현 | bdev_disk_changed()의 sync·invalidate·capacity |
parsed_partitions.parts[p] | 파서가 채운 임시 from·size·info | blk_add_partition() 뒤 free_partitions() |
disk->part_tbl | 등록된 파티션 block_device의 번호별 테이블 | 두 버전 모두 add_partition()에서 xa_insert() |
bdev_partno() / bd_start_sect | 파티션 번호와 전체 디스크 안의 시작 섹터 | add_partition()의 장치 이름·범위 설정 |
bd_meta_info | 파서가 제공할 때만 복사되는 PARTNAME/PARTUUID 정보 | 없어도 파티션 장치는 등록 가능 |
실행 단계와 상태 변화
재검색은 등록 완료 뒤의 단순 후속 단계가 아닙니다. 이전 파티션을 정리하고 현재 용량에서 다시 해석하며, native capacity를 열었을 때 같은 함수의 rescan 레이블로 되돌아갑니다. 이 레이블은 disk_live() 검사 뒤에 있으므로 재시도는 열린 파티션 검사부터 반복합니다. KOBJ_CHANGE와 파티션의 KOBJ_ADD는 서로 다른 이벤트입니다.
- 최초 진입
open_mutex보유와disk_live()를 확인합니다. 이 단계는 재시도 때 반복하지 않습니다. - rescan 레이블
open_partitions를 확인하고, part0 sync·invalidate 후 기존 파티션을 제거하며GD_NEED_PART_SCAN을 지웁니다. - 새 표 해석capacity가 있으면
blk_add_partitions(); 0이면서 invalidate라면 디스크 변경 이벤트만 보냅니다. - 결과 또는 재시도
-EAGAIN이면 rescan으로 돌아갑니다. 성공한 항목은 장치 객체가 남습니다.
마지막 칸의 -EAGAIN은 두 번째 칸의 rescan 레이블로 되돌아갑니다. disk_live()는 반복 검사하지 않습니다. 이벤트 순서는 디스크 등록 중 uevent 억제 여부에 영향을 받습니다.
| 상황 | 동작 | 주의 |
|---|---|---|
| 정상 파서 결과 | KOBJ_CHANGE 호출이 항목별 blk_add_partition() 호출보다 앞섬 | 디스크 uevent 억제 상태에서는 사용자 공간에 바로 보이지 않을 수 있음 |
| 파티션 장치 등록 | add_partition()의 KOBJ_ADD는 디스크 uevent 억제 상태에 따라 즉시 또는 나중에 전달 | 항상 네 단계 끝에 한 번만 발생하는 이벤트가 아님 |
| 열린 파티션 | open_partitions가 있으면 -EBUSY | 기존 항목을 지우기 전에 중단 |
| native capacity 재설정 | -EAGAIN → rescan: 재진입 | disk_live()를 다시 검사하지 않고 open_partitions 검사부터 반복 |
| NULL / 읽기 오류 | NULL → 0, 해결 불가 오류 → -EIO | NULL은 미인식 표와 할당 실패가 모두 가능 |
불변 조건과 실패 사례
bdev_disk_changed()는 open_mutex 보유를 확인합니다. open_partitions가 0이 아니면 기존 객체를 지우지 않고 -EBUSY를 반환합니다.
blk_add_partition()은 시작점이 EOD 밖인지, 끝이 EOD를 넘는지 확인합니다. native capacity를 열 수 있으면 재시도하고, 그렇지 않은 일부 잘못된 표는 디스크 끝으로 제한합니다.
유효한 파싱 결과는 성공·zoned 생략·재시도 분기 모두 out_free_state에서 해제합니다. 등록한 파티션 객체 자체와 혼동하면 안 됩니다.
bdev_unhash() → invalidate_bdev() → drop_partition() 순서로 기존 파티션을 제거합니다. 동일한 “파티션 제거” 목적이어도 내부 helper는 버전별로 다릅니다.
계측과 검증
먼저 파티션 객체가 있는지와 이벤트 순서를 확인합니다. 일반 I/O의 block:block_bio_queue는 파티션 검색의 완료 여부를 직접 표시하지 않습니다. blk_add_partitions()는 정적 함수이므로 추적 가능 여부를 대상 커널의 tracefs 목록에서 확인해야 합니다.
| 관찰 수단 | 확인할 값 | 한계 |
|---|---|---|
lsblk -o NAME,TYPE,START,SIZE,PARTUUID | 등록된 디스크와 파티션의 시작·크기 | 현재 결과이지 파서 내부 분기를 보여주지 않음 |
/sys/block/<disk>/<part>/start 및 size | 등록된 파티션의 섹터 범위 | 커널 빌드·장치 이름에 맞는 경로 확인 필요 |
udevadm monitor --kernel --subsystem-match=block | change와 add 이벤트 순서 | 이벤트 관찰만으로 등록 성공 전체를 보증하지 않음 |
tracefs available_filter_functions | bdev_disk_changed 등 실제 추적 가능한 함수 | 정적 helper는 빌드·최적화에 따라 목록에서 빠질 수 있음 |
lsblk -o NAME,TYPE,START,SIZE,PARTUUID
udevadm monitor --kernel --subsystem-match=block
grep -E '^(bdev_disk_changed|blk_add_partitions)( |$)' /sys/kernel/tracing/available_filter_functions추가 확인 사항
- 현재 검색은 초기 디스크 등록, BLKRRPART, 매체 변경, 드라이버의 명시적 호출 중 어디서 시작했는가?
- NULL 결과가 미인식 표인지 상태 할당 실패인지, 읽기 오류 포인터와 구분했는가?
- 열린 파티션 때문에 -EBUSY가 반환됐는가, EOD 때문에 -EAGAIN 재검색이 일어났는가?
- 관찰한 uevent는 디스크 KOBJ_CHANGE인가, 파티션 KOBJ_ADD인가? 초기 등록 중 억제 여부는 어떤가?