Block I/O 서브시스템
Linux 커널의 블록 I/O 계층을 심층 분석합니다. VFS/Page Cache와 디바이스 드라이버 사이에서 동작하는 bio 구조체, blk-mq 멀티큐 아키텍처, I/O 스케줄러, Direct I/O, I/O 배리어, 블록 디바이스 토폴로지, 디버깅 기법을 다룹니다.
핵심 요약
- bio — 블록 I/O의 기본 단위. 디스크 섹터와 메모리 페이지의 매핑을 기술합니다.
- blk-mq — Multi-Queue Block Layer. NVMe 등 고속 디바이스를 위한 멀티큐 아키텍처입니다.
- I/O 스케줄러 — mq-deadline, BFQ, kyber 등. I/O 요청 순서를 최적화합니다.
- request — 인접한 bio를 병합한 I/O 요청. 디바이스 드라이버에 전달되는 단위입니다.
- Direct I/O — 페이지 캐시를 우회하여 디바이스에 직접 읽기/쓰기합니다.
단계별 이해
- I/O 경로 파악 — 애플리케이션 → VFS → 페이지 캐시 → Block Layer → 디바이스 드라이버 → 하드웨어.
Block Layer는 VFS와 드라이버 사이에서 I/O 요청을 관리합니다.
- bio 이해 — 파일시스템이
submit_bio()로 bio를 블록 계층에 제출합니다.각 bio는 디스크의 시작 섹터, 크기, 방향(읽기/쓰기), 메모리 페이지를 포함합니다.
- blk-mq 이해 — 하드웨어 큐와 소프트웨어 큐의 매핑을 관리합니다.
cat /sys/block/nvme0n1/queue/nr_requests로 큐 깊이를 확인합니다. - 모니터링 —
iostat -x 1로 디바이스별 I/O 통계를 실시간 모니터링합니다.blktrace/blkparse로 I/O 요청의 상세 흐름을 추적할 수 있습니다.
Block I/O 계층 개요
블록 I/O 계층(Block Layer)은 VFS/Page Cache와 물리 디바이스 드라이버 사이에 위치하며, 디스크, SSD, NVMe 등 블록 디바이스에 대한 I/O 요청을 관리합니다. 상위 계층이 논리적 블록 단위로 데이터를 요청하면, 블록 계층이 이를 최적화하여 하드웨어에 전달합니다.
블록 계층의 역할
- I/O 요청 표현:
struct bio를 통해 디스크 영역과 메모리 페이지의 매핑을 기술 - 요청 병합: 인접한 I/O를 하나의
struct request로 통합하여 디바이스 효율을 극대화 - I/O 스케줄링: 디바이스 특성에 맞는 전략으로 요청 순서를 조정
- 멀티큐 디스패치: blk-mq를 통해 멀티코어 환경에서 병렬 I/O 처리
- 디바이스 추상화: HDD, SSD, NVMe, 스택 디바이스(md, dm) 등을 통일된 인터페이스로 지원
Legacy single-queue에서 blk-mq로
Linux 3.13(2014) 이전에는 단일 request_queue에 하나의 스핀락으로 모든 I/O를 직렬화했습니다. 이 single-queue 모델은 NVMe처럼 수백만 IOPS를 지원하는 디바이스에서 심각한 병목이 되었습니다. blk-mq(Multi-Queue Block I/O)는 per-CPU 소프트웨어 큐와 디바이스의 하드웨어 큐를 직접 매핑하여, 단일 락 경합 없이 병렬 I/O를 가능하게 합니다. Linux 5.0부터 legacy single-queue 코드가 완전히 제거되어, 모든 블록 드라이버가 blk-mq를 사용합니다.
struct bio
struct bio는 블록 계층의 기본 I/O 단위입니다. 하나의 bio는 디스크의 연속된 섹터 범위와 메모리의 페이지 세그먼트 목록을 연결합니다. 파일시스템이나 Direct I/O 경로에서 생성되어 블록 계층을 통해 디바이스 드라이버까지 전달됩니다.
핵심 구조체
/* include/linux/bio.h */
struct bio {
struct bio *bi_next; /* 리스트 연결 (request 내) */
struct block_device *bi_bdev; /* 대상 블록 디바이스 */
blk_opf_t bi_opf; /* 연산 + 플래그 (REQ_OP_* | REQ_*) */
unsigned short bi_flags; /* BIO_* 상태 플래그 */
struct bvec_iter bi_iter; /* 현재 반복 위치 */
bio_end_io_t *bi_end_io; /* I/O 완료 콜백 */
void *bi_private; /* 콜백 전용 데이터 */
unsigned short bi_vcnt; /* bio_vec 개수 */
unsigned short bi_max_vecs; /* bio_vec 배열 최대 크기 */
struct bio_vec *bi_io_vec; /* bio_vec 배열 */
/* ... */
};
struct bio_vec {
struct page *bv_page; /* 메모리 페이지 */
unsigned int bv_len; /* 이 세그먼트의 바이트 수 */
unsigned int bv_offset; /* 페이지 내 오프셋 */
};
struct bvec_iter {
sector_t bi_sector; /* 디스크 시작 섹터 */
unsigned int bi_size; /* 남은 I/O 바이트 */
unsigned int bi_idx; /* 현재 bio_vec 인덱스 */
unsigned int bi_bvec_done; /* 현재 bio_vec 내 완료 바이트 */
};
bi_opf 플래그
| 연산 (REQ_OP_*) | 값 | 설명 |
|---|---|---|
REQ_OP_READ | 0 | 블록 읽기 |
REQ_OP_WRITE | 1 | 블록 쓰기 |
REQ_OP_FLUSH | 2 | 디바이스 캐시 플러시 |
REQ_OP_DISCARD | 3 | 블록 무효화 (TRIM) |
REQ_OP_SECURE_ERASE | 5 | 보안 삭제 |
REQ_OP_WRITE_ZEROES | 9 | 제로 블록 쓰기 (하드웨어 최적화) |
추가 플래그는 비트 OR로 결합됩니다: REQ_SYNC(동기 I/O), REQ_META(메타데이터), REQ_PRIO(우선순위), REQ_PREFLUSH(사전 플러시), REQ_FUA(Force Unit Access).
bio 생명주기
/* bio 할당 → 설정 → 제출 → 완료 */
struct bio *bio = bio_alloc(bdev, nr_vecs, opf, GFP_KERNEL);
bio->bi_iter.bi_sector = sector; /* 시작 섹터 설정 */
bio_add_page(bio, page, len, offset); /* 페이지 세그먼트 추가 */
bio->bi_end_io = my_end_io; /* 완료 콜백 등록 */
bio->bi_private = private_data;
submit_bio(bio); /* 블록 계층에 제출 — bio 소유권 이전 */
/* 완료 콜백에서: */
static void my_end_io(struct bio *bio)
{
if (bio->bi_status)
pr_err("I/O error: %d\n", blk_status_to_errno(bio->bi_status));
/* 후처리 ... */
bio_put(bio); /* 참조 카운트 감소, 0이면 해제 */
}
struct request와 request_queue
struct request는 하나 이상의 bio를 병합한 I/O 단위로, 실제 디바이스 드라이버에 전달되는 작업 단위입니다. 블록 계층은 인접한 bio들을 자동으로 병합하여 디바이스 호출 횟수를 줄입니다.
요청 병합 (Merge)
- Back merge: 새 bio의 시작 섹터가 기존 request의 끝 섹터와 연속 → 뒤에 추가
- Front merge: 새 bio의 끝 섹터가 기존 request의 시작 섹터와 연속 → 앞에 추가
- Merge with next: 병합 후 인접한 request끼리 다시 결합
Plugging 메커니즘
커널은 blk_start_plug() / blk_finish_plug()으로 I/O를 일시적으로 보류(plug)했다가 한꺼번에 해제(unplug)하여 병합 기회를 극대화합니다. blk_plug 구조체에 per-task 리스트로 bio를 모았다가 unplug 시 일괄 제출합니다.
struct blk_plug plug;
blk_start_plug(&plug);
for (i = 0; i < nr_pages; i++) {
bio = bio_alloc(...);
/* bio 설정 */
submit_bio(bio); /* plug에 보류됨 */
}
blk_finish_plug(&plug); /* 모아둔 bio를 병합 후 일괄 디스패치 */
Queue Limits
| 한도 | 설명 | 기본값 (예시) |
|---|---|---|
max_hw_sectors | 하드웨어가 처리 가능한 최대 섹터 | 2048 (1MB) |
max_sectors | 단일 request의 최대 섹터 | 1280 (640KB) |
max_segments | scatter-gather 최대 세그먼트 수 | 128 |
max_segment_size | 단일 세그먼트 최대 바이트 | 65536 |
logical_block_size | 디바이스의 최소 주소 단위 | 512 |
physical_block_size | 디바이스 물리 블록 크기 | 4096 |
/* request 내 bio_vec 세그먼트 순회 */
struct req_iterator iter;
struct bio_vec bvec;
rq_for_each_segment(bvec, rq, iter) {
struct page *page = bvec.bv_page;
unsigned int len = bvec.bv_len;
unsigned int offset = bvec.bv_offset;
/* 각 세그먼트 처리 */
}
I/O 제출 경로
사용자 공간의 read()/write() 시스템 콜은 VFS → 파일시스템 → 블록 계층을 거쳐 디바이스에 도달합니다. 블록 계층 내부의 핵심 경로를 살펴봅니다.
submit_bio() 흐름
submit_bio(bio)→ 제출 회계(accounting) 시작submit_bio_noacct(bio)→ 스택 디바이스(dm, md) 재진입 시 직접 호출__submit_bio()→ 드라이버의submit_bio콜백 또는 기본blk_mq_submit_bio()- bio가 큐 한도(
max_sectors등)를 초과하면bio_split()으로 분할 후 재제출 - plug된 상태면
blk_mq_plug_issue_direct()/blk_add_rq_to_plug()으로 보류 - unplug 시 또는 직접 디스패치 시 하드웨어 큐로 전달
스택 디바이스 (md, dm, bcache)
Device Mapper(dm)나 md(software RAID)는 submit_bio 콜백에서 bio를 변환(remap)하여 하위 디바이스에 재제출합니다. 예를 들어, dm-linear는 섹터 오프셋을 조정하여 하위 디바이스의 submit_bio_noacct()를 호출합니다. 다단계 스택(dm 위에 dm)도 가능하며, 재진입 방지를 위해 current->bio_list 큐잉을 사용합니다.
blk-mq (Multi-Queue Block I/O)
blk-mq는 현대 멀티코어 시스템과 고속 저장 장치를 위해 설계된 블록 I/O 프레임워크입니다. 기존 single-queue의 단일 락 병목을 해결하여, NVMe 디바이스에서 수백만 IOPS를 달성할 수 있게 합니다.
아키텍처
- Software Queue (ctx): per-CPU 큐. 각 CPU가 자신의 큐에 request를 넣어 락 경합을 제거
- Hardware Queue (hctx): 디바이스의 실제 제출 큐에 1:1 또는 N:1로 매핑
- Tag: 각 in-flight request에 고유 번호를 부여하여 완료 시 식별. NVMe command ID와 직접 매핑
blk_mq_ops 콜백
static const struct blk_mq_ops my_mq_ops = {
.queue_rq = my_queue_rq, /* request를 HW에 전송 */
.commit_rqs = my_commit_rqs, /* 배치 전송 완료 알림 (doorbell) */
.complete = my_complete, /* softirq에서 완료 처리 */
.init_hctx = my_init_hctx, /* HW queue 초기화 */
.init_request = my_init_rq, /* request 슬랩 초기화 */
.timeout = my_timeout, /* 타임아웃 처리 */
.map_queues = my_map_queues, /* CPU → HW queue 매핑 */
};
blk-mq 드라이버 스켈레톤
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *rq = bd->rq;
blk_mq_start_request(rq); /* 타임아웃 타이머 시작 */
/* 하드웨어에 커맨드 전송 */
my_hw_submit(rq);
return BLK_STS_OK;
}
static int my_blk_probe(struct platform_device *pdev)
{
struct blk_mq_tag_set *tag_set;
struct gendisk *disk;
tag_set = kzalloc(sizeof(*tag_set), GFP_KERNEL);
tag_set->ops = &my_mq_ops;
tag_set->nr_hw_queues = 4; /* HW 큐 수 */
tag_set->queue_depth = 256; /* 큐당 최대 in-flight */
tag_set->numa_node = NUMA_NO_NODE;
tag_set->cmd_size = sizeof(struct my_cmd);
tag_set->flags = BLK_MQ_F_SHOULD_MERGE;
blk_mq_alloc_tag_set(tag_set);
disk = blk_mq_alloc_disk(tag_set, NULL, NULL);
disk->major = my_major;
disk->first_minor = 0;
disk->minors = 16;
disk->fops = &my_fops;
snprintf(disk->disk_name, DISK_NAME_LEN, "myblk0");
set_capacity(disk, sectors);
add_disk(disk); /* /dev/myblk0 등록 */
return 0;
}
blk-mq 태그셋 플래그
| 플래그 | 설명 |
|---|---|
BLK_MQ_F_SHOULD_MERGE | request 병합 활성화 (대부분의 디바이스) |
BLK_MQ_F_TAG_QUEUE_SHARED | 여러 장치가 tag set 공유 (SCSI HBA 등) |
BLK_MQ_F_BLOCKING | queue_rq에서 sleep 허용 (스케줄링 기반 동기 I/O) |
BLK_MQ_F_NO_SCHED | I/O 스케줄러 비활성화 (NVMe에서 직접 디스패치) |
완료 경로
디바이스가 I/O를 완료하면 인터럽트 핸들러에서 blk_mq_complete_request()를 호출합니다. 이 함수는 해당 request를 제출한 CPU의 softirq 컨텍스트에서 blk_mq_ops->complete를 실행하여, 캐시 친화성을 유지합니다. complete 콜백은 blk_mq_end_request()를 호출하여 bio 체인의 bi_end_io를 트리거하고, 태그를 반환합니다.
I/O 스케줄러
I/O 스케줄러는 blk-mq의 소프트웨어 큐와 하드웨어 큐 사이에 위치하여, request의 제출 순서를 조정합니다. Linux 커널은 4가지 스케줄러를 제공하며, 디바이스 유형에 따라 자동 선택됩니다.
스케줄러 비교 요약
| 스케줄러 | 알고리즘 | 적합 디바이스 | 오버헤드 | 특징 |
|---|---|---|---|---|
mq-deadline | 데드라인 기반 FIFO + 정렬 큐 | HDD, 범용 | 낮음 | 읽기/쓰기 데드라인 보장, starvation 방지 |
bfq | 예산 기반 공정 큐잉 (Budget Fair Queueing) | 데스크톱, 느린 디바이스 | 높음 | 프로세스별 대역폭 공정 분배, 대화형 작업 우선 |
kyber | 토큰 기반 지연 목표 (latency target) | 고속 SSD/NVMe | 매우 낮음 | 읽기/쓰기 대기시간 목표로 자동 조절 |
none | 스케줄링 없음 (FIFO 직행) | NVMe, 가상 디바이스 | 없음 | 디바이스 자체 스케줄링에 위임 |
elevator 프레임워크
커널의 I/O 스케줄러는 elevator 프레임워크 위에 구현됩니다. 각 스케줄러는 struct elevator_mq_ops에 콜백을 등록하며, blk-mq 디스패치 루프가 이 콜백을 호출하여 다음에 처리할 request를 결정합니다.
/* include/linux/elevator.h — elevator 콜백 구조체 (주요 멤버) */
struct elevator_mq_ops {
int (*init_sched)(struct request_queue *, struct elevator_type *);
void (*exit_sched)(struct elevator_queue *);
/* bio → request 삽입 */
void (*insert_requests)(struct blk_mq_hw_ctx *hctx,
struct list_head *list, blk_insert_t flags);
/* 다음 디스패치할 request 반환 */
struct request *(*dispatch_request)(struct blk_mq_hw_ctx *hctx);
/* 요청 병합 가능 여부 */
bool (*allow_merge)(struct request_queue *, struct request *,
struct bio *);
/* 완료 통지 (latency 추적 등) */
void (*completed_request)(struct request *, u64 now);
/* I/O 우선순위 변경 시 호출 */
void (*depth_updated)(struct blk_mq_hw_ctx *hctx);
/* sysfs 파라미터 */
const struct blk_mq_debugfs_attr *queue_debugfs_attrs;
};
/* elevator 등록 예시 (mq-deadline) */
static struct elevator_type mq_deadline = {
.ops = {
.insert_requests = dd_insert_requests,
.dispatch_request = dd_dispatch_request,
.completed_request= dd_completed_request,
.init_sched = dd_init_sched,
.exit_sched = dd_exit_sched,
/* ... */
},
.elevator_name = "mq-deadline",
.elevator_alias = "deadline",
};
blk-mq 디스패치 루프의 핵심 경로는 다음과 같습니다:
/* block/blk-mq-sched.c — 디스패치 핵심 루프 (간략화) */
bool blk_mq_sched_dispatch_requests(struct blk_mq_hw_ctx *hctx)
{
struct elevator_queue *e = hctx->queue->elevator;
/* 1. 스케줄러가 있으면 스케줄러에게 다음 request 요청 */
if (e && e->type->ops.dispatch_request) {
struct request *rq;
while ((rq = e->type->ops.dispatch_request(hctx)) != NULL) {
blk_mq_dispatch_rq_list(hctx, &list, count);
}
} else {
/* 2. none 스케줄러: SW 큐에서 직접 가져와 디스패치 */
blk_mq_flush_busy_ctxs(hctx, &list);
blk_mq_dispatch_rq_list(hctx, &list, count);
}
}
mq-deadline 심층 분석
mq-deadline은 Linux 블록 계층의 기본 스케줄러로, 요청의 데드라인(만료 시간)을 보장하면서도 디스크 탐색(seek)을 최소화하는 이중 큐 구조를 사용합니다. 원래의 단일큐 deadline 스케줄러를 blk-mq에 맞게 재설계한 것입니다.
자료구조
/* block/mq-deadline.c — 핵심 자료구조 */
struct deadline_data {
/*
* 읽기/쓰기 각각 2개의 큐 = 총 4개의 큐를 관리
* [DD_READ] — 읽기용 정렬큐 + FIFO큐
* [DD_WRITE] — 쓰기용 정렬큐 + FIFO큐
*/
struct rb_root sort_list[2]; /* RB 트리: 섹터 번호 순 정렬 */
struct list_head fifo_list[2]; /* FIFO: 제출 순서 (만료 시간 순) */
struct request *next_rq[2]; /* 다음 정렬 순서 요청 (캐싱) */
/* 튜닝 파라미터 */
unsigned int batching; /* 현재 배치에서 디스패치한 수 */
unsigned int fifo_batch; /* 한 방향 연속 디스패치 최대 수 (기본 16) */
int fifo_expire[2]; /* [READ]=500ms, [WRITE]=5000ms */
int writes_starved; /* 쓰기 양보 횟수 (기본 2) */
int front_merges; /* 앞쪽 병합 허용 여부 (기본 1) */
int starved; /* 현재 쓰기 기아 카운터 */
int last_dir; /* 마지막 디스패치 방향 (READ/WRITE) */
struct dd_per_prio per_prio[3]; /* IOPRIO_CLASS_RT/BE/IDLE 별 큐 */
spinlock_t lock; /* zone lock (ZNS 디바이스) */
};
디스패치 알고리즘
mq-deadline의 디스패치 로직은 다음 우선순위로 request를 선택합니다:
- 만료 확인: 읽기 FIFO 큐의 head가 데드라인을 초과했으면 즉시 디스패치 (읽기 우선)
- 쓰기 기아 방지: 읽기가
writes_starved회 연속 선택되면 쓰기를 강제 디스패치 - 쓰기 만료 확인: 쓰기 FIFO 큐의 head가 데드라인을 초과했으면 디스패치
- 정렬 순서: 만료된 요청이 없으면, 현재 방향의 정렬 큐에서 다음 섹터를 선택 (seek 최소화)
- 배치 제한: 한 방향으로
fifo_batch개를 초과하면 방향 전환
/* block/mq-deadline.c — dd_dispatch_request() 간략화 */
static struct request *dd_dispatch_request(struct blk_mq_hw_ctx *hctx)
{
struct deadline_data *dd = hctx->queue->elevator->elevator_data;
struct request *rq;
enum dd_data_dir data_dir;
/* 우선순위 순서: RT → BE → IDLE */
for (prio = DD_RT_PRIO; prio <= DD_IDLE_PRIO; prio++) {
/* 읽기 FIFO에 만료된 요청이 있는가? */
if (deadline_fifo_request(dd, DD_READ) &&
deadline_check_fifo(dd, DD_READ)) {
rq = deadline_fifo_request(dd, DD_READ);
data_dir = DD_READ;
goto dispatch;
}
/* 쓰기가 writes_starved번 이상 양보했으면 쓰기 우선 */
if (dd->starved > dd->writes_starved) {
data_dir = DD_WRITE;
} else {
data_dir = DD_READ;
if (!deadline_next_request(dd, DD_READ))
data_dir = DD_WRITE; /* 읽기 큐 비었으면 쓰기 */
}
/* 정렬 큐에서 다음 섹터 순서 request 선택 */
rq = deadline_next_request(dd, data_dir);
dispatch:
dd->batching++;
dd->last_dir = data_dir;
return rq;
}
return NULL;
}
읽기 우선 정책의 이유: 읽기 요청은 대부분 동기적으로 프로세스를 블록시키므로 지연에 민감합니다. 쓰기는 Page Cache에 의해 비동기로 처리되어 지연에 관대합니다. 따라서 읽기 데드라인(500ms)이 쓰기(5000ms)보다 10배 짧습니다.
mq-deadline 튜닝 파라미터
| 파라미터 | 경로 (sysfs) | 기본값 | 설명 |
|---|---|---|---|
read_expire | queue/iosched/read_expire | 500 (ms) | 읽기 요청 최대 지연. 이 시간 내에 반드시 디스패치 |
write_expire | queue/iosched/write_expire | 5000 (ms) | 쓰기 요청 최대 지연 |
fifo_batch | queue/iosched/fifo_batch | 16 | 한 방향(읽기 또는 쓰기)에서 연속 디스패치 최대 수. 값이 크면 throughput 증가, seek이 많아질 수 있음 |
writes_starved | queue/iosched/writes_starved | 2 | 쓰기 양보 횟수. 읽기가 이 횟수만큼 연속 선택되면 쓰기를 강제 |
front_merges | queue/iosched/front_merges | 1 | 앞쪽 병합(front merge) 허용. HDD에서는 1, 일부 SSD에서는 0이 유리 |
prio_aging_expire | queue/iosched/prio_aging_expire | 10000 (ms) | 낮은 I/O 우선순위 요청의 에이징 시간. 이 시간 경과 시 우선순위 승격 |
# mq-deadline 튜닝 예시: HDD 순차 처리량 최적화
# fifo_batch를 높여 한 방향 연속 처리 증가 → seek 감소
$ echo 32 > /sys/block/sda/queue/iosched/fifo_batch
# 데이터베이스 서버: 읽기 데드라인을 짧게 설정
$ echo 200 > /sys/block/sda/queue/iosched/read_expire
$ echo 2000 > /sys/block/sda/queue/iosched/write_expire
# 쓰기 집중 워크로드: 쓰기 기아 방지 강화
$ echo 1 > /sys/block/sda/queue/iosched/writes_starved
BFQ (Budget Fair Queueing) 심층 분석
BFQ는 프로세스별 예산(budget)을 할당하여 대역폭을 공정하게 분배하는 I/O 스케줄러입니다. CFQ(Completely Fair Queueing)의 blk-mq 후속으로, 특히 대화형 작업의 응답성을 극대화하도록 설계되었습니다. 데스크톱 환경에서 백그라운드 I/O가 발생해도 전경 애플리케이션의 체감 성능을 유지합니다.
BFQ 내부 아키텍처
/* block/bfq-iosched.h — BFQ 핵심 자료구조 */
struct bfq_data {
struct request_queue *queue;
/* B-WF²Q+ 스케줄링 트리 (가중치 공정 큐잉) */
struct bfq_sched_data root_group->sched_data;
unsigned int num_bfqq; /* 활성 bfq_queue 수 */
unsigned int peak_rate; /* 디바이스 최대 처리율 추정치 */
unsigned int hw_tag; /* HW 큐 태그 여부 */
bool strict_guarantees; /* 엄격한 대역폭 보장 모드 */
/* 대화형 감지 */
unsigned long bfq_wr_max_time; /* weight raising 최대 시간 */
unsigned long bfq_wr_min_idle_time; /* 대화형 판별 유휴 임계 */
};
/* 프로세스별 큐 */
struct bfq_queue {
struct rb_root sort_list; /* 섹터 순 정렬된 request */
struct list_head fifo; /* 제출 순서 FIFO */
int entity.budget; /* 현재 할당 예산 (섹터 단위) */
unsigned short entity.weight; /* 가중치 (100 기본, ionice로 설정) */
bool wr_coeff; /* weight raising 배수 (대화형 부스트) */
unsigned long last_idle_time; /* 마지막 유휴 시간 (대화형 판별) */
pid_t pid; /* 소유 프로세스 PID */
};
B-WF²Q+ 스케줄링
BFQ는 B-WF²Q+ (Budget Worst-case Fair Weighted Fair Queueing) 알고리즘을 사용합니다. 각 프로세스는 bfq_queue를 갖고, 이 큐에 예산(budget)이 할당됩니다. 예산은 해당 큐가 한 번에 디스패치할 수 있는 최대 섹터 수입니다.
- 예산 할당: 큐가 활성화되면 디바이스의 추정 처리율과 가중치에 비례하여 예산을 받음
- 독점 서비스: 예산이 있는 동안 해당 큐가 디스패치를 독점 (idling 포함)
- 예산 소진: 예산을 모두 사용하거나 유휴 타임아웃 발생 시 다음 큐로 전환
- 가상 시간: WF²Q+ 알고리즘의 가상 시간을 기반으로 다음 서비스할 큐를 결정 (가중치에 비례하는 대역폭 분배)
대화형 작업 감지 (Weight Raising)
BFQ의 가장 중요한 기능은 weight raising입니다. 대화형 프로세스(짧은 I/O를 간헐적으로 발생시키는 패턴)를 자동으로 감지하여 가중치를 일시적으로 높여, 백그라운드의 대량 I/O 중에도 빠른 응답을 보장합니다.
/* 대화형 판별 조건 (간략화) */
/*
* 1. 큐가 유휴 상태에서 새 요청 도착
* 2. 유휴 기간이 bfq_wr_min_idle_time 이상
* 3. 이전 서비스에서 적은 양의 I/O만 수행
* → "대화형"으로 판별, wr_coeff를 30배까지 증가
* → bfq_wr_max_time 동안 유지 후 점진적 감소
*/
static bool bfq_bfqq_update_budg_for_activation(...)
{
if (bfq_bfqq_non_blocking_wait_rq(bfqq) &&
idle_for >= bfqd->bfq_wr_min_idle_time) {
/* weight raising 적용 */
bfqq->wr_coeff = bfqd->bfq_wr_coeff; /* 기본 30 */
bfqq->wr_cur_max_time = bfqd->bfq_wr_max_time;
}
}
BFQ + cgroups: BFQ는 cgroup v2의 io.bfq.weight를 통해 그룹 단위 대역폭 제어를 지원합니다. 이를 통해 컨테이너 간 I/O 격리가 가능합니다.
# cgroup v2에서 BFQ 가중치 설정
$ echo "8:0 200" > /sys/fs/cgroup/my_group/io.bfq.weight
큐 idling 메커니즘
BFQ는 현재 서비스 중인 큐가 비어도 일정 시간(slice_idle) 동안 대기합니다. 이는 두 가지 목적이 있습니다:
- 순차 접근 보호: 프로세스가 순차 읽기를 하는 경우, 다음 요청이 곧 도착할 것이므로 대기. 다른 프로세스의 랜덤 I/O가 끼어들면 seek이 발생하여 순차 성능이 급감
- 공정성 보장: 동기 I/O를 수행하는 프로세스는 이전 요청 완료 후 다음 요청을 생성하므로, 비동기 대량 I/O와 경쟁 시 기아 발생. idling이 이를 방지
BFQ의 오버헤드: 프로세스별 큐 관리, weight raising 추적, B-WF²Q+ 가상 시간 계산 등으로 인해 고속 NVMe 디바이스에서는 CPU 오버헤드가 병목이 될 수 있습니다. NVMe에서 100만 IOPS 이상을 처리해야 하는 경우 none 또는 kyber를 권장합니다.
BFQ 튜닝 파라미터
| 파라미터 | 경로 (sysfs) | 기본값 | 설명 |
|---|---|---|---|
slice_idle | queue/iosched/slice_idle | 8 (ms) | 큐 비어도 대기하는 시간. 0이면 idling 비활성. SSD에서는 0이 유리할 수 있음 |
slice_idle_us | queue/iosched/slice_idle_us | 8000 (us) | slice_idle의 마이크로초 버전 (정밀 제어) |
low_latency | queue/iosched/low_latency | 1 | 대화형 감지(weight raising) 활성화. 0이면 순수 공정 큐잉만 사용 |
timeout_sync | queue/iosched/timeout_sync | 124 (ms) | 동기 큐의 서비스 타임아웃 |
max_budget | queue/iosched/max_budget | 0 (자동) | 큐당 최대 예산. 0이면 디바이스 처리율 기반 자동 계산 |
strict_guarantees | queue/iosched/strict_guarantees | 0 | 1이면 엄격한 대역폭 보장 (추가 오버헤드 발생) |
# BFQ 튜닝 예시: 데스크톱 환경 최적화
$ echo bfq > /sys/block/sda/queue/scheduler
# SSD에서 idling 비활성 (seek 비용 없으므로)
$ echo 0 > /sys/block/sda/queue/iosched/slice_idle
# 서버에서 공정 큐잉만 사용 (weight raising 비활성)
$ echo 0 > /sys/block/sda/queue/iosched/low_latency
Kyber 심층 분석
Kyber는 고속 SSD/NVMe를 위해 설계된 경량 I/O 스케줄러입니다. 프로세스별 큐나 정렬 트리 없이, 토큰 기반 입장 제어(admission control)로 I/O 요청의 동시 실행 수를 조절하여 지연 시간(latency) 목표를 달성합니다.
토큰 기반 아키텍처
/* block/kyber-iosched.c — Kyber 자료구조 */
enum {
KYBER_READ, /* 읽기 도메인 */
KYBER_WRITE, /* 쓰기 도메인 */
KYBER_DISCARD, /* discard/trim 도메인 */
KYBER_OTHER, /* flush 등 기타 */
KYBER_NUM_DOMAINS,
};
struct kyber_queue_data {
struct request_queue *q;
/* 도메인별 토큰 수 (동시 실행 가능 I/O 수) */
unsigned int async_depth; /* HW 큐 깊이 기반 */
struct sbitmap_queue domain_tokens[KYBER_NUM_DOMAINS];
/* 지연 목표 */
u64 read_lat_nsec; /* 읽기 지연 목표 (기본 2ms) */
u64 write_lat_nsec; /* 쓰기 지연 목표 (기본 10ms) */
/* 지연 히스토그램 — 완료 지연 분포 추적 */
struct kyber_cpu_latency __percpu *cpu_latency;
};
/* per-HW-ctx 디스패치 큐 */
struct kyber_hctx_data {
/* 도메인별 대기 큐 (토큰 대기 중인 request) */
struct list_head rqs[KYBER_NUM_DOMAINS];
unsigned int cur_domain; /* 라운드 로빈 디스패치 */
unsigned int batching; /* 현재 도메인 연속 디스패치 수 */
};
동작 메커니즘
Kyber의 핵심 아이디어는 단순합니다: 동시 실행 I/O 수를 줄이면 지연이 감소한다. 토큰이 부족하면 새 I/O가 대기 큐에서 기다립니다.
- 토큰 획득: request가 디스패치되려면 해당 도메인(읽기/쓰기)의 토큰을 획득해야 함
- 지연 모니터링: 완료된 request의 실제 지연을 per-CPU 히스토그램에 기록
- 토큰 조절: 주기적으로(timer callback) 히스토그램을 분석하여 목표 지연을 초과한 비율이 높으면 토큰 수를 감소, 낮으면 증가
- 도메인 분리: 읽기와 쓰기의 토큰 풀이 독립이므로, 대량 쓰기가 읽기 지연에 미치는 영향을 최소화
/* block/kyber-iosched.c — 토큰 조절 로직 (간략화) */
static void kyber_timer_fn(struct timer_list *t)
{
struct kyber_queue_data *kqd = from_timer(kqd, t, timer);
unsigned int orig_depth, depth;
/* 각 도메인에 대해 */
for (domain = 0; domain < KYBER_NUM_DOMAINS; domain++) {
/* per-CPU 히스토그램을 취합하여 지연 분포 계산 */
good = kyber_lat_percentage(kqd, domain, GOOD);
bad = kyber_lat_percentage(kqd, domain, BAD);
orig_depth = kqd->domain_tokens[domain].depth;
if (bad > KYBER_LAT_BAD_THRESHOLD) {
/* 지연 초과 비율 높음 → 깊이 감소 (최소 1) */
depth = max(orig_depth - 1, 1U);
} else if (good > KYBER_LAT_GOOD_THRESHOLD) {
/* 지연이 양호 → 깊이 증가 (최대 async_depth) */
depth = min(orig_depth + 1, kqd->async_depth);
}
if (depth != orig_depth)
sbitmap_queue_resize(&kqd->domain_tokens[domain], depth);
}
}
Kyber가 적합한 환경: 하드웨어 큐가 충분하고 자체 스케줄링을 잘 하지만, 소프트웨어 레벨에서 읽기/쓰기 간 간섭을 줄이고 싶을 때 유용합니다. none보다 약간의 오버헤드가 있지만, 혼합 워크로드에서 읽기 tail latency를 크게 개선할 수 있습니다.
Kyber 튜닝 파라미터
| 파라미터 | 경로 (sysfs) | 기본값 | 설명 |
|---|---|---|---|
read_lat_nsec | queue/iosched/read_lat_nsec | 2000000 (2ms) | 읽기 지연 목표. 이 값을 기준으로 동시 I/O 수를 자동 조절 |
write_lat_nsec | queue/iosched/write_lat_nsec | 10000000 (10ms) | 쓰기 지연 목표 |
# Kyber 튜닝 예시
$ echo kyber > /sys/block/nvme0n1/queue/scheduler
# 읽기 지연 목표를 1ms로 강화 (고성능 NVMe)
$ echo 1000000 > /sys/block/nvme0n1/queue/iosched/read_lat_nsec
# 쓰기 지연 목표를 5ms로 강화
$ echo 5000000 > /sys/block/nvme0n1/queue/iosched/write_lat_nsec
none 스케줄러
none은 스케줄러를 완전히 우회하여, per-CPU 소프트웨어 큐의 request를 FIFO 순서 그대로 하드웨어 큐에 전달합니다. 정렬, 병합(기본 병합 제외), 우선순위 조정 어떤 것도 하지 않습니다.
NVMe에서 none 스케줄러를 권장하는 이유: NVMe 컨트롤러는 자체적으로 수천 개의 I/O를 병렬 처리하며, 하드웨어 큐가 충분합니다. 소프트웨어 스케줄러의 추가 오버헤드가 오히려 지연을 증가시킵니다. 반면 HDD는 디스크 헤드의 탐색(seek) 비용이 크므로 mq-deadline으로 요청을 정렬하는 것이 효과적입니다.
none이 최적인 경우:
- NVMe SSD: 하드웨어 큐 다수 보유, 내부 FTL이 최적화 수행
- 가상 디바이스: virtio-blk, Xen blkfront 등 호스트가 이미 스케줄링
- 스택 디바이스: dm, md 등 하위 디바이스에서 스케줄링 수행
- 극한 IOPS: 소프트웨어 오버헤드를 최소화해야 하는 경우
스케줄러 자동 선택과 설정 가이드
Linux 커널은 디바이스 등록 시 elevator_get_default()를 통해 스케줄러를 자동 선택합니다. 선택 기준은 하드웨어 큐 수와 디바이스 유형입니다:
/* block/elevator.c — 기본 스케줄러 선택 로직 (간략화) */
static struct elevator_type *elevator_get_default(struct request_queue *q)
{
/* 단일 HW 큐(예: HDD) → mq-deadline */
if (q->nr_hw_queues == 1)
return elevator_find("mq-deadline");
/* 다중 HW 큐(예: NVMe) 또는 BLK_MQ_F_NO_SCHED → none */
return NULL; /* none */
}
| 디바이스 유형 | HW 큐 수 | 기본 스케줄러 | 권장 스케줄러 | 이유 |
|---|---|---|---|---|
| HDD (SATA/SAS) | 1 | mq-deadline | mq-deadline | seek 비용 높음, 정렬+데드라인 필요 |
| SATA SSD | 1 | mq-deadline | mq-deadline / bfq | 단일 큐이므로 소프트웨어 스케줄링 유효 |
| NVMe SSD | 다수 | none | none / kyber | HW 큐 충분, 소프트웨어 오버헤드 불필요 |
| 데스크톱 (HDD) | 1 | mq-deadline | bfq | 대화형 응답성 중요 |
| virtio-blk | 다수 | none | none | 호스트 측에서 스케줄링 |
| NVMe (혼합 워크로드) | 다수 | none | kyber | 읽기 tail latency 개선 |
영구적 스케줄러 설정
# udev rule로 디바이스 유형별 영구 설정
# /etc/udev/rules.d/60-ioscheduler.rules
# 회전 디스크(HDD) → mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", \
ATTR{queue/scheduler}="mq-deadline"
# 비회전 디스크(SSD) → mq-deadline 또는 none
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", \
ATTR{queue/scheduler}="none"
# NVMe → none
ACTION=="add|change", KERNEL=="nvme[0-9]*n[0-9]*", \
ATTR{queue/scheduler}="none"
# 커널 부트 파라미터로 전역 기본값 변경
# GRUB: GRUB_CMDLINE_LINUX="elevator=bfq"
I/O 우선순위 (ioprio)
Linux는 프로세스별로 I/O 우선순위를 설정할 수 있습니다. I/O 스케줄러(특히 mq-deadline, BFQ)가 이 우선순위를 참고하여 디스패치 순서를 결정합니다.
| 클래스 | 값 | 범위 | 설명 |
|---|---|---|---|
IOPRIO_CLASS_RT | 1 | 0~7 (0=최고) | 실시간. 다른 모든 I/O보다 먼저 처리. root만 설정 가능 |
IOPRIO_CLASS_BE | 2 | 0~7 (0=최고) | Best-effort. 기본값. nice 값 기반으로 자동 매핑 |
IOPRIO_CLASS_IDLE | 3 | N/A | 다른 모든 I/O가 없을 때만 처리. 백그라운드 작업에 적합 |
# ionice 유틸리티로 I/O 우선순위 설정
# 실시간 클래스, 우선순위 0 (최고)
$ ionice -c 1 -n 0 dd if=/dev/sda of=/dev/null bs=1M
# Best-effort 클래스, 우선순위 7 (최저)
$ ionice -c 2 -n 7 rsync -a /src /dst
# Idle 클래스 (다른 I/O 없을 때만 실행)
$ ionice -c 3 updatedb
# 실행 중 프로세스의 I/O 우선순위 변경
$ ionice -c 2 -n 4 -p 1234
# 현재 프로세스의 I/O 우선순위 확인
$ ionice -p $$
best-effort: prio 4
/* 커널 내부: ioprio 시스템 콜 */
#include <linux/ioprio.h>
/* I/O 우선순위 설정 */
syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS, pid,
IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, 4));
/* mq-deadline에서의 우선순위 처리:
* - RT/BE/IDLE 각각 별도의 dd_per_prio 큐 세트를 가짐
* - RT 큐 → BE 큐 → IDLE 큐 순서로 디스패치
* - prio_aging_expire(기본 10초) 경과 시 낮은 우선순위도 서비스 */
스케줄러 변경 및 확인
# 현재 스케줄러 확인 (대괄호 안이 활성 스케줄러)
$ cat /sys/block/sda/queue/scheduler
[mq-deadline] kyber bfq none
# 스케줄러 변경
$ echo bfq > /sys/block/sda/queue/scheduler
# 현재 스케줄러의 튜닝 파라미터 목록
$ ls /sys/block/sda/queue/iosched/
fifo_batch front_merges read_expire write_expire writes_starved
# 읽기 데드라인을 100ms로 설정
$ echo 100 > /sys/block/sda/queue/iosched/read_expire
# Kyber 읽기/쓰기 지연 목표 (나노초)
$ cat /sys/block/nvme0n1/queue/iosched/read_lat_nsec
2000000 # 2ms
# 사용 가능한 스케줄러 커널 모듈 확인
$ ls /lib/modules/$(uname -r)/kernel/block/
bfq-iosched.ko kyber-iosched.ko mq-deadline.ko
Block Device Operations
struct block_device_operations는 블록 디바이스의 파일 연산 콜백을 정의합니다. VFS의 file_operations가 파일에 대한 것이라면, 이 구조체는 디바이스 전체에 대한 연산을 담당합니다.
static const struct block_device_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open, /* 디바이스 열기 */
.release = my_release, /* 디바이스 닫기 */
.ioctl = my_ioctl, /* ioctl 처리 */
.compat_ioctl = my_compat_ioctl, /* 32비트 호환 ioctl */
.getgeo = my_getgeo, /* 디스크 기하 정보 (fdisk용) */
.report_zones = my_report_zones, /* Zoned 디바이스 존 보고 */
};
gendisk 생명주기
/* 1. 디스크 할당 (blk-mq 기반) */
struct gendisk *disk = blk_mq_alloc_disk(tag_set, NULL, NULL);
/* 2. 디스크 속성 설정 */
disk->major = my_major;
disk->first_minor = 0;
disk->minors = 16; /* 파티션 수 */
disk->fops = &my_fops;
set_capacity(disk, nr_sectors); /* 512바이트 섹터 단위 */
/* 3. 시스템에 등록 → /dev/myblk0, /sys/block/myblk0 생성 */
add_disk(disk);
/* 4. 제거 시 */
del_gendisk(disk); /* /dev에서 제거, 진행 중 I/O 완료 대기 */
put_disk(disk); /* 참조 카운트 감소, 최종 해제 */
gendisk와 파티션 관리에 대한 상세 내용은 디스크 파티션 페이지를 참조하세요.
Direct I/O vs Buffered I/O
일반적인 파일 I/O는 Page Cache를 경유하여 커널이 읽기 선행(readahead)과 쓰기 지연(write-back)을 최적화합니다. 반면 Direct I/O(O_DIRECT)는 Page Cache를 우회하여 사용자 버퍼와 디바이스 간 직접 DMA를 수행합니다.
트레이드오프
- Buffered I/O: 읽기 캐싱으로 반복 접근 시 매우 빠름, 쓰기 병합으로 디바이스 효율 극대화. 단, 이중 복사(user ↔ page cache ↔ device)와 예측 불가 지연(dirty writeback) 존재
- Direct I/O: 예측 가능한 지연, 메모리 절약(대용량 데이터). 단, 랜덤 접근 시 캐시 미스, 정렬 요구사항 엄격. 데이터베이스(자체 캐시 보유), 대규모 순차 I/O에 적합
커널 내부 경로
/* fs/direct-io.c — Direct I/O 기본 경로 */
/*
* 파일시스템의 .read_iter / .write_iter에서 IOCB_DIRECT 플래그 감지 시
* iomap 기반 Direct I/O 또는 레거시 __blockdev_direct_IO() 호출
*/
/* iomap 기반 (현대 파일시스템: ext4, XFS, btrfs) */
ssize_t iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
const struct iomap_ops *ops,
const struct iomap_dio_ops *dops,
unsigned int dio_flags);
/* 1. iomap_begin()으로 파일 오프셋 → 블록 매핑 획득
* 2. bio 생성: 사용자 페이지를 직접 bio_vec에 연결 (pin_user_pages)
* 3. submit_bio()로 블록 계층에 제출
* 4. 동기(IOCB_WAITQ) 또는 비동기(IOCB_NOWAIT) 완료 */
정렬 요구사항: Direct I/O는 버퍼 주소, 오프셋, 크기가 모두 디바이스의 logical_block_size(보통 512B 또는 4KB)로 정렬되어야 합니다. 정렬되지 않은 요청은 -EINVAL을 반환합니다. io_uring의 IORING_OP_READ_FIXED는 등록된 버퍼를 사용하여 매번 pin/unpin 오버헤드를 제거합니다.
I/O 배리어와 플러시
전원 손실 시 데이터 무결성을 보장하려면 디바이스의 쓰기 캐시에 있는 데이터가 영구 미디어에 기록되어야 합니다. Linux는 REQ_PREFLUSH와 REQ_FUA 플래그로 이를 제어합니다.
쓰기 순서 보장 플래그
| 플래그 | 동작 | 사용 예 |
|---|---|---|
REQ_PREFLUSH | 이 요청 전에 디바이스 캐시를 플러시 | 저널 커밋 전 데이터 기록 보장 |
REQ_FUA | 이 쓰기를 디바이스 캐시를 거치지 않고 직접 미디어에 기록 | 저널 커밋 블록 자체 |
REQ_PREFLUSH | REQ_FUA | 캐시 플러시 후 FUA 쓰기 (가장 강력한 보장) | 파일시스템 배리어 (fsync) |
| 플래그 없음 | 순서 보장 없음, 디바이스 캐시에 머물 수 있음 | 일반 데이터 쓰기 |
사용 예제
/* 파일시스템 저널 커밋: PREFLUSH + FUA */
struct bio *bio = bio_alloc(bdev, 1,
REQ_OP_WRITE | REQ_PREFLUSH | REQ_FUA,
GFP_NOIO);
bio->bi_iter.bi_sector = journal_sector;
bio_add_page(bio, commit_page, 4096, 0);
bio->bi_end_io = journal_commit_endio;
submit_bio(bio);
/*
* 실행 순서:
* 1. 디바이스 쓰기 캐시 전체 플러시 (PREFLUSH)
* 2. 커밋 블록 쓰기 (FUA: 캐시 무시, 직접 미디어 기록)
* → 전원 손실 시에도 이전 데이터 + 커밋 블록 모두 영구 저장 보장
*/
블록 디바이스 토폴로지
블록 디바이스는 논리/물리 블록 크기, 정렬, 최적 I/O 크기 등의 토폴로지 정보를 커널에 보고합니다. 파일시스템과 블록 계층은 이 정보를 활용하여 I/O를 최적화합니다.
블록 크기
- Logical Block Size: 디바이스가 주소 지정하는 최소 단위 (512B 또는 4KB). 이보다 작은 I/O는 불가
- Physical Block Size: 디바이스 내부의 실제 쓰기 단위. 논리 블록보다 클 수 있음 (512e: logical 512B, physical 4KB)
- Optimal I/O Size: RAID stripe 크기 등 최적 전송 단위
Discard / TRIM / Write Zeroes
SSD의 TRIM (REQ_OP_DISCARD)은 사용하지 않는 블록을 디바이스에 알려 가비지 컬렉션을 돕습니다. REQ_OP_WRITE_ZEROES는 하드웨어가 효율적으로 제로 블록을 기록합니다. 커널은 큐 한도에서 디바이스의 discard 지원 여부와 최대 크기를 확인합니다.
Zoned Block Devices (ZBC/ZAC)
SMR(Shingled Magnetic Recording) HDD와 ZNS(Zoned Namespace) SSD는 순차 쓰기 전용 존을 가집니다. 커널은 blk_revalidate_disk_zones()로 존 정보를 관리하며, 파일시스템(btrfs, f2fs, zonefs)이 존 특성에 맞춰 I/O를 발행합니다.
/* queue 한도 조회 API */
struct request_queue *q = bdev_get_queue(bdev);
unsigned int lb_size = bdev_logical_block_size(bdev); /* 512 or 4096 */
unsigned int pb_size = bdev_physical_block_size(bdev); /* 4096 (4Kn/512e) */
unsigned int io_opt = bdev_io_opt(bdev); /* RAID stripe 등 */
unsigned int max_disc = bdev_max_discard_sectors(bdev); /* 최대 TRIM 크기 */
bool zoned = bdev_is_zoned(bdev); /* Zoned 디바이스? */
4Kn vs 512e: 4Kn(4K native) 디바이스는 논리/물리 블록 모두 4KB입니다. 512e(512-byte emulation)는 논리 블록이 512B이지만 물리 블록은 4KB로, 정렬되지 않은 쓰기 시 Read-Modify-Write 패널티가 발생합니다. lsblk -t로 디바이스의 PHY-SEC/LOG-SEC를 확인할 수 있습니다.
NVMe 심층 분석
NVMe(Non-Volatile Memory Express)는 PCIe 버스에 직접 연결된 고속 저장 장치를 위한 프로토콜입니다. 기존 AHCI/SCSI 스택의 레거시 오버헤드를 제거하고, 수만 개의 병렬 I/O 큐와 마이크로초 단위 지연을 실현합니다. Linux 커널의 NVMe 드라이버는 blk-mq를 최초로 활용한 드라이버 중 하나이며, 현대 스토리지 스택의 핵심 구성 요소입니다.
NVMe 하드웨어 아키텍처
NVMe 컨트롤러는 PCIe 기반 메모리 매핑 I/O(MMIO)를 사용합니다. 호스트와 컨트롤러는 공유 메모리에 위치한 Submission Queue(SQ)와 Completion Queue(CQ)를 통해 커맨드를 교환합니다.
- Submission Queue (SQ): 호스트가 NVMe 커맨드(64바이트)를 기록하는 원형 버퍼. 호스트가 SQ Tail Doorbell을 쓰면 컨트롤러가 커맨드를 페치
- Completion Queue (CQ): 컨트롤러가 완료 엔트리(16바이트)를 기록하는 원형 버퍼. 호스트가 CQ Head Doorbell을 쓰면 엔트리 소비 완료를 알림
- Admin Queue: 컨트롤러 관리 커맨드 전용 (SQ0/CQ0). Identify, Create I/O Queue, Set Features 등
- I/O Queue Pair: 데이터 I/O 전용. 최대 65,535개 큐 쌍, 큐당 최대 65,536개 엔트리 지원
- Doorbell Register: PCIe BAR0에 매핑된 레지스터. SQ Tail / CQ Head 포인터를 업데이트하여 컨트롤러와 동기화
NVMe 커맨드 구조
모든 NVMe 커맨드는 64바이트 고정 크기의 Submission Queue Entry(SQE)입니다. 완료 엔트리(CQE)는 16바이트입니다.
/* NVMe Submission Queue Entry (64 bytes) — 간략화 */
struct nvme_command {
__u8 opcode; /* 커맨드 opcode */
__u8 flags; /* FUSE, PSDT 등 */
__u16 command_id; /* blk-mq tag과 1:1 매핑 */
__le32 nsid; /* 네임스페이스 ID */
__le64 metadata; /* 메타데이터 포인터 */
union nvme_data_ptr dptr; /* PRP 또는 SGL */
union {
struct nvme_rw_command rw; /* Read/Write */
struct nvme_identify identify; /* Identify */
struct nvme_features features; /* Set/Get Features */
struct nvme_create_cq create_cq;
struct nvme_create_sq create_sq;
struct nvme_dsm_cmd dsm; /* Dataset Management (TRIM) */
struct nvme_write_zeroes_cmd write_zeroes;
struct nvme_zone_mgmt_send zms; /* ZNS 존 관리 */
/* ... */
};
};
/* NVMe Completion Queue Entry (16 bytes) */
struct nvme_completion {
__le32 result; /* 커맨드별 결과값 */
__le32 rsvd;
__le16 sq_head; /* SQ Head 포인터 (flow control) */
__le16 sq_id; /* 이 CQE가 속한 SQ ID */
__u16 command_id; /* 완료된 커맨드의 ID */
__le16 status; /* 상태 코드 + Phase Tag */
};
| Admin 커맨드 | Opcode | 설명 |
|---|---|---|
Identify | 0x06 | 컨트롤러/네임스페이스 정보 조회 |
Create I/O CQ | 0x05 | I/O Completion Queue 생성 |
Create I/O SQ | 0x01 | I/O Submission Queue 생성 |
Set Features | 0x09 | 컨트롤러 기능 설정 (큐 수, 인터럽트 합산 등) |
Get Log Page | 0x02 | SMART/Health, Error, FW Slot 정보 조회 |
Format NVM | 0x80 | 네임스페이스 포맷 (LBA 크기 변경 등) |
Namespace Management | 0x0D | 네임스페이스 생성/삭제 |
| I/O 커맨드 | Opcode | 설명 |
|---|---|---|
Read | 0x02 | LBA 범위 읽기 |
Write | 0x01 | LBA 범위 쓰기 |
Flush | 0x00 | 휘발성 캐시 → 비휘발성 미디어 플러시 |
Write Zeroes | 0x08 | LBA 범위를 0으로 초기화 (데이터 전송 없이) |
Dataset Management | 0x09 | TRIM/Deallocate — 사용하지 않는 LBA 알림 |
Compare | 0x05 | LBA 데이터와 호스트 데이터 비교 (atomic 연산) |
Write Uncorrectable | 0x04 | 특정 LBA에 읽기 오류를 발생시키도록 마킹 |
Linux NVMe 드라이버 구조
Linux NVMe 드라이버는 drivers/nvme/ 하위에 세 가지 전송 계층으로 나뉩니다.
| 디렉토리 | 전송 | 설명 |
|---|---|---|
drivers/nvme/host/core.c | 공통 | NVMe 프로토콜 로직, 네임스페이스 관리, 에러 처리 |
drivers/nvme/host/pci.c | PCIe | 로컬 PCIe NVMe 디바이스 드라이버 |
drivers/nvme/host/rdma.c | RDMA | NVMe-oF RDMA 전송 (InfiniBand, RoCE) |
drivers/nvme/host/tcp.c | TCP | NVMe-oF TCP 전송 |
drivers/nvme/host/fc.c | FC | NVMe-oF Fibre Channel 전송 |
drivers/nvme/host/multipath.c | 공통 | 다중 경로(multipath) 지원 |
drivers/nvme/target/ | 타겟 | NVMe-oF 타겟 서브시스템 (스토리지 서버 측) |
/* drivers/nvme/host/pci.c — PCIe NVMe 드라이버 핵심 구조체 */
struct nvme_dev {
struct nvme_ctrl ctrl; /* 공통 컨트롤러 추상화 */
struct pci_dev *pci_dev; /* PCI 디바이스 */
void __iomem *bar; /* BAR0 MMIO 매핑 (Doorbell 포함) */
struct nvme_queue *queues; /* 큐 배열 [0]=admin, [1..n]=I/O */
unsigned int num_vecs; /* MSI-X 인터럽트 벡터 수 */
u32 db_stride; /* Doorbell 레지스터 간격 */
struct dma_pool *prp_page_pool; /* PRP 리스트 DMA 풀 */
struct dma_pool *prp_small_pool; /* 소규모 PRP DMA 풀 */
};
/* 개별 큐 (Admin 또는 I/O) */
struct nvme_queue {
struct nvme_dev *dev;
struct nvme_command *sq_cmds; /* SQ 커맨드 링 버퍼 (DMA) */
struct nvme_completion *cqes; /* CQ 엔트리 링 버퍼 (DMA) */
dma_addr_t sq_dma_addr; /* SQ의 DMA 주소 */
dma_addr_t cq_dma_addr; /* CQ의 DMA 주소 */
u32 __iomem *q_db; /* Doorbell 레지스터 포인터 */
u32 q_depth; /* 큐 깊이 */
u16 sq_tail; /* SQ Tail (호스트 관리) */
u16 cq_head; /* CQ Head (호스트 관리) */
u16 qid; /* 큐 ID (0=admin) */
u8 cq_phase; /* Phase Tag (새 CQE 감지) */
};
NVMe와 blk-mq 매핑
NVMe 드라이버는 blk-mq의 가장 직접적인 사용자입니다. I/O 큐 쌍을 blk-mq HW 큐에 1:1로 매핑하며, blk-mq tag이 NVMe command_id에 직접 대응합니다.
/* drivers/nvme/host/pci.c — blk-mq 콜백 등록 */
static const struct blk_mq_ops nvme_mq_ops = {
.queue_rq = nvme_queue_rq, /* request → NVMe SQE 변환 후 SQ Tail Doorbell 갱신 */
.commit_rqs = nvme_commit_rqs, /* 배치 제출: 마지막에 한 번만 Doorbell 쓰기 */
.complete = nvme_pci_complete_rq, /* CQE → request 완료 처리 */
.map_queues = nvme_pci_map_queues, /* CPU → NVMe I/O Queue 매핑 */
.timeout = nvme_timeout, /* I/O 타임아웃 복구 */
};
/*
* nvme_queue_rq() 핵심 흐름:
* 1. blk-mq request에서 NVMe command 구성 (nvme_setup_cmd)
* 2. 데이터 버퍼의 DMA 주소를 PRP/SGL로 변환 (nvme_map_data)
* 3. SQ Tail에 커맨드 기록
* 4. writel(sq_tail, q_db) — Doorbell로 컨트롤러에 알림
*/
I/O 큐 수 최적화: Linux NVMe 드라이버는 기본적으로 online CPU 수 + 1(admin)개의 큐를 생성합니다. nvme.io_queue_depth(기본 1024)와 nvme.write_queues, nvme.poll_queues 커널 파라미터로 큐 구성을 조정할 수 있습니다. 대부분의 워크로드에서 기본값이 최적이며, 특수 환경(대규모 NUMA, 고빈도 polling)에서만 조정이 필요합니다.
NVMe 네임스페이스
NVMe 컨트롤러는 하나 이상의 네임스페이스(Namespace)를 노출합니다. 각 네임스페이스는 독립적인 블록 디바이스로, 고유한 NSID(Namespace ID), 크기, LBA 포맷을 가집니다. Linux에서는 /dev/nvmeXnY 형태로 나타납니다 (X=컨트롤러, Y=네임스페이스).
| 디바이스 노드 | 의미 |
|---|---|
/dev/nvme0 | 컨트롤러 캐릭터 디바이스 (Admin 커맨드 전용) |
/dev/nvme0n1 | 컨트롤러 0의 네임스페이스 1 (블록 디바이스) |
/dev/nvme0n1p1 | 네임스페이스 1의 파티션 1 |
/dev/nvme1n1 | 컨트롤러 1의 네임스페이스 1 (다른 물리 디바이스 또는 다른 경로) |
/* drivers/nvme/host/core.c — 네임스페이스 등록 */
struct nvme_ns {
struct gendisk *disk; /* /dev/nvmeXnY */
struct nvme_ctrl *ctrl; /* 소속 컨트롤러 */
struct nvme_ns_head *head; /* multipath: 공유 네임스페이스 헤더 */
unsigned ns_id; /* 네임스페이스 ID */
u8 lba_shift; /* log2(LBA 크기) */
u16 ms; /* 메타데이터 크기 */
bool ext; /* 확장 LBA (메타 인라인) */
};
/* Identify Namespace에서 주요 정보 추출 */
/* NSZE: 네임스페이스 총 크기 (LBA 수)
* NCAP: 할당 가능한 용량
* NUSE: 현재 사용 중인 용량
* FLBAS: 활성 LBA 포맷 인덱스
* LBAF[n]: 각 포맷의 LBA 크기 + 메타데이터 크기 + 성능 등급 */
PRP과 SGL (DMA 주소 지정)
NVMe는 호스트 메모리와 컨트롤러 간 데이터 전송을 위해 두 가지 DMA 주소 지정 방식을 지원합니다.
| 방식 | 구조 | 특징 |
|---|---|---|
| PRP (Physical Region Page) | 페이지 정렬 주소의 리스트 | NVMe 1.0부터 지원. 간단하지만 페이지 경계 정렬 필수. 커맨드에 PRP1/PRP2 필드, 추가 엔트리는 PRP List 페이지 |
| SGL (Scatter-Gather List) | 가변 길이 세그먼트 디스크립터 | NVMe 1.1+ 선택 지원. 임의 오프셋/길이 가능. 대규모 전송 효율적. 커널 nvme.use_sgls=1로 활성화 |
/* PRP 엔트리 레이아웃 (각 8바이트 물리 주소) */
/*
* SQE.dptr.prp1 = 첫 번째 데이터 페이지 물리 주소
* SQE.dptr.prp2 = 두 번째 페이지 주소 (단일 페이지면 0)
* 또는 PRP List 페이지 주소 (3페이지 이상)
*
* PRP List 페이지 (4KB):
* [0]: 3번째 데이터 페이지 주소
* [1]: 4번째 데이터 페이지 주소
* ...
* [511]: 마지막 데이터 페이지 또는 다음 PRP List 주소
*/
/* drivers/nvme/host/pci.c — DMA 매핑 */
static blk_status_t nvme_map_data(struct nvme_dev *dev,
struct request *req,
struct nvme_command *cmnd)
{
/* 1. bio의 scatter-gather 목록을 DMA 매핑
* 2. DMA 세그먼트 수에 따라:
* - 1개: PRP1만 설정 (가장 빠른 경로)
* - 2개: PRP1 + PRP2
* - 3+개: PRP1 + PRP List (DMA 풀에서 할당)
* 또는 SGL 사용 시 SGL 디스크립터 체인 구성
*/
}
NVMe Multipath
NVMe multipath는 동일한 네임스페이스에 여러 경로(컨트롤러)로 접근할 때 경로 선택과 장애 전환을 담당합니다. 듀얼 포트 SSD나 NVMe-oF 환경에서 고가용성과 성능 향상을 제공합니다.
# NVMe multipath 구성 확인
$ cat /sys/module/nvme_core/parameters/multipath
Y
# 멀티패스 디바이스 구조
# /dev/nvme0n1 ← 컨트롤러 0 경로 (hidden)
# /dev/nvme1n1 ← 컨트롤러 1 경로 (hidden)
# /dev/nvme0c0n1 ← 통합 multipath 디바이스 (visible)
# 경로 상태 확인
$ nvme list-subsys /dev/nvme0n1
nvme-subsys0 - NQN=nqn.2014-08.org.nvmexpress:...
\
+- nvme0 pcie 0000:03:00.0 live optimized
+- nvme1 pcie 0000:04:00.0 live non-optimized
# 경로 정책 (I/O 분배 방식)
$ cat /sys/class/nvme-subsystem/nvme-subsys0/iopolicy
numa # NUMA 로컬 경로 우선
# 정책 변경: round-robin, numa, queue-depth
$ echo round-robin > /sys/class/nvme-subsystem/nvme-subsys0/iopolicy
| I/O 정책 | 설명 | 적합 환경 |
|---|---|---|
numa | 요청을 발행한 CPU와 같은 NUMA 노드의 컨트롤러 우선 사용 | NUMA 시스템, 로컬 PCIe NVMe |
round-robin | 모든 활성 경로에 순환 분배 | 대칭 경로, NVMe-oF |
queue-depth | 큐 깊이가 가장 낮은 경로 선택 (Linux 6.x+) | 비대칭 대역폭 경로 |
NVMe-oF (NVMe over Fabrics)
NVMe-oF는 NVMe 프로토콜을 네트워크로 확장하여 원격 NVMe 스토리지에 접근합니다. RDMA, TCP, Fibre Channel 전송을 지원하며, 로컬 NVMe와 동일한 커맨드 셋과 의미론을 유지합니다.
# ───── NVMe-oF 타겟 구성 (nvmet) ─────
# 서브시스템 생성
$ mkdir /sys/kernel/config/nvmet/subsystems/my-nvme-target
$ echo 1 > /sys/kernel/config/nvmet/subsystems/my-nvme-target/attr_allow_any_host
# 네임스페이스 추가 (로컬 NVMe를 백엔드로)
$ mkdir /sys/kernel/config/nvmet/subsystems/my-nvme-target/namespaces/1
$ echo /dev/nvme0n1 > /sys/kernel/config/nvmet/subsystems/my-nvme-target/namespaces/1/device_path
$ echo 1 > /sys/kernel/config/nvmet/subsystems/my-nvme-target/namespaces/1/enable
# TCP 포트 바인딩 (RDMA는 trtype=rdma)
$ mkdir /sys/kernel/config/nvmet/ports/1
$ echo ipv4 > /sys/kernel/config/nvmet/ports/1/addr_adrfam
$ echo tcp > /sys/kernel/config/nvmet/ports/1/addr_trtype
$ echo 192.168.1.100 > /sys/kernel/config/nvmet/ports/1/addr_traddr
$ echo 4420 > /sys/kernel/config/nvmet/ports/1/addr_trsvcid
$ ln -s /sys/kernel/config/nvmet/subsystems/my-nvme-target \
/sys/kernel/config/nvmet/ports/1/subsystems/my-nvme-target
# ───── NVMe-oF 호스트 연결 (nvme-cli) ─────
# 원격 타겟 검색
$ nvme discover -t tcp -a 192.168.1.100 -s 4420
# 연결 (로컬 NVMe와 동일하게 /dev/nvmeXnY 생성)
$ nvme connect -t tcp -n my-nvme-target -a 192.168.1.100 -s 4420
# 연결 확인
$ nvme list
Node SN Model Namespace Usage Format FW Rev
/dev/nvme1n1 ... Linux 1 107.37 GB ... 512 B + 0 B 6.8.0
| 전송 | 커널 모듈 | 특징 | 일반 지연 |
|---|---|---|---|
| RDMA (RoCEv2) | nvme-rdma | 커널 바이패스, 제로카피 DMA | ~10-30µs |
| TCP | nvme-tcp | 표준 이더넷, 특수 하드웨어 불필요 | ~50-200µs |
| Fibre Channel | nvme-fc | 기존 FC SAN 인프라 활용 | ~20-50µs |
ZNS (Zoned Namespaces)
ZNS는 NVMe 네임스페이스를 순차 쓰기 전용 존으로 분할합니다. 호스트가 데이터 배치를 제어하여 SSD 내부 GC(Garbage Collection)를 최소화하고, 쓰기 증폭(Write Amplification)을 줄여 수명과 성능을 개선합니다.
# ZNS 디바이스 존 정보 확인
$ nvme zns report-zones /dev/nvme0n1 -d 0 -s 0
nr_zones: 256
SLBA: 0x000000 WP: 0x000800 Cap: 0x000800 State: 0x20 (Implicitly Opened) Type: 0x2 (Sequential Write Required)
SLBA: 0x000800 WP: 0x000800 Cap: 0x000800 State: 0x10 (Empty) Type: 0x2
# 존 관리 커맨드
$ nvme zns zone-mgmt-send /dev/nvme0n1 -s 0 -a 1 # Open Zone
$ nvme zns zone-mgmt-send /dev/nvme0n1 -s 0 -a 2 # Close Zone
$ nvme zns zone-mgmt-send /dev/nvme0n1 -s 0 -a 4 # Reset Zone (데이터 삭제)
ZNS를 지원하는 파일시스템: btrfs(네이티브 존 지원), f2fs(순차 쓰기 최적화), zonefs(존을 파일로 노출). dm-zoned는 존 디바이스 위에 일반 블록 디바이스 인터페이스를 제공합니다.
NVMe 에러 처리와 복구
NVMe 드라이버는 다단계 에러 복구 메커니즘을 구현합니다. CQE의 status 필드로 커맨드 수준 오류를 감지하고, 컨트롤러 리셋으로 심각한 장애를 복구합니다.
| 복구 수준 | 트리거 | 동작 |
|---|---|---|
| 커맨드 재시도 | CQE 상태가 재시도 가능한 오류 (DNR 비트 미설정) | blk-mq retry로 재제출, 최대 횟수 후 I/O 에러 반환 |
| I/O 타임아웃 | blk-mq 타임아웃 (기본 30초) | nvme_timeout()에서 abort 커맨드 시도 → 실패 시 컨트롤러 리셋 |
| 컨트롤러 리셋 | 타임아웃 복구 실패, CSTS.CFS 설정 | 모든 I/O 큐 비활성화 → 컨트롤러 CC.EN=0 → 재초기화 → 큐 재생성 → 대기 중 I/O 재제출 |
| 컨트롤러 제거 | PCIe 링크 다운, 리셋 반복 실패 | 디바이스 오프라인, 모든 대기 I/O에 에러 반환 |
/* CQE 상태 코드 분류 */
/* Status Code Type (SCT) */
#define NVME_SCT_GENERIC 0x0 /* 범용 에러 */
#define NVME_SCT_CMD_SPECIFIC 0x1 /* 커맨드별 에러 */
#define NVME_SCT_MEDIA 0x2 /* 미디어/데이터 무결성 에러 */
#define NVME_SCT_PATH 0x3 /* 경로 관련 에러 (multipath) */
/* Status Code (SC) 주요 값 */
#define NVME_SC_SUCCESS 0x0
#define NVME_SC_INVALID_OPCODE 0x1
#define NVME_SC_INVALID_FIELD 0x2
#define NVME_SC_DATA_XFER_ERROR 0x4
#define NVME_SC_INTERNAL 0x6 /* 컨트롤러 내부 오류 */
#define NVME_SC_ABORT_REQ 0x7
#define NVME_SC_NS_NOT_READY 0x82 /* 네임스페이스 미준비 */
#define NVME_SC_UNWRITTEN_BLOCK 0x87 /* ZNS: 쓰지 않은 블록 읽기 */
/* DNR (Do Not Retry) 비트: 1이면 재시도 무의미 */
#define NVME_SC_DNR 0x4000
nvme-cli 관리 도구
nvme-cli는 NVMe 디바이스의 사용자 공간 관리 도구입니다. /dev/nvmeX 캐릭터 디바이스를 통해 Admin/I/O 커맨드를 직접 발행합니다.
# 시스템의 모든 NVMe 디바이스 목록
$ nvme list
Node SN Model Namespace Usage Format FW Rev
/dev/nvme0n1 S5GXNG0R401234 Samsung 990 PRO 2TB 1 1.02 TB / 2.00 TB 512 B + 0 B 4B2QJXD7
# 컨트롤러 상세 정보 (Identify Controller)
$ nvme id-ctrl /dev/nvme0 | head -20
NVME Identify Controller:
vid : 0x144d # Samsung
ssvid : 0x144d
sn : S5GXNG0R401234
mn : Samsung SSD 990 PRO 2TB
fr : 4B2QJXD7
mdts : 9 # Max Data Transfer Size = 2^9 * MPSMIN = 512 pages
cntlid : 0x6
nn : 1 # Number of Namespaces
sqes : 0x66 # SQ Entry Size: min=64, max=64
cqes : 0x44 # CQ Entry Size: min=16, max=16
# 네임스페이스 상세 정보 (Identify Namespace)
$ nvme id-ns /dev/nvme0n1
NVME Identify Namespace 1:
nsze : 3907029168 # 네임스페이스 크기 (LBA 수)
ncap : 3907029168 # 할당 가능 용량
nuse : 1996488704 # 현재 사용량
flbas : 0 # 활성 LBA 포맷 인덱스
lbaf 0 : ms:0 lbads:9 rp:0 # LBA=512B, 메타=0
lbaf 1 : ms:0 lbads:12 rp:0 # LBA=4096B, 메타=0
# SMART / Health 로그
$ nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning : 0
temperature : 42 C
available_spare : 100%
available_spare_threshold : 10%
percentage_used : 3%
data_units_read : 15,234,567
data_units_written : 12,345,678
host_read_commands : 234,567,890
host_write_commands : 123,456,789
power_cycles : 156
power_on_hours : 8,760
unsafe_shutdowns : 3
media_errors : 0
# LBA 포맷 변경 (4Kn으로) — 주의: 데이터 삭제됨
$ nvme format /dev/nvme0n1 -l 1 -f # lbaf 1 = 4096B
# Firmware 업데이트
$ nvme fw-download /dev/nvme0 -f firmware.bin
$ nvme fw-activate /dev/nvme0 -s 1 -a 1
NVMe 성능 튜닝
| 파라미터 | 경로 / 커맨드 | 설명 |
|---|---|---|
| I/O 스케줄러 | echo none > /sys/block/nvme0n1/queue/scheduler | NVMe는 none이 기본 최적값 |
| 큐 깊이 | nvme.io_queue_depth=1024 (커널 파라미터) | 큐당 최대 in-flight I/O. 기본 1024 |
| Polling 큐 | nvme.poll_queues=4 | IRQ 대신 busy-poll 사용하는 큐 수. 초저지연 워크로드 |
| 쓰기 전용 큐 | nvme.write_queues=2 | 읽기/쓰기 큐 분리. 혼합 워크로드 격리 |
| Readahead | blockdev --setra 256 /dev/nvme0n1 | 순차 읽기 선행 크기 (KB). NVMe는 낮은 값이 유리할 수 있음 |
| nr_requests | echo 1024 > /sys/block/nvme0n1/queue/nr_requests | 스케줄러당 최대 request 수 |
| IRQ 친화성 | /proc/irq/<N>/smp_affinity_list | NVMe IRQ를 특정 CPU에 고정 (보통 자동 최적) |
| APST | nvme set-feature /dev/nvme0 -f 0x0c -v 0 | Autonomous Power State Transition 비활성화 (지연 민감 환경) |
# ───── fio를 이용한 NVMe 벤치마크 예제 ─────
# 4K 랜덤 읽기 (IOPS 측정)
$ fio --name=nvme-randread \
--filename=/dev/nvme0n1 \
--ioengine=io_uring \
--direct=1 \
--bs=4k \
--rw=randread \
--iodepth=128 \
--numjobs=4 \
--runtime=30 \
--group_reporting
# 4K 랜덤 쓰기 (io_uring + polling)
$ fio --name=nvme-randwrite \
--filename=/dev/nvme0n1 \
--ioengine=io_uring \
--hipri=1 \
--direct=1 \
--bs=4k \
--rw=randwrite \
--iodepth=64 \
--numjobs=4 \
--runtime=30 \
--group_reporting
# 순차 읽기 대역폭 (128K)
$ fio --name=nvme-seqread \
--filename=/dev/nvme0n1 \
--ioengine=io_uring \
--direct=1 \
--bs=128k \
--rw=read \
--iodepth=32 \
--numjobs=1 \
--runtime=30
NVMe 과열 스로틀링: NVMe SSD는 지속적인 고부하 시 TCTTEMP(Composite Temperature)가 임계값을 초과하면 자동으로 성능을 제한합니다. nvme smart-log의 temperature와 critical_warning 비트 1을 모니터링하세요. 서버 환경에서는 적절한 에어플로우와 NVMe 히트싱크가 필수입니다.
블록 I/O 디버깅
블록 I/O 문제 진단을 위한 커널 도구와 사용법을 정리합니다.
blktrace / blkparse
blktrace는 블록 계층의 이벤트를 캡처하고, blkparse가 이를 사람이 읽을 수 있는 형태로 출력합니다.
# blktrace 시작 (10초간 /dev/sda 추적)
$ blktrace -d /dev/sda -w 10 -o trace
# 결과 파싱
$ blkparse -i trace -d trace.bin
# 출력 예시:
# 8,0 0 1 0.000000000 1234 Q WS 2048 + 8 [dd] ← Queue
# 8,0 0 2 0.000001234 1234 G WS 2048 + 8 [dd] ← Get request
# 8,0 0 3 0.000002345 1234 I WS 2048 + 8 [dd] ← Insert to scheduler
# 8,0 0 4 0.000003456 1234 D WS 2048 + 8 [dd] ← Dispatch to driver
# 8,0 0 5 0.000100000 0 C WS 2048 + 8 [0] ← Complete
# btt(Block Trace Timeline)로 통계 분석
$ btt -i trace.bin
# Q2C (전체 지연), D2C (디바이스 지연), Q2D (스케줄러 지연) 등
/sys/block/*/stat 통계
# 블록 디바이스 I/O 통계
$ cat /sys/block/sda/stat
# read_ios read_merges read_sectors read_ticks
# write_ios write_merges write_sectors write_ticks
# in_flight io_ticks time_in_queue
# discard_ios discard_merges discard_sectors discard_ticks
# flush_ios flush_ticks
# 더 읽기 쉬운 형식:
$ iostat -x 1
# r/s, w/s, rkB/s, wkB/s, await, %util 등
blk-mq debugfs
# blk-mq 내부 상태 확인
$ ls /sys/kernel/debug/block/sda/
hctx0/ hctx1/ state requeue_list ...
# 하드웨어 큐 0의 상태
$ cat /sys/kernel/debug/block/sda/hctx0/tags
# nr_tags, nr_reserved_tags, active_queues 등
$ cat /sys/kernel/debug/block/sda/hctx0/dispatch
# 디스패치 대기 중인 request 목록
$ cat /sys/kernel/debug/block/sda/hctx0/cpu_list
# 이 HW 큐에 매핑된 CPU 목록
BPF 기반 도구
# biolatency: bio 지연 분포 히스토그램
$ biolatency -D
# disk = sda
# usecs : count distribution
# 0 -> 1 : 0 | |
# 2 -> 3 : 0 | |
# 4 -> 7 : 12 |** |
# 8 -> 15 : 156 |**********************|
# 16 -> 31 : 89 |************ |
# biosnoop: 개별 bio 이벤트 추적
$ biosnoop
# TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
# 0.000 dd 1234 sda W 2048 4096 0.12
# biotop: 프로세스별 블록 I/O 사용량 (top 형식)
$ biotop
# PID COMM D MAJ MIN DISK I/O Kbytes AVGms
# 1234 postgres R 8 0 sda 45 180 0.85