• 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);

    }

  • 相关阅读:
    反向运算放大器
    torch版本对应的torch_geometric与torch-sprse/cluster/scatter库的正确安装
    UIStackView嵌套的Label多行文字动态高度
    [算法笔记]最长公共连续子串
    vscode基于cmake结果调试运行
    GDB调试多线程代码
    C Primer Plus(6) 中文版 第1章 初识C语言 1.7 使用C语言的7个步骤
    PHP8中调换数组中的键值和元素值-PHP8知识详解
    为全志T507-H开发板配置Samba服务,高效实现跨系统的文件共享
    【Ubuntu22使用过程问题记录】
  • 原文地址:https://blog.csdn.net/zhuzongpeng/article/details/126823359