浅谈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 sn[20]; // 序列号

char mn[40]; // 型号

__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 lbaf[16]; // 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 驱动初始化过程中最关键的数据采集手段,直接决定了后续队列创建、命名空间管理及块设备注册的准确性。

相关推荐
AOwhisky2 小时前
Linux防火墙管理指南
linux·运维·服务器
礼拜天没时间.2 小时前
Linux 系统规范配置:建立标准目录结构、 repo 源获取、修改终端变色
linux·服务器·centos·repo·终端变色
liqb3652 小时前
RUN_TO_PARITY特性对调度延时的影响
linux
Ephemeral Memories2 小时前
ubuntu安装软件失败以及运行闪退
linux·ubuntu
网安CILLE2 小时前
PHP四大输出语句
linux·开发语言·python·web安全·网络安全·系统安全·php
ghostmen2 小时前
openEuler 安装 K3S
linux·k3s
RisunJan3 小时前
Linux命令-iptables(配置防火墙规则的核心工具)
linux·运维·服务器
KL's pig/猪头/爱心/猪头3 小时前
写一个rv1106的led驱动1-整体架构
linux·驱动开发
叁金Coder3 小时前
【CentOS-Stream-9 配置网卡信息】
linux·运维·centos