Linux NVMe 드라이버 아키텍처 및 nvme_core_init() 함수 분석
참고: https://blog.csdn.net/zhuzongpeng/category_12008684.html?spm=1001.2014.3001.5482
Linux NVMe Driver 이야기 시리즈는 참고 사이트의 전개 방식을 답습하면서 한글로 추가 보완/정리한 내용입니다.
Linux NVMe 드라이버 아키텍처 및 nvme_core_init() 함수 분석
1. Linux NVMe 드라이버 아키텍처
Linux NVMe 드라이버는 Linux 아키텍처에서 블록 계층 아래에 있으며 NVMe 장치와의 상호 작용을 담당합니다. (2013 FMS)
현재 NVMe 드라이버는 아래 그림과 같이 NVMe over Fabric 관련 장치도 지원합니다. (2016 SDC). 여기에서 우리는 주로 PCIe를 통한 NVMe에 분석의 초점을 맞춥니다.
Linux 4.10.6에 포함된 NVMe 드라이버의 디렉토리는(linux-4.10.6\drivers\nvme) 두 개의 하위 폴더(host 및 target)와 두 개의 파일(Kconfig 및 Makefile)이 포함되어 있습니다.
드라이버를 분석할 때 드라이버와 관련된 kconfig 및 Makefile 파일을 먼저 살펴보는 것이 가장 좋습니다. 파일 구조를 이해하고 관련 소스 코드를 읽어 보시기 바랍니다.
Kconfig 파일의 역할은 다음과 같습니다.
make menuconfig 실행시 나타나는 구성 옵션입니다.
사용자 구성 인터페이스 선택에 따라 구성 결과를 .config 구성 파일에 저장합니다(이 파일은 컴파일할 커널 구성 요소 및 컴파일 방법을 결정하기 위해 Makefile에서 이용됨).
먼저 drivers/nvme/host/kconfig의 내용을 살펴봅시다. drivers/nvme/target 디렉토리를 포함하여 NVMeOF 관련 내용은 생략합니다.
config NVME_CORE
tristate
config BLK_DEV_NVME
tristate "NVM Express block device"
depends on PCI && BLOCK
select NVME_CORE
config BLK_DEV_NVME_SCSI
bool "SCSI emulation for NVMe device nodes"
depends on NVME_CORE
다음으로 NVME_CORE, BLK_DEV_NVME, BLK_DEV_NVME_SCSI와 관련된 drivers/nvme/host/Makefile의 내용을 확인합니다.
obj-$(CONFIG_NVME_CORE) += nvme-core.o
obj-$(CONFIG_BLK_DEV_NVME) += nvme.o
nvme-core-y := core.o
nvme-core-$(CONFIG_BLK_DEV_NVME_SCSI) += scsi.o
nvme-y += pci.o
Kconfig 및 Makefile 분석을 통해 NVMe over PCIe와 관련된 core.c, pci.c, scsi.c 세 파일을 살펴 봅니다.
2. nvme_core_init() 함수
drivers/nvme/host/core.c 파일에서 module_init(nvme_core_init)을 찾습니다. 바로 위에 위치한 nvme_core_init() 함수는 주로 두 가지 작업을 수행합니다.
- __register_chrdev() 함수를 호출하여 “nvme”라는 캐릭터 디바이스를 등록합니다.
- class_create() 함수를 호출하여 장치의 논리 클래스를 동적으로 생성하고 일부 필드의 초기화를 완료한 다음 커널에 추가합니다. 생성된 논리 클래스는 /sys/class/에서 확인 가능합니다.
int __init nvme_core_init(void)
{
int result;
//1. 캐릭터 디바이스 "nvme" 등록
result = __register_chrdev(nvme_char_major, 0, NVME_MINORS, "nvme", &nvme_dev_fops);
if (result < 0)
return result;
else if (result > 0)
nvme_char_major = result;
//2. 새로운 nvme 클래스 생성, 소유자(Owner)는 THIS_MODULE
nvme_class = class_create(THIS_MODULE, "nvme");
//에러 발생시 캐릭터 디바이스 nvme 삭제
if (IS_ERR(nvme_class)) {
result = PTR_ERR(nvme_class);
goto unregister_chrdev;
}
return 0;
unregister_chrdev:
__unregister_chrdev(nvme_char_major, 0, NVME_MINORS, "nvme");
return result;
}
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0");
module_init(nvme_core_init);
module_exit(nvme_core_exit);
◈ 캐릭터 디바이스를 등록 시, 장치 번호 관련 지식
캐릭터 디바이스 또는 블록 디바이스에는 주 장치 번호(Major)와 보조 장치 번호(Minor)가 있습니다. 주 장치 번호는 특정 드라이버를 식별하는데 사용되고 보조 번호는 해당 드라이버를 사용하는 개별 장치를 식별하는데 사용됩니다. 예를 들어 Linux 시스템에 두 개의 NVMe SSD가 있는 경우, 주 장치 번호에 자동으로 번호(예: 8)가 할당되고 보조 장치 번호는 각각 1과 2가 됩니다.
예를 들어, 32비트 머신에서 장치 번호는 총 32비트이며 상위 12비트는 주 장치 번호를 나타내고 하위 20비트는 보조 장치 번호를 나타냅니다.
주요 장비 번호 부장치번호 12자리 20자리
3. nvme_dev_fops 구조체
위에서 등록한 캐릭터 디바이스로 open, ioctl, release 인터페이스를 동작시킬 수 있습니다. nvme 캐릭터 디바이스의 파일 연산 구조 nvme_dev_fops는 다음과 같이 정의됩니다.
static const struct file_operations nvme_dev_fops = {
.owner = THIS_MODULE,
// open()은 장치를 여는데 사용되며 이 함수에서 장치를 초기화할 수 있습니다.
.open = nvme_dev_open,
// release()는 open() 함수에서 요청한 리소스를 해제하는 데 사용됩니다.
.release = nvme_dev_release,
// nvme_dev_ioctl()은 장치별 명령을 실행하는 방법을 제공합니다.
.unlocked_ioctl = nvme_dev_ioctl,
.compat_ioctl = nvme_dev_ioctl,
/* 2개의 unlocked_ioctl과 compat_ioctl이 있는데, 이 2개가 동시에 존재할 경우 unlocked_ioctl이 먼저 호출되고,
compat_ioctl의 경우 CONFIG_COMPAT가 선언된 경우에만 호출됩니다.
obj-$(CONFIG_COMPAT) += compat.o compat_ioctl.o */
};
3.1 nvme_dev_open() 함수
모든 nvme 장치는 nvme_ctrl_list에 추가됩니다. 여기서 list_for_each_entry를 호출하여 nvme_ctrl_list를 탐색하여 인스턴스 하위 장치 번호에 해당하는 nvme 장치를 찾습니다. 그런 다음 ctrl->admin_q 및 ctrl->kref가 null인지 확인합니다. 최종적으로 찾은 nvme 디바이스는 file->private_data 영역에 위치합니다.
static int nvme_dev_open(struct inode *inode, struct file *file)
{
struct nvme_ctrl *ctrl;
int instance = iminor(inode); // Incode에서 부 장치 번호를 가져옵니다.
int ret = -ENODEV; // 기본적으로 아직 특정 장치에 할당되지 않았습니다.
// spin lock은 두 가지 옵션이 있습니다.
// 끝날 때까지 기다리거나, 현재 프로세스를 일시 중단하고 다른 프로세스가 실행되도록 예약하는 것입니다.
spin_lock(&dev_list_lock);
list_for_each_entry(ctrl, &nvme_ctrl_list, node) {
if (ctrl->instance != instance)
continue;
if (!ctrl->admin_q) {
ret = -EWOULDBLOCK;
break;
}
if (!kref_get_unless_zero(&ctrl->kref))
break;
file->private_data = ctrl;
ret = 0;
break;
}
spin_unlock(&dev_list_lock); //잠금 해제
return ret;
}
3.2 nvme_dev_release() 함수
nvme_dev_release는 상대적으로 간단하며 open() 함수에서 요청된 리소스를 해제하는데 사용됩니다.
static int nvme_dev_release(struct inode *inode, struct file *file)
{
nvme_put_ctrl(file->private_data);
return 0;
}
3.3 nvme_dev_ioctl() 함수
코드에서 총 5개의 명령이 nvme_dev_ioctl에 구현되어 있음을 알 수 있습니다. NVME_IOCTL_ADMIN_CMD 및 NVME_IOCTL_IO_CMD는 NVMe Spec에서 정의한 Admin 및 I/O CMD입니다. 구체적인 정의는 NVMe 이야기 #2: Queue Management를 참조하십시오. 한 가지 주의할 점은 NVME_IOCTL_IO_CMD는 네임스페이스가 여러 개인 NVMe 장치를 지원하지 않는다는 것입니다.
static long nvme_dev_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct nvme_ctrl *ctrl = file->private_data;
void __user *argp = (void __user *)arg;
switch (cmd) {
case NVME_IOCTL_ADMIN_CMD:
return nvme_user_cmd(ctrl, NULL, argp);
case NVME_IOCTL_IO_CMD:
return nvme_dev_user_cmd(ctrl, argp);
case NVME_IOCTL_RESET:
dev_warn(ctrl->device, "resetting controller\n");
return ctrl->ops->reset_ctrl(ctrl);
case NVME_IOCTL_SUBSYS_RESET:
return nvme_reset_subsystem(ctrl);
case NVME_IOCTL_RESCAN:
nvme_queue_scan(ctrl);
return 0;
default:
return -ENOTTY; //잘못된 ioctrl 명령
}
}
NVME_IOCTL_RESET 및 NVME_IOCTL_SUBSYS_RESET 명령은 nvme_ctrl_ops를 통해 레지스터에 직접 접근합니다. 전자는 reset_ctrl을 호출하고 후자는 reg_write32를 호출합니다.
static inline int nvme_reset_subsystem(struct nvme_ctrl *ctrl)
{
if (!ctrl->subsystem)
return -ENOTTY;
return ctrl->ops->reg_write32(ctrl, NVME_REG_NSSR, 0x4E564D65);
}
4. nvme_pci_ctrl_ops 구조체
nvme_ctrl_ops는 nvme 초기화에서 nvme_probe()를 호출할 때 nvme_pci_ctrl_ops 값이 할당됩니다. nvme_pci_ctrl_ops 구조체의(pci.c) 정의는 다음과 같습니다.
static const struct nvme_ctrl_ops nvme_pci_ctrl_ops = {
.name = "pcie",
.module = THIS_MODULE,
.reg_read32 = nvme_pci_reg_read32,
.reg_write32 = nvme_pci_reg_write32,
.reg_read64 = nvme_pci_reg_read64,
.reset_ctrl = nvme_pci_reset_ctrl,
.free_ctrl = nvme_pci_free_ctrl,
.submit_async_event = nvme_pci_submit_async_event,
};
결국, NVME_IOCTL_RESET은 nvme_pci_reset_ctrl() 함수를 호출하고 NVME_IOCTL_SUBSYS_RESET는 nvme_pci_reg_write32() 함수를 호출한다는 것을 알 수 있습니다.
4.1 nvme_pci_reset_ctrl() 함수
to_nvme_dev() 함수를 호출하여 nvme_ctrl의 주소를 얻어 nvme_dev에 할당한 후, nvme_reset() 함수를 호출하여 reset을 수행함을 알 수 있습니다.
static int nvme_pci_reset_ctrl(struct nvme_ctrl *ctrl)
{
struct nvme_dev *dev = to_nvme_dev(ctrl);
int ret = nvme_reset(dev);
if (!ret)
flush_work(&dev->reset_work);
return ret;
}
4.1.1 nvme_reset() 함수
현재 작업이 바쁘면 reset이 실패 합니다. 즉, nvme 컨트롤러를 reset하려면 유휴 상태여야 합니다. 유휴 상태인 경우 queue_work() 함수를 호출하여 nvme 컨트롤러를 reset 합니다.
reset_work 구조체는 nvme 초기화에서 nvme_probe() 호출시에 할당되며, 해당 작업에 대한 핸들러 함수는 nvme_reset_work()입니다. 이와 관련된 내용은 nvme_probe()함수 분석에서 확인하실 수 있습니다.
static int nvme_reset(struct nvme_dev *dev)
{
if (!dev->ctrl.admin_q || blk_queue_dying(dev->ctrl.admin_q))
return -ENODEV;
if (work_busy(&dev->reset_work))
return -ENODEV;
if (!queue_work(nvme_workq, &dev->reset_work))
return -EBUSY;
return 0;
}
작업(work)은 리눅스 커널 WorkQueue의 work_struct 구조체를 통해서 만듭니다. 리눅스 커널은 work_struct 을 편리하게 만들 수 있도록 다음과 같이 매크로 정의하고 있습니다.
static DECLARE_WORK(test_work, wq_test_func);
DECLARE_WORK에 전달하는 첫번째 파라미터인 test_work는 work_struct 구조체입니다. 두번째 파라미터인 wq_test_func는 작업(work)이 실행될 때 호출되는 작업 핸들러 함수 입니다. 즉, test_work라는 이름으로 work_struct 구조체를 만들고 wq_test_func을 연결합니다. 이렇게 만든 test_work 구조체를 queue_work() 함수의 두번째 파라미터로 전달하면, 커널에서 이 구조체를 workqueue로 등록하고 여기에 연결한 함수 wq_test_func이 실행됩니다. 아래의 코드는 사용자가 정의한 함수인 wq_test_func이 커널 내부에서 스케쥴링되어서 실행된다는 것에 의미가 있습니다.
ret = queue_work(system_long_wq, &test_work);
DECLARE_WORK에서 work_struct을 만들어서 queue_work()을 통하여 커널에 전달하여 실행되는 과정을 블럭도로 요약하면 다음과 같습니다.
4.2 nvme_pci_reg_write32() 함수
NVME_IOCTL_SUBSYS_RESET에 의해 호출되는 nvme_pci_reg_write32() 함수는 아래와 같습니다.
static int nvme_pci_reg_write32(struct nvme_ctrl *ctrl, u32 off, u32 val)
{
writel(val, to_nvme_dev(ctrl)->bar + off);
return 0;
}
nvme_reset_subsystem() 함수와 결부해 해석해 보면 BAR 레지스터에 0x4E564D65를 직접 쓰는 것임을 알 수 있습니다.
static inline int nvme_reset_subsystem(struct nvme_ctrl *ctrl)
{
if (!ctrl->subsystem)
return -ENOTTY;
return ctrl->ops->reg_write32(ctrl, NVME_REG_NSSR, 0x4E564D65);
}