提要
本系列文章,旨在带你开发一个NVMe SSD控制器的前端协议逻辑,只不过是在QEMU虚拟机环境中。
澄清关于PCIe BAR空间的误解
前面我们说过了NVMe的BAR空间是个什么东东,结果在群里就被山哥指出来理解错误,BAR寄存器里面的地址并不是CPU的存储器域空间,而是PCI域,只是因为X86架构这两个用一样的地址,所以给人一种误解。到了PowerPC处理器,就是不一样的地址,需要Host PCI桥或者PCIe的RC做地址转换。
上图所示的处理器系统由一个CPU,一个DRAM控制器和两个HOST主桥组成。在这个处理器系统中,包含CPU域、DRAM域、存储器域和PCI总线域地址空间。其中HOST主桥x和HOST主桥y分别管理PCI总线x域与PCI总线y域。PCI设备访问存储器域时,也需要通过HOST主桥,并由HOST主桥进行PCI总线域到存储器域的地址转换;CPU访问PCI设备时,同样需要通过HOST主桥进行存储器域到PCI总线域的地址转换。
Memory Address和IO Address
再来回顾一下之前的两个概念,PCIe设备可以申请两类地址空间,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。
QEMU中NVMe BAR空间初始化
我们知道QEMU虚拟机其实本没有物理内存,他的内存是QEMU从宿主机的内存中虚拟出来的。我们虚拟的设备要获得内存地址或者IO地址,就需要向QEMU注册申请。我们这里NVMe是需要IO地址,所以首先通过cpu_register_io_memory注册,获得io_mem_write/io_mem_read的索引。该索引还有需要的IO空间大小等参数传给cpu_register_physical_memory函数,获得QEMU虚拟机的IO地址空间。
对于PCI设备来说,IO地址注册就要多一步,因为要进行PCI bar地址与IO的映射,所以必须先调用下面函数来给bar注册PCI地址。关键参数说明:第一个是PCI设备指针,第三个是我们需要注册IO地址的空间长度,最后一个是我们要进行IO操作映射的初始化函数指针。
1 /* Register BAR 0 (and bar 1 as it is 64bit). */ 2 pci_register_bar((struct PCIDevice *)&n->dev, 3 0, ((n->dev.cap_present & QEMU_PCI_CAP_MSIX) ? 4 n->dev.msix_bar_size : n->bar0_size), 5 (PCI_BASE_ADDRESS_SPACE_MEMORY , 6 PCI_BASE_ADDRESS_MEM_TYPE_64), 7 nvme_mmio_map);
IO初始化函数如下,关键参数说明:第一个依然是PCI设备指针,第三个是PCI地址映射的PIO起始地址,这个起始地址是在上面的函数注册PCI地址的时候,PCI总线通过计算比较PIO地址空间得到的一个PIO地址起始空间。所以在我们注册设备PIO空间的时候必须将这个地址作为注册IO空间的起始地址。这个函数是在更新bar映射的时候被调用的,里面调用了前面说的cpu_register_physical_memory,其实就是QEMU从宿主机申请一段内存用来映射到虚拟机。
1 static void nvme_mmio_map(PCIDevice *pci_dev, int reg_num, pcibus_t addr, 2 pcibus_t size, int type)
在阿呆看的版本中,MSIX中断向量数为32个,BAR 0空间大小为8KB,NVMe队列为64个(包括Admin队列)。
NVMe设备参数初始化
关键的工作搞定,后面的工作就简单了。首先是类似于PCI设备,从NVME_device_NVME_config文件读取NVMe相关寄存器的参数给对应变量赋值。例如:
1 <REG> 2 CFG_NAME = CNTRLREG 3 NAME = "AQA" 4 OFFSET = 0x24 5 LENGTH = 0x04 6 VALUE = 0x00000000 7 RO_MASK = 0xF000F000 8 RW_MASK = 0x0FFF0FFF 9 RWC_MASK = 0x00000000 10 RWS_MASK = 0x00000000 11 DESC = "Admin Queue Attributes" 12 </REG>
接下来的内容比较杂,主要是:
- 是各种配置参数初始化,比如温度的阈值,队列数。
- 给每个namespace的Identify页赋值。
- 一些Log页的赋值。
- SQ处理和Async事件处理两个Timer的初始化。
Timer初始化之后,其实NVMe设备就开始工作了,能响应主机的需求。
SSD模拟器初始化
SSD其实就是个存储空间,所以QEMU用了文件来模拟。读写SSD相当于读写宿主机上的一些文件。但是读写文件不太方便,所以这里使用了Linux的mmap函数,这个函数把文件映射到一个地址,后来写这个文件就跟写内存一样方便,可以按照偏移地址去写。
引用
sailing, 《浅谈PCIe体系结构》, http://blog.sina.com.cn/s/blog_6472c4cc0100qfau.html
http://blog.csdn.net/yearn520/article/details/6560851
http://people.cs.nctu.edu.tw/~chenwj/dokuwiki/doku.php?id=qemu