内核版本较低的nvme驱动代码不多,而且使用的是单队列的架构,阅读起来会轻松一点。
这个版本涉及到的nvme驱动源码文件一共就4个,两个nvme.h文件,分别在include/linux ,include/uapi/linux目录下,nvme-core.c是主要源码文件,还有nvme-scsi.c,这个文件是scsi协议转nvme协议的。
先从模块入口函数开始吧。
static int __init nvme_init(void)
{
int result;
nvme_thread = kthread_run(nvme_kthread, NULL, "nvme");//创建内核线程,并开始运行
if (IS_ERR(nvme_thread))
return PTR_ERR(nvme_thread);
result = register_blkdev(nvme_major, "nvme");//块设备驱动注册,得到主设备号
if (result < 0)
goto kill_kthread;
else if (result > 0)
nvme_major = result;//返回的主设备号
result = pci_register_driver(&nvme_driver);//注册pci驱动
if (result)
goto unregister_blkdev;
return 0;
unregister_blkdev:
unregister_blkdev(nvme_major, "nvme");
kill_kthread:
kthread_stop(nvme_thread);//停止内核线程的运行
return result;
}
static void __exit nvme_exit(void)
{
pci_unregister_driver(&nvme_driver);
unregister_blkdev(nvme_major, "nvme");
kthread_stop(nvme_thread);//nvme_thread内核线程停止运行
}
MODULE_AUTHOR("Matthew Wilcox " );
MODULE_LICENSE("GPL");
MODULE_VERSION("0.8");
module_init(nvme_init);
module_exit(nvme_exit);
模块入口函数主要干了三件事。
1:创建了一个内核线程,主要作用是处理不满足prp条件时,队列函数提交的bio,这个后面在详细说。
2:注册了一个块设备驱动,作用是得到了一个主设备号,后面是需要这个设备号的。
3:注册了pci驱动,作用大家都知道的。
模块退出函数与模块入口函数做的事情刚好相反,也没什么说的。
接着看probe函数:
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int result = -ENOMEM;
struct nvme_dev *dev;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/*
后续保存中断向量的信息
*/
dev->entry = kcalloc(num_possible_cpus(), sizeof(*dev->entry), GFP_KERNEL);
if (!dev->entry)
goto free;
//queue[0] queue[1]存放的是指针
dev->queues = kcalloc(num_possible_cpus() + 1, sizeof(void *), GFP_KERNEL); //sizeof(void *) 8 or 4
if (!dev->queues)
goto free;
INIT_LIST_HEAD(&dev->namespaces);//初始化链表头,后续将nvme的namespaces以链表的形式链接起来
dev->pci_dev = pdev;
result = nvme_set_instance(dev);//获取一个instance值,用于后面的磁盘名称命名
if (result)
goto free;
result = nvme_setup_prp_pools(dev);//prp地址的申请,即prp list,后续用来保存bio转换出来的dma地址
if (result)
goto release;
result = nvme_dev_start(dev);//admin queue 和 io queu的配置
if (result) {
if (result == -EBUSY)
goto create_cdev;
goto release_pools;
}
result = nvme_dev_add(dev); //和内核块设备提供的函数操作有关
if (result)
goto shutdown;
create_cdev:
//利用miscdev结构体提供一些字符设备的操作(回调函数),用户空间可以下发一些nvme的命令等
scnprintf(dev->name, sizeof(dev->name), "nvme%d", dev->instance);
dev->miscdev.minor = MISC_DYNAMIC_MINOR;
dev->miscdev.parent = &pdev->dev;
dev->miscdev.name = dev->name;
dev->miscdev.fops = &nvme_dev_fops;
result = misc_register(&dev->miscdev);
if (result)
goto remove;
kref_init(&dev->kref);//设备引用计数初始化,值为1
return 0;
remove:
nvme_dev_remove(dev);
shutdown:
nvme_dev_shutdown(dev);
release_pools:
nvme_free_queues(dev);
nvme_release_prp_pools(dev);
release:
nvme_release_instance(dev);
free:
kfree(dev->queues);
kfree(dev->entry);
kfree(dev);
return result;
}
static void nvme_remove(struct pci_dev *pdev)
{
struct nvme_dev *dev = pci_get_drvdata(pdev);
misc_deregister(&dev->miscdev);
//引用计数为0,调用nvme_free_dev函数
kref_put(&dev->kref, nvme_free_dev);
}
static DEFINE_PCI_DEVICE_TABLE(nvme_id_table) = {
{ PCI_DEVICE_CLASS(0x010802, 0xffffff) }, //class满足0x010802就会调用probe函数
{ 0,}
};
MODULE_DEVICE_TABLE(pci, nvme_id_table);
static struct pci_driver nvme_driver = {
.name = "nvme",
.id_table = nvme_id_table,
.probe = nvme_probe,
.remove = nvme_remove,
};
struct nvme_dev结构体定义:
struct nvme_dev {
struct list_head node;
struct nvme_queue **queues;
u32 __iomem *dbs;
struct pci_dev *pci_dev;
struct dma_pool *prp_page_pool;
struct dma_pool *prp_small_pool;
int instance;
int queue_count;
int db_stride;
u32 ctrl_config;
struct msix_entry *entry;
struct nvme_bar __iomem *bar;
struct list_head namespaces;
struct kref kref;
struct miscdevice miscdev;
char name[12];
char serial[20];
char model[40];
char firmware_rev[8];
u32 max_hw_sectors;
u32 stripe_size;
u16 oncs;
};
总结下probe函数干的事情:
1:struct nvme_dev *dev 申请内存,这个结构体就代表一个nvme设备。
2:dev->entry申请内存,大小是num_possible_cpus() * sizeof(*dev->entry),后面用于保存向量的相关信息,因为nvme是支持多队列的,所以后面可以将队列和特定的中断向量进行绑定,这个后面遇到相关代码再说。至于num_possible_cpus()的值是多少,什么意思,也不用研究那么多,主要知道它是一个数值就行了,比如是4.
3:dev->queues这个是一个二级指针,所以它是保存一级指针的值,也就是保存的是struct nvme_queue的地址,上面代码也进行了注释。
4:namespaces的初始化以及dev->pci_dev = pdev赋值
5:nvme_set_instance函数的调用,里面怎么实现的不用管,主要知道调用完它以后,dev->instance得到一个值就行了,这个值用于磁盘的命名,比如在dev目录下看到的/dev/nvmexxx就和这个值有关系。
5:nvme_setup_prp_pools函数调用,主要就是申请一大块内存,然后用于保存prp的地址。
它的函数实现如下:
static int nvme_setup_prp_pools(struct nvme_dev *dev)//放prp指针
{
struct device *dmadev = &dev->pci_dev->dev;
dev->prp_page_pool = dma_pool_create("prp list page", dmadev, PAGE_SIZE, PAGE_SIZE, 0);
if (!dev->prp_page_pool)
return -ENOMEM;
dev->prp_small_pool = dma_pool_create("prp list 256", dmadev, 256, 256, 0); //Optimisation for I/Os between 4k and 128k
if (!dev->prp_small_pool) {
dma_pool_destroy(dev->prp_page_pool);
return -ENOMEM;
}
return 0;
}
只需要知道调用结束以后,dev->prp_page_pool和dev->prp_small_pool不是空就行了,后面用到的时候再解释,为什么这里需要两个值。
6:调用nvme_dev_start
7:调用nvme_dev_add
8:利用miscdev做一些用户层可以用ioctl下发一些操作。
其中,6,7,8是重点,后面会逐一分析。
remove函数就不用多说了,模块退出时资源的释放。
先聊这么多。