Linux NVMe Driver学习笔记之3:nvme_probe函数解析
这篇文章紧接上回分解,想知道小编为啥要解析nvme_probe函数,请先移步翻阅上一篇文章:
直接进入主题,打开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_map,nvme_setup_prp_pools,nvme_init_ctrl,nvme_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结构及实例详细解析,请参考:
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,涉及内容较大,放在后续文章慢慢展开解析~
更多推荐
所有评论(0)