本文主要参考2篇相关的解析 1’ 2 和 linux
源码 3。
此处推荐一个可以便捷查看 linux
源码的网站 bootlin
4。
更新:2022 / 02 / 11
驱动 | Linux | NVMe - 1. 概述与nvme_core_init函数解析
- NVMe 的前世今生
- NVMe Command
- PCI 总线
- 从架构角度看 NVMe 驱动
- NVMe 驱动的文件构成
- NVMe Driver 工作原理
- 参考链接
NVMe 的前世今生
NVMe
离不开 PCIe
,NVMe
SSD
是 PCIe
的 endpoint
。PCIe
是 x86
平台上一种流行的外设总线,由于其 Plug and Play
的特性,目前很多外设都通过 PCI Bus
与 Host
通信,甚至不少CPU
的集成外设都通过 PCI Bus
连接,如 APIC
等。
NVMe SSD
在 PCIe
接口上使用新的标准协议 NVMe
,由大厂 Intel
推出并交由 nvmexpress
组织推广,现在被全球大部分存储企业采纳。
NVMe Command
NVMe
Host
( Server
)和 NVMe
Controller
( SSD
)通过 NVMe
Command
进行信息交互。NVMe
Spec
中定义了 NVMe
Command
的格式,占用 64
字节。
NVMe
Command
分为 Admin
Command
和 IO
Command
两大类,前者主要是用于配置,后者用于数据传输。
NVMe
Command
是 Host
与 SSD
Controller
交流的基本单元,应用的 I/O
请求也要转化成NVMe
Command
。
或许可以将 NVMe Command
理解为英语,host和SSD controller分别为韩国人和日本人,这两个 对象
需要通过英语统一语法来进行彼此的沟通和交流,Admin Command
是语法 负责语句的主谓宾结构,IO Command
是单词 构成语句的具体使用词语。一句话需要语法组织语句结构和单词作为语句的填充。
PCI 总线
- 在操作系统启动时,
BIOS
会枚举整个PCI
的总线,之后将扫描到的设备通过ACPI
tables
传给操作系统。 - 当操作系统加载时,
PCI
Bus
驱动则会根据此信息读取各个PCI
设备的PCI Header Config
空间,从class code
寄存器获得一个特征值。
class code
是PCI
bus
用来选择哪个驱动加载设备的唯一根据。
NVMe
Spec
定义的class code
是010802h
。NVMe
SSD
内部的Controller PCIe Header
中class code
都会设置成010802h
。
所以,需要在驱动中指定 class code
为 010802h
,将 010802h
放入 pci_driver nvme_driver
的id_table
,如下所示 5:
static const struct pci_device_id nvme_id_table[] = {
{ PCI_VDEVICE(INTEL, 0x0953), /* Intel 750/P3500/P3600/P3700 */
.driver_data = NVME_QUIRK_STRIPE_SIZE |
NVME_QUIRK_DEALLOCATE_ZEROES, },
......
{ PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x2005),
.driver_data = NVME_QUIRK_SINGLE_VECTOR |
NVME_QUIRK_128_BYTES_SQES |
NVME_QUIRK_SHARED_TAGS |
NVME_QUIRK_SKIP_CID_GEN |
NVME_QUIRK_IDENTIFY_CNS },
{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, nvme_id_table);
之后当 nvme_driver
注册到 PCI
Bus
后,PCI
Bus
就知道这个驱动是给 class code=010802h
的设备使用的。
nvme_driver
中有一个 probe
函数,nvme_probe()
,这个函数才是真正加载设备的处理函数。
从架构角度看 NVMe 驱动
学习 Linux
NVMe
Driver
之前,先看一下 Driver
在 Linux
架构中的位置,如下图所示:
NVMe driver
在 Block Layer
之下,负责与 NVMe
设备交互。
为了紧跟时代的大趋势,现在的 NVMe
driver
已经很强大了,也可以支持 NVMe over Fabric
相关设备,如下图所示:
不过,本文还是以 NVMe over PCIe
为主。
NVMe 驱动的文件构成
最新的代码位于 linux/drivers/nvme/
6。
其文件目录构成如下所示:
nvme ----
在分析一个 driver
时,最好先看这个 driver
相关的 kconfig
及 Makefile
文件,了解其文件架构,再阅读相关的 source code
。
Kconfig
文件的作用是:
- 控制
make menuconfig
时,出现的配置选项; - 根据用户配置界面的选择,将配置结果保存在
.config
配置文件(该文件将提供给Makefile
使用,用以决定要编译的内核组件以及如何编译)
先看一下 linux/drivers/nvme/host/Kconfig
7 的内容(NVMeOF相关内容已省略,后续不再注明),如下所示:
# SPDX-License-Identifier: GPL-2.0-only
config NVME_CORE
tristate
select BLK_DEV_INTEGRITY_T10 if BLK_DEV_INTEGRITY
......
接着,再看一下 linux/drivers/nvme/host/Makefile
8 的内容,如下所示:
# SPDX-License-Identifier: GPL-2.0
ccflags-y += -I$(src)
obj-$(CONFIG_NVME_CORE) += nvme-core.o
obj-$(CONFIG_BLK_DEV_NVME) += nvme.o
obj-$(CONFIG_NVME_FABRICS) += nvme-fabrics.o
obj-$(CONFIG_NVME_RDMA) += nvme-rdma.o
obj-$(CONFIG_NVME_FC) += nvme-fc.o
obj-$(CONFIG_NVME_TCP) += nvme-tcp.o
obj-$(CONFIG_NVME_APPLE) += nvme-apple.o
......
从 Kconfig
和 Makefile
来看,了解 NVMe over PCIe
相关的知识点,我们主要关注 core.c
,pci.c
就好。
NVMe Driver 工作原理
从 core.c
找到程序入口 module_init(nvme_core_init);
,如下所示:
static int __init nvme_core_init(void)
{
int result = -ENOMEM;
......
// 1. 注册字符设备 "nvme"
result = alloc_chrdev_region(&nvme_ctrl_base_chr_devt, 0,
NVME_MINORS, "nvme");
if (result < 0)
goto destroy_delete_wq;
// 2. 新建一个nvme class,拥有者(Owner)是为THIS_MODULE
// 如果有Error发生,删除字符设备nvme
nvme_class = class_create(THIS_MODULE, "nvme");
if (IS_ERR(nvme_class)) {
result = PTR_ERR(nvme_class);
goto unregister_chrdev;
}
nvme_class->dev_uevent = nvme_class_uevent;
nvme_subsys_class = class_create(THIS_MODULE, "nvme-subsystem");
if (IS_ERR(nvme_subsys_class)) {
result = PTR_ERR(nvme_subsys_class);
goto destroy_class;
}
result = alloc_chrdev_region(&nvme_ns_chr_devt, 0, NVME_MINORS,
"nvme-generic");
if (result < 0)
goto destroy_subsys_class;
nvme_ns_chr_class = class_create(THIS_MODULE, "nvme-generic");
if (IS_ERR(nvme_ns_chr_class)) {
result = PTR_ERR(nvme_ns_chr_class);
goto unregister_generic_ns;
}
result = nvme_init_auth();
if (result)
goto destroy_ns_chr;
return 0;
从上面来看,nvme_core_init
主要做了两件事:
- 调用
alloc_chrdev_region
9 函数,如下所示,注册一个名为nvme
的字符设备。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
- 调用
class_create
函数 10,如下所示,动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加到内核中。创建的逻辑类位于/sys/class/
。
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
#endif /* _DEVICE_CLASS_H_ */
在注册字符设备时,涉及到了设备号的知识点:
一个字符设备或者块设备都有一个主设备号(
Major
)和次设备号(Minor
)。主设备号用来表示一个特定的驱动程序,次设备号用来表示使用该驱动程序的各个设备。比如,我们在Linux
系统上挂了两块NVMe
SSD
,那么主设备号就可以自动分配一个数字 (比如8
),次设备号分别为1
和2
。
例如,在32
位机子中,设备号共32
位,高12
位表示主设备号,低20
位表示次设备号。
有了上面已经注册的字符设备,我们就可以通过 open
,ioctrl
,release
接口对其进行操作了。
nvme
字符设备的文件操作结构体 nvme_dev_fops
11 定义如下:
static const struct file_operations nvme_dev_fops = {
.owner = THIS_MODULE,
.open = nvme_dev_open,
.release = nvme_dev_release,
.unlocked_ioctl = nvme_dev_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.uring_cmd = nvme_dev_uring_cmd,
};
参考链接
Linux中nvme驱动详解 ↩︎
Linux NVMe Driver学习笔记之1:概述与nvme_core_init函数解析 ↩︎
linux ↩︎
bootlin ↩︎
linux/drivers/nvme/host/pci.c ↩︎
linux/drivers/nvme/ ↩︎
linux/drivers/nvme/host/Kconfig ↩︎
linux/drivers/nvme/host/Makefile ↩︎
linux/fs/char_dev.c ↩︎
linux/include/linux/device.h ↩︎
linux/drivers/nvme/host/core.c ↩︎