阿呆实战NVMe 1-6合集

原创内容,转载请注明:  [http://www.ssdfans.com]  谢谢!

 


 

 

 

看完了蛋蛋的NVMe系列,你是不是有点手痒痒了,毕竟这是一门技术,不是艺术,只能欣赏,却不能亵玩,实在是太不让人尽兴了。三俗的来说,估计你也是看了上面的女神进来看文章的,但是,是不是下面的苍老师才能让你真正动手实践?

 

 

八卦蛋蛋

 

作为ssdfans的招牌,蛋蛋现在已经有了很多粉丝。你估计也想知道蛋蛋到底是个什么样的人,阿呆有幸略知一二。首先,蛋蛋博士真的是一位博士,饱读诗书,而且智商也很高。肯定有人要问了,你怎么知道他智商高呢,把跑分亮出来看看。其实阿呆也没跑过分,只知道蛋蛋家族有高智商基因,出了不少高考状元。乡亲们因为有了这样一家邻居,教育孩子不得不格外卖力气: 天赋不如人已经输在了起跑线上,后天可得加倍努力啊!

 

阿呆以前搞不懂NVMe,都是英文文档,好不容易看完也不能消化,花个把星期很多也是云里雾里。但是,看了蛋蛋写的NVMe系列,通俗易懂,最多半天就能掌握精髓。想到这里,阿呆不得不像古人一样发出慨叹:

 

天不生蛋蛋,万古长如夜!

 

QEMU

 

好了,后面我们不扯蛋了,言归正传。尽管阿呆想动手实践NVMe,可是舍不得花钱买昂贵的PCIe SSD练手,而且买了也是人家的控制器,自己怎么在上面搞开发啊,真是愁死人了。后来,阿呆想到一个好主意:真的玩不起,咱还不能搞个假的吗?果然找到一个免费的操作系统Linux上免费的虚拟机软件QEMU。估计很多人玩过VMWare,QEMU差不多,只不过是开源的,所以手闲的人可以在上面搞开发。如下图,在Linux系统上用QEMU虚拟了一个Windows系统。

 

说到这里,不得不提一下大名鼎鼎的KVM了。总是听人家讲什么虚拟化,KVM之类的,听起来挺时髦的,那KVM到底是个什么东东? 现在所说的虚拟化,一般都是指在CPU硬件支持基础之上的虚拟化技术,就是有些CPU是支持虚拟化指令的。KVM也一样依赖此项技术,全称Kernel-based Virtual Machine,其实就是虚拟了一个Linux内核,而且通过CPU的专有虚拟化指令可以达到很高的效率。但是我们知道Linux操作系统分为内核层和用户层,光有KVM这个内核层,我们还跑不起来任何图形界面等用户应用。KVM就相当于是开发商,只会盖房子,而且盖的非常坚固,但是盖好房子发现都是毛坯,不能住人,这时候他们发现了另一个开发商QEMU,他们也盖房子,还搞装修,装修的本领很不错,盖得房子实在不咋地。于是,这两家开发商战略合作,KVM负责盖房子,QEMU负责搞装修,人们交了房就能住进精装修的房子了,又坚固又漂亮好用。所以现在往往是KVM+QEMU搞一个虚拟机,KVM是内核态,QEMU是用户态。如下图。

 

 

说来惭愧,阿呆的知识其实都是百度上搜的。如果你想更深入地了解KVM,就得拜师了,上哪里找老师呢?哈哈,远在天边,近在眼前,我们藏龙卧虎的ssdfans微信群里就有位大牛,西山居的系统运维经理肖力,大名鼎鼎的《深度实践KVM》就是他写的。

 

云计算与虚拟化

 

总是听人说云计算,虚拟化,为什么他俩关系这么铁?阿呆也不懂内情,但是知道云计算公司搞了很多虚拟机,因为我们www.ssdfans.com网站就是买了阿里云的ECS服务,所谓的ECS弹性云计算其实就是用虚拟机实现的。我们传统企业建设自己的机房,要配置很多电脑,交换机,存储阵列等,来搭建网站,实现邮件服务器,内部OA等服务。到了云计算这里,还是买很多机器,只不过这些机器都是虚拟的,所谓弹性就是系统的CPU,内存,硬盘,带宽等资源都是可以配置的,谁叫人家是虚拟的呢,玩虚的就是任性啊。

 

不过最近总是有读者反映ssdfans网站从手机访问缓慢,电脑却还可以接受,阿呆还没好好看看是什么原因,希望有专家能够指点迷津。我们网站使用的是wordpress,文件存放在阿里云ECS,数据库采用了阿里云的RDS,服务器只有杭州的,还没有买全国各地的CDN加速。

 

CPU的虚拟化指令

 

后面阿呆要开始无耻的大段摘抄《深度实践KVM》了, 嘿嘿,广告也不是白做的~~如下图,X86的指令集一般分为4种特权模式:Ring 0,Ring 1,Ring 2,Ring 3,操作系统一般是Ring 0,应用程序Ring 3,驱动程序Ring 1,Ring 2.可是到了虚拟机就遇到问题了:你敢让虚拟机的Ring 0指令真的跑在真实CPU的Ring 0模式下吗?毕竟都是虚拟的,搞不好就出问题了,但如果都跑在Ring 3模式下,那白白浪费了CPU的这个设计,性能又上不去了。

 

 

所以Intel等CPU厂商推出了虚拟化指令。Intel的VT-x技术推出了两种操作模式:VMX root operation,VMX non-root operation。我们知道虚拟机包含两部分,虚拟层VMM(virtual machine monitor)和虚拟出来的系统,虚拟层其实就是我们物理系统里的一个应用,所以它跑在VMX root operation,和正常模式一样。而虚拟出来的系统跑在VMX non-root operation,是处在VMM控制之下的一个环境。这样虚拟机跑在自己的CPU轨道里,物理机也跑在自己的原来的CPU轨道里,相互隔离,性能也得到了提高。

 

 

这就像是孩子长大娶了老婆,和父母住在一起,生活已经是两家人了,可是共用厨房和厕所,首先经常上厕所得排队,憋得受不了,其次,小两口的隐私也得不到保护。老爹没办法,可怜天下父母心啊,又花钱盖了一套厕所和厨房,大家同在屋檐下,再也不冲突了,相安无事。

 

I/O虚拟化

 

一家人和睦了一段时间之后,新的问题又出现了。小两口消费观念新潮,总是需要买各种东西,但是又没时间去,所以只能让爹妈每天去超市代买,垃圾也让二老去倒。老人嫌累,小两口嫌他们少买了这个那个。老父亲好好想了想,找出症结了:尽管我们空间分开了,但是生活的原材料还没分开。后来,小两口不从超市购物了,他俩的东西全都选择网购,更自由,更方便,垃圾让快递小哥顺手帮忙带走,什么都搞定了。这就是I/O虚拟化啊:两家人的消费和废物处理隔离。

 

我们都知道,影响电脑性能的主要是CPU和内存,玩游戏还要显卡,下电影要网速和硬盘快。虚拟机也有这些问题,CPU虚拟化提升了处理能力,但是数据操作也需要高性能啊,关键在于解决I/O设备与虚拟机数据交换的问题,而这部分主要相关的是DMA直接内存存取,以及IRQ中断请求,只要解决好这两个方面的隔离、保护以及性能问题,就是成功的I/O虚拟化。

 

慈祥的老父亲Intel开发了VT-d技术,在北桥(现在改叫MCH,因为南桥退休了)提供DMA虚拟化和IRQ虚拟化。传统的IOMMU(memory management unit,内存管理单元)集中管理所有DMA,不容易实现DMA隔离,而VT-d实现了多个DMA保护区域的存在,实现DMA虚拟化。

 

传统设备中断请求有两种方式:一是通过I/O中断控制器路由,另一种是DMA写请求直接发出去的MSI(message signaled interrupt,消息中断)。由于使用了DMA,需要访问所有内存地址,没办法实现中断隔离,其实就是虚拟机的没办法区分。VT-d的中断重映射架构重新定义了MSI的格式,尽管MSI依然是DMA写请求,但是不嵌入内存地址,而是消息ID,通过消息ID区分不同的虚拟机区域。

 

啰啰嗦嗦说了这么多,怎么还没进入NVMe这个主题啊,没办法,阿呆就是这么个人,做足了前戏,慢慢的进入。

 

后面阿呆将会带你一起在QEMU虚拟机虚拟出一个NVMe设备,同时,在虚拟机Linux操作系统中开发一个NVMe驱动来使用它。

 

引用

 

肖力等,《深度实践KVM》

http://wenku.baidu.com/view/79bbd89851e79b89680226cb.html

 


 

从前,计算机还是奢侈品,有一位屌丝程序员叫蛋蛋,尽管他每天开发电脑软件,但是码砖的住不起房子,码代码的买不起电脑。对蛋蛋来说,买个电脑跟愚公移山一样难,可是他太喜欢电脑了,怎么办?蛋蛋终于想出了一个主意:真的买不起,还不能搞一个假的吗?蛋蛋的想法是用软件模拟一个电脑,把所有的硬件都用软件来虚拟。隔壁Cubicle的程序员智叟听说了之后,讥笑他:”蛋蛋啊,你也太傻了,微软的软件高管说成熟的工程师一年只能写4000行代码,你什么时候才能模拟出那台电脑?”蛋蛋斩钉截铁地回答:”我死了,还有我的儿子,儿子死了还有孙子,子子孙孙无穷无尽地码代码,就能开发成功!”蛋蛋想出了一个子子孙孙继承事业的好办法,他用了面向对象的技术,自己只是写了顶层的几个类,确定好框架,后来的人想要加个新的硬件进来,就一层层继承各种类,完善接口就能使用了。没想到,子孙还没接班,其他世界各地的程序员也加入进来帮忙,很快就开发成了这个著名的虚拟机——QEMU。

 

记得以前有位同学说他去一家牛公司面试,一道面试题就是C语言怎么开发面向对象的代码?我听了这道题觉得还是有点难度,不过没好好细想。如今看了QEMU的代码,才发现这就是C语言实现面向对象的绝佳例子啊!他如果当初看过QEMU的源码,估计能和面试官谈笑风生了。当然,本文的主要目的还是为了介绍NVMe在QEMU中的数据结构,教你面试题是副产品。代码不是阿呆自己写的,要那样我就牛了,QEMU的NVMe虚拟机代码是GIT上的一个开源项目https://github.com/nvmeqemu谁都可以下载下来玩。

 

面向对象三大特点

 

如上图,我们知道面向对象有三大特征:

  1. 封装:这个好理解,就是在一个struct/class里面把各种变量,函数都打包塞进来。这个大部分人很快就能想到,很多宣称面向对象的C语言代码其实只是实现到了这一步。比如阿呆之前写的大话EXT4文件系统,Linux内核的VFS说是面向对象设计,其实就是做了个封装而已。
  2. 继承。继承的意思就程序员太懒了,要开发一个机器猫,想起以前搞过猫的对象,所以就让机器猫把猫这个对象包起来,这样猫对象所有的属性和接口都被继承了。再加一些机器猫独有的属性和接口,代码搞定。猫是父类,机器猫是子类,还可以继续子子孙孙,繁衍不息。这个咱C语言也有办法,就真的在struct里面包一个父类的struct不就完了,只不过纯手工,麻烦一点。
  3. 多态。
    1. 重载:就是函数名一样,参数不一样。这个比较好搞定,编译的时候编译成不一样的函数名就可以了。
    2. 覆盖:父类和子类同样名称和参数的函数,子类重新定义覆盖父类的。执行的时候,如果子类对象赋给父类指针,还得执行子类的新的函数,这个叫做动态绑定。不太好弄,需要包含一个表格来实时查询。

我们下面来看看NVMe的数据结构,顺便看看QEMU实现了面向对象的哪些功能。

 

QEMU面向谁的对象?

 

下图是一个PIT设备在QEMU中的结构,很明显,是个继承的关系,子类包含了一个父类的对象,继承了父亲的一切(其实应该是母亲的一切,谁叫码农是男的居多呢,parent自然翻译成了父亲。但是生物上来说,那些没有两性生殖的生物,一切基因应该是来自于母亲。),不像我们两性生物,父母各取一半。不过新版的QEMU已经有点区别了,我看代码,NVMe设备继承向上到了DeviceState就结束了。

 

QEMU里面的设备分为三类:

Block是Linux的块设备,比如磁盘IO之类,Device就是各种外设,比如PCIe设备,Machine是CPU等的虚拟。NVMe是一个PCI设备,所以是Device类型。描述NVMe设备的对象叫NVMEState,继承关系为:

 

 

 

那一个子类在QEMU中如果做到初始化?其实是要一级级初始化的,QEMU用一种递归的方法,如果子类的父类还没有初始化,那就先初始化父类,调用父类的构造函数。

 

NVMe设备怎么定义

 

尽管说NVMe设备的对象是NVMEState,但是nvme.c里面只看到了一个NVMe相关的对象定义:

那这个nvme_info是个什么东东?咱们且听下回分解。

 

引用

 

https://github.com/nvmeqemu

QEMU设备模拟,mnstory.net

 


 

 

有人说阿呆你搞个虚拟的东西来玩NVMe,看起来是很爽,代码随便写,然并卵有个什么鸟用呢?其实在实际产品研发中还是有点用的,QEMU最大的好处是可以用GDB Debug。一般实体机如果发生了Panic,只能通过Linux Kernel Panic Dump的方式查看堆栈,找原因,有些藏得比较深的问题比较难发现。而QEMU虚拟机在出错的时候会停下来,就能直接查看出错时各个变量的现场值,找到Root Cause。你如果开发了Linux NVMe驱动,可以用QEMU Debug,也能做一些简单的测试。

 

我们上回说到,蛋蛋定义了QEMU的一套顶层架构,这样后来的人加新的硬件进来,就可以直接套用。本文就通过NVMe设备的追根溯源来看看QEMU使用了何种巧妙的架构来驾驭那么多复杂的硬件。

 

怎样在QEMU注册一个NVMe设备?

 

上回说nvme.c里面定义了一个PCIDeviceInfo对象nvme_info,如下图,包含了构造和析构函数,还有其他配置与接口赋值。那这个对象是怎样在QEMU之中被使用的呢?

 

 

如下面代码,nvme_register_devices注册了一个PCI设备nvme_info。然后,用一个宏device_init来声明这个注册函数。看得出来,奥秘就在这个宏里面。

 

 

如下,可以看到device_init宏变成了module_init,而这个module_init宏是个有constructor attribute的函数,起什么作用呢?在GCC中,这两个编译的attribute用于修饰某个函数, 经过constructor属性修饰过的函数, 可以在main函数运行前就先运行完毕, 同理destructor在进程exit之前执行。相当于构造和析构函数。要知道Linux分为内核态和用户态,内核态的程序都是通过modprobe .ko文件形式加载,没有main函数一说。而用户态都是从main函数进来的,而虚拟机本身是跑在用户态的,所以也是从main函数进来。不过,module_init宏声明的函数在main函数之前就执行了,就是为main函数做一些配置工作。

 

我们说QEMU把硬件分成了Block,Device和Machine三种,最终三种硬件的注册函数都汇总到了一个函数:register_module_init(void (*fn)(void), module_init_type type).

 

于是,我们又来一探这个通用的注册函数干了些什么。如下面的代码,功能非常简单,就是每种硬件类型,在QEMU中都有一个设备链表,链表中每个节点的内容就是这个设备的注册函数。所以,在QEMU中注册一个设备就是把这个设备的ModuleEntry挂到链表的尾巴上。

 

 

QEMU初始化

 

NVMe设备的注册是完成了,非常的简单。那么QEMU又怎么来使用这个注册的设备呢?请你回过头看看,其实折腾了半天,只是在main函数调用之前把nvme_register_devices这个函数注册到了device设备的链表里面,真正的NVMe初始化还没弄呢。不过别着急,NVMe设备初始化肯定是和nvme_register_devices函数内容分不开了。那我们就来看看这个函数做了什么。

看起来是给继承的DeviceInfo qdev赋初值。下面是PCIDeviceInfo的定义,包含了构造和析构函数,PCI设备的config读写接口,还有vendor id等参数。

拿这些注册的DeviceInfo的init函数到底什么时候调用?要找到答案,我们就得去看看main函数做了些什么,毕竟注册PCIDeviceInfo对象是在main之前完成,那main肯定不会不用注册的结果。QEMU包含了很多main函数,因为有很多工具,但是真正的main函数位于Vl.c。

首先初始化最核心的Machine设备,CPU之类的。

中间有1000多行代码不知道在干嘛,反正还没轮到Device设备。直到1000多行以后,Device设备才被初始化。

但是当我们看到module_call_init函数的内容,还是很失望,它只是调用了e->init()。

e->init()是什么,请翻到本文开头,nvme_register_devices函数就是e->init()!!是不是觉得搞了半天,又回来了。悲哀啊,怎么这么麻烦啊,那本文开头的nvme_info的那些init函数到底什么时候调用?等下回阿呆搞明白了再告诉你。

 

引用

 

https://github.com/nvmeqemu

 


 

蝶恋花 【宋】欧阳修

 

庭院深深深几许?杨柳堆烟,帘幕无重数。玉勒雕鞍游冶处,楼高不见章台路。

雨横风狂三月暮,门掩黄昏,无计留春住。泪眼问花花不语,乱红飞过秋千去。

 

看完前面一篇,我相信你的心情就跟欧阳修这首《蝶恋花》中说的一样,感到:”庭院深深深几许?杨柳堆烟,帘幕无重数。”杨柳依依,一重又一重堵在前面,像一层层帘幕一样,我们翻了这么多层代码,根本看不到NVMe初始化在哪里( ▼-▼ )

 

 

再探DeviceInfo

 

读者君,还记得NVMe设备的注册函数吗?我们再来过一遍,

 

首先有一个static类型的全局变量nvme_info,是个PCIDeviceInfo,内容为

接着,通过一连串我们上文中发现的初始化流程,下面这个函数在main函数中通过调用module_call_init(MODULE_INIT_DEVICE)而被执行。所以我们再来一级级深入剖析下面这个函数:

PCIDeviceInfo对象nvme_info通过pci_qdev_register注册,而PCIDeviceInfo继承了DeviceInfo类,

所以类似,函数pci_qdev_register就是把这个pci设备的父对象info->qdev通过qdev_register函数注册。

如下,nvme_info的父对象DeviceInfo添加到了全局变量device_info_list当中,这个是所有device设备的链表。后面就好办了,我们只要查找这个全局变量调用的代码,就能找到初始化的地方了。

 

 

离真相只有几步

 

功夫不负有心人,阿呆终于搞清楚了来龙去脉,下面就是揭晓奇迹的时刻。上文中我们看到main函数调用了

但是,没有继续留意后面的代码,其实再看几十行,就有一段有点不太直接的代码。

这段代码就是device设备初始化的地方,人世间很多事都是这样,往往我们在离真相很近的时候提前放弃了,等到花了大工夫搞明白之后,才追悔莫及。文科生就不像我们理科生这么纠结,他们能用文学化悲痛为美好:众里寻他千百度,蓦然回首,那人却在灯火阑珊处。

 

那么问题来了,qemu_opts_foreach和qemu_find_opts都是来干嘛的?这里我们需要来看一条QEMU启动的命令:

 

qemu-system-x86_64 –enable-kvm -cpu host -smp cores=4,threads=2,sockets=4 -m 16384 -k en-us -hda /pps/guohongwei/vm_test/ubuntu.img -monitor stdio -device nvme

 

这里启动了QEMU,并且配置了各种外设,使用了KVM内核,CPU和内存配置,硬盘镜像,最后是device设备,加载了nvme设备。上面的两个函数qemu_opts_foreach(qemu_find_opts(“device”), …)就是从参数列表中找到device设备,并且遍历。

 

这里遍历了注册的Device设备,通过device_init_func一个个初始化。我们来看初始化流程。

qdev_device_add函数首先通过qdev_find_info查询,从我们前面看到的DeviceInfo注册的链表device_info_list中查到要初始化的device。我们翻到开头nvme_info的初始值就知道,nvme_info把里面DeviceInfo的name初始化为”nvme”,所以qdev_find_info就可以找到”-device nvme”对应的DeviceInfo。

得到DeviceInfo之后,创建DeviceState对象qdev。

首先来看qdev_create_from_info,创建了DeviceState,真的吗?真的仅仅是创建了一个DeviceState对象吗?注意这里mallocz的大小是info->size,而开头的时候,DeviceInfo的size变量我们给的是sizeof(NVMEState)!也就是说,其实这里创建的是一个NVMEState对象!并且给props赋初始值。那这些props是怎么来的?

请再回开头看看nvme_info的初始化,尾巴上有下面一段,通过Property结构体,给定了初始值。通过这种方式给NVMEState对象的变量赋初值。num_namespace和ns_size都是NVMEState类的成员。

create之后,再来看后面的qdev_init函数,其实就是调用了DeviceInfo里面的init函数。

请往前翻pci_qdev_register函数内容,就知道DeviceInfo里的init函数是pci_qdev_init。所以,我们又来看它里面做了什么。一进来,就通过container_of宏得到DeviceInfo的子类PCIDeviceInfo。为什么,因为我们在PCIDeviceInfo里面有一个对象是DeviceInfo qdev,知道了二者的偏移关系,就可以qdev的内存地址减去偏移量得到PCIDeviceInfo的内存地址,就是对象的指针。container_of这个宏就是把这堆复杂的计算弄成了一个宏。在linux编程中还是很常见的,尤其是已知struct内部变量地址,要计算struct地址的时候。

还有,DeviceState指针直接转成了PCIDevice指针,为什么可以这样?来看看PCIDevice的定义:第一个变量就是DeviceState,很巧妙吧,这样就能做到子类到父类指针的轻松切换。为了面向对象,QEMU真是煞费苦心啊。

接着,又调用了info->init函数,不过请注意,这里的info已经变成了PCIDeviceInfo,所以此init非彼init了。

我们再回过头看nvme_info的初始化,就知道init函数是pci_nvme_init。看到这里,憋了很久的阿呆忍不住大吼一声:”终于轮到NVME的初始化了!”

 

是啊,看了三篇代码,理清了QEMU买下的一个个,才轮到NVMe出场,不容易啊!下期我们就来看看NVMe的初始化,不过请先复习蛋蛋的《蛋蛋读NVME之X》系列文章。

 

本文以一首北宋词开头,再以一首南宋词结尾,来表达阿呆此刻的心情。

 

青玉案·元夕 【宋】辛弃疾

 

东风夜放花千树。更吹落、星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。

蛾儿雪柳黄金缕。笑语盈盈暗香去。众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。

 

引用

 

https://github.com/nvmeqemu

 


 


 

提要

 

我们知道,一般的SSD控制器里面分为前端,核心,后端,如上图是希捷收购的SandForce SF3700控制器的架构,三星不少SSD主控也是类似,三个部分分别用三个CPU管理。功能分别为:

  • 前端:实现SATA,PCIe等物理电路和ATA,NVMe等协议。
  • 核心:FTL,垃圾回收等等SSD核心算法。
  • 后端:RAID,ECC,NAND Flash接口实现。

所以,跟NVMe相关的是前端部分,从本文开始,我们正式进入NVMe内容,开发一个NVMe SSD控制器的前端协议逻辑。

 

前戏

记得去年阿呆家小呆呆还没出生,有人忽悠阿呆说胎教很管用。阿呆开始每天背唐诗宋词,晚上睡前隔着妹子肚皮背给娃听。不知道他有没有听进去,阿呆却发现原来诗词意境这么美,夜里躺床上听到雨滴落在别人家的雨篷上,滴滴答答,整个世界在春雨声中安静了,从耳朵到内心。我就想起老年的陆游进京面见宋孝宗,在客店里听了一夜的春雨,折腾了大半辈子,他已经不相信有人会派他去收复河山了,反而闲情逸致写了首诗,果然宋孝宗找他也没什么事。不过那时候的人重视文化,陆游写的这首诗很快传遍了临安,传进了皇宫,宋孝宗反复诵读,赞叹不已。所以我建议各位读者晚上回家少刷朋友圈,看看诗词,这样睡得更香,第二天看ssdfans就有精神。

 

临安春雨初霁

年代: 宋 作者: 陆游

世味年来薄似纱,谁令骑马客京华。

小楼一夜听春雨,深巷明朝卖杏花。

矮纸斜行闲作草,晴窗细乳戏分茶。

素衣莫起风尘叹,犹及清明可到家。

 

最近老是贴代码,有点枯燥,蛋蛋没有工作后休息的这几周ssdfans公众号订阅人数增量看来不能达标了,希望大家多看看精华文章,分享一下蛋蛋的精品。不过别担心,往后不会贴那么多代码了(这是真的吗?)。

 

不知上次阿呆提醒之后,你有没有扫一下《蛋蛋读NVMe之x》系列文章,尤其是最后一篇。从本篇起我们要进入NVMe相关的内容了,冒出个名词你得有心理准备,比如namespace,这个是《蛋蛋读NVMe之六》里面提到的。说牛逼一点,我们要开始做一件激动人心的事:开发一个NVMe Controller了!尽管这个过程比较枯燥,但是只要你耐心看完,就一定有收获。毕竟理论联系实践才是王道。

 

NVMEState

 

今天的任务不重,只是看看NVMEState这个NVMe设备的数据结构。治大国如烹小鲜,NVMe协议很庞杂,但只要我们读透NVMe的数据结构这把刀,后面看代码就如庖丁解牛般游刃有余。

 

typedef
struct NVMEState {

PCIDevice dev;


int mmio_index;


void
*bar0;


int bar0_size;

uint8_t nvectors;

 


/* Space for NVME Ctrl Space except doorbells */

uint8_t *cntrl_reg;


/* Masks for NVME Ctrl Registers */

uint8_t *rw_mask;
/* RW/RO mask */

uint8_t *rwc_mask;
/* RW1C mask */

uint8_t *rws_mask;
/* RW1S mask */

uint8_t *used_mask;
/* Used/Resv mask */

 


struct nvme_features feature;

 

NVMEIOCQueue cq[NVME_MAX_QS_ALLOCATED];

NVMEIOSQueue sq[NVME_MAX_QS_ALLOCATED];

 

DiskInfo *disk;

uint32_t ns_size;

uint32_t num_namespaces;

uint32_t instance;

 

time_t start_time;

 


/* Used to store the AQA,ASQ,ACQ between resets */


struct AQState aqstate;

 


/* TODO

* These pointers have been defined since

* present code uses the older defined strucutres

* which has been replaced by pointers.

* Once each and every reference is replaced by

* offset from cntrl_reg, remove these pointers

* becasue bit field structures are not portable

* especially when the memory locations of the bit fields

* have importance

*/

NVMECtrlCap *ctrlcap;

NVMEVersion *ctrlv;

NVMECtrlConf *cconf;
/* Ctrl configuration */

NVMECtrlStatus *cstatus;
/* Ctrl status */

NVMEAQA *admqattrs;
/* Admin queues attributes. */

 

QEMUTimer *sq_processing_timer;

int64_t sq_processing_timer_target;


/* Used for PIN based and MSI interrupts */

uint32_t intr_vect;


/* Page Size used by the hardware */

uint32_t page_size;


/* Pointer to Identify Controller Strucutre */

NVMEIdentifyController *idtfy_ctrl;


/* Pointer to Firmware slot info log page */

NVMEFwSlotInfoLog fw_slot_log;

uint8_t last_fw_slot;

 

uint8_t temp_warn_issued;

 

QEMUTimer *async_event_timer;

 

uint16_t async_cid[ASYNC_EVENT_REQ_LIMIT +
1];

uint16_t outstanding_asyncs;

 

QSIMPLEQ_HEAD(async_queue, AsyncEvent) async_queue;


/* Masks for async event requests */

uint8_t err_sts_mask;
/* error status event mask */

uint8_t smart_mask;
/* smart/health status event mask */

} NVMEState;

 

一个个来看。

  • PCIDevice dev
    • NVMe所继承的PCI设备对象。

       

  • int mmio_index
    • NVMe设备在QEMU内存中的索引。

       

  • void
    *bar0

    • PCI设备BAR空间的地址。NVMe设备中的寄存器,doorbell(host有事了就按门铃告诉设备:您有新短消息,请注意查收~)都通过这段内存地址实现。

       

  • int bar0_size
    • bar0空间大小。

       

  • uint8_t nvectors
    • 这个其实就是NVMe的队列个数。

       

  • uint8_t *cntrl_reg

顾名思义,这就是NVMe的控制寄存器指针了。后面四个小兄弟是寄存器的MASK,啥意思,就是每个bit加了个限制。

uint8_t *rw_mask;
/* RW/RO mask */
可读写或只读

uint8_t *rwc_mask;
/* RW1C mask */
1清零

uint8_t *rws_mask;
/* RW1S mask */
11

uint8_t *used_mask;
/* Used/Resv mask */
是否在用

  • struct nvme_features feature;
    • NVMe设备的各种特征值。

struct nvme_features {

uint32_t arbitration;

uint32_t power_management;

uint32_t LBA_range_type;
/* uses memory buffer */

uint32_t temperature_threshold;

uint32_t error_recovery;

uint32_t volatile_write_cache;

uint32_t number_of_queues;

uint32_t interrupt_coalescing;

uint32_t interrupt_vector_configuration;

uint32_t write_atomicity;

uint32_t asynchronous_event_configuration;

uint32_t software_progress_marker;

};

 

  • NVMEIOCQueue cq[NVME_MAX_QS_ALLOCATED];

NVMEIOSQueue sq[NVME_MAX_QS_ALLOCATED];

  • CQ和SQ,SQ是host发命令的队列,CQ是NVMe回结果的队列。

     

  • DiskInfo *disk;

    既然我们虚拟的是一个NVMe SSD,那么里面肯定是有磁盘的,这个就是虚拟磁盘的数据结构。其实这个磁盘是放在文件里面,DIskInfo里有文件指针,读写偏移,还有比如我们熟悉的Identify页内容等。

     

  • uint32_t ns_size;

uint32_t num_namespaces;

namespace大小和个数。不知道namespace为何物的请查看精华文章,查看《蛋蛋读NVMe之六》。

 

  • uint32_t instance;

    注明这是第几个NVMe设备。

     

  • time_t start_time

    启动时间,用来算SMART Log里面的上电时间。

 

  • struct AQState aqstate;

    控制器reset之前保存Admin队列AQA,ASQ,ACQ的状态。

 

  • NVMECtrlCap *ctrlcap;

NVMEVersion *ctrlv;

NVMECtrlConf *cconf;
/* Ctrl configuration */

NVMECtrlStatus *cstatus;
/* Ctrl status */

NVMEAQA *admqattrs;
/* Admin queues attributes. */

这是NVMe设备5个寄存器的指针,设备初始化的时候把寄存器内存地址赋给它们。不过注释里也说了,这种指针表示法可移植性差。以后会直接用控制寄存器的偏移来访问。

 

  • QEMUTimer *sq_processing_timer;

int64_t sq_processing_timer_target;

sq_processing_timer是处理SQ所用的timer,里面注册了回调函数,sq_processing_timer_target是触发timer的时间,当时间到了target之后,timer里面注册的回调函数就会被调用。timer的用途很广,比如让NVMe控制器定期处理SQ队列里的新命令,就可以每次检查完把target设置为一定时间以后再次触发,这样无穷无尽循环下去。

 

  • uint32_t intr_vect;

    NVMe设备中断向量。

 

  • uint32_t page_size;

    NVMe控制器和Host数据交互的页大小。

 

  • NVMEIdentifyController *idtfy_ctrl;

NVMe控制器Identify Controller的指针。里面就是设备相关的一些参数,比如vendor id, subsystem vendor id等,具体定义请参考NVM Express 1.0b Chapter 5.11 Identify command。

 

  • NVMEFwSlotInfoLog fw_slot_log;

    uint8_t last_fw_slot;

NVMe控制器Firmware slot info log的指针。里面是固件相关的一些参数,比如固件版本。last_fw_slot是上次固件的slot值。

 

  • uint8_t temp_warn_issued;

    NVMe控制器温度预警了吗?

 

  • QEMUTimer *async_event_timer;

    uint16_t async_cid[ASYNC_EVENT_REQ_LIMIT +
    1];

    uint16_t outstanding_asyncs;

    QSIMPLEQ_HEAD(async_queue, AsyncEvent) async_queue;

     

    这又是一个timer,搞虾米用的呢?顾名思义,NVMe Admin命令有一个叫Asynchronous Event Request,这个timer就是为这类异步请求设置的。Host有时候给NVMe控制器打招呼:小弟,有个人帮大哥盯着,出现了就报告。NVMe小弟赶快记下来,就是这个异步事件,有空了就留心一下。初始化注册回调函数,事件发生了就会调用回调函数。不相信?请看《蛋蛋读NVMe之一》,阿呆帮你把图贴过来。async_cid是异步请求的command id。outstanding_asyncs是NVMe控制器还没完成的异步事件数。async_queue就是异步事件队列。


  • uint8_t err_sts_mask;
    /* error status event mask */

uint8_t smart_mask;
/* smart/health status event mask */

当NVMe出现了error或者smart事件,对应的mask置1.

 

你是不是还很疑惑PCI的bar和mmio到底是怎么用的?下期为你解惑。

 

引用

 

https://github.com/nvmeqemu

 


 

上文中我们提到NVMe设备其实是个PCI设备,里面用了bar0和MMIO,但是不了解PCI的人肯定对这两个概念有所疑惑。阿呆本想写一些,不过发现网上有篇文章讲的很透彻,所以就不班门弄斧了,友情转载过来。

 

PCI设备(PCI device)都有一个配置空间,大小为256字节,实际上是一组连续的寄存器,位于设备上。其中头部64字节是PCI标准规定的,格式如下:

 


 

剩余的部分是PCI设备自定义的。

PCI配置空间头部有6个BAR(Base Address Registers),BAR记录了设备所需要的地址空间的类型(memory space或者I/O space),基址以及其他属性。BAR的格式如下:

 


 

可以看出,设备可以申请两类地址空间,memory space和I/O space,它们用BAR的最后一位区别开来。

 

说到地址空间,计算机系统中,除了我们常说的memory address(包括逻辑地址、虚拟地址(线性地址)、CPU地址(物理地址)),还有I/O address,这是为了访问I/O设备(主要是设备中的寄存器)而设立的,大部分体系结构中,memory address和I/O address都是分别编址的,且使用不同的寻址指令,构成了两套地址空间,也有少数体系结构将memory address和I/O address统一编址(如ARM)。

 

有两套地址空间并不意味着计算机系统中需要两套地址总线,实际上,memory address和I/O address是共用一套地址总线,但通过控制总线上的信号区别当前地址总线上的地址是memory address还是I/O address。北桥芯片(Northbridge,Intel称其Memory Controller Hub,MCH)负责地址的路由工作,它内部有一张address map,记录了memory address,I/O address的映射信息,一个典型的address map如图:

070116_1237_NVMe1657.png
我们来看北桥是如何进行地址路由的。根据控制总线上的信号,北桥首先可以识别地址属于memory space还是I/O space,然后分别做处理。

 

比如若是memory space,则根据address map找出目标设备(DRAM或Memory Mapped I/O),若是DRAM或VGA,则转换地址然后发送给内存控制器或VGA控制器,若是其它I/O设备,则发送给南桥。

若是I/O space,则发送给南桥(Southbridge,Intel称其I/O Controller Hub,ICH),南桥负责解析出目标设备的bus, device, function号,并发送信息给它。

 

PCI设备会向计算机系统申请很多资源,比如memory space, I/O space, 中断请求号等,相当于在计算机系统中占位,使得计算机系统认识自己。

 

PCI设备可以通过两种方式将自己的I/O存储器(Registers/RAM/ROM)暴露给CPU:

在memory space申请地址空间,或者在I/O space申请地址空间。

 

这样,PCI设备的I/O存储器就分别被映射到CPU-relative memory space和CPU-relative I/O space,使得驱动以及操作系统得以正常访问PCI设备。对于没有独立I/O space的体系结构(如ARM),memory space和I/O space是统一编址的,也就是说memory space与I/O space等价了,这时,即使PCI设备在BAR表明了要申请I/O space,实际上也是分配在memory space的,所以驱动无法使用I/O端口指令访问I/O,只能使用访存指令。在Windows驱动开发中,PCM_PARTIAL_RESOURCE_DESCRIPTOR记录了为PCI设备分配的硬件资源,可能有CmResourceTypePort, CmResourceTypeMemory等,后者表示一段memory地址空间,顾名思义,是通过memory space访问的,前者表示一段I/O地址空间,但其flag有CM_RESOURCE_PORT_MEMORY和CM_RESOURCE_PORT_IO两种,分别表示通过memory space访问以及通过I/O space访问,这就是PCI请求与实际分配的差异,在x86下,CmResourceTypePort的flag都是CM_RESOURCE_PORT_IO,即表明PCI设备请求的是I/O地址空间,分配的也是I/O地址空间,而在ARM或Alpha等下,flag是CM_RESOURCE_PORT_MEMORY,表明即使PCI请求的I/O地址空间,但分配在了memory space,我们需要通过memory space访问I/O设备(通过MmMapIoSpace映射物理地址空间到虚拟地址空间,当然,是内核的虚拟地址空间,这样驱动就可以正常访问设备了)。

 

为了为PCI设备分配CPU-relative space,计算机系统需要知道其所申请的地址空间的类型、基址等,这些信息记录在设备的BAR中,每个PCI配置空间拥有6个BAR,因此每个PCI设备最多能映射6段地址空间(实际很多设备用不了这么多)。PCI配置空间的初始值是由厂商预设在设备中的,于是设备需要哪些地址空间都是其自己定的,可能造成不同的PCI设备所映射的地址空间冲突,因此在PCI设备枚举(也叫总线枚举,由BIOS或者OS在启动时完成)的过程中,会重新为其分配地址空间,然后写入PCI配置空间中。

 

通过memory space访问设备I/O的方式称为memory mapped I/O,即MMIO,这种情况下,CPU直接使用普通访存指令即可访问设备I/O。

通过I/O space访问设备I/O的方式称为port I/O,或者port mapped I/O,这种情况下CPU需要使用专门的I/O指令如IN/OUT访问I/O端口。

 

常见的MMIO例子有,VGA card将framebuffer映射到memory space,NIC将自己的片上缓冲映射到memory space,实际上,最典型的MMIO应该是DRAM,它将自己的存储空间映射到memory space,是占用CPU地址空间最多的”设备”。

 

 

转载自

作者:使命召唤@jilinxpd, http://www.cnblogs.com/zszmhd/archive/2012/05/08/2490105.html

分类目录 未分类.
扫一扫二维码或者微信搜索公众号ssdfans关注(添加朋友->点最下面的公众号->搜索ssdfans),可以经常看到SSD技术和产业的文章(SSD Fans只推送干货)。
ssdfans微信群介绍
技术讨论群 覆盖2000多位中国和世界华人圈SSD以及存储技术精英
固件、软件、测试群 固件、软件和测试技术讨论
异构计算群 讨论人工智能和GPU、FPGA、CPU异构计算
ASIC-FPGA群 芯片和FPGA硬件技术讨论群
闪存器件群 NAND、3D XPoint等固态存储介质技术讨论
企业级 企业级SSD、企业级存储
销售群 全国SSD供应商都在这里,砍砍价,会比某东便宜20%
工作求职群 存储行业换工作,发招聘,要关注各大公司招聘信息,赶快来
高管群 各大SSD相关存储公司高管和创始人、投资人

想加入这些群,请微信扫描下面二维码,或搜索nanoarchplus,加阿呆为微信好友,介绍你的昵称-单位-职务,注明群名,拉你进群。SSD业界需要什么帮助,也可以找阿呆聊。