NVME驱动分析
一、NVME驱动介绍
- 协议支持:NVMe(Non-Volatile Memory Express)是为PCIe SSD设计的高性能存储协议,提供低延迟、高吞吐(相比SATA SSD提升7倍以上)
- 内核集成:自Linux 3.3版本起原生支持NVMe驱动,通过nvme.ko模块实现设备管理、队列调度和命令处理。
- 核心组件:
- Block层:对接文件系统,处理标准I/O请求(如读/写)。
- NVMe核心层:管理控制器(nvme_ctrl)、命名空间(nvme_ns)和I/O队列。
- PCIe驱动层:处理设备枚举、BAR空间映射和中断。
主机设备上电驱动加载简要流程(参考其他文章)
1、主机上电,开始扫描PCIE总线,深度优先搜索;
2、识别总线上的PCI设备;
3、为设备分配bar空间;
4、产生ACPI表,OS根据此表获取PCI设备;
5、OS启动,识别PCIE设备为NVME设备,probe驱动开始工作。
二、NVME驱动关键函数
(一)nvme_probe
功能模块 | 描述 | 技术细节 | 关键函数 |
设备识别与驱动绑定 | 当PCIe总线检测到NVMe设备时,通过设备ID匹配驱动并触发nvme_probe入口函数。 | 检查PCI设备的Vendor ID/Device ID,匹配nvme_id_table,创建struct nvme_dev上下文结构体。 | nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id) |
资源分配与初始化 | 为控制器分配内存资源,包括nvme_dev和queue | 通过kzalloc_node分配nvme_dev结构体,使用dma_alloc_coherent分配队列内存。 | kcalloc_node |
PCI配置与BAR空间映射 | 配置PCI设备的BAR空间,将其映射到内核虚拟地址以访问控制器寄存器。 | 调用pci_request_mem_regions申请内存区域,通过ioremap将BAR0映射到dev->bar。 | nvme_dev_map(struct nvme_dev *dev) pci_request_mem_regions nvme_remap_bar |
初始化工作队列 | 初始化工作队列,包括nvme复位、移除流程 | nvme_reset_work流程进一步说明 | INIT_WORK(&dev->ctrl.reset_work, nvme_reset_work) INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work); |
初始化PRP内存池 | 通过预分配 PRP 池,驱动可避免每次 I/O 请求时动态申请内存,从而减少延迟和内存碎片 | dma_pool_create() 创建多个不同大小的内存池(如 4KB、8KB),适配不同大小的数据传输请求;池的大小通常基于 NVMe 控制器支持的最大物理页(Page Size)和队列深度(Queue Depth)动态计算 | result = nvme_setup_prp_pools(dev); |
iod_mempool初始化 | 主要用于管理 NVMe 设备进行 DMA 数据传输时所需的内存资源 | 计算单个 I/O 描述符(IOD)的内存分配大小;创建 NUMA 节点绑定的内存池 | nvme_pci_iod_alloc_size mempool_create_node |
nvme ctrl初始化 | 初始化nvmectrl结构体;初始化工作队列:scan ns、async event、fw act、delete nvme;分配nvme字符号,添加到系统; | nvme ctrl初始化关键函数 | nvme_init_ctrl |
nvme_async_probe函数提交到内核的异步任务队列 | 执行控制器复位、Admin队列配置、命名空间扫描等操作 | 执行工作队列内的函数 | nvme_async_probe |
错误处理与资源释放 | 在初始化失败时释放已分配的资源(如释放BAR映射、DMA内存等)。 | 调用pci_release_mem_regions释放PCI资源,kfree释放nvme_dev结构体。 | mempool_destroy nvme_release_prp_pools、nvme_dev_unmap |
表格记录probe的关键流程,主要在于:
1、分配资源 -相关结构体分配;
2、PCI bar空间资源与虚拟内存映射,方便主机访问;
3、初始化工作队列,主要是nvme_reset_work和nvme_scan_work
4、初始化PRP内存池,方便IO过程使用PRP地址;
5、nvme ctrl初始化,主要初始化nvme_ctrl结构体,分配nvme节点,初始化字符设备并添加到字符设备中,方便上层应用访问;
6、执行nvme_aync_probe,即转向nvme_reset_work->nvme_scan_work
(二)nvme_reset_work
功能模块 | 描述 | 技术细节 | 关键函数 |
初始化PCI硬件资源 | 重新映射PCI BAR空间,配置DMA | 使能PCI设备 DMA MSI-X等 | nvme_pci_enable |
admin queue配置 | 1. nvme_configure_admin_queue():设置 Admin 队列属性(大小、中断等)。 2. nvme_init_queue():初始化队列结构体。 3. nvme_alloc_admin_tags():分配 Admin 命令标签。 | ASQ ACQ配置完成,主机可发admin命令 | nvme_pci_configure_admin_queue nvme_alloc_admin_tags |
发生identity命令识别盘片信息 | 调用 nvme_init_identify读取控制器信息(如命名空间数量、特性支持等) | 获取设备能力参数(如最大队列数、LBA 大小),以便于后续资源分配。 | nvme_init_identify |
IO队列创建 | 1、计算所需IO队列数; 2、向控制器申请queue资源; 3、创建并初始化IO队列 | 为每个CPU核心分配一个IO队列,提高并发性能 | nvme_setup_io_queues |
状态切换与启动 | 1. 调用 nvme_change_ctrl_state() 将状态切换为 NVME_CTRL_LIVE。 2. 启动 I/O 队列。 | 标记ctrl运行状态,激活IO流程 | nvme_start_queues |
启动控制器任务 | 创建IO queue后,启动scan work和async event work | NS扫描流程和响应异步事件AER处理 | nvme_start_ctrl |
表格记录nvme_reser_work的关键步骤:
1、初始化PCI硬件资源,使能PCI设备;
2、配置admin queue,如ACQ ASQ地址,初始化队列;
3、identitfy命令获取盘片能力以及支持特性;
4、IO SQ/CQ创建;
5、Controller状态切换,并转向nvme_scan_work;
(三) nvme_scan_work
功能模块 | 描述 | 技术细节 | 关键函数 |
发送identify命令获取NS数量 | 控制器信息获取与命名空间数量解析 | 获取来自盘符的NS数量 | nvme_identify_ctrl(ctrl, &id) le32_to_cpu(id->nn) |
NS扫描 | 扫描NS list | 遍历NS | nvme_scan_ns_sequential |
上表描述扫盘过程,关键步骤如下:
1、identify命令获取NS相关信息;
2、扫描NS信息。
三、NVME IO执行流程
主机发送NVME命令,会经历如下流程:
1、调用系统调用SYSCALL_DEFINE3,转向VFS;
2、进入VFS文件系统,命令生成bio,转向mq_block层;
3、进入block层后,调用submit_io传输命令到mq list,开始多队列调度;
4、多队列调度分发到hw queue,使用blk_mq_run_hw_queue,最终调用nvme_queue_rq;
5、进入nvme驱动层,把提交的cmd填入SQ中,并更新doorbell tail指针;
6、SSD设备检测到doorbell tail不等于head,意味着主机有命令写入,开始从SQ中取指令执行。
简要流程如下图:
四、控制器复位
nvme中主要有两个常见的复位方式,其一是nvme subsystem reset,其二是nvme Controller reset,在驱动中如下:
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 nvme_reset_ctrl_sync(ctrl);case NVME_IOCTL_SUBSYS_RESET:return nvme_reset_subsystem(ctrl);case NVME_IOCTL_RESCAN:nvme_queue_scan(ctrl);return 0;default:return -ENOTTY;}
}
针对subsystem reset复位方式如下:
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);
}
nvme协议中有描述:
Controller reset:
int nvme_reset_ctrl(struct nvme_ctrl *ctrl)
{if (!nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING))return -EBUSY;if (!queue_work(nvme_reset_wq, &ctrl->reset_work))return -EBUSY;return 0;
}
EXPORT_SYMBOL_GPL(nvme_reset_ctrl);int nvme_reset_ctrl_sync(struct nvme_ctrl *ctrl)
{int ret;ret = nvme_reset_ctrl(ctrl);if (!ret) {flush_work(&ctrl->reset_work);if (ctrl->state != NVME_CTRL_LIVE &&ctrl->state != NVME_CTRL_ADMIN_ONLY)ret = -ENETRESET;}return ret;
}
EXPORT_SYMBOL_GPL(nvme_reset_ctrl_sync);
最后执行的是nvme_reset_work
五、常见nvme驱动问题
nvme驱动相关问题主要集中在nvme timeout函数中,函数如下:
static enum blk_eh_timer_return nvme_timeout(struct request *req, bool reserved)
{struct nvme_iod *iod = blk_mq_rq_to_pdu(req);struct nvme_queue *nvmeq = iod->nvmeq;struct nvme_dev *dev = nvmeq->dev;struct request *abort_req;struct nvme_command cmd;u32 csts = readl(dev->bar + NVME_REG_CSTS);/* If PCI error recovery process is happening, we cannot reset or* the recovery mechanism will surely fail.pcie链路发生异常*/mb();if (pci_channel_offline(to_pci_dev(dev->dev)))return BLK_EH_RESET_TIMER;/** Reset immediately if the controller is failed, 需要重启controller*/if (nvme_should_reset(dev, csts)) {nvme_warn_reset(dev, csts);nvme_dev_disable(dev, false);nvme_reset_ctrl(&dev->ctrl);return BLK_EH_DONE;}/** Did we miss an interrupt?*/if (__nvme_poll(nvmeq, req->tag)) {dev_warn(dev->ctrl.device,"I/O %d QID %d timeout, completion polled\n",req->tag, nvmeq->qid);return BLK_EH_DONE;}/** Shutdown immediately if controller times out while starting. The* reset work will see the pci device disabled when it gets the forced* cancellation error. All outstanding requests are completed on* shutdown, so we return BLK_EH_DONE.*///如果控制器在启动时超时,请立即关闭。复位将看到pci设备在出现强制取消错误时被禁用。所有未完成的请求都在关机时完成,因此我们返回BLK_EH_DONE。switch (dev->ctrl.state) {case NVME_CTRL_CONNECTING:case NVME_CTRL_RESETTING:dev_warn_ratelimited(dev->ctrl.device,"I/O %d QID %d timeout, disable controller\n",req->tag, nvmeq->qid);nvme_dev_disable(dev, false);nvme_req(req)->flags |= NVME_REQ_CANCELLED;return BLK_EH_DONE;default:break;}/** Shutdown the controller immediately and schedule a reset if the* command was already aborted once before and still hasn't been* returned to the driver, or if this is the admin queue.*///如果命令之前已经被abort中止过一次,但仍未返回给驱动程序,或者这是管理队列(qid不为0),请立即关闭控制器并安排重置。if (!nvmeq->qid || iod->aborted) {dev_warn(dev->ctrl.device,"I/O %d QID %d timeout, reset controller\n",req->tag, nvmeq->qid);nvme_dev_disable(dev, false);nvme_reset_ctrl(&dev->ctrl);nvme_req(req)->flags |= NVME_REQ_CANCELLED;return BLK_EH_DONE;}if (atomic_dec_return(&dev->ctrl.abort_limit) < 0) {atomic_inc(&dev->ctrl.abort_limit);return BLK_EH_RESET_TIMER;}iod->aborted = 1;构造abort命令,发送设备以终止命令执行memset(&cmd, 0, sizeof(cmd));cmd.abort.opcode = nvme_admin_abort_cmd;cmd.abort.cid = req->tag;cmd.abort.sqid = cpu_to_le16(nvmeq->qid);dev_warn(nvmeq->dev->ctrl.device,"I/O %d QID %d timeout, aborting\n",req->tag, nvmeq->qid);abort_req = nvme_alloc_request(dev->ctrl.admin_q, &cmd,BLK_MQ_REQ_NOWAIT, NVME_QID_ANY);if (IS_ERR(abort_req)) {atomic_inc(&dev->ctrl.abort_limit);return BLK_EH_RESET_TIMER;}abort_req->timeout = ADMIN_TIMEOUT;abort_req->end_io_data = NULL;blk_execute_rq_nowait(abort_req->q, NULL, abort_req, 0, abort_endio);/** The aborted req will be completed on receiving the abort req.* We enable the timer again. If hit twice, it'll cause a device reset,* as the device then is in a faulty state.*/return BLK_EH_RESET_TIMER;
}
根据nvme timout函数描述,可能会发生的情况:
1、上层应用执行IO超时,进入nvme_timeout,发送abort命令终止IO,并成功执行;
2、上层应用执行IO超时,进入nvme_timeout,发送abort命令终止IO,并成功执行,IO继续执行,开始复位Controller;
3、上层应用执行IO超时,进入nvme_timeout,发送abort命令终止IO,并成功执行,IO继续执行,开始复位Controller,执行复位流程中超时,禁用Controller,主机开始不识别盘符;
4、正常复位过程超时,进入nvme_timeout开始重新复位。
5、由于链路异常,Controller状态改变,进入timeout直接退出。