e820
- e820 从硬件获取内存分布 | Kernel Exploring
- 🌟 E820 内存管理器
- E820 on seaBIOS
- E820 on Qemu/KVM
- Detecting Memory (x86) - OSDev Wiki
- 🌟《QEMU/KVM 源码解析与应用》
- 3.2 fw_cfg 设备介绍
- 3.4 SeaBIOS 分析
QEMU fw_cfg
QEMU/KVM 作为虚拟化核心组建,其可以用于模拟 X86 运行环境,因此其具备构建 X86 平台硬件信息的能力,QEMU/KVM 同样提供了创建 E820 的机制,用于为 seaBIOS 模拟 E820 相关的硬件信息,其中包括了 BIOS 的 CMOS 内存,hw_cfg 固件信息等。
seabios
BIOS POST (Power On Self Test) 上点自检之后,会从 CMOS 中获得内存相关的信息,这些信息用于构建 BIOS 的 BDA/EBDA 信息,以便 BIOS 构建自己的 E820 表,BIOS 构建的 E820 表构建完毕之后会将 E820 表里的内存区域与 BIOS IVT 的 0x15/E820 中断例程进行绑定,当内核初始化时通过与 BIOS IVT 进行交互,BIOS 将 E820 表传递给了内核。BIOS 在初始化过程中也会预留一些内存区域,这些内存区域也会一同传递给内核。
reset_vector->entry_post->dopost->maininit->interface_init
qemu_cfg_init
qemu_cfg_e820
/* 通过 qemu 的 fw_cfg 接口,获取 e820 table */
qemu_cfg_read_entry
e820_add
2
3
4
5
6
seabios 在 IVT 里提供了很多中断入口。
在 X86 架构 Linux 内核发展的过程中,物理内存支持的长度从 16MB 发展到 64MB、4GB 再到今天支持 64-bit 的地址总线,BIOS 在不同的历史阶段提供了不同的中断调用, 以满足内核从 BIOS 中获得内存区域相关的信息。 linux 发起 int 0x15 中断,通过 BIOS 中断例程探测物理内存
handle_15
handle_15e8
handle_15e820
2
3
进入 vmlinux 前(实模式)
linux kernel 在实模式进行最早期的初始化,通过 BIOS 提供的 IVT 表分别从 0x15/0xE820、0x15/0xE801 以及 0x15/0x88 中获得物理内存信息,并将信息存储在 boot_params 数据结构里面。之后进入保护模式。
/* arch/x86/boot/main.c 被编译进 setup.elf 了
此时在实模式
*/
main
detect_memory
detect_memory_e820();
/* 通过 BIOS INT 0x15/E820 调用获得系统内存布局信息 */
ireg.ax = 0xe820;
intcall(0x15, &ireg, &oreg);
boot_params.e820_entries = count;
detect_memory_e801();
detect_memory_88();
2
3
4
5
6
7
8
9
10
11
12
有两个地方定义了 boot_params 变量,分别是 setup.bin 和 vmlinux.bin 里(这两个会并成 bzImage)。 是 setup.bin 里的 boot_params 先初始化,然后会把 boot_params 传给 vmlinux:
/* setup.bin 里 */
go_to_protected_mode
/* 把 arch/x86/boot/main.c 里的 boot_params 的地址传过去了(第二个参数) */
protected_mode_jump
/* 然后进入保护模式跳转到 setup.bin 的 startup_64 !
https://docs.hust.openatom.club/linux-insides-zh/booting/linux-bootstrap-4 */
startup_64
/* 把 arch/x86/boot/main.c 里的 boot_params 的地址传到了这个函数 */
__startup_64
common_startup_64
early_setup_idt
initial_code 也即 x86_64_start_kernel
copy_bootdata
/* 把 arch/x86/boot/main.c 里的 boot_params 拷贝到 arch/x86/kernel/setup.c
里的 boot_params */
/* 如果是 CONFIG_PVH,则是拷贝了 pvh_bootparams */
memcpy(&boot_params, real_mode_data, sizeof(boot_params));
x86_64_start_reservations->start_kernel
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如果不用 bzImage 启动(CONFIG_PVH),而是直接用 vmlinux 启动,那则是在 init_pvh_bootparams 里初始化的?
进入 vmlinux 后(保护模式)
内核在初始化过程中,实时模式中从 BIOS 中获得并构建 E820 表,然后结合 CMDLINE 参数和 boot_paras 参数构建内核自身的 E820 表,并基于该表构建系统的 IORESOURCE 信息和固件信息,最后基于 E820 表中可用内存区域信息构建 BootMEM 或者 MEMBLOCK 内存分配器。
构建 e820_table、e820_table_kexec 和 e820_table_firmware 三个表,并将 e820_table 按一定的顺序进行排序。处理完表之后, 内核计算出 max_pfn/max_low_pfn/high_memory 三个变量的值,接着内核将 e820_table 的可用内存区域添加到 MEMBLOCK 分配器,MEMBLOCK 分配器以这些信息为基础构建内核 第一个内存分配器。内核接着将 e820_table 中的预留区域插入到 iomem_resource 进行 管理,并将 e820_table_firmware 加入系统固件层进行管理。
内核根据这些信息,为之后的 MEMBLOCK 内存管理器的初始化提供了基础。
注意,此时内核用的 boot_params 是 arch/x86/kernel/setup.c 里定义的,而非 arch/x86/boot/main.c 里定义的那个
start_kernel->setup_arch
e820__memory_setup();
/* 根据 boot_params 记录的 e820 信息,构建 e820_table 并排序 */
e820__memory_setup_default()
/* 再拷贝成两个新的表 e820_table_kexec 和 e820_table_firmware */
memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));
memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));
/* 此时打印的是原始 BIOS E820 信息 */
e820__print_table()
/* boot_params.e820_table 只能容纳 128 个,多于的通过 boot_params.hdr.setup_data 传递,
会更新 e820_table */
parse_setup_data();
e820__memory_setup_extended()
/* 解析用 early_param 定义过的参数,比如 mem= 和 memmap= 对 e820_table 做一些调整 */
parse_early_param();
/* 将 e820_table 大于 mem=size 的区域移除 */
parse_memopt()
e820__range_remove(mem_size, ULLONG_MAX - mem_size, E820_TYPE_RAM, 1);
/* 添加或修改 e820_table 内存区域信息 */
parse_memmap_opt()
/* 将 boot_params.hdr.setup_data 内存区域进行保留,这是因为我们后面可能还会用到这些 setup_data
比如 pci_device_add->pcibios_device_add */
e820__reserve_setup_data();
/* 更新 mem= 调整过后的 e820_table */
e820__finish_early_params();
/* 由于 BIOS 复杂性或是 CMDLINE 中包含了 “memmap=exactmap” 或 “memmap=xxM$yyM”
导致内核本身占用的内存区域不是 E820_TYPE_RAM,就需要将内核本身占用的区域标记为 E820_TYPE_RAM */
e820_add_kernel_range();
/* max_pfn 是最大物理页帧号,max_low_pfn 是 4GB 内最大的物理页帧号 */
...
/* 基于 e820_table 的 E820_TYPE_RAM 类型的区域,构建 memblock 内存分配器 */
e820__memblock_setup();
for memblock_add(entry->addr, entry->size);
/* 从 memblock 分配器中为 mptable 分配 4KB 物理内存,并在 e820_table_kexec 中做预留,
相关 cmdline 有 alloc_mptable update_mptable */
e820__memblock_alloc_reserved_mpc_new();
/* 将 e820_table 中预留区域内存插入到 iomem_resource 资源树下,以便维护,
还在 /sys/firmware/memmap 里加了些 */
e820__reserve_resources();
/* E820_TYPE_RESERVED_KERN 和 E820_TYPE_RAM 属于 System RAM,其他都是 IO 内存 */
res->flags = e820_type_to_iomem_type(entry);
/* TODO IORESOURCE_BUSY 是指什么?是指这个资源已被占用,驱动不能使用这个资源?
比如,要为 BAR 空间分配一段地址区域,那就不能申请 IORESOURCE_BUSY 的?*/
if do_mark_busy()
res->flags |= IORESOURCE_BUSY;
insert_resource(&iomem_resource, res); /* 插入到 iomem_resource 管理 */
firmware_map_add_early() /* /sys/firmware/memmap */
/* kernel_init 线程 */
kernel_init
free_initmem
/* 清除 initdata 之前,重新为 e820_table 分配内存 */
e820__reallocate_tables
n = kmemdup(e820_table, size, GFP_KERNEL);
e820_table = n;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
TODO e820_table_kexec 和 e820_table_firmware
/sys/firmware/memmap
drivers/firmware/memmap.c
~ # ls /sys/firmware/memmap/
0 1 2 3 4 5 6 7 8
~ # cat /sys/firmware/memmap/0/start
0x0
~ # cat /sys/firmware/memmap/0/end
0x9fbff
~ # cat /sys/firmware/memmap/0/type
System RAM
2
3
4
5
6
7
8
struct biosregs
struct biosregs 数据结构用于维护一套寄存器组,以便用于 BIOS 调用时候交换寄存器。 struct biosregs 数据结构内部使用一个 union 联合体包含了三套寄存器。分别对应保护模式、实模式、兼容 8086 模式。
cmdline mem=
memmap=
在初始化接下来的内存分配器之前,内核可以提供一些手段来修改 E820 Table 表来控制未来系统的内 存布局,这种修改比较常见的就是给系统 CMDLINE 机制提供选项,实现某段区域的预留,或者控制系统可用物理内存的长度等功能。
E820 内存管理器提供了 memmap=
和 mem=
选项,用于用户添加或修改 E820 内存区域。通过这些选项,用户可以简单的添加一块 “系统预留区域”、 “ACPI Data 区域”、”系统保护区域”,或者可以修改 e820_table 中某段内存区域的类型。 这些 cmdline 选项还可以修改系统最大可用物理内存或者系统可以使用的物理内存范围。
/* arch/x86/kernel/e820.c */
early_param("mem", parse_memopt);
early_param("memmap", parse_memmap_opt);
2
3
Documentation/admin-guide/kernel-parameters.txt
这里的 nn 是指 Size,ss 则是指 Address ?
mem=nn[KMG] [KNL,BOOT,EARLY] Force usage of a specific amount
of memory Amount of memory to be used in cases
as follows:
1 for test;
2 when the kernel is not able to see the whole system memory;
3 memory that lies after 'mem=' boundary is excluded from
the hypervisor, then assigned to KVM guests.
4 to limit the memory available for kdump kernel.
[ARC,MICROBLAZE] - the limit applies only to low memory,
high memory is not affected.
[ARM64] - only limits memory covered by the linear
mapping. The NOMAP regions are not affected.
[X86] Work as limiting max address. Use together
with memmap= to avoid physical address space collisions.
Without memmap= PCI devices could be placed at addresses
belonging to unused RAM.
Note that this only takes effects during boot time since
in above case 3, memory may need be hot added after boot
if system memory of hypervisor is not sufficient.
memmap=exactmap [KNL,X86,EARLY] Enable setting of an exact
E820 memory map, as specified by the user.
Such memmap=exactmap lines can be constructed based on
BIOS output or other requirements. See the memmap=nn@ss
option description.
memmap=nn[KMG]@ss[KMG]
[KNL, X86,MIPS,XTENSA,EARLY] Force usage of a specific region of memory.
Region of memory to be used is from ss to ss+nn.
If @ss[KMG] is omitted, it is equivalent to mem=nn[KMG],
which limits max address to nn[KMG].
Multiple different regions can be specified,
comma delimited.
Example:
memmap=100M@2G,100M#3G,1G!1024G
memmap=nn[KMG]#ss[KMG]
[KNL,ACPI,EARLY] Mark specific memory as ACPI data.
Region of memory to be marked is from ss to ss+nn.
memmap=nn[KMG]$ss[KMG]
[KNL,ACPI,EARLY] Mark specific memory as reserved.
Region of memory to be reserved is from ss to ss+nn.
Example: Exclude memory from 0x18690000-0x1869ffff
memmap=64K$0x18690000
or
memmap=0x10000$0x18690000
Some bootloaders may need an escape character before '$',
like Grub2, otherwise '$' and the following number
will be eaten.
memmap=nn[KMG]!ss[KMG,EARLY]
[KNL,X86] Mark specific memory as protected.
Region of memory to be used, from ss to ss+nn.
The memory region may be marked as e820 type 12 (0xc)
and is NVDIMM or ADR memory.
memmap=<size>%<offset>-<oldtype>+<newtype>
[KNL,ACPI,EARLY] Convert memory within the specified region
from <oldtype> to <newtype>. If "-<oldtype>" is left
out, the whole region will be marked as <newtype>,
even if previously unavailable. If "+<newtype>" is left
out, matching memory will be removed. Types are
specified as e820 types, e.g., 1 = RAM, 2 = reserved,
3 = ACPI, 12 = PRAM.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
mem=nn[KMG]
参数用于指明系统最大可用物理内存的长度,即系统可用物理内存范围是 “0-nn”, 超过这一长度的物理内存系统将不可见。
memmap=
可以用来:
- 强制使用某段内存
memmap=nn@ss
- 预留一段 ACPI data 区域
memmap=nn#ss
- 预留一段物理内存
memmap=nn$ss
- 预留一段保护区域
memmap=nn!ss
一般给 NVDIMM 和 ADR 内存使用 - 修改一段内存区域的类型
memmap=<size>%<offset>-<oldtype>+<newtype>
type 见 enum e820_type
一般 “mem=” 和 “memmap=” 配合使用,以此避免物理空间冲突。如果设置了最大物理可用内存之后,如果没有使用 “memmap=” 进行 “ACPI” 区域的设置,那么 “ACPI” 区域将设置为不可用的 RAM 区域。 TODO 「“ACPI” 区域将设置为不可用的 RAM 区域」怎么理解?
启动 8G 虚拟机,但是带 mem=6G memmap=1M$0x20000000,1M!0x20100000
参数,
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x00000000000fffff] reserved
# 差不多 2GB
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000007ffdffff] usable
[ 0.000000] BIOS-e820: [mem 0x000000007ffe0000-0x000000007fffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000b0000000-0x00000000bfffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000feffc000-0x00000000feffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
# 4GB-10GB 也就是 6GB 大小的区域
[ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000027fffffff] usable
# e820__finish_early_params 函数,如果没有 mem= 或 memmap= 参数,是没有这一段消息的:
[ 0.000000] user-defined physical RAM map:
[ 0.000000] user: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] user: [mem 0x000000000009fc00-0x00000000000fffff] reserved
[ 0.000000] user: [mem 0x0000000000100000-0x000000001fffffff] usable
# 1M$0x20000000 预留的物理内存
[ 0.000000] user: [mem 0x0000000020000000-0x00000000200fffff] reserved
# 1M!0x20100000 预留的保护区域
[ 0.000000] user: [mem 0x0000000020100000-0x00000000201fffff] persistent (type 12)
[ 0.000000] user: [mem 0x0000000020200000-0x000000007ffdffff] usable
[ 0.000000] user: [mem 0x000000007ffe0000-0x000000007fffffff] reserved
[ 0.000000] user: [mem 0x00000000b0000000-0x00000000bfffffff] reserved
[ 0.000000] user: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[ 0.000000] user: [mem 0x00000000feffc000-0x00000000feffffff] reserved
[ 0.000000] user: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
# 可以看到这里相比与 e820 table,变成了 4GB-6GB,也就是 2GB 大小的区域
# 说明 mem=6G 让物理地址 6G 以上的都不可见了!
[ 0.000000] user: [mem 0x0000000100000000-0x000000017fffffff] usable
~ # cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-000fffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000f0000-000fffff : System ROM
00100000-1fffffff : System RAM
01000000-023fffff : Kernel code
02400000-0354afff : Kernel rodata
03600000-0396b63f : Kernel data
042bc000-047fffff : Kernel bss
#
20000000-200fffff : Reserved
#
20100000-201fffff : Persistent Memory (legacy)
20200000-7ffdffff : System RAM
7ffe0000-7fffffff : Reserved
# 2G ~ 4G
80000000-afffffff : PCI Bus 0000:00 # 这部分没在 e820 table 里出现
b0000000-bfffffff : PCI ECAM 0000 [bus 00-ff]
b0000000-bfffffff : Reserved
b0000000-bfffffff : pnp 00:04
c0000000-febfffff : PCI Bus 0000:00 # 这部分没在 e820 table 里出现
fec00000-fec003ff : IOAPIC 0
fed00000-fed003ff : HPET 0
fed00000-fed003ff : PNP0103:00
fed1c000-fed1ffff : Reserved
fed1f410-fed1f414 : iTCO_wdt.0.auto
feffc000-feffffff : Reserved
fffc0000-ffffffff : Reserved
# 4G ~ 6G
100000000-17fffffff : System RAM
# 6G ~ 10G 没有出现
# 10G ~ ...
280000000-a7fffffff : PCI Bus 0000:00 # 这部分没在 e820 table 里出现
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70