内核页表初始化
- 暂时用
2.6.11
x86 平台的内核来做讲解。- 从
bois
中获取各种配置信息,我也不懂,所以仅介绍结果- 该篇文章已假设你了解了内核的内存总体的分布,如果没有请参考我的这篇文章
- 我们以常见的非连续性内存模型来讲解,即
CONFIG_DISCONTIGMEM
已定义的代码。
物理内存分布
首先可用的物理内存(地址)不是连续分布的,有很多空洞。因为这些空洞要么不可用,要么被其他外设占用,所以在内核初始化时,必须从bois中获取可用于物理内存。另外由于内存只识别 4KB 对齐的内存,所以那些处于被对齐位置的小于 4KB 的内存也不能为内核所用。相关代码如下:
e820
映射表
内存把可用的物理地址分布存放在此数据结构中
struct e820map {
int nr_map;
struct e820entry {
unsigned long long addr; /* start of memory segment */
unsigned long long size; /* size of memory segment */
unsigned long type; /* type of memory segment */
} map[32];
};
/*内核内存映射表,将根据其中的内存分布初始化内核页表*/
struct e820map e820;
- 从
bois
参数中获取物理内存
内核完成实模式的启动后,在开启分页机制需要获取物理内存的分布,函数如下:
static char * __init machine_specific_memory_setup(void)
{
/* 一般情况 copy_e820_map() 从bois参数中获取内存分布回成功,
* 如果是失败则会使用默认的内存分布进行初始化 */
if (copy_e820_map(E820_MAP, E820_MAP_NR) < 0) {
unsigned long mem_size;
if (ALT_MEM_K < EXT_MEM_K) {
mem_size = EXT_MEM_K;
} else {
mem_size = ALT_MEM_K;
}
e820.nr_map = 0;
add_memory_region(0, 0x9f000, E820_RAM);
add_memory_region(1024*1024, mem_size << 10, E820_RAM);
}
return who;
}
static int __init copy_e820_map(struct e820entry * biosmap, int nr_map)
{
do {
unsigned long long start = biosmap->addr;
unsigned long long size = biosmap->size;
unsigned long long end = start + size;
unsigned long type = biosmap->type;
/* 地址溢出64位就会失败 */
if (start > end)
return -1;
/*添加到映射表中*/
add_memory_region(start, size, type);
} while (biosmap++,--nr_map);
return 0;
}
static void __init add_memory_region(unsigned long long start,
unsigned long long size, int type)
{
int x = e820.nr_map;
e820.map[x].addr = start;
e820.map[x].size = size;
e820.map[x].type = type;
e820.nr_map++;
}
- 查找最大页帧号
在内核很多地方都需要知道最大的可用物理地址,我们可以从上面得到内存分布中计算出最大可用的物理地址,相比物理地址,内核更喜欢使用页帧号来表示物理地址。计算出的页帧号存储在 max_pfn
中。
void __init find_max_pfn(void)
{
int i;
max_pfn = 0;
for (i = 0; i < e820.nr_map; i++) {
unsigned long start, end;
start = PFN_UP(e820.map[i].addr);
end = PFN_DOWN(e820.map[i].addr + e820.map[i].size);
if (start >= end)
continue;
if (end > max_pfn)
max_pfn = end;
}
}
- 最小页帧号
最小页帧号与内核镜像的映射有直接关系,由于这一部分与硬件、内核的编译、汇编等有直接的关系,本人能力有限,先仅给出一个结果。内核将从 物理内存 1MB
的位置出开始放置内核镜像的数据(代码段和数据段),这些数据之后的物理内存才能被用于其他(包括后续初始化以及运行中的内存分配)。启用分页后,所有寻址都通过页表进行,这部分内核镜像数据也不例外,所以需要为了分页下访问这部分数据的而建立初始化页表。从 vmlinux.lds.S
中我们可以看到镜像的分布:
pfn = 0
+----------+-------------------------------+-----+-----+-----+
| hole | text | data | init data | ... | bss | pg0 | pg1 |
+----------+-------------------------------+-----+-----+-----+
| |
+---------+ +------------+
| ... | swapper_pg_dir | ... |
+----------------------------+
以上的分布,hole
是空洞部分,这部分物理地址可能被其他硬件使用,为了将内核镜像连续的映射,所以选择跳过;后续就是内核镜像的各个数据段,直到 pg0
结束。其中 bss
段中包含了 全局目录页的空间 swapper_pg_dir
,pg0
被用作建立初始化页表的 中间目录页,如果内核的镜像需要 2张
中间目录页,也就是大于 4MB
小于等于 8MB
,那么还需要一页的数据 pg1
,具体使用多少张中间目录页,以此类推。用作页表初始化的中间目录页之后的物理地址就是在内核初始化起初可以最小页帧的起始位置,使用 init_pg_tables_end
来记录。后续这个值会因为其他初始化变化。
- 最大直接映射页帧号
我们知道32位平台内存只能最多映射 896MB
的内存,一切大于这个值的物理内存都视为高端内存,内核通过对物理内存大小的判断来计算直接映射的结束位置,并存储在 max_low_pfn
中。
unsigned long __init find_max_low_pfn(void)
{
unsigned long max_low_pfn;
max_low_pfn = max_pfn;
/*MAXMEM_PFN(保留高端内存后的内存大小,一般情况下是896MB)可以直接映射的内存页帧最大编号*/
if (max_low_pfn > MAXMEM_PFN) {
/*如果系统可用物理内存大于896mb
*减去 MAXMEM_PFN 就得到高端页数
*/
highmem_pages = max_pfn - MAXMEM_PFN;
max_low_pfn = MAXMEM_PFN;
} else {
/*如果系统可用物理内存小于等于896MB,就没有高端页框*/
highmem_pages = 0;
}
return max_low_pfn;
}
我们不关心启动项参数 highmem=
带来的影响,所以移除了一些判断代码。
NUMA
与物理内存
NUMA
全称 Non-Uniform Memory Access
,译为 非一致性内存访问
,简单的来说就是将内存分组,多 CPU
系统可以在访问离它越近(电路上)的内存时,速度越快。离它最近的称为本地内存,这些分组称为 node
节点并且被编号 nid
,内核一般情况只要是编号都使用位图来实现。
- 明确节点管理页帧范围
内存被分组,但是物理地址是统一的,所以内核在抽象这些 节点
前,必须先搞清楚每个节点的物理地址起止,同样内核使用页帧号来表述,相关函数如下:
static inline void get_memcfg_numa(void)
{
/*初始化node和其中的页帧起止号*/
if (get_memcfg_numaq())
return;
/* 如果失败则强制使用平坦模式,
* 即使有多真是的node,我们也只视为仅有一个node,管理所有的页帧 */
get_memcfg_numa_flat();
}
int __init get_memcfg_numaq(void)
{
int node;
struct eachquadmem *eq;
struct sys_cfg_data *scd =
(struct sys_cfg_data *)