NVDIMM是PCDIMM的子类型,所以整体的流程基本差不多,着重讲一下几个不同之处。
全局
我们先来看一下全局上,一个nvdimm设备初始化并加入系统有哪些步骤。
Copy main()
qemu_opts_foreach(qemu_find_opts("device"), device_init_func, NULL, NULL)
...
device_set_realized()
hotplug_handler_pre_plug
...
memory_device_pre_plug
nvdimm_prepare_memory_region (1)
if (dc->realize) {
dc->realize(dev, &local_err);
}
hotplug_handler_plug
...
nvdimm_plug
nvdimm_build_fit_buffer (2)
qemu_run_machine_init_done_notifiers()
pc_machine_done
acpi_setup
acpi_build
nvdimm_build_acpi (3)
nvdimm_build_ssdt
nvdimm_build_nfit
acpi_add_rom_blob(build_state, tables.table_data,
"etc/acpi/tables", 0x200000);
acpi_add_rom_blob(build_state, tables.linker->cmd_blob,
"etc/table-loader", 0);
从上面的图中可以看到NVDIMM设备初始化和PCDIMM设备类似,也是有这么几个步骤
其中大部分的工作和PCDIMM一样,只有标注了1,2,3的三个地方需要额外的工作。
分别是:
分配label区域
按照当前的实现,使用nvdimm设备时,在分配空间的最后挖出一块做label。这个工作在nvdimm_prepare_memory_region中完成。
这块label区域在虚拟机内核中通过DSM方法来操作,后面我们会看到DSM方法是如何虚拟化的。
填写nfit信息
nfit表是用来描述nvdimm设备上地址空间组织形式的,所以这部分需要在计算好了dimm地址后进行,有nvdimm_build_fit_buffer完成。
展开这个函数我们可以看到
Copy for (; device_list; device_list = device_list->next) {
DeviceState *dev = device_list->data;
/* build System Physical Address Range Structure. */
nvdimm_build_structure_spa(structures, dev);
/*
* build Memory Device to System Physical Address Range Mapping
* Structure.
*/
nvdimm_build_structure_memdev(structures, dev);
/* build NVDIMM Control Region Structure. */
nvdimm_build_structure_dcr(structures, dev);
}
也就是分别构建了 SPA, MEMDEV和DCR三个信息。而这些信息就在内核函数add_table中处理。
不看具体内容,我们只看add_table中类型的定义:
Copy enum acpi_nfit_type {
ACPI_NFIT_TYPE_SYSTEM_ADDRESS = 0,
ACPI_NFIT_TYPE_MEMORY_MAP = 1,
ACPI_NFIT_TYPE_INTERLEAVE = 2,
ACPI_NFIT_TYPE_SMBIOS = 3,
ACPI_NFIT_TYPE_CONTROL_REGION = 4,
ACPI_NFIT_TYPE_DATA_REGION = 5,
ACPI_NFIT_TYPE_FLUSH_ADDRESS = 6,
ACPI_NFIT_TYPE_CAPABILITIES = 7,
ACPI_NFIT_TYPE_RESERVED = 8 /* 8 and greater are reserved */
};
上面三个类型对应了这个枚举类型的0,1,4。
构建acpi
最终nvdimm的信息要填写到acpi中,一共有两张表:SSDT和NFIT。这部分工作在nvdimm_build_acpi中完成。
其中NFIT表在nvdimm_build_nfit函数中完成,其实没有太多内容。就是将上面创建好的表内容拷贝过来。
而SSDT这张表就讲究多了,这里面涉及了acpi的一些概念。
ACPI Table
在进入代码之前,还是要先了解一下acpi的一些概念。这里并不是系统性介绍acpi,只是补充一些代码阅读中涉及的概念。
ACPI(Advanced Configuration and Power Management Interface),简单理解就是用来描述硬件配置的。
为了做好管理,acpi当然提供了很多复杂的功能,其中我们关注的就是acpi table。而这些表的作用比较直观,那就是按照一定的规范来描述硬件设备的属性。
规范中定义了很多表,而这些表按照我的理解可以分成两类:
DSDT & SSDT 虽然也叫表,但是他们使用AML语言编写的,可以被解析执行的。也就是这两张表是可编程的。 而其他的表则是静态的,按照标准写好的数据结构。
查看Table
在linux上可以很方便得查看acpi table。因为他们都在sysfs上。
/sys/firmware/acpi/tables/
文件的内容还需要使用工具查看。
这个命令将会把所有的acpi table的二进制形式导出到.dat文件,并保存在当前目录下。
比如我们就看nvdimm中创建的NFIT表,那么再执行
就能解析成可阅读的文件了。在我的虚拟机上显示如下,而且正好就是spa, memdev, dcr三个部分。
Copy [000h 0000 4] Signature : "NFIT" [NVDIMM Firmware Interface Table]
[004h 0004 4] Table Length : 000000E0
[008h 0008 1] Revision : 01
[009h 0009 1] Checksum : BF
[00Ah 0010 6] Oem ID : "BOCHS "
[010h 0016 8] Oem Table ID : "BXPCNFIT"
[018h 0024 4] Oem Revision : 00000001
[01Ch 0028 4] Asl Compiler ID : "BXPC"
[020h 0032 4] Asl Compiler Revision : 00000001
[024h 0036 4] Reserved : 00000000
[028h 0040 2] Subtable Type : 0000 [System Physical Address Range]
[02Ah 0042 2] Length : 0038
[02Ch 0044 2] Range Index : 0002
[02Eh 0046 2] Flags (decoded below) : 0003
Add/Online Operation Only : 1
Proximity Domain Valid : 1
[030h 0048 4] Reserved : 00000000
[034h 0052 4] Proximity Domain : 00000000
[038h 0056 16] Address Range GUID : 66F0D379-B4F3-4074-AC43-0D3318B78CDB
[048h 0072 8] Address Range Base : 00000001C0000000
[050h 0080 8] Address Range Length : 0000000278000000
[058h 0088 8] Memory Map Attribute : 0000000000008008
[060h 0096 2] Subtable Type : 0001 [Memory Range Map]
[062h 0098 2] Length : 0030
[064h 0100 4] Device Handle : 00000001
[068h 0104 2] Physical Id : 0000
[06Ah 0106 2] Region Id : 0000
[06Ch 0108 2] Range Index : 0002
[06Eh 0110 2] Control Region Index : 0003
[070h 0112 8] Region Size : 0000000278000000
[078h 0120 8] Region Offset : 0000000000000000
[080h 0128 8] Address Region Base : 0000000000000000
[088h 0136 2] Interleave Index : 0000
[08Ah 0138 2] Interleave Ways : 0001
[08Ch 0140 2] Flags : 0000
Save to device failed : 0
Restore from device failed : 0
Platform flush failed : 0
Device not armed : 0
Health events observed : 0
Health events enabled : 0
Mapping failed : 0
[08Eh 0142 2] Reserved : 0000
[090h 0144 2] Subtable Type : 0004 [NVDIMM Control Region]
[092h 0146 2] Length : 0050
[094h 0148 2] Region Index : 0003
[096h 0150 2] Vendor Id : 8086
[098h 0152 2] Device Id : 0001
[09Ah 0154 2] Revision Id : 0001
[09Ch 0156 2] Subsystem Vendor Id : 0000
[09Eh 0158 2] Subsystem Device Id : 0000
[0A0h 0160 2] Subsystem Revision Id : 0000
[0A2h 0162 1] Valid Fields : 00
[0A3h 0163 1] Manufacturing Location : 00
[0A4h 0164 2] Manufacturing Date : 0000
[0A6h 0166 2] Reserved : 0000
[0A8h 0168 4] Serial Number : 00123456
[0ACh 0172 2] Code : 0301
[0AEh 0174 2] Window Count : 0000
[0B0h 0176 8] Window Size : 0000000000000000
[0B8h 0184 8] Command Offset : 0000000000000000
[0C0h 0192 8] Command Size : 0000000000000000
[0C8h 0200 8] Status Offset : 0000000000000000
[0D0h 0208 8] Status Size : 0000000000000000
[0D8h 0216 2] Flags : 0000
Windows buffered : 0
[0DAh 0218 6] Reserved1 : 000000000000
AML & ASL
开始我们也说了,acpi table分成两类。刚才的facs属于第二类,也就是静态的数据结构。
而对于第一种表,他们则复杂得多,是用AML编写的。
AML: ACPI Machine Language
ASL: ACPI source language
所以前者相当于机器语言,而后者相当于汇编。
解析这两张表的方式和刚才的一样也是通过iasl命令,但是样子就完全不一样了。我们就直接拿nvdimm构造ssdt来看。
NVDIMM SSDT
刚开始看确实有点头大,不过慢慢就好了。我稍微来解释一下。
定义函数
Copy Method (NCAL, 5, Serialized)
所以看到这个表中定义了好些函数: _DSM, RFIT, _FIT 。
值得注意的是以_ 开头的函数是在规范中定义的,而其他的则是可以自己添加的。
定义变量
如果说函数定义还比较好理解,那么变量的定义就有点不那么直观了。
最简单的方式是赋值:
当然这个变量Local6是自定义的,只要直接使用就好。
另外一种就是比较特殊的,我称之为指针方式 。因为看着和c语言的指针访问很像。而且这种方式需要做两步。
先定义出一块空间,里面包含了这块空间对应的起始地址和长度。
Copy OperationRegion (NPIO, SystemIO, 0x0A18, 0x04)
然后定义这块空间的field,这点上看和c语言的结构体很像。
Copy Field (NPIO, DWordAcc, NoLock, Preserve)
{
NTFI, 32
}
这样相当于得到了一个指针变量NFIT,这个地址是指向0x0A18的4个字节。
最后访问这个区域就是直接用赋值语句:
就相当于往NTFI这个指针写了Local6的值。
模拟nvdimm的_DSM
好了,绕了这么大一圈,终于可以回到正题看看nvdimm的_DSM是如何模拟的。
所谓模拟,也就是在guest执行_DSM方法时截获该操作,由qemu进行模拟。
guest内核
所以正式开始前,我们还得看一眼内核中执行_DSM时做了点什么。
下面的代码截取自acpi_evaluate_dsm()
Copy params[0].type = ACPI_TYPE_BUFFER;
params[0].buffer.length = 16;
params[0].buffer.pointer = (u8 *)guid;
params[1].type = ACPI_TYPE_INTEGER;
params[1].integer.value = rev;
params[2].type = ACPI_TYPE_INTEGER;
params[2].integer.value = func;
if (argv4) {
params[3] = *argv4;
} else {
params[3].type = ACPI_TYPE_PACKAGE;
params[3].package.count = 0;
params[3].package.elements = NULL;
}
ret = acpi_evaluate_object(handle, "_DSM", &input, &buf);
别的不管,主要看一共传进来了四个参数。比如有版本号和函数。这里先记着,留着后面再来看。
AML
回到之前看得acpi表总的来说,SSDT表在地址空间上构建了这么两段:
Copy 0xA18 MEMA
+------------------+--+------------------+------+-------------+
| | | | | |
+------------------+--+------------------+------+-------------+
/ \
/ \
+--------------+
|HDLE |
|REVS |
|FUNC |
|FARG |
+--------------+
其目的就是创建了一块共享内存,用来在guest和qemu之间传递模拟_DSM的参数和返回值。
比如我们可以在aml文件中看到
Copy REVS = Arg1
FUNC = Arg2
而其中Arg1就是内核传递下来的参数rev,Arg2是内核传递参数的func。
Qemu
当AML执行了 NFIT = Local6 的时候,接下去的操作就被Qemu截获了。而这个Local6参数的值是什么呢?
Local6 = MEMA
所以,实际上就是告诉了Qemu一块共享内存的空间。这下是不是明白了?
那Qemu截获后是如何操作?这还要看nvdimm_dsm_ops了。因为是写操作,所以只看write方法。
其中有一个变量很引人注目:NvdimmDsmIn。
Copy NvdimmDsmIn *in;
in->revision = le32_to_cpu(in->revision);
in->function = le32_to_cpu(in->function);
in->handle = le32_to_cpu(in->handle);
而这个变量类型定义为:
Copy struct NvdimmDsmIn {
uint32_t handle;
uint32_t revision;
uint32_t function;
/* the remaining size in the page is used by arg3. */
union {
uint8_t arg3[4084];
};
} QEMU_PACKED;
这下是不是和AML中共享内存的结构对应起来了?
好了,再详细的细节就和具体的函数实现相关了。留给大家自己去探索把~
最后的疑问
如果大家仔细看,比较qemu中aml的代码和guest中SSDT生成的文件,其中还有一个问题没有解决。
那就是 Local6 = MEMA 的这个MEMA的地址是如何得到的。
这个就牵扯到了另外两个超纲的知识点,将在FW_CFG 章节中解开这个谜团。