Block / Storage · Linux 6.18.37 LTS · source + diagram note

partition scan

기존 코드 발췌를 읽으실 때

기존 blk_add_partitions 발췌가 다른 함수의 md_autodetect_dev 호출에서 시작해 함수 제목과 맞지 않습니다. 아래 대표 코드를 해당 버전의 실제 함수 시작부터 끝까지 다시 실었습니다. 개념 설명의 줄별 해설도 함께 확인할 수 있습니다.

디스크의 파티션 표를 해석하고 구간별 block_device를 등록하는 경로를 고정 소스와 대조합니다.

전체 흐름

파티션 검색은 파일시스템 I/O의 bio → blk-mq → 완료 경로 자체가 아닙니다. 디스크를 등록하거나 다시 검색할 때 장치의 파티션 표를 읽어 각 구간을 block_device로 노출하는 절차입니다. 일반 데이터 I/O는 그 뒤 해당 장치를 사용할 수 있습니다.

그림 1. 검색 요청에서 파티션 장치 등록까지
  1. 등록 또는 재검색 요청device_add_disk() 뒤 용량이 있으면 검색을 요청할 수 있고, 명시적 재검색도 같은 하위 경로에 도달합니다.
  2. bdev_disk_changed()open_mutex 아래 기존 파티션을 정리하고 필요하면 blk_add_partitions()를 호출합니다.
  3. check_partition()파티션 형식 파서를 시도해 NULL·오류 포인터·유효한 parsed_partitions 중 하나를 반환합니다.
  4. 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()

고정 원문: block/partitions/core.c 581–640행
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()

고정 원문: block/partitions/core.c 642–710행
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를 공개하는 함수가 아니라 파티션 표를 해석해 항목을 등록하는 함수입니다.

그림 2. 파서 반환값별 서로 다른 경로
검색 생략!disk_has_partscan(disk)이면 파서를 호출하지 않고 0을 반환합니다.
NULL 또는 읽기 실패check_partition()가 NULL이면 0, 오류 포인터이면 주로 -EIO입니다. NULL은 표를 인식하지 못한 경우뿐 아니라 상태 할당 실패에서도 나올 수 있습니다. EOD 초과를 풀 수 있으면 -EAGAIN을 돌려 재검색합니다.
유효한 표parsed_partitions를 얻으면 zoned 제약과 EOD를 확인하고, 1번부터 항목을 순회해 장치를 만들고 임시 결과를 해제합니다.

세 칸은 순차 단계가 아닌 배타적인 결과 범주입니다. 유효한 표에서도 zoned·EOD 분기가 다시 있습니다.

표 1. 두 버전의 검색 조건과 결과
판정Linux 6.18.37 LTS 고정 코드결과
검색 대상인가!disk_has_partscan(disk)이면이 생략 조건이 참이면 파서 호출 없이 0
파서 반환값NULL / ERR_PTR / 유효한 parsed_partitionsNULL은 미인식 또는 할당 실패 / 읽기 오류·재시도 / 항목 검사
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를 하나의 수명 체인처럼 보였습니다. 실제로는 파서의 임시 결과에서 영속 파티션 객체를 만들며, 메타데이터는 선택적입니다.

그림 3. 전체 디스크·임시 해석 결과·등록된 장치
gendisk / part0전체 디스크, 용량, 파티션 테이블 part_tbl을 소유합니다. open_mutex가 갱신을 직렬화합니다.
parsed_partitionscheck_partition()의 임시 해석 결과입니다. 각 항목의 from, size, info를 담고 등록 후 해제됩니다.
partition block_deviceadd_partition()이 시작 섹터·길이·장치 번호와 선택적 메타데이터를 채우고 part_tbl에 넣습니다.

가운데 임시 객체는 등록 후 해제됩니다. 오른쪽 파티션 장치는 번호별로 0개 이상 존재할 수 있습니다.

표 2. 객체를 호출 순서로 혼동하지 않기
객체·필드수명 또는 값고정 코드에서 확인할 지점
gendisk->part0전체 디스크를 표현bdev_disk_changed()의 sync·invalidate·capacity
parsed_partitions.parts[p]파서가 채운 임시 from·size·infoblk_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는 서로 다른 이벤트입니다.

그림 4. bdev_disk_changed()의 재검색 고리
  1. 최초 진입open_mutex 보유와 disk_live()를 확인합니다. 이 단계는 재시도 때 반복하지 않습니다.
  2. rescan 레이블open_partitions를 확인하고, part0 sync·invalidate 후 기존 파티션을 제거하며 GD_NEED_PART_SCAN을 지웁니다.
  3. 새 표 해석capacity가 있으면 blk_add_partitions(); 0이면서 invalidate라면 디스크 변경 이벤트만 보냅니다.
  4. 결과 또는 재시도-EAGAIN이면 rescan으로 돌아갑니다. 성공한 항목은 장치 객체가 남습니다.

마지막 칸의 -EAGAIN은 두 번째 칸의 rescan 레이블로 되돌아갑니다. disk_live()는 반복 검사하지 않습니다. 이벤트 순서는 디스크 등록 중 uevent 억제 여부에 영향을 받습니다.

표 3. 이벤트와 오류를 시간 순서로 읽기
상황동작주의
정상 파서 결과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, 해결 불가 오류 → -EIONULL은 미인식 표와 할당 실패가 모두 가능

불변 조건과 실패 사례

잠금과 열린 파티션

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 목록에서 확인해야 합니다.

표 4. 파티션 검색과 일반 I/O를 구분하는 관찰
관찰 수단확인할 값한계
lsblk -o NAME,TYPE,START,SIZE,PARTUUID등록된 디스크와 파티션의 시작·크기현재 결과이지 파서 내부 분기를 보여주지 않음
/sys/block/<disk>/<part>/start 및 size등록된 파티션의 섹터 범위커널 빌드·장치 이름에 맞는 경로 확인 필요
udevadm monitor --kernel --subsystem-match=blockchange와 add 이벤트 순서이벤트 관찰만으로 등록 성공 전체를 보증하지 않음
tracefs available_filter_functionsbdev_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인가? 초기 등록 중 억제 여부는 어떤가?