建立数据结构
特定于体系结构的设置
由于统一的体系结构提升了AMD64的地位,使之成为内核支持的最重要的体系结构之一,因此我也会考虑AMD64和IA-32在一些特定于体系结构的细节上的差别。
1.内核在内存中得分布局
配置内核二进制代码在物理内存中的初始位置。此外,一些嵌入式系统也需要这种能力。配置选项PHYSICAL_START用于确定内存中的位置,会受到配置选项PHYSICAL_ALLGN设置的物理对其方式的影响。
前4kib是第一个页帧,一般会忽略,因为通常保留给BIOS使用。接下来的640KIB原则上是可用的,但也不用于内核加载。器原因是,该区域之后紧邻的区域由系统保留的,用于映射各种ROM。不可能像映射ROM的区域写入数据,。但内核总是会装1仔到一个连续的内存中,如果要从4KIB出作为起始位置来装载内核映像,则要求内核必须小于640kib
内核占据的内存分为几个段,器边界保存在变量中。
*_text和_etext是代码段的起始和结束地址,包含编译后的内核代码。
*数据段位于_etext和_edata之间,保存了大部分内核变量。
*出事化数据在内核启动过程结束后不再需要保存在最后一段,从_endata到_end。在内核初始化完成后,器重的大部分数据都可以从内存删除,给应用程序流出更多空间。这一段内存区划分为更小的子区间,一控制那些可以删除,那些不能删除,但这对于我们现在的讨论没有多大意义。
只有在目标文件链接完成后,才能知道去确切的数值,接下来则打包为二进制文件。该操作是由arch/arch/vmlinux.ld.S控制的。
准确的数值依内核配置而异,因为每种配置的代码段和数据段长度都不相同,这取决启用和禁止用了内核的那些部分。只有起始地址(_text)总是相同的。
在用户和内存地址空间之间采用标准的3:1划分时,内核段的起始地址。该地址是虚拟地址,因为物理内存映射到虚拟地址空间的时候,采用了从该地址并开始的线性映射方式。减去0xc0000000,则可获得对应的物理地址。
2.初始化步骤
调用machine_specific_memory_setup, 创建一个列表,包括系统占据的内存区和空闲内存区。
在系统启动时,找到内存区由内核函数print_memory_map显示。
内核接下来用parse_cmdline_early分析命令行,主要类似mem=XXX\highmem=XXX或memmap=XXX
下一步主要步骤在setup_memory中执行,该函数有两个版本。一个用于连续内存系统(arch/x86/kernel/setup_32.c),另一个用于不连续内存系统(arch/.x86/mm/discontig_32.c)
paging_init初始化内核页并启用内存分页,因为IA-32计算机默认情况下分页是禁用的。
调用zone_sizes_init会初始化系统中所有节点的pgdat_t实例。首先使用add_active_range,对可用的物理内存建立一个性对简单的列表。体系结构无关的函数free_area_init_nodes接下来使用该信息建立完备的内核数据结构。
3 分页机制的初始化
*paging_init负责建立只能用于内核的页表,用户空间无法访问。
*物理内存页则映射内核地址空间的起始处,以便内核直接访问,而无须复杂的页表操作。
内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧。
*__pa(vaddr)返回与虚拟地址vaddr相关的物理地址。
*__va(paddr)则计算出对应物理地址paddr的虚拟地址。
注:还可以完全去掉划分机制,引入两个4G地址空间,一个用于内核,领一个用于每个用户空间程序情况下,内核和用户状态之间的上下文切换代价会更高。
内核地址空间的最后128M用途:
(1)虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配乱改机制过程用户过程,内核自身会试图尽力试图避免非连续的物理地址。
(2)持久映射用于将高端内存域中的非持久页映射到内核。
(3)固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以选择,它与通过固定公式与物理内存关联的直接映射也相反,虚拟固定映射地址与物理内存的关联可以自行定义,关联建立后内核总是会注意的。,
在这里有两个与处理符号很重要:__VMALLOC_RESERVE设置了vmalloc区域长度,而则表示内核可以直接寻址的物理内存的最大可能数量。
#include/asm-x86/fixmap_32.h
#define __fix_to_virt(x)
从顶部开始,内核退后n页,以确定地N个映射向的虚拟地址地址。整个计算同样也只使用了常数,编译器能够子啊编译时计算结构。
固定映射虚拟地址与物理内存也之间的关联是有set_fixmap(fixmpa, page_nr)和fixmap_nocache建立的。
*备选划分方式
将虚拟地址空间按3:1比例话费呢不是唯一的选项。
本质上,手工修改内核源嗲吗是可以重新配置内核划分方式的,但内核也提供了一些默认的一些设置参数
*划分虚拟地址
pagetable_init首先初始化系统的页表,以swapper_pg_dir为基础。
借助于kernel_physical_mapping_init,将物理内存也映射到虚拟地址空间中PAGE_OFFSET开始位置。内核接下来扫描各个项目录所有相关项,奖指针设置为正确的值。
mm/page_alloc.c
zone_pcp_init负责初始化该缓存。gia函数有free_area_init_nodes调用
mm/page_alloc.c
zone_batchsize算出批量大侠后,代码将遍历系统中的所有CPU,同时调用setup_pageset填充每个per_cpu_pageset实例的常量。
pcp->batch决定了在重新填充列表时,有多少页会立即使用。处于性能方面的考虑,一般会列表添加连续的多页,而不是但页
4.注册活动内存区
活动内ucnqu就是不包含空洞的内存区。必须使用add_active_range在全局变量early_node_map中注册内存区。
mm/page_alloc.c
static struct node_active_reging __meminitdata eraly_node_map[MAX_ACTIVE_REGIONS];
static int __meminitdata nr_nodemap_entries;
mm/page_alloc.c
void __init add_active-range();
在注册两个毗邻的内存时,add_active_regions确保级那个他们合并为一个。
max_low_pfn和highend_pfn是全局变量,分别制定了低端3:1划分,通常和高端内存中最高的页号。
*在AMD64上注册内存区
arch/x86/kernel/e820_64.c
e820_register_active_regions()
本质上,上述代码就是根据BIOS提供的信息遍历所有的内存区,并针对每个内存区找到活动内存。
void __init paging_init(void)
5.AMD64地址空间的设置
AMD64系统地址空间的设置在某些方面比IA-32容易,但在另一些方面要困难。
虚拟内存映射(virtual memory map,VMM)内存区进阶这vmalloc内存区之后,长为1Tib。只有内核使用了细说内存模型,gia内存区才能是有用的。
include/asm-x86/page_64.h
include/asm-x86/pgtable_64.h
启动过程期间的内存管理
在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段早期发呢陪内存。
内核开发者决定实现一个最先适配(first-fit)分配器用于在启动阶段管理内存,这是可能想到最简单的方式。
1.数据结构
即使最先适配分配器也必须管理一些数据。
<bootmem.h>
typedef struct bootmem_data {
unsigned long node_boot_start;
unsigned long node_low_pfn;
void * node_bootmem_map;
unsigned long last_offset;
unsigned long last_pos;
unsigned long last_success;
struct list_head lost;
}bootmem_data_t;
*node_boot_start保存了系统中第一个页的编号
*node_low_pfn是可以直接管理的物理地址空间中最后一页的编号.(ZONE_NORMAL)
*node_bootmem_map是指向存储分配位图的内存区的指针。
*last_pos是上一次发呢陪的也的编号
*last_success指定位图中上一次陈公公分配内存的位置,新的分配将由此开始。
2.初始化
bootmem分配器的初始化是有ige特定于体系结构的过程,此外还取决于所属计算机的内存布局。
setup_bootmem_allocatior接下来负责发起所有必要的步骤,一初始化bootmen分配器。他首先调用通用函数init_bootmem, 该函数是init_bootmem_core的一个前段。
init_bootmem_core的目的在于执行bootmem分配器的第一个初始化步骤。先前检测到低端内存也帧的方位bootmem_data_t实例中,这里conting_bootmem_data。最初在位图contig_bootmemdata->node_bootmem_map中,所有的也都标记为已用。
该标记过程由两个特定体系结构的。register_bootmem_low_pages通过将位图中对应的比特位清零,释放所有潜在可用的内存页。
*AMD64的初始化
首先,bootmem_bootmap_bitmap计算bootmem位图所需页的数目。
然后,使用init_bootmen将信息填充到体系结构无关的bootmem数据接结构中。
3.对内核的结构
*分配内存
内核提供了各种函数,用于在初始化期间分配内存。
*alloc_bootmem(size)和alloc_bootmem_pages(size)按指定大小在ZONE_ORMALN内存与,分配内存。数据是对齐的,这使得内存或者从可使用L1高速缓存的理想位置开始,或者从业边界开始。
*alloc_bootmem_low和alloc_bootmem_low_pages的工作方式类似上述函数。
__alloc_bootmem_core函数的功能相对广泛,因为该函数主要实现了前文已经描述过的最先适配算法。
(1)从goal开始,扫描位图,查找满足分配请求的空闲内存区
(2)如果目标页紧接着上一次分配的页,bootmem_data->last_pos,内核会检查bootmem_data->last_offset,判断所需的内ucnshifou能够在上一页分配或从上一页开始发呢陪
(3)新分配的也在位图对ing的比特位设置为1.
*释放内存
内核提供了free_bootmem函数来释放内存。他需要两个参数:需要释放的内存区的起始地址和长度。
<bootmen.h>
void free_bootmem(unsigned long addr, unsignefd long size);
void free_bootmem_node(pg_data_t *pgdat, unsigned long addr, unsigned long szie)
4.停用bootmem分配器
在系统初始化进行伙伴系统分配器能够承担内存管理的责任后,必须停用bootmem分配器,毕竟不能同时用两个分配器管理内存。
5.释放初始化数据
许多内核代码块和数据表只在系统初始化阶段需要。
内核提供了两个属性(__init和__initcall)用于标记初始化函数和数据。这些必须置于函数或数据的声明之前。
int __init hyper_hopper_probe(struct net_device *dev)
__init属性插入到函数声明中返回类型和函数名之间。
初始化函数实现的背后,器一般性的思想在于,将数据保持在内核影像的一个特定部分,在启动接收时可以完全从内存删除。
free_initmem负责释放用于初始化的内存区,并将相关的页返回给伙伴系统。在启动过程刚好结束时会调用该函数,紧接起皱init作为系统中第一个进程启动。启动日志包含了一条信息,指出释放了多少内存。