先把结论说清楚:SRAT 和 SLIT 不是存在某个 QEMU 文件里,也不是存在 Guest 磁盘上,而是 QEMU 在启动时动态拼出来、放进「Guest 物理内存里的 ACPI 表区域」,再通过 RSDP→XSDT→SRAT/SLIT 的链式指针暴露给 Guest 内核;Linux 启动早期直接用 ACPI 子系统解析,用户态再看/sys/firmware/acpi/tables/下的镜像。
下面分「QEMU 侧放哪」和「Linux 侧怎么拿」两层说。
一、QEMU 里 SRAT / SLIT 存在哪里(构建期 + 运行期)
1. 构建期:在GArray缓冲区里拼二进制
ACPI 表是 QEMU 运行时生成的,不是读文件。
入口:
hw/i386/acpi-build.c的acpi_build()(ARM 在hw/arm/virt-acpi-build.c)拼表函数:
SRAT:
hw/acpi/aml-build.c的build_srat()→ 调build_srat_memory()等追加子结构,最后build_header()封头,签名"SRAT"SLIT:
hw/acpi/aml-build.c的build_slit()→ 先推AcpiTableHeader,再写节点数 + N×N 距离矩阵,封头签名"SLIT"
这些字节先写在
tables_blob(一个GArray<uint8_t>) 里,此时还在 QEMU 进程的用户态内存,不是 Guest 内存。
数据来源就是前面读的那些结构:
SRAT 内容 ←
MachineState::numa_state->nodes[](NodeInfo.present/has_cpu/node_memdev等)SLIT 内容 ←
NodeInfo.distance[],且必须numa_state->have_numa_distance == true才生成
2. 运行期:通过 fw_cfg + BIOSLinker 搬进 Guest 物理内存
acpi_build()拼完所有表后:
把
tables_blob交给BIOSLinker(hw/acpi/bios-linker-loader.c)Linker 在表尾计算每张表的物理偏移,把XSDT 里的 8 字节指针槽 填成 SRAT/SLIT 在 blob 中的偏移
整个 blob 通过fw_cfg 设备(
-fw_cfg)暴露给 SeaBIOS/EDK2/OVMF固件(SeaBIOS/EDK2)把 blob 拷贝到 Guest 物理内存的某个预留区(一般靠近 0xFFFFFFF0 附近的 ACPI reclaim/Reserved 区),并把RSDP 地址 写进 EBDA 或
0x000E0000~0x00100000的 BIOS 区域Guest 开机后,CPU 复位 → 固件交权给内核 → 内核从 RSDP 开始扫
所以最终存放位置是:
Guest 物理地址空间中的「ACPI 表保留内存区」(类型在 e820 里标
ACPI Reclaimable或ACPI NVS,取决于表),由 RSDP(实模式可见地址)→ XSDT(64 位指针)→ SRAT/SLIT 三级串起来。
QEMU 自己构建完就不管了,表里的内容对 Guest 来说就是「硬件固件给的」。
二、Linux 通过什么接口访问
1. 内核启动早期:ACPI 子系统直接解析物理内存里的表
路径(x86 为例):
setup_arch() └─ initmem_init() └─ x86_numa_init() └─ x86_acpi_numa_init() └─ acpi_numa_init() /* drivers/acpi/numa/srat.c */ ├─ acpi_get_table(ACPI_SIG_SRAT, ...) 取 SRAT 基址 ├─ acpi_table_parse_srat(...) 拆 CPU/Memory Affinity ├─ acpi_get_table(ACPI_SIG_SLIT, ...) └─ acpi_parse_slit() 填 node_distance[]内核用
acpi_get_table()从 RSDP/XSDT 链拿到 SRAT/SLIT 的物理地址,临时映射解析解析结果不直接留「表原文」,而是转成内核结构:
numa_meminfo:(起始地址,长度,node)node_distance[i][j]:来自 SLITcpu_to_node()映射:来自 SRAT APIC Affinity
之后
/proc/sys/kernel/numa_distances、调度器、内存分配器用的都是这些内核结构,不是每次都重新读表
2. 用户态:两个接口
(a) 原始表镜像(调试/取证最常用)
/sys/firmware/acpi/tables/SRAT /sys/firmware/acpi/tables/SLIT /sys/firmware/acpi/tables/data/ /* 某些发行版拆了子项 */这些是内核把解析前的 ACPI 表原样 expose 出来的struct acpi_table_header二进制,可以直接iasl -d SRAT反编译看。
(b) 解析后的 NUMA 视图(日常用)
/sys/devices/system/node/node0/cpulist /sys/devices/system/node/node0/meminfo /sys/devices/system/node/node0/distance /* 就是 SLIT 里那一行 */numactl -H也是读这些 sysfs +syscalls出来的。
注意:用户态不能通过某个
ioctl直接「再读一次 SRAT」,SRAT 只在 boot 阶段被内核读一次;用户态要看原表只能读/sys/firmware/acpi/tables/里的快照。
三、把前后知识串起来(对应 QEMU 结构)
QEMU 命令行 -numa node,memdev=,cpus= → NumaNodeOptions → NodeInfo[] -numa dist,src,dst,val → NumaDistOptions → NodeInfo.distance[] │ ▼ hw/acpi/aml-build.c build_srat() ← NodeInfo.present/has_cpu/node_memdev build_slit() ← NodeInfo.distance[] │ BIOSLinker + fw_cfg ▼ Guest 物理内存: RSDP → XSDT → SRAT / SLIT │ ▼ Linux drivers/acpi/numa/srat.c acpi_numa_init() → numa_meminfo + node_distance[] │ ┌────────────────────┴─────────────────────┐ ▼ ▼ 内核 NUMA 子系统 /sys/firmware/acpi/tables/{SRAT,SLIT}一句话收口:
SRAT/SLIT 在 QEMU 里是
aml-build.c拼出来的内存块,经 fw_cfg 搬进 Guest 物理内存的 ACPI 保留区;Linux 启动期由
acpi_numa_init()通过 RSDP/XSDT 链解析物理内存里的表,用户态再通过/sys/firmware/acpi/tables/看原始表、通过/sys/devices/system/node/看解析结果。