《early boot memory-memblock》中可以看到memblock中的region添加是由E820模块调用e820__memblock_setup()函数将e820_table 中的内存物理信息添加到memoryblock中。

E820由来

e820的由来得益与BIOS 15H的设置操作码AX=E820而得名。X86架构中,机器设备在上电后,首先是由BIOS进行基础的硬件初始化,并从CMOS中获取到内存相关信息,这些信息用于构建BIOS的BDA/EDA信息,以便BIOS构建自己的E820表,接着BIOS会将PC指针表跳转到特地位置以便引导内核启动,最后将执行权限交给操作系统内核。

BIOS通过设置相关的中断将物理内存信息通知给操作系统内核,内核根据相关信息建立e820_table,之后添加到memblock中,最终通过sparse、zone初始化等建立buddy内存管理系统。

INT 0x15/AX=E820H

INT 0x15/AX=E820H又称为Query System Address Map名为查询系统地址映射关系。上电之后,BIOS根据获取到的内存信息以及其他地址配置信息, 通过向该中断将物理内存映射地址描述信息写入到该中断,当BIOS将权限传递给内核后,将会由内核通过读取到该中断信息,并对信息进行筛检做进一步管理。

以下是《Advanced Configuration and Power Interface (ACPI) Specification V6.3》版本中对INT 0x15/AX=E820H中断说明:

This interface is used in real mode only on IA-PC-based systems and provides a memory map for all of the installed RAM, and of physical memory ranges reserved by the BIOS. The address map is returned through successive invocations of this interface; each returning information on a single range of physical addresses. Each range includes a type that indicates how the range of physical addresses is to be treated by the OSPM.

在X86 real mode模式下,该接口以memory map的形式提供实际系统的RAM物理内存情况,以及被BIOS 做保留的地址范围(在实际情况中,尤其是64位系统下,实际的物理内存远远达不到64位用尽的场景,BIOS将一部分物理地址空间分配给PCI以及 ACPI使用),每个memory map映射包括base(基地址)、size(大小)、type(类型)等信息。在每块memory map之内的内存是连续的,而两个memory map之间为不连续,这就造成了空洞现象。物理地址的空洞是由多种原因造成的,一种是由于历史原因,需要兼容旧的硬件架构而造成的,另外一种原因是由于内存出现坏块原因,还由一种原因由于BIOS强制进行分配管理,将连续的物理内存之间划分一块空间给其他使用,造成内存在物理空间上人为被强制隔离开来造成空洞。

INT 0x15/AX=E820H操作分为读取和设置两个部分,设置是由BIOS进行设置,kernel 读取该设置。

设置INT 0x15/AX=E820H

当BIOS 设置INT 0x15/AX=E820H中断时,如下:

  • EAX: function code 可以看出是功能码,BIOS在写入地址映射关系时首先需要把EAX设置为0xE820h。
  • EBX:由于系统中会存在多个memory map映射,当设置INT 0x15/AX=E820H时,该寄存器存返回前面一条memory map映射,如果是第一次调用该EBX则为0。
  • ES:DI:当前memory map的地址范围描述信息(Address Range Descriptior structure)。
  • ECX: address range descriptior structure 结构大小,必须至少大于20,由于该结构存在多个版本,必须指明该结构大小。
  • EDX:必须是534D4150h(‘SWAP')。

Address Range Descriptior structure

地址范围描述结构(address range descriptior structure)如下:

主要分为以下四个部分:

  • BaseAddr: memory 基地址大小为8个字节
  • Length: memory大小 ,8个字节
  • Type: 4个字节 memory 类型
  • Extended Attributes:可扩展部分 4个字节

其在不使用Extended Attributes属性下对应的内核代码结构为boot_e820_entry(该结构定义在arch\x86\include\uapi\asm\bootparam.h文件中):

struct boot_e820_entry {__u64 addr;__u64 size;__u32 type;
} __attribute__((packed));

读取INT 0x15/AX=E820H

读取INT 0x15/AX=E820H时,该系列寄存器状态:

  •  CF: carry flag 当读取d820时,该状态被置位,则说明读取出现错误
  • EAX: 固定值’SWAP',当读取到的EAX与'SWAP'不相等时则说明出现错误,主要用于验证
  • ES:DI:此时为读取到的地址描述信息address range descriptior structure
  • ECX:返回的地址描述信息结构address range descriptior structure大小
  • EBX:指向下一个要读取的memory map,每次读取时都需要将其读取到的EBX值赋值给下一次读取使用。

内核中采用轮询方式读取系统内存信息,每次通过从INT 0x15中一次读取一个memory map信息,由于BIOS中规定最多只有128个 memory map,所以在kernel 启动时 最多轮询读取128次,代码如下:


static void detect_memory_e820(void)
{int count = 0;struct biosregs ireg, oreg;struct boot_e820_entry *desc = boot_params.e820_table;static struct boot_e820_entry buf; /* static so it is zeroed */initregs(&ireg);ireg.ax  = 0xe820;ireg.cx  = sizeof(buf);ireg.edx = SMAP;ireg.di  = (size_t)&buf;/** Note: at least one BIOS is known which assumes that the* buffer pointed to by one e820 call is the same one as* the previous call, and only changes modified fields.  Therefore,* we use a temporary buffer and copy the results entry by entry.** This routine deliberately does not try to account for* ACPI 3+ extended attributes.  This is because there are* BIOSes in the field which report zero for the valid bit for* all ranges, and we don't currently make any use of the* other attribute bits.  Revisit this if we see the extended* attribute bits deployed in a meaningful way in the future.*/do {intcall(0x15, &ireg, &oreg);ireg.ebx = oreg.ebx; /* for next iteration... *//* BIOSes which terminate the chain with CF = 1 as opposedto %ebx = 0 don't always report the SMAP signature onthe final, failing, probe. */if (oreg.eflags & X86_EFLAGS_CF)break;/* Some BIOSes stop returning SMAP in the middle ofthe search loop.  We don't know exactly how the BIOSscrewed up the map at that point, we might have apartial map, the full map, or complete garbage, sojust return failure. */if (oreg.eax != SMAP) {count = 0;break;}*desc++ = buf;count++;} while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_table));boot_params.e820_entries = count;
}
  • 每次调用intcall从0x15中读取一个memory map。
  • 接下来将本次读取 到的ebx 保存到下一次中ireg.ebx = oreg.ebx,方便下次读取。
  • 对carry flag状态进行检查,如不被置位则说明出现错误不再进行读取。
  • 对eax进行验证是否等于SMAP。
  • 将读取到的memory 信息保存到*desc++ = buf;最终是保存到boot_params.e820_table中。
  • count++计数+1。
  • 如果irg.ebx为零 或者count大于128,则说明读取完毕。
  • boot_params.e820_entries记录有多少个memory 信息。

E820源码

启动过程

系统上电,从BIOS进入到kernel后,系统管理权限从BIOS转移到kernel中,由此进入到kernel main函数中(arch\x86\boot\main.c), main函数中通过调用detect_memory()函数,从INT 0x15/AX=E820H 去读内存信息,将系统内存相关信息读取到boot_params.e820_table中,接下来在setup_arch()启动过程中调用e820__memory_setup_default(),将boot_params.e820_table中的e820_table,代码如下:

char *__init e820__memory_setup_default(void)
{char *who = "BIOS-e820";/** Try to copy the BIOS-supplied E820-map.** Otherwise fake a memory map; one section from 0k->640k,* the next section from 1mb->appropriate_mem_k*/if (append_e820_table(boot_params.e820_table, boot_params.e820_entries) < 0) {u64 mem_size;/* Compare results from other methods and take the one that gives more RAM: */if (boot_params.alt_mem_k < boot_params.screen_info.ext_mem_k) {mem_size = boot_params.screen_info.ext_mem_k;who = "BIOS-88";} else {mem_size = boot_params.alt_mem_k;who = "BIOS-e801";}e820_table->nr_entries = 0;e820__range_add(0, LOWMEMSIZE(), E820_TYPE_RAM);e820__range_add(HIGH_MEMORY, mem_size << 10, E820_TYPE_RAM);}/* We just appended a lot of ranges, sanitize the table: */e820__update_table(e820_table);return who;
}

boot_params.e820_table 内存映射关系,通过调用e820__range_add添加到e820_table中。

整个启动过程如下:

main()(arch/x86/boot/header.S)-->detect_memory()-->x86_64_start_kernel-->x86_64_start_reservations-->start_kernel-->setup_arch()-->e820__memory_setup()|-->x86_init.resources.memory_setup=e820__memory_setup_default-->e820__memblock_setup()

e820_table

e820_table是E820模块内部内存管理数据结构,结构如下:

 * The whole array of E820 entries:*/
struct e820_table {__u32 nr_entries;struct e820_entry entries[E820_MAX_ENTRIES];
};

e820_table中将每块内存管理信息抽象成一条条e820_entry进行管理,每条entry信息如下

struct e820_entry {u64           addr;u64            size;enum e820_type     type;
} __attribute__((packed));
  • addr: 该块内存基地址
  • size:内存大小
  • type:内存类型

整个e820_table 组织形式如下图所示:

 每个table包含一个一定大小的内存空间,共128个。

查看e820_table

linux提供了两种查看e820_table方法:

第一种方法,访问位于/sys/firmware/memp目录,在该目录下会有从0~N的目录,每个目录代表着一个table,最多有boot_params.e820_entries目录,如下图所示:

每个子目录内有三个文件分别为start、end以及type,分别代表了该内存table的起始地址、结束地址以及内存类型:

第二种方法,使用dmesg 查看启动过程信息,启动信息过程中会将所有table打印出来:

注意 demsg中打印的e820实际上是e820 tab

e820__memory_setup()

该函数对e820 memory 进行安装:

void __init e820__memory_setup(void)
{char *who;/* This is a firmware interface ABI - make sure we don't break it: */BUILD_BUG_ON(sizeof(struct boot_e820_entry) != 20);who = x86_init.resources.memory_setup();memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));pr_info("BIOS-provided physical RAM map:\n");e820__print_table(who);
}

主要分为三个部分:

  • 调用x86_init.resources.memory_setup函数实际上就是调用的e820__memory_setup_default(),将boot_params.e820_table中的table 读取到e820_table中。
  • 继续将e820_table分别copy 到e820_table_kexec和e820_table_firmware两个表中,e820_table_firmware表即为BIOS中的读取到的最原始系统内存表,/sys/firmware/memmap就是读取的该表,e820_table_kexec还包括了可扩展部分即超过129个table时。
  • 打印e820_table表,即dmesg查看的第二种方法。

e820__memblock_setup()

e820__memblock_setup()将e820中的table 按照需要加载到memblock中,之后将内存管理权限移交给memblock:


void __init e820__memblock_setup(void)
{int i;u64 end;/** The bootstrap memblock region count maximum is 128 entries* (INIT_MEMBLOCK_REGIONS), but EFI might pass us more E820 entries* than that - so allow memblock resizing.** This is safe, because this call happens pretty late during x86 setup,* so we know about reserved memory regions already. (This is important* so that memblock resizing does no stomp over reserved areas.)*/memblock_allow_resize();for (i = 0; i < e820_table->nr_entries; i++) {struct e820_entry *entry = &e820_table->entries[i];end = entry->addr + entry->size;if (end != (resource_size_t)end)continue;if (entry->type == E820_TYPE_SOFT_RESERVED)memblock_reserve(entry->addr, entry->size);if (entry->type != E820_TYPE_RAM && entry->type != E820_TYPE_RESERVED_KERN)continue;memblock_add(entry->addr, entry->size);}/* Throw away partial pages: */memblock_trim_memory(PAGE_SIZE);memblock_dump_all();
}

其中E820_TYPE_SOFT_RESERVED 内存类型加载到memblock reserve中,E820_TYPE_RAM和E820_TYPE_RESERVED_KERN内存类型加载到memblock memory中。

CMDLINE mem=/memap=

除了上述由BIOS中读取到实际物理内存情况之外,还支持通过在内核启动过程中通过CMDLINE mem或者memap强制修改分区状况:

mem=” 参数指明了系统最大物理地址,因此 e820_table 中可用物理内存区域 的范围只要超过这个值的,那么内存区域的类型将会被修改为 “E820_TYPE_RESERVED”。 同理,”memmap=” 参数用于修改一个或多个内存区域的信息,内核根据该修改动态调整 e820_table 表里相关内存区域的信息

该命令行源码位于parse_memmap_one()函数中:


static int __init parse_memmap_one(char *p)
{char *oldp;u64 start_at, mem_size;if (!p)return -EINVAL;if (!strncmp(p, "exactmap", 8)) {e820_table->nr_entries = 0;userdef = 1;return 0;}oldp = p;mem_size = memparse(p, &p);if (p == oldp)return -EINVAL;userdef = 1;if (*p == '@') {start_at = memparse(p+1, &p);e820__range_add(start_at, mem_size, E820_TYPE_RAM);} else if (*p == '#') {start_at = memparse(p+1, &p);e820__range_add(start_at, mem_size, E820_TYPE_ACPI);} else if (*p == '$') {start_at = memparse(p+1, &p);e820__range_add(start_at, mem_size, E820_TYPE_RESERVED);} else if (*p == '!') {start_at = memparse(p+1, &p);e820__range_add(start_at, mem_size, E820_TYPE_PRAM);} else if (*p == '%') {enum e820_type from = 0, to = 0;start_at = memparse(p + 1, &p);if (*p == '-')from = simple_strtoull(p + 1, &p, 0);if (*p == '+')to = simple_strtoull(p + 1, &p, 0);if (*p != '\0')return -EINVAL;if (from && to)e820__range_update(start_at, mem_size, from, to);else if (to)e820__range_add(start_at, mem_size, to);else if (from)e820__range_remove(start_at, mem_size, from, 1);elsee820__range_remove(start_at, mem_size, 0, 0);} else {e820__range_remove(mem_size, ULLONG_MAX - mem_size, E820_TYPE_RAM, 1);}return *p == '\0' ? 0 : -EINVAL;
}

用于对e820的table强制进行修改。

e820__update_table

e820__update_table()函数用于插入一个table 并更新e820_table:


int __init e820__update_table(struct e820_table *table)
{struct e820_entry *entries = table->entries;u32 max_nr_entries = ARRAY_SIZE(table->entries);enum e820_type current_type, last_type;unsigned long long last_addr;u32 new_nr_entries, overlap_entries;u32 i, chg_idx, chg_nr;/* If there's only one memory region, don't bother: */if (table->nr_entries < 2)return -1;BUG_ON(table->nr_entries > max_nr_entries);/* Bail out if we find any unreasonable addresses in the map: */for (i = 0; i < table->nr_entries; i++) {if (entries[i].addr + entries[i].size < entries[i].addr)return -1;}/* Create pointers for initial change-point information (for sorting): */for (i = 0; i < 2 * table->nr_entries; i++)change_point[i] = &change_point_list[i];/** Record all known change-points (starting and ending addresses),* omitting empty memory regions:*/chg_idx = 0;for (i = 0; i < table->nr_entries; i++)    {if (entries[i].size != 0) {change_point[chg_idx]->addr = entries[i].addr;change_point[chg_idx++]->entry  = &entries[i];change_point[chg_idx]->addr   = entries[i].addr + entries[i].size;change_point[chg_idx++]->entry   = &entries[i];}}chg_nr = chg_idx;/* Sort change-point list by memory addresses (low -> high): */sort(change_point, chg_nr, sizeof(*change_point), cpcompare, NULL);/* Create a new memory map, removing overlaps: */overlap_entries = 0;   /* Number of entries in the overlap table */new_nr_entries = 0;    /* Index for creating new map entries */last_type = 0;         /* Start with undefined memory type */last_addr = 0;       /* Start with 0 as last starting address *//* Loop through change-points, determining effect on the new map: */for (chg_idx = 0; chg_idx < chg_nr; chg_idx++) {/* Keep track of all overlapping entries */if (change_point[chg_idx]->addr == change_point[chg_idx]->entry->addr) {/* Add map entry to overlap list (> 1 entry implies an overlap) */overlap_list[overlap_entries++] = change_point[chg_idx]->entry;} else {/* Remove entry from list (order independent, so swap with last): */for (i = 0; i < overlap_entries; i++) {if (overlap_list[i] == change_point[chg_idx]->entry)overlap_list[i] = overlap_list[overlap_entries-1];}overlap_entries--;}/** If there are overlapping entries, decide which* "type" to use (larger value takes precedence --* 1=usable, 2,3,4,4+=unusable)*/current_type = 0;for (i = 0; i < overlap_entries; i++) {if (overlap_list[i]->type > current_type)current_type = overlap_list[i]->type;}/* Continue building up new map based on this information: */if (current_type != last_type || current_type == E820_TYPE_PRAM) {if (last_type != 0)    {new_entries[new_nr_entries].size = change_point[chg_idx]->addr - last_addr;/* Move forward only if the new size was non-zero: */if (new_entries[new_nr_entries].size != 0)/* No more space left for new entries? */if (++new_nr_entries >= max_nr_entries)break;}if (current_type != 0)   {new_entries[new_nr_entries].addr = change_point[chg_idx]->addr;new_entries[new_nr_entries].type = current_type;last_addr = change_point[chg_idx]->addr;}last_type = current_type;}}/* Copy the new entries into the original location: */memcpy(entries, new_entries, new_nr_entries*sizeof(*entries));table->nr_entries = new_nr_entries;return 0;
}

参数 table 用于指向一张 e820 表。 当向一张 e820 表中插入内存区域信息之后,e820 表将新的内存区域信息插入到表的 末尾,因此会导致 e820 表中的内存区域并不是按起始物理地址进行排序,这会给内核 查找指定的内存区域带来不便以及效率上的印象,并且可能出现新插入的内存区域与表 内的内存区域存在相邻和重叠的情况,因此 e820__update_table() 函数用于整理 e820 表,首先将表中的内存区域信息进行排序,按内存区域信息的起始物理地址从低到高进行 排序,然后将存在重叠和相邻的同类型内存区域合并并只用一个内存区域信息进行描述。

调用关系

e820->memblock->sparse启动调用关系如下图所示:

参考资料

《Advanced Configuration and Power Interface (ACPI) Specification》

https://biscuitos.github.io/blog/MMU-E820/#D02008

linux内核那些事之E820相关推荐

  1. linux内核那些事之buddy(慢速申请内存__alloc_pages_slowpath)(5)

    内核提供__alloc_pages_nodemask接口申请物理内存主要分为两个部分:快速申请物理内存get_page_from_freelist(linux内核那些事之buddy(快速分配get_p ...

  2. linux内核那些事之buddy(anti-fragment机制)(4)

    程序运行过程中,有些内存是短暂的驻留 用完一段时间之后就可以将内存释放以供后面再次使用,但是有些内存一旦申请之后,会长期使用而得不到释放.长久运行有可能造成碎片.以<professional l ...

  3. linux内核那些事之mmap_region流程梳理

    承接<linux内核那些事之mmap>,mmap_region()是申请一个用户进程虚拟空间 并根据匿名映射或者文件映射做出相应动作,是实现mmap关键函数,趁这几天有空闲时间 整理下mm ...

  4. linux内核那些事之buddy

    buddy算法是内核中比较古老的一个模块,很好的解决了相邻物理内存碎片的问题即"内碎片问题",同时有兼顾内存申请和释放效率问题,内核从引入该算法之后一直都能够在各种设备上完好运行, ...

  5. linux内核那些事之pg_data_t、zone结构初始化

    free_area_init 继续接着<linux内核那些事之ZONE>,分析内核物理内存初始化过程,zone_sizes_init()在开始阶段主要负责对各个类型zone 大小进行计算, ...

  6. linux内核那些事之Sparse vmemmap

    <inux内核那些事之物理内存模型之SPARCE(3)>中指出在传统的sparse 内存模型中,每个mem_section都有一个属于自己的section_mem_map,如下图所示: 而 ...

  7. linux内核那些事之buddy(anti-fragment机制-steal page)(5)

    继<linux内核那些事之buddy(anti-fragment机制)(4)>,在同一个zone内指定的migrate type中没有足够内存,会启动fallback机制,从fallbac ...

  8. linux内核那些事之Memory protection keys(硬件原理)

    mprotect/map原理及缺陷 <linux 关于虚拟内存的几个系统调用>,提及到用户程序可以通过mprotect或者map(MAP_FIXED)系统调用可以根据需要再次修改已经申请过 ...

  9. linux内核那些事之mmap

    <linux mmap系统调用>主要描述了用户空间内mmap的使用及其注意事项,mmap最终还是要进入到内核态,如果没有指定addr则由内核分配一段可用的vma,如果已经指定addr则内核 ...

最新文章

  1. 《Science》评选2017年十大科学突破,看看有哪些吧!
  2. 【AngularJs】获取URL查询参数
  3. Springboot-Vue-MybatisPlus 返回给前端的 Long类型数据失去精度怎么办 之 Long类型作为实体类的一个属性
  4. pythongui程序,python第一个GUI程序
  5. chrome插件网站
  6. 消息称蚂蚁集团提前至11月5日挂牌,估值达3.6万亿港元
  7. BZOJ 2406 LuoguP4194 矩阵 有上下界可行流
  8. MyEclipse10破解方法
  9. 顶级 Vue.js 开发工具
  10. mysql -h_MySQL登录数据库 h参数
  11. 输入年份和月份输出该月有多少天python_输入年份和月份,输出该月有多少天,判断这一天是该年的第几天...
  12. 离散数学学习笔记 第二章 命题逻辑
  13. 标书制作,全流程视频教程大全
  14. Mac下编译WebRTC(Mac和iOS版本)
  15. php 超炫 页面,dedecms织梦后台模板,超炫界面风格
  16. 老牌安全公司CYBER ARK眼中的RPA部署安全问题
  17. 使用VS Code通过Markdown语法快速画流程图时序图等
  18. 软件(程序)编写通法
  19. 阿里云服务器是干什么用的?
  20. 关于微信小程序云开发以及云开发实例展示

热门文章

  1. JeeWx 微信开发公开课(Jeewx-API 专题),今晚8点不见不散
  2. 解决tomcat中temp文件夹出现项目的副本的情况
  3. 用javascript缓存ajax数据
  4. Linux IPC实践(10) --Posix共享内存
  5. 前后端配合实现密码找回功能思路
  6. Redmine incompatible character encodings: UTF-8 and ASCII-8BIT
  7. 用模糊查询like语句时如果要查是否包含%字符串该如何写
  8. ajax利用FormData、FileReader实现多文件上传php获取
  9. 想要写好的程序应该远离计算机
  10. IBM Power System P550双机系统方案