Block I/O 서브시스템

Linux 커널의 블록 I/O 계층을 심층 분석합니다. VFS/Page Cache와 디바이스 드라이버 사이에서 동작하는 bio 구조체, blk-mq 멀티큐 아키텍처, I/O 스케줄러, Direct I/O, I/O 배리어, 블록 디바이스 토폴로지, 디버깅 기법을 다룹니다.

관련 표준: NVMe Specification 2.0 (NVMe 인터페이스), SCSI SAM-6 (SCSI 아키텍처 모델), ATA/ACS-4 (ATA 명령 세트) — 블록 I/O 계층이 지원하는 스토리지 인터페이스 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 메모리 관리(페이지 캐시 개념)와 디바이스 드라이버(디바이스 모델)를 먼저 읽으세요.
일상 비유: Block I/O 계층은 물류 센터의 주문 처리 시스템과 같습니다. bio는 개별 주문서(어떤 데이터를 어디서 읽을지/쓸지), request는 같은 방향의 주문을 묶은 배송 묶음, I/O 스케줄러는 배송 순서를 최적화하는 물류 관리자, blk-mq는 여러 배송 트럭(하드웨어 큐)에 작업을 분배하는 시스템입니다.

핵심 요약

  • 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 — 페이지 캐시를 우회하여 디바이스에 직접 읽기/쓰기합니다.

단계별 이해

  1. I/O 경로 파악 — 애플리케이션 → VFS → 페이지 캐시 → Block Layer → 디바이스 드라이버 → 하드웨어.

    Block Layer는 VFS와 드라이버 사이에서 I/O 요청을 관리합니다.

  2. bio 이해 — 파일시스템이 submit_bio()로 bio를 블록 계층에 제출합니다.

    각 bio는 디스크의 시작 섹터, 크기, 방향(읽기/쓰기), 메모리 페이지를 포함합니다.

  3. blk-mq 이해 — 하드웨어 큐와 소프트웨어 큐의 매핑을 관리합니다.

    cat /sys/block/nvme0n1/queue/nr_requests로 큐 깊이를 확인합니다.

  4. 모니터링iostat -x 1로 디바이스별 I/O 통계를 실시간 모니터링합니다.

    blktrace/blkparse로 I/O 요청의 상세 흐름을 추적할 수 있습니다.

Block I/O 계층 개요

블록 I/O 계층(Block Layer)은 VFS/Page Cache와 물리 디바이스 드라이버 사이에 위치하며, 디스크, SSD, NVMe 등 블록 디바이스에 대한 I/O 요청을 관리합니다. 상위 계층이 논리적 블록 단위로 데이터를 요청하면, 블록 계층이 이를 최적화하여 하드웨어에 전달합니다.

블록 계층의 역할

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를 사용합니다.

VFS / Page Cache Block Layer struct bio I/O Scheduler blk-mq SW Queue (CPU0) SW Queue (CPU1) SW Queue (CPUn) HW Queue 0 HW Queue 1 Device Driver / HW

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_READ0블록 읽기
REQ_OP_WRITE1블록 쓰기
REQ_OP_FLUSH2디바이스 캐시 플러시
REQ_OP_DISCARD3블록 무효화 (TRIM)
REQ_OP_SECURE_ERASE5보안 삭제
REQ_OP_WRITE_ZEROES9제로 블록 쓰기 (하드웨어 최적화)

추가 플래그는 비트 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 bio bi_bdev → /dev/sda bi_opf = REQ_OP_WRITE bi_iter.bi_sector = 2048 bi_iter.bi_size = 12288 bi_vcnt = 3 bi_end_io → callback bi_io_vec → bio_vec[0] page=P1 len=4096 off=0 bio_vec[1] page=P2 len=4096 off=0 bio_vec[2] page=P3 len=4096 off=0 Page Frame (P1) Page Frame (P2) Page Frame (P3) → 디스크 섹터 2048~2071 (12KB, 연속 기록)

struct request와 request_queue

struct request는 하나 이상의 bio를 병합한 I/O 단위로, 실제 디바이스 드라이버에 전달되는 작업 단위입니다. 블록 계층은 인접한 bio들을 자동으로 병합하여 디바이스 호출 횟수를 줄입니다.

요청 병합 (Merge)

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_segmentsscatter-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() 흐름

  1. submit_bio(bio) → 제출 회계(accounting) 시작
  2. submit_bio_noacct(bio) → 스택 디바이스(dm, md) 재진입 시 직접 호출
  3. __submit_bio() → 드라이버의 submit_bio 콜백 또는 기본 blk_mq_submit_bio()
  4. bio가 큐 한도(max_sectors 등)를 초과하면 bio_split()으로 분할 후 재제출
  5. plug된 상태면 blk_mq_plug_issue_direct() / blk_add_rq_to_plug()으로 보류
  6. unplug 시 또는 직접 디스패치 시 하드웨어 큐로 전달
submit_bio(bio) submit_bio_noacct(bio) bio > limit? Yes bio_split() No blk_mq_submit_bio() plugged? Yes plug 리스트 No HW Queue 전송

스택 디바이스 (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를 달성할 수 있게 합니다.

아키텍처

CPU 0 CPU 1 CPU 2 CPU 3 CPU n SW Queue 0 SW Queue 1 SW Queue 2 SW Queue 3 SW Queue n I/O Scheduler (optional: mq-deadline, BFQ, Kyber, none) HW Queue 0 HW Queue 1 NVMe / SCSI / virtio-blk Device

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_MERGErequest 병합 활성화 (대부분의 디바이스)
BLK_MQ_F_TAG_QUEUE_SHARED여러 장치가 tag set 공유 (SCSI HBA 등)
BLK_MQ_F_BLOCKINGqueue_rq에서 sleep 허용 (스케줄링 기반 동기 I/O)
BLK_MQ_F_NO_SCHEDI/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, 가상 디바이스없음디바이스 자체 스케줄링에 위임
SW Queues (per-CPU) I/O Scheduler (elevator) mq-deadline sorted + FIFO 이중 큐 bfq budget 기반 공정 큐잉 kyber 토큰 기반 latency 목표 none FIFO 직행 (bypass) dispatch → HW Queue HW Queues hctx[0] hctx[1] hctx[2] ... hctx[N] per-CPU SW 큐 → I/O 스케줄러(elevator) → 하드웨어 디스패치 큐

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를 선택합니다:

  1. 만료 확인: 읽기 FIFO 큐의 head가 데드라인을 초과했으면 즉시 디스패치 (읽기 우선)
  2. 쓰기 기아 방지: 읽기가 writes_starved회 연속 선택되면 쓰기를 강제 디스패치
  3. 쓰기 만료 확인: 쓰기 FIFO 큐의 head가 데드라인을 초과했으면 디스패치
  4. 정렬 순서: 만료된 요청이 없으면, 현재 방향의 정렬 큐에서 다음 섹터를 선택 (seek 최소화)
  5. 배치 제한: 한 방향으로 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_expirequeue/iosched/read_expire500 (ms)읽기 요청 최대 지연. 이 시간 내에 반드시 디스패치
write_expirequeue/iosched/write_expire5000 (ms)쓰기 요청 최대 지연
fifo_batchqueue/iosched/fifo_batch16한 방향(읽기 또는 쓰기)에서 연속 디스패치 최대 수. 값이 크면 throughput 증가, seek이 많아질 수 있음
writes_starvedqueue/iosched/writes_starved2쓰기 양보 횟수. 읽기가 이 횟수만큼 연속 선택되면 쓰기를 강제
front_mergesqueue/iosched/front_merges1앞쪽 병합(front merge) 허용. HDD에서는 1, 일부 SSD에서는 0이 유리
prio_aging_expirequeue/iosched/prio_aging_expire10000 (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)이 할당됩니다. 예산은 해당 큐가 한 번에 디스패치할 수 있는 최대 섹터 수입니다.

  1. 예산 할당: 큐가 활성화되면 디바이스의 추정 처리율과 가중치에 비례하여 예산을 받음
  2. 독점 서비스: 예산이 있는 동안 해당 큐가 디스패치를 독점 (idling 포함)
  3. 예산 소진: 예산을 모두 사용하거나 유휴 타임아웃 발생 시 다음 큐로 전환
  4. 가상 시간: 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) 동안 대기합니다. 이는 두 가지 목적이 있습니다:

BFQ의 오버헤드: 프로세스별 큐 관리, weight raising 추적, B-WF²Q+ 가상 시간 계산 등으로 인해 고속 NVMe 디바이스에서는 CPU 오버헤드가 병목이 될 수 있습니다. NVMe에서 100만 IOPS 이상을 처리해야 하는 경우 none 또는 kyber를 권장합니다.

BFQ 튜닝 파라미터

파라미터경로 (sysfs)기본값설명
slice_idlequeue/iosched/slice_idle8 (ms)큐 비어도 대기하는 시간. 0이면 idling 비활성. SSD에서는 0이 유리할 수 있음
slice_idle_usqueue/iosched/slice_idle_us8000 (us)slice_idle의 마이크로초 버전 (정밀 제어)
low_latencyqueue/iosched/low_latency1대화형 감지(weight raising) 활성화. 0이면 순수 공정 큐잉만 사용
timeout_syncqueue/iosched/timeout_sync124 (ms)동기 큐의 서비스 타임아웃
max_budgetqueue/iosched/max_budget0 (자동)큐당 최대 예산. 0이면 디바이스 처리율 기반 자동 계산
strict_guaranteesqueue/iosched/strict_guarantees01이면 엄격한 대역폭 보장 (추가 오버헤드 발생)
# 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가 대기 큐에서 기다립니다.

  1. 토큰 획득: request가 디스패치되려면 해당 도메인(읽기/쓰기)의 토큰을 획득해야 함
  2. 지연 모니터링: 완료된 request의 실제 지연을 per-CPU 히스토그램에 기록
  3. 토큰 조절: 주기적으로(timer callback) 히스토그램을 분석하여 목표 지연을 초과한 비율이 높으면 토큰 수를 감소, 낮으면 증가
  4. 도메인 분리: 읽기와 쓰기의 토큰 풀이 독립이므로, 대량 쓰기가 읽기 지연에 미치는 영향을 최소화
/* 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_nsecqueue/iosched/read_lat_nsec2000000 (2ms)읽기 지연 목표. 이 값을 기준으로 동시 I/O 수를 자동 조절
write_lat_nsecqueue/iosched/write_lat_nsec10000000 (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이 최적인 경우:

스케줄러 자동 선택과 설정 가이드

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)1mq-deadlinemq-deadlineseek 비용 높음, 정렬+데드라인 필요
SATA SSD1mq-deadlinemq-deadline / bfq단일 큐이므로 소프트웨어 스케줄링 유효
NVMe SSD다수nonenone / kyberHW 큐 충분, 소프트웨어 오버헤드 불필요
데스크톱 (HDD)1mq-deadlinebfq대화형 응답성 중요
virtio-blk다수nonenone호스트 측에서 스케줄링
NVMe (혼합 워크로드)다수nonekyber읽기 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_RT10~7 (0=최고)실시간. 다른 모든 I/O보다 먼저 처리. root만 설정 가능
IOPRIO_CLASS_BE20~7 (0=최고)Best-effort. 기본값. nice 값 기반으로 자동 매핑
IOPRIO_CLASS_IDLE3N/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를 수행합니다.

트레이드오프

커널 내부 경로

/* 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_uringIORING_OP_READ_FIXED는 등록된 버퍼를 사용하여 매번 pin/unpin 오버헤드를 제거합니다.

I/O 배리어와 플러시

전원 손실 시 데이터 무결성을 보장하려면 디바이스의 쓰기 캐시에 있는 데이터가 영구 미디어에 기록되어야 합니다. Linux는 REQ_PREFLUSHREQ_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: 캐시 무시, 직접 미디어 기록)
 * → 전원 손실 시에도 이전 데이터 + 커밋 블록 모두 영구 저장 보장
 */
Data Writes (캐시에 존재) PREFLUSH 캐시 → 미디어 FUA Write 직접 미디어 기록 Complete 데이터가 캐시에 축적 캐시 전체를 영구 기록 커밋 블록을 미디어에 직접 기록 완료 알림

블록 디바이스 토폴로지

블록 디바이스는 논리/물리 블록 크기, 정렬, 최적 I/O 크기 등의 토폴로지 정보를 커널에 보고합니다. 파일시스템과 블록 계층은 이 정보를 활용하여 I/O를 최적화합니다.

블록 크기

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)를 통해 커맨드를 교환합니다.

Host (CPU / Memory) Admin SQ (SQ0) Admin CQ (CQ0) I/O SQ 1 I/O CQ 1 I/O SQ 2 I/O CQ 2 ⋮ (per-CPU 큐 쌍) I/O SQ n I/O CQ n PRP / SGL (DMA 주소 목록) NVMe Controller Doorbell Registers (BAR0) SQ Tail DB × N | CQ Head DB × N Command Arbitration FTL (Flash Translation Layer) NAND Channel 0 NAND Channel n DRAM Cache / HMB MMIO Doorbell Write DMA CQ Entry + MSI-X

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설명
Identify0x06컨트롤러/네임스페이스 정보 조회
Create I/O CQ0x05I/O Completion Queue 생성
Create I/O SQ0x01I/O Submission Queue 생성
Set Features0x09컨트롤러 기능 설정 (큐 수, 인터럽트 합산 등)
Get Log Page0x02SMART/Health, Error, FW Slot 정보 조회
Format NVM0x80네임스페이스 포맷 (LBA 크기 변경 등)
Namespace Management0x0D네임스페이스 생성/삭제
I/O 커맨드Opcode설명
Read0x02LBA 범위 읽기
Write0x01LBA 범위 쓰기
Flush0x00휘발성 캐시 → 비휘발성 미디어 플러시
Write Zeroes0x08LBA 범위를 0으로 초기화 (데이터 전송 없이)
Dataset Management0x09TRIM/Deallocate — 사용하지 않는 LBA 알림
Compare0x05LBA 데이터와 호스트 데이터 비교 (atomic 연산)
Write Uncorrectable0x04특정 LBA에 읽기 오류를 발생시키도록 마킹

Linux NVMe 드라이버 구조

Linux NVMe 드라이버는 drivers/nvme/ 하위에 세 가지 전송 계층으로 나뉩니다.

디렉토리전송설명
drivers/nvme/host/core.c공통NVMe 프로토콜 로직, 네임스페이스 관리, 에러 처리
drivers/nvme/host/pci.cPCIe로컬 PCIe NVMe 디바이스 드라이버
drivers/nvme/host/rdma.cRDMANVMe-oF RDMA 전송 (InfiniBand, RoCE)
drivers/nvme/host/tcp.cTCPNVMe-oF TCP 전송
drivers/nvme/host/fc.cFCNVMe-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로 컨트롤러에 알림
 */
blk-mq Layer HW Queue 0 HW Queue 1 HW Queue n blk-mq tag = NVMe command_id 1:1 매핑 NVMe Queue Pairs SQ1 / CQ1 SQ2 / CQ2 SQn / CQn MSI-X 인터럽트 벡터 per-queue IRQ Affinity CPU n → MSI-X n → CQ n → HW Queue n Polling Queues (io_poll) IRQ 비활성, busy-poll로 CQ 직접 확인
💡

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
TCPnvme-tcp표준 이더넷, 특수 하드웨어 불필요~50-200µs
Fibre Channelnvme-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/schedulerNVMe는 none이 기본 최적값
큐 깊이nvme.io_queue_depth=1024 (커널 파라미터)큐당 최대 in-flight I/O. 기본 1024
Polling 큐nvme.poll_queues=4IRQ 대신 busy-poll 사용하는 큐 수. 초저지연 워크로드
쓰기 전용 큐nvme.write_queues=2읽기/쓰기 큐 분리. 혼합 워크로드 격리
Readaheadblockdev --setra 256 /dev/nvme0n1순차 읽기 선행 크기 (KB). NVMe는 낮은 값이 유리할 수 있음
nr_requestsecho 1024 > /sys/block/nvme0n1/queue/nr_requests스케줄러당 최대 request 수
IRQ 친화성/proc/irq/<N>/smp_affinity_listNVMe IRQ를 특정 CPU에 고정 (보통 자동 최적)
APSTnvme set-feature /dev/nvme0 -f 0x0c -v 0Autonomous 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-logtemperaturecritical_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