浅谈nvme驱动中的nvme_alloc_ns函数的实现原理和底层逻辑

核心目标:

nvme_alloc_ns 的主要任务是为一个物理 NVMe 命名空间创建一个软件层面的表示 (struct nvme_ns),并将其关联到内核的块设备子系统,使其成为一个可被识别、访问和操作的磁盘设备 (例如 /dev/nvme0n1)。

函数位置和调用者:

通常定义在 drivers/nvme/host/core.c。

被 nvme_scan_work 函数调用(在控制器探测或重置后执行扫描任务时),或者在某些情况下被 nvme_validate_ns 调用。

对于每个在控制器上发现的有效命名空间 (NSID),都会被调用一次。

实现原理和底层逻辑步骤详解:

1.参数解析与基础检查:

函数签名通常为:int nvme_alloc_ns(struct nvme_ctrl *ctrl, unsigned nsid)。

接收指向控制器的结构体指针 ctrl 和要分配的命名空间 ID nsid。

首先进行基础参数有效性检查(如 ctrl 是否有效、nsid 是否为 0 或无效)。

(重要安全机制) 检查是否已经为该 nsid 分配过 nvme_ns 结构,避免重复创建。

2.分配 struct nvme_ns 内存:

使用 kzalloc_node 或 kzalloc 分配一个 struct nvme_ns 结构体,并初始化为零。kzalloc_node 尝试在 NUMA 节点感知的内存节点上分配,提高性能。

分配失败返回 -ENOMEM。

3.初始化 nvme_ns 核心字段:

ns->ctrl: 设置为传入的 ctrl 指针,表明此命名空间属于哪个控制器。

ns->queue: 初始化为 NULL(稍后创建)。

ns->disk: 初始化为 NULL(稍后创建)。

ns->nsid: 设置为传入的 nsid。

kref_init(&ns->kref): 初始化引用计数,用于后续的安全引用管理和释放。

list_add_tail(&ns->list, &ctrl->namespaces): 将此 ns 结构添加到控制器的 namespaces 链表中,方便统一管理。

ns->lba_shift: 初始化一个默认值(通常是 9,表示 512 字节扇区的 shift),后续会根据实际 LBA 格式更新。

ns->head: 初始化为 NULL(用于多路径支持)。

4.获取命名空间信息 (Identify NS):

调用 nvme_get_ns_info 函数(这是关键!)。

该函数向控制器发送 Identify Namespace 命令 (CNS=0x00),获取指定 nsid 的详细信息 (struct nvme_id_ns)。

解析识别信息:

状态(是否已附着、可用)。

命名空间大小 (nsze),即总逻辑块数量。

命名空间容量 (ncap),即可用逻辑块数量。

LBA 数据格式 (lbaf): 确定当前有效的 LBA 格式(索引),以及该格式下逻辑块大小(如 512B, 4K)和元数据大小/位置(如是否支持端到端保护)。

是否支持 DULBE (Deallocated or Unwritten Logical Block Error)。

薄配置信息(如 nvmcap)。

根据识别的信息计算实际可用的容量大小。

设置 ns->lba_shift: 根据有效的 LBA 格式中的 lbads 字段(指明逻辑块大小的 2 的幂次)计算得出。例如,lbads=9 表示 512 字节块,shift=9;lbads=12 表示 4096 字节块,shift=12。这个 shift 值在后续将 LBA 转换为字节偏移量时至关重要。

5.创建块设备请求队列 (blk_mq_init_queue):

调用 blk_mq_alloc_disk_for_queue 或者直接调用 blk_mq_init_queue(根据内核版本和具体实现略有差异,5.10 常用 blk_mq_alloc_disk / blk_mq_init_allocated_queue 的组合)。

传入为 NVMe 驱动定义的队列操作集 (nvme_fops 或 nvme_mq_admin_ops / nvme_mq_ops),其中包含了处理 I/O 请求的核心函数(如 queue_rq)。

这个队列代表了此命名空间的 I/O 通道(通常是多队列,MQ)。

底层逻辑:内核块层 MQ 子系统初始化一个高效的、支持多硬件队列的请求队列结构 (struct request_queue)。NVMe 驱动提供的 ops 确保了当上层提交 I/O 请求(读/写)时,最终会调用到 NVMe 驱动实现的 queue_rq 函数,该函数负责将请求构造成 NVMe SQ 条目并通知控制器。

6.分配 struct gendisk 结构体:

通常是 blk_mq_alloc_disk 或其他相关 API 的一部分。gendisk 是内核表示一个完整磁盘设备的核心结构。

初始化 disk->fops 为 nvme_fops,定义了块设备文件操作(如 open, release, ioctl)。这使得用户空间操作 /dev/nvme0n1 时能调用到正确的驱动函数。

disk->private_data 设置为指向 struct nvme_ns *ns。这是连接块设备层和具体 NVMe 命名空间驱动数据的桥梁。任何后续通过 disk 发起的操作,驱动都能通过这个指针找到对应的 ns 结构。

设置磁盘名称(如 disk->disk_name = "nvmeXnY",其中 X 是控制器实例号,Y 是命名空间号)。

设置 disk->flags:例如,如果命名空间是只读的,设置 GENHD_FL_READONLY;设置 GENHD_FL_EXT_DEVT 以支持扩展的设备号管理。

设置 disk->queue 为上一步创建好的请求队列指针 (ns->queue)。

7.设置磁盘容量:

调用 set_capacity(disk, capacity)。这里的 capacity 是在步骤 4 中,根据 ncap 或 nsze 以及 lba_shift 计算出的 扇区数。

底层逻辑:capacity 的计算公式大致为:capacity_bytes = (ncap) << ns->lba_shift。但是 set_capacity 需要的是 512 字节扇区的数量 (无论实际 LBA 大小是多少)。因此通常这样计算:

size = nvme_lba_to_sect(ns, ncap); // 或者直接用: ncap * (1 + ms) << (ns->lba_shift - 9)

set_capacity(ns->disk, size);

nvme_lba_to_sect(ns, ncap) 本质上是计算:(ncap) * (actual_block_size_in_bytes) / 512。这确保了文件系统和工具看到的始终是 512 字节扇区单位,简化了管理。驱动内部在真正处理 I/O 请求时,会将这个扇区号再转换回基于 ns->lba_shift 的 LBA。

8.处理可选的特性:

写入保护 (Write Protect): 如果控制器或命名空间被配置为写保护,设置 disk->read_only = true。

薄配置 (Thin Provisioning): 如果命名空间支持薄配置 (nvmcap > ncap),可能需要设置相应的块设备标志或暴露为 SCSI 设备时处理 UNMAP。

多路径 (Multipathing): 调用 nvme_mpath_alloc_disk(ns) 或类似函数。此函数:

分配一个 struct nvme_ns_head (head) 结构(如果还不存在该通用标识符的命名空间)。

将 ns->head 指向这个 head。

将 disk 的 private_data 设置为 head 而不是 ns。这是多路径的核心:head 代表一个逻辑磁盘,它聚合了多个指向同一个物理命名空间的 ns 路径(来自不同控制器)。head 负责选择最佳路径进行 I/O(故障转移)。

为 head 创建一个多路径感知的请求队列。

nvme_mpath_init_identify 可能在这里被调用,检查控制器是否支持Namespace Granularity Capability (NGC)或Namespace Sharing Capability (NSC),这些是多路径的关键特性。

9.将磁盘添加到系统 (device_add_disk):

调用 device_add_disk(ctrl->device, disk, nvme_ns_id_attr_groups) 或 add_disk(disk)。

向内核块层注册这个 gendisk。

触发内核事件 (kobject_uevent),通知用户空间(udev)有一个新的块设备可用。

用户空间的 udev 会根据规则创建设备节点 (/dev/nvme0n1)。

nvme_ns_id_attr_groups 提供了在 sysfs (/sys/block/nvme0n1/...) 下暴露命名空间特定信息的属性(如 wwid, nsid, uuid, nguid, eui, model, firmware_rev, serial 等)。这些信息大部分来自步骤 4 的 Identify NS 数据。

10.配置 I/O 调度器 (可选):

可能在此处或稍后(如 nvme_start_ctrl 后)调用 blk_queue_io_opt(ns->queue, optimal_io_size) 设置最优 I/O 大小提示(通常基于 nvme_id_ns->nabspf 或 nvme_id_ctrl->mdts 计算)。

可能设置 queue_limits (blk_queue_logical_block_size, blk_queue_physical_block_size, blk_queue_max_hw_sectors, blk_queue_write_cache 等)。其中部分信息(如块大小)在步骤 4 和 5 中可能已经设置过。

11.返回与清理:

如果所有步骤成功,函数返回 0,表示命名空间成功创建并注册。

错误处理: 如果在任何步骤失败(内存不足、Identify命令失败、队列创建失败、添加磁盘失败等),函数需要执行必要的清理:

销毁已创建但未成功注册的请求队列 (blk_cleanup_queue / blk_mq_destroy_queue).

如果已分配 gendisk,释放它 (put_disk).

将 ns 从控制器的命名空间链表移除 (list_del).

释放 struct nvme_ns 内存 (kfree(ns)).

返回对应的错误码 (-ENOMEM, -EIO, -ENODEV 等)。

关键数据结构总结:

struct nvme_ctrl: 代表一个 NVMe 控制器实例。

struct nvme_ns: 核心! 代表驱动内部的一个 NVMe 命名空间,包含指向控制器、请求队列、gendisk、LBA格式信息、多路径head等的指针。

struct gendisk: 内核块层表示一个磁盘设备的核心结构。

struct request_queue: 代表一个 I/O 请求队列,管理着进出块设备的请求流。指向 NVMe 驱动提供的操作函数集。

struct nvme_id_ns: 从控制器读取的 Identify Namespace 数据结构,描述命名空间的物理特性。

struct nvme_ns_head: (多路径相关) 代表一个逻辑命名空间,聚合了多个物理路径 (struct nvme_ns)。

核心逻辑流程图简化:

Start -> Check params & if ns exists? --(No)--> Alloc struct nvme_ns

| |

|<--(Yes)------------------------------|

|

v

Call nvme_get_ns_info --> (Identify NS Command) --> Parse & Calc info (incl. lba_shift, capacity)

|

v

Allocate blk_mq request queue (nvme_mq_ops)

|

v

Alloc & init struct gendisk (fops = nvme_fops, private_data = ns)

|

v

Set capacity (ns->lba_shift -> 512B sectors)

|

v

(Optional) Handle Write Protect/Thin Provisioning

|

v

(Optional) Setup Multipathing (nvme_mpath_alloc_disk -> creates head)

|

v

device_add_disk() --> Register disk with block layer, trigger uevent

|

v

(Optional) Configure queue limits/scheduler

|

v

Success: Return 0\] \[Error: Rollback allocations -\> Return -ERRNO

总结:

nvme_alloc_ns 是 NVMe 驱动中实现命名空间管理的核心枢纽。它完成了从底层硬件抽象到上层块设备接口的关键转化:

硬件交互: 通过发送 Identify Namespace 命令获取物理属性。

软件建模: 创建 nvme_ns 结构作为软件模型。

I/O 通道建立: 初始化块层请求队列,关联 NVMe 驱动的处理函数。

块设备呈现: 创建并注册 gendisk 结构体,使命名空间成为一个 /dev/ 节点。

容量计算与设置: 基于 LBA 格式精确计算并设置以 512B 为单位的设备容量。

特性适配: 处理写保护、薄配置等。

多路径集成: 支持通过多个控制器访问同一物理命名空间的复杂场景。

错误处理: 确保任何步骤失败都能安全回滚。

补充identify命令使用

  1. 核心功能

通过向控制器提交 IDENTIFY 命令,驱动可获取以下关键信息:

控制器结构(CNS=0x01):

控制器能力(CAP)、版本(VS)、支持的队列数量、电源状态等。

命名空间列表(CNS=0x02):

返回当前控制器管理的所有有效命名空间 ID(NSID)列表。

命名空间结构(CNS=0x00):

指定 NSID 的详细信息(LBA 大小、容量、格式化参数等)。

其他对象(如 CNS=0x03 获取命名空间标识符列表)。

//发送nvme identify命令

static int nvme_identify_ns(struct nvme_ctrl *ctrl, unsigned nsid,

struct nvme_ns_ids *ids, struct nvme_id_ns **id)

{

struct nvme_command c = { };

c.identify.opcode = nvme_admin_identify;

c.identify.nsid = cpu_to_le32(nsid);

c.identify.cns = NVME_ID_CNS_NS;

*id = kmalloc(sizeof(**id), GFP_KERNEL);

error = nvme_submit_sync_cmd(ctrl->admin_q, &c, *id, sizeof(**id));

}

enum {

NVME_ID_CNS_NS = 0x00,

NVME_ID_CNS_CTRL = 0x01,

NVME_ID_CNS_NS_ACTIVE_LIST = 0x02,

NVME_ID_CNS_NS_DESC_LIST = 0x03,

NVME_ID_CNS_CS_NS = 0x05,

NVME_ID_CNS_CS_CTRL = 0x06,

NVME_ID_CNS_NS_PRESENT_LIST = 0x10,

NVME_ID_CNS_NS_PRESENT = 0x11,

NVME_ID_CNS_CTRL_NS_LIST = 0x12,

NVME_ID_CNS_CTRL_LIST = 0x13,

NVME_ID_CNS_SCNDRY_CTRL_LIST = 0x15,

NVME_ID_CNS_NS_GRANULARITY = 0x16,

NVME_ID_CNS_UUID_LIST = 0x17,

};

  1. 内核调用场景

在 Linux 驱动中,该命令主要用于:

控制器初始化(nvme_probe 阶段):

// drivers/nvme/host/pci.c

static int nvme_alloc_admin_queue(struct nvme_dev *dev) {

// 发送IDENTIFY命令 (CNS=0x01)

nvme_identify_ctrl(dev, &dev->ctrl_info); // 内部调用nvme_admin_identify

}

获取 struct nvme_id_ctrl 数据,填充控制器能力(如 mqes 最大队列数)。

命名空间扫描(nvme_scan_work 阶段):

// drivers/nvme/host/core.c

static void nvme_scan_ns_list(struct nvme_ctrl *ctrl) {

// 发送IDENTIFY命令 (CNS=0x02)

nvme_identify_ns_list(ctrl, 0, &ns_list); // 获取NSID列表

}

static void nvme_validate_ns(struct nvme_ctrl *ctrl, unsigned nsid) {

// 发送IDENTIFY命令 (CNS=0x00)

nvme_identify_ns(ctrl, nsid, &id); // 获取指定命名空间信息

}

命令执行流程(内核视角)

  1. 命令构造

驱动填充 struct nvme_command:

struct nvme_command {

__le32 cdw0; // 操作码=0x06 | CNS<<8 等标志位

__le32 nsid; // 目标命名空间ID (CNS=0x00时有效)

__le64 metadata; // 通常为0

__le64 prp1, prp2; // 数据缓冲区物理地址

__le32 cdw10, cdw11; // 参数: CNS值、返回数据结构长度等

};

  1. 关键参数说明

cdw0:

低 8 位:操作码 0x06

高 8 位:控制信息(如 CNS 类型)

cdw10:

Bit 0-7:CNS(Controller or Namespace Structure)

0x00:获取命名空间结构

0x01:获取控制器结构

0x02:获取命名空间列表

Bit 16-31:返回数据结构长度(通常为 0x1000 对应 4KB)

  1. 命令提交

通过 Admin 队列提交命令:

// drivers/nvme/host/pci.c

int nvme_submit_sync_cmd(struct nvme_queue *nvmeq, struct nvme_command *cmd,

void *buffer, unsigned bufflen) {

// 将命令写入Admin SQ

nvme_submit_cmd(nvmeq, cmd);

// 等待CQ完成中断

nvme_poll_cq(nvmeq);

// 解析完成状态

}

返回数据结构(部分)

  1. 控制器信息 (struct nvme_id_ctrl)

struct nvme_id_ctrl {

__le16 vid; // PCI厂商ID

__le16 ssvid; // PCI子系统厂商ID

char sn20; // 序列号

char mn40; // 型号

__le32 nn; // 命名空间数量

__le16 sqes; // SQ条目大小要求 (e.g., 0x0066=128B)

__le16 cqes; // CQ条目大小要求

__le32 cmic; // 控制器多路径特性

__le32 oaes; // 异步事件支持

// ... (其他关键字段)

};

  1. 命名空间信息 (struct nvme_id_ns)

struct nvme_id_ns {

__le64 nsze; // 命名空间总扇区数

__le64 ncap; // 命名空间可用容量

__le64 nuse; // 命名空间已用容量

__le8 lbaf16; // LBA格式列表

__u8 flbas; // 当前使用的LBA格式索引

// ... (其他关键字段)

};

nvme_probe-->nvme_alloc_admin_queue-->发送IDENTIFY CNS=0x01-->解析控制器能力-->nvme_setup_io_queues-->nvme_dev_add-->nvme_scan_work-->发送IDENTIFY CNS=0x02-->遍历NSID列表-->对每个NSID发送IDENTIFY CNS=0x00-->创建nvme_ns结构

关键点总结

协议基石:

nvme_admin_identify 是驱动获取硬件信息的唯一标准方式,符合 NVMe 1.4 规范第 5.15 章定义。

动态配置依赖:

驱动根据返回的 sqes/cqes 调整队列大小,依据 nn 和 NS 列表动态创建命名空间。

错误处理:

若命令超时或返回错误状态(如 NVME_SC_INVALID_NS),驱动会跳过无效命名空间或触发复位。

用户空间交互:

用户可通过 nvme-cli 工具直接调用此命令(如 nvme id-ctrl /dev/nvme0)。

此命令是 NVMe 驱动初始化过程中最关键的数据采集手段,直接决定了后续队列创建、命名空间管理及块设备注册的准确性。

相关推荐
夜听莺儿鸣2 小时前
502-003_Linux 中断与异常(一):从Linux角度理解中断与异常
linux·中断与异常·linux中断
杨云龙UP2 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
向成科技3 小时前
XC3576H工控主板|深度适配Ubuntu 26.04 LTS,释放边缘AI与工业开发新潜能
linux·人工智能·ubuntu·机器人·硬件·主板·边缘ai
Fcy6484 小时前
Linux下 进程间关系与守护进程
linux·运维·服务器·守护进程
码农小韩4 小时前
Linux驱动理论(二)——Linux字符设备驱动
linux·嵌入式软件开发·linux操作系统·linux应用开发·linux驱动理论
well06124 小时前
Linux粘滞位与Makefile机制深度解析
linux·运维·服务器
再写一行代码就下班5 小时前
linux sh脚本在windows修改导致无法使用解决方式
java·linux·centos
额额额对了5 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
子木HAPPY阳VIP5 小时前
Ubuntu 关闭防火墙操作步骤
linux·运维·ubuntu
王振超wzc5 小时前
嵌入式开发环境搭建--WM软件安装,Ubuntu操作系统安装
linux·运维·ubuntu