这篇文章紧接上回分解,想知道小编为啥要解析nvme_probe函数,请先移步翻阅上一篇文章:

Linux NVMe Driver学习笔记之2:初始化

直接进入主题,打开Pci.c找到nvme_probe函数(因为是真正干活的,所以代码有点长):

static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)

{

int node, result = -ENOMEM;

struct nvme_dev *dev;

      // 通过调用dev_to_node 得到这个pci_dev的numa节点,如果没有制定的话,默认用first_memory_node,也就是第一个numa节点. 

      //关于numa相关介绍请参考: 

node = dev_to_node(&pdev->dev);

if (node == NUMA_NO_NODE)

set_dev_node(&pdev->dev, first_memory_node);

       // 为nvme dev节点分配空间

dev = kzalloc_node(sizeof(*dev), GFP_KERNEL, node);

if (!dev)

return -ENOMEM;

          // 为每个cpu core分配queue。queues为每个core分配一个io queue,所有的core共享一个admin queue。这里的queue的概念,更严格的说,是一组submission queue和completion quque。

         // 关于SQ和CQ的概念请参考: 

dev->queues = kzalloc_node((num_possible_cpus() + 1) * sizeof(void *),

GFP_KERNEL, node); //这里之所以多1,是因为admin-queue

if (!dev->queues)

goto free;

     dev->dev = get_device(&pdev->dev); //增加设备对象的引用计数

pci_set_drvdata(pdev, dev); //为设备设置私有数据指针 

       // 获得PCI Bar的虚拟地址

result = nvme_dev_map(dev);

if (result)

goto free;

      // 初始化两个work变量, 放在nvme_workq中执行

INIT_WORK(&dev->reset_work, nvme_reset_work);

INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work);

setup_timer(&dev->watchdog_timer, nvme_watchdog_timer,

(unsigned long)dev); // 初始化定时器watchdog

mutex_init(&dev->shutdown_lock); // 初始化互斥锁

init_completion(&dev->ioq_wait);  // 初始化完成量

       // 设置DMA需要的PRP内存池

result = nvme_setup_prp_pools(dev);

if (result)

goto put_pci;

      // 初始化NVMe Controller结构

result = nvme_init_ctrl(&dev->ctrl, &pdev->dev, &nvme_pci_ctrl_ops,

id->driver_data);

if (result)

goto release_pools;

       // 打印日志

dev_info(dev->ctrl.device, "pci function %s\n", dev_name(&pdev->dev));

        // 将reset_work放入nvme_workq工作队列

queue_work(nvme_workq, &dev->reset_work);

return 0;

 release_pools:

nvme_release_prp_pools(dev);

 put_pci:

put_device(dev->dev);

nvme_dev_unmap(dev);

 free:

kfree(dev->queues);

kfree(dev);

return result;

}

看完上面的代码,我们总结一下nvme_probe主要做的事情:

  • 为dev,dev->queues分配空间。为每个cpu core分配一个。queues为每个core分配一个io queue,所有的core共享一个admin queue。这里的queue的概念,更严格的说,是一组submission queue和completion queue的集合。

  • 调用nvme_dev_map获得PCI Bar的虚拟地址。

  • 初始化两个work变量,定时器,互斥锁,完成量。

  • 调用nvme_setup_prp_pools设置DMA需要的PRP内存池。

  • 调用nvme_init_ctrl初始化NVMe Controller结构。

  • 通过workqueue调度dev->reset_work,也就是调度nvme_reset_work函数,来reset nvme controller。

我们再接着着重学习一下,在nvme_probe中调用的四个函数:nvme_dev_mapnvme_setup_prp_poolsnvme_init_ctrlnvme_reset_work

1. nvme_dev_map

static int nvme_dev_map(struct nvme_dev *dev)

{

struct pci_dev *pdev = to_pci_dev(dev->dev);

if (pci_request_mem_regions(pdev, "nvme"))

return -ENODEV;

       //将一个IO地址空间映射到内核的虚拟地址空间上去

dev->bar = ioremap(pci_resource_start(pdev, 0), 8192);

if (!dev->bar)

goto release;

return 0;

  release:

pci_release_mem_regions(pdev);

return -ENODEV;

}

继续贴代码,

static inline int

pci_request_mem_regions(struct pci_dev *pdev, const char *name)

{

return pci_request_selected_regions(pdev,

   pci_select_bars(pdev, IORESOURCE_MEM), name);

}

贴完了代码,我们了解到nvme_dev_map的执行过程可以分为三步:

第一步:调用pci_select_bars,其返回值为mask。因为pci设备的header配置空间有6个32位的Bar寄存器(如下图),所以mark中的每一位的值就代表其中一个Bar是否被置起。

第二步:调用pci_request_selected_regions,这个函数的一个参数就是之前调用pci_select_bars返回的mask值,作用就是把对应的这个几个bar保留起来,不让别人使用。 

第三步:调用ioremap。在linux中我们无法直接访问物理地址,需要映射到虚拟地址,ioremap就是这个作用。映射完后,我们访问dev->bar就可以直接操作nvme设备上的寄存器了。但是代码中,并没有根据pci_select_bars的返回值来决定映射哪个bar,而是直接映射bar0,原因是nvme协议中强制规定了bar0就是内存映射的基址。

2. nvme_setup_prp_pools

static int nvme_setup_prp_pools(struct nvme_dev *dev)

{

dev->prp_page_pool = dma_pool_create("prp list page", dev->dev,

PAGE_SIZE, PAGE_SIZE, 0);

if (!dev->prp_page_pool)

return -ENOMEM;

/* Optimisation for I/Os between 4k and 128k */

dev->prp_small_pool = dma_pool_create("prp list 256", dev->dev,

256, 256, 0);

if (!dev->prp_small_pool) {

dma_pool_destroy(dev->prp_page_pool);

return -ENOMEM;

}

return 0;

}

从上面的代码可以了解到nvme_setup_prp_pools,主要是创建了两个dma pool,后面就可以通过其他dma函数从dma pool中获得memory了。prp_small_pool里提供的是块大小为256字节的内存,prp_page_pool提供的是块大小为Page_Size(格式化时确定,例如4KB)的内存,主要是为了对于不一样长度的prp list来做优化。

PRP结构及实例详细解析,请参考:

NVMe系列专题之四:寻址模型PRP和SGL解析

3. nvme_init_ctrl

int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev,

const struct nvme_ctrl_ops *ops, unsigned long quirks)

{

int ret;

ctrl->state = NVME_CTRL_NEW;

spin_lock_init(&ctrl->lock);

INIT_LIST_HEAD(&ctrl->namespaces);

mutex_init(&ctrl->namespaces_mutex);

kref_init(&ctrl->kref);

ctrl->dev = dev;

ctrl->ops = ops;

ctrl->quirks = quirks;

INIT_WORK(&ctrl->scan_work, nvme_scan_work);

INIT_WORK(&ctrl->async_event_work, nvme_async_event_work);

ret = nvme_set_instance(ctrl);

if (ret)

goto out;

ctrl->device = device_create_with_groups(nvme_class, ctrl->dev,

MKDEV(nvme_char_major, ctrl->instance),

ctrl, nvme_dev_attr_groups,

"nvme%d", ctrl->instance);

if (IS_ERR(ctrl->device)) {

ret = PTR_ERR(ctrl->device);

goto out_release_instance;

}

get_device(ctrl->device);

ida_init(&ctrl->ns_ida);

spin_lock(&dev_list_lock);

list_add_tail(&ctrl->node, &nvme_ctrl_list);

spin_unlock(&dev_list_lock);

return 0;

out_release_instance:

nvme_release_instance(ctrl);

out:

return ret;

}

从上面的代码中,我们可以了解到,nvme_init_ctrl的作用主要是调用device_create_with_groups函数创建一个名字叫nvme0的字符设备。这个nvme0中的0是通过nvme_set_instance获得的。这个过程中,通过ida_get_new获得唯一的索引值。

static int nvme_set_instance(struct nvme_ctrl *ctrl)

{

int instance, error;

do {

if (!ida_pre_get(&nvme_instance_ida, GFP_KERNEL))

return -ENODEV;

spin_lock(&dev_list_lock);

error = ida_get_new(&nvme_instance_ida, &instance);

spin_unlock(&dev_list_lock);

} while (error == -EAGAIN);

if (error)

return -ENODEV;

ctrl->instance = instance;

return 0;

}

4. nvme_reset_work

nvme_rest_work是一个很长的work flow,涉及内容较大,放在后续文章慢慢展开解析~

GitHub 加速计划 / nv / nvm
78.06 K
7.82 K
下载
nvm-sh/nvm: 是一个 Node.js 版本管理器,用于在不同的 Node.js 版本之间进行切换。它可以帮助开发者轻松管理多个 Node.js 版本,方便进行开发和测试。特点包括轻量级、易于使用、支持跨平台等。
最近提交(Master分支:2 个月前 )
9c9ff4ba Moved issue template into ISSUE_TEMPLATE folder 9 天前
51ea809d - 9 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐