Linux NVMe Driver学习笔记之1:概述与nvme_core_init函数解析

学习Linux NVMe Driver之前,我们得先了解一下Driver在Linux架构中的位置,如下图. NVMe driver在Block Layer之下,负责与NVMe设备交互。

为了紧跟时代的大趋势,现在的NVMe driver已经很强大了,也可以支持NVMe over Fabric相关设备,如下图。不过,我们这里的学习目前还主要以NVMe over PCIe为主。

小编这里学习的NVMe driver内容是Linux-4.10.6中的相关代码,先看一下NVMe driver的文件目录:包括两个子文件夹: host和target,还有两个文件: Kconfig和Makefile.

关于如何分析driver的内容时,古猫一开始也是不得其法,后得到大神的指点,说:"在分析一个driver时,最好先看这个driver相关的kconfig及Makefile文件,了解其文件架构,再阅读相关的source code". 此时,已经是柳暗花明咯~

 Kconfig 文件的作用是:

1.控制make menuconfig时, 出现的配置选项;

2.根据用户配置界面的选择,将配置结果保存在.config 配置文件(该文件将提供给Makefile使用,用以决定要编译的内核组件以及如何编译)

先看一下nvme/host/kconfig的内容(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/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.

那我们就先从core.c开始学习,打开core.c文件,找到程序入口module_init(nvme_core_init), 

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 class,拥有者(Owner)是为THIS_MODULE

nvme_class = class_create(THIS_MODULE, "nvme");

        //如果有Error发生,删除字符设备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;

}

从上面的code来看,nvme_core_init主要做了两件事:

  1. 调用__register_chrdev函数,注册一个名为"nvme"的字符设备.

  2. 调用class_create函数,动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加到内核中。创建的逻辑类位于/sys/class/.

在注册字符设备时,涉及到了设备号的知识点:

一个字符设备或者块设备都有一个主设备号(Major)和次设备号(Minor)。主设备号用来表示一个特定的驱动程序,次设备号用来表示使用该驱动程序的各个设备。比如,我们在Linux系统上挂了两块NVMe SSD. 那么主设备号就可以自动分配一个数字(比如8),次设备号分别为1和2.

例如,在32位机子中,设备号共32位,高12位表示主设备号,低20位表示次设备号。

主设备号12位次设备号20位

有了上面已经注册的字符设备,我们就可以通过open,ioctrl,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,

        /* 这里的ioctl有两个unlocked_ioctl和compat_ioctl,如果这两个同时存在的话,优先调用unlocked_ioctl,对于compat_ioctl只有打来了CONFIG_COMPAT 才会调用compat_ioctl。obj-$(CONFIG_COMPAT) += compat.o compat_ioctl.o */

};

先查看一下nvme_dev_open的定义:

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;

}

所有nvme设备都会加入nvme_ctrl_list, 这里调用list_for_each_entry遍历nvme_ctrl_list找到与instance次设备号对应的nvme设备,接着检查ctrl->admin_q和ctrl->kref 不能为null,最后找到的nvme设备放到file->private_data区域。

再来看看nvme_dev_release的定义:

static int nvme_dev_release(struct inode *inode, struct file *file)

{

nvme_put_ctrl(file->private_data);

return 0;

}

nvme_dev_release比较简单,就是用来释放open()函数中申请的资源。

最后来看看nvme_dev_ioctl的定义:

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命令

}

}

从上面的code中,我们可以发现在nvme_dev_ioctl 中总共实现了五种Command:

NVME_IOCTL_ADMIN_CMDNVME_IOCTL_IO_CMD是NVMe Spec定义的两种命令:Admin、IO命令,具体定义请参考之前的文章介绍: NVMe系列专题之二:队列(Queue)管理. 有一点需要注意的是,NVME_IOCTL_IO_CMD不支持NVMe设备有多个namespace的情况。

NVME_IOCTL_RESETNVME_IOCTL_SUBSYS_RESET 命令则是通过nvme的ops直接写register来Reset Ioctrl和Ioctrl_subsys。

前者调用的是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);

}

这个nvme的ops名字叫做"nvme_ctrl_ops", 这个ops在nvme初始化中调用nvme_probe()时赋值为"nvme_pci_ctrl_ops".

nvme初始化以及nvme_probe相关内容在后续文章中再展开解析。

看一下nvme_pci_ctrl_ops的定义:

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,

};

看到这里,我们可以发现reset Ioctrl调用的是nvme_pci_reset_ctrl函数,而reset Ioctrl_subsys调用的是nvme_pci_reg_write32函数。

一步一步剖析,先看nvme_pci_reset_ctrl的定义:

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;

}

从上面的定义中,可以看到,通过调用to_nvme_dev获取nvme_ctrl的地址并赋值于nvme_dev. 接着调用nvme_reset执行reset。

再深一步,看nvme_reset的定义:

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 是busy的话,则reset失败,也就是reset的时候nvme controller 必须处于空闲状态,如果处于空闲状态,则调用dev->reset_work 来reset nvme controller,reset_work同样是在nvme初始化中调用nvme_probe()时赋值,赋值的结果是"nvme_reset_work".这个函数比较长,这里就不展开咯~

回过头来,我再看看reset Ioctrl_subsys调用的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的定义(如下),我们发现Ioctrl_subsys的reset就是直接往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);

}

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古猫先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值