리눅스 커널 - NVMe PCI Driver Basic
Linux NVMe PCI 드라이버 상세 분석
Linux 커널의 NVMe PCI 드라이버는 PCIe를 통해 NVMe 스토리지 디바이스와 통신하는 핵심 모듈입니다.
용어설명
- iod = I/O Descriptor: 각 I/O 명령마다 필요한 메타데이터 구조체
- cdev: 캐릭터 디바이스 -> /dev/nvme0이 캐릭터 디바이스 (컨트롤러 단위의 admin/passthru 명령을 ioctl (입출력제어를 위한 시스템콜, 장치 설정 변경/조회에 사용됨) 형태로 보내는데 사용됨), /dev/nvme0n1은 블록 디바이스로, read/write I/O가 이 블록 디바이스 경로를 통해 처리된다.
NVMe PCI 드라이버의 주요 구조와 함수들을 다음과 같이 분류할 수 있습니다.
1. 드라이버 초기화 및 프로브(Probe)
드라이버의 진입점은 다음과 같이 정의됩니다:
cstatic struct pci_driver nvme_driver = { .name = "nvme", .id_table = nvme_id_table, .probe = nvme_probe, .remove = nvme_remove, .shutdown = nvme_shutdown, .driver = { .pm = &nvme_dev_pm_ops, }, .err_handler = &nvme_err_handler, };
주요 함수들:
- nvme_probe(): 새로운 NVMe 디바이스가 시스템에 연결될 때 호출됩니다. 이 함수는 디바이스를 인식하고 드라이버를 해당 디바이스에 바인딩합니다.
- BAR(Base Address Register) 매핑
- 컨트롤러 리소스 할당
- Admin 큐(Queue) 초기화
- I/O 큐 설정
- PRP Pool 설정 - nvme_setup_prp_pools(dev);
nvme_remove(): 디바이스가 제거될 때 호출되어 할당된 리소스를 정리합니다.
nvme_shutdown(): 시스템 종료 시 디바이스를 조용히 정지시킵니다.
2. 핵심 데이터 구조
cstruct nvme_dev { struct pci_dev *pdev; struct nvme_ctrl ctrl; // BAR과 레지스터 접근 void __iomem *bar; struct nvme_ctrl_regs __iomem *regs; // 큐 관리 struct nvme_queue **queues; unsigned int online_queues; // 중단 처리 struct work_struct reset_work; struct work_struct remove_work; };
cstruct nvme_queue { struct nvme_dev *dev; spinlock_t sq_lock; // Submission Queue (제출 큐) void *sq_cmds; // 명령어를 저장할 버퍼 volatile u16 *sq_tail; // Tail Doorbell 레지스터 // Completion Queue (완료 큐) struct nvme_completion *cqes; volatile u16 *cq_head; // Head Doorbell 레지스터 u16 q_depth; // 큐 크기 u16 cq_phase; // Phase 비트 (CQE 유효성 확인용) u16 cq_head; // 현재 완료 큐 헤드 포인터 u16 sq_tail; // 현재 제출 큐 테일 포인터 };
3. 큐 초기화 및 관리
NVMe 컨트롤러는 Admin 큐(Queue ID 0)부터 시작합니다. Admin 큐는 컨트롤러 설정 및 관리 명령어를 처리합니다:
c// CAP 레지스터에서 큐 크기 읽기 u16 mqes = NVME_CAP_MQES(cap); // Admin 제출 큐 주소 설정 (레지스터 0x28) nvme_write_reg(0x28, asq_addr); // Admin 완료 큐 주소 설정 (레지스터 0x30) nvme_write_reg(0x30, acq_addr);
nvme_setup_io_queues() 함수는 다음 순서로 동작합니다:
- 필요한 I/O 큐 개수 결정
- MSI-X/MSI 인터럽트 할당
- 각 I/O 큐에 대해 Create I/O Submission/Completion Queue 명령어 전송
- blk-mq(Block Multi-Queue) 레이어와 통합
4. 명령 제출(Command Submission) 경로
c// 1. 제출 큐의 현재 위치에 명령어 복사 memcpy(&sq_cmds[sq_tail], cmd, sizeof(*cmd)); // 2. Submission Queue Tail Doorbell 레지스터 업데이트 // 레지스터 주소: 0x1000 + (2 * QID) * Stride nvme_write_sq_db(nvmeq, false);
이 함수는 spin_lock으로 보호되며, 원자성을 보장합니다.
nvme_queue_rq() - 블록 레이어에서 호출
cstatic blk_status_t nvme_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd) { struct nvme_queue *nvmeq = hctx->driver_data; struct request *req = bd->rq; // 1. 데이터 매핑 (PRP 리스트 생성) ret = nvme_map_data(dev, req, &iod->cmd); // 2. 명령어 설정 nvme_setup_cmd(nvmeq, req); // 3. 제출 큐에 명령어 복사 nvme_submit_cmd(nvmeq, &cmnd, bd->last); return BLK_STS_OK; }
5. 완료 처리(Completion Handling)
cstatic irqreturn_t nvme_irq(int irq, void *data) { struct nvme_queue *nvmeq = data; struct io_comp_batch iob; if (nvme_poll_cq(nvmeq, &iob)) { if (!rq_list_empty(iob.req_list)) nvme_pci_complete_batch(&iob); return IRQ_HANDLED; } return IRQ_NONE; }
cstatic inline void __nvme_process_cq(struct nvme_queue *nvmeq, struct io_comp_batch *iob) { while (nvme_cqe_pending(nvmeq)) { // 1. Phase 비트 확인으로 유효한 CQE인지 검증 // Phase 비트는 컨트롤러가 CQE를 작성할 때마다 토글됨 // 2. 메모리 배리어 (DMA 일관성 보장) dma_rmb(); // 3. CQE 처리 nvme_handle_cqe(nvmeq, iob, nvmeq->cq_head); // 4. 완료 큐 헤드 포인터 업데이트 nvme_update_cq_head(nvmeq); } // 5. Completion Queue Head Doorbell 업데이트 nvme_ring_cq_doorbell(nvmeq); }
Phase 비트는 완료 큐 엔트리의 유효성을 판단하는 중요한 메커니즘입니다:
드라이버는 cq_phase 변수로 다음에 올 엔트리의 예상 phase 값을 추적
커널 부팅 시 cq_phase = 1로 시작
완료 큐가 바뀔 때마다 (head == q_depth) cq_phase를 토글 (1 → 0 → 1)
각 CQE의 status 필드의 최하위 비트가 phase 비트
cstatic inline bool nvme_cqe_pending(struct nvme_queue *nvmeq) { struct nvme_completion *hcqe = &nvmeq->cqes[nvmeq->cq_head]; // Status 필드의 최하위 비트가 cq_phase와 일치하면 유효 return (le16_to_cpu(READ_ONCE(hcqe->status)) & 1) == nvmeq->cq_phase; }
cstatic inline void nvme_handle_cqe(struct nvme_queue *nvmeq, struct io_comp_batch *iob, u16 idx) { struct nvme_completion *cqe = &nvmeq->cqes[idx]; u16 command_id = READ_ONCE(cqe->command_id); // 1. Command ID로 원본 요청 찾기 struct request *req = nvmeq->cmds[command_id]; // 2. 상태 확인 및 재시도 결정 if (!nvme_try_complete_req(req, cqe->status, cqe->result)) { // 배치 완료 또는 개별 완료 처리 blk_mq_add_to_batch(req, iob, status, complete_fn); } }
6. 인터럽트 설정 및 관리
cstatic int nvme_setup_irqs(struct nvme_dev *dev, unsigned int nr_io_queues) { struct irq_affinity affd = { .calc_sets = nvme_calc_irq_sets, .priv = dev, }; unsigned int irq_queues, flags = PCI_IRQ_ALL_TYPES; // MSI-X/MSI 인터럽트 벡터 할당 // 최소 1개, 최대 nr_io_queues + 1개 (admin queue 포함) return pci_alloc_irq_vectors_affinity(pdev, 1, irq_queues, flags | PCI_IRQ_AFFINITY, &affd); }
각 I/O 큐별로 개별 IRQ 핸들러가 등록되며, Admin 큐와 하나 이상의 I/O 큐는 같은 인터럽트 벡터를 공유할 수 있습니다.
7. 폴링(Polling) 지원
cstatic int nvme_poll(struct blk_mq_hw_ctx *hctx, unsigned int tag) { struct nvme_queue *nvmeq = hctx->driver_data; // phase 비트를 확인하여 유효한 CQE가 있는지 검사 if (nvme_cqe_valid(nvmeq, nvmeq->cq_head, nvmeq->cq_phase)) { spin_lock_irq(&nvmeq->q_lock); __nvme_process_cq(nvmeq, &tag); spin_unlock_irq(&nvmeq->q_lock); if (tag == -1) return 1; // 완료 발견 } return 0; // 완료 없음 }
이 함수는 인터럽트 대신 적극적으로 완료 큐를 폴링하여 지연 시간을 줄입니다.
8. 디바이스 리셋 및 에러 처리
cstatic void nvme_reset_work(struct work_struct *work) { struct nvme_dev *dev = container_of(work, struct nvme_dev, reset_work); // 1. 디바이스 비활성화 nvme_dev_disable(dev, false); // 2. Admin 큐 재초기화 nvme_configure_admin_queue(dev); // 3. I/O 큐 설정 nvme_setup_io_queues(dev); // 4. 컨트롤러 활성화 nvme_init_identify(&dev->ctrl); }
9. 메모리 관리 및 DMA
PRP (Physical Region Page) 리스트 생성
대용량 데이터 전송을 위해 SGL(Scatter-Gather List) 또는 PRP를 사용합니다:
cstatic blk_status_t nvme_setup_prps(struct nvme_dev *dev, struct request *req, struct nvme_rw_command *cmnd) { // 1. 4KB 페이지 단위로 물리 주소 추출 // 2. 첫 페이지는 명령어에 직접 포함 (PRP1) // 3. 두 번째 이상의 페이지는 PRP List (PRP2)에 기록 }
10. 리소스 정리
cstatic void nvme_remove(struct pci_dev *pdev) { struct nvme_dev *dev = pci_get_drvdata(pdev); // 1. 모든 I/O 큐 삭제 (Delete SQ/CQ 명령 전송) nvme_disable_io_queues(dev); // 2. Admin 큐 삭제 nvme_delete_queue(&dev->queues[0]); // 3. 리소스 해제 nvme_free_queues(dev); nvme_release_prp_pools(dev); // 4. BAR 언매핑 pci_iounmap(pdev, dev->bar); }
요약
Linux NVMe PCI 드라이버는 다음과 같은 특징을 가집니다:
효율적인 큐 관리: Admin 큐와 다중 I/O 큐를 통한 병렬 처리
Phase 비트 기반 검증: 메모리 손상 감지 및 안정성 보장
유연한 인터럽트 처리: 벡터 인터럽트, 공유 인터럽트, 폴링 모드 지원
DMA 일관성: dma_rmb() 등의 메모리 배리어로 순서 보장
에러 복구: 리셋 및 재설정 메커니즘
blk-mq 통합: 최신 블록 디바이스 아키텍처와의 원활한 통합
이러한 설계는 고속 NVMe 디바이스의 높은 처리량과 낮은 지연 시간을 효과적으로 활용합니다.
댓글
댓글 쓰기