深入分析Linux内核源代码6-Linux 内存管理(1)

在这里插入图片描述
每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你!

————零声学院

存储器是一种必须仔细管理的重要资源。在理想的情况下,每个程序员都喜欢无穷大、
快速并且内容不易变(即掉电后内容不会丢失)的存储器,同时又希望它是廉价的。但不幸
的是,当前技术没有能够提供这样的存储器,因此大部分的计算机都有一个存储器层次结构,
即少量、快速、昂贵、易变的高速缓存(cache);若干兆字节的中等速度、中等价格、易变
的主存储器(RAM);数百兆或数千兆的低速、廉价、不易变的磁盘。如图 6.1 所示,这些资
源的合理使用与否 ,直接关系着系统的效率。
在这里插入图片描述
Linux 内存管理是内核最复杂的任务之一。主要是因为它用到许多 CPU 提供的功能,而
且和这些功能密切相关。正因为如此,我们在第二章较详细地介绍了 Intel 386 的段机制和
页机制,可以说这是进行内存管理分析的物质基础 。这一章用大量的篇幅描述了 Linux 内存
管理所涉及到的各种机制,如内存的初始化机制、地址映射机制、请页机制、交换机制、内
存分配和回收机制、缓存和刷新机制以及内存共享机制,最后还分析了程序的创建和执行。

6.1 Linux 的内存管理概述

Linux 是为多用户多任务设计的操作系统,所以存储资源要被多个进程有效共享;且由
于程序规模的不断膨胀,要求的内存空间比从前大得多。 Linux 内存管理的设计充分利用了
计算机系统所提供的虚拟存储技术,真正实现了虚拟存储器管理。
第二章介绍的 Intel 386 的段机制和页机制是 Linux 实现虚拟存储管理的一种硬件平
台。实际上, Linux 2.0 以上的版本不仅仅可以运行在 Intel 系列个人计算机上,还可以运
行在 Apple 系列、DEC Alpha 系列、MIPS 和 Motorola 68k 等系列上, 这些平台都支持虚拟
存储器管理,我们之所以选择 Intel 386,是因为它具有代表性和普遍性。
Linux 的内存管理主要体现在对虚拟内存的管理。我们可以把 Linux 虚拟内存管理功能
概括为以下几点:
• 大地址空间;
• 进程保护;
• 内存映射;
• 公平的物理内存分配;
• 共享虚拟内存。
关于这些功能的实现,我们将会陆续介绍。

6.1.1 Linux 虚拟内存的实现结构
我们先从整体结构上了解 Linux 对虚拟内存的实现结构,如图 6.2 所示。
在这里插入图片描述
从图 6.2 中可看到实现虚拟内存的组成模块。其实现的源代码大部分放在/mm 目录下。
(1)内存映射模块(mmap):负责把磁盘文件的逻辑地址映射到虚拟地址,以及把虚拟地
址映射到物理地址。
(2)交换模块(swap):负责控制内存内容的换入和换出,它通过交换机制,使得在物
理内存的页面(RAM 页)中保留有效的页 ,即从主存中淘汰最近没被访问的页,保存近来访
问过的页。
(3)核心内存管理模块(core):负责核心内存管理功能,即对页的分配、回收、释放
及请页处理等,这些功能将被别的内核子系统(如文件系统)使用。
(4)结构特定的模块:负责给各种硬件平台提供通用接口,这个模块通过执行命令来改
变硬件 MMU 的虚拟地址映射,并在发生页错误时,提供了公用的方法来通知别的内核子系统。
这个模块是实现虚拟内存的物理基础。

6.1.2 内核空间和用户空间
从第二章我们知道,Linux 简化了分段机制,使得虚拟地址与线性地址总是一致,因此,
Linux 的虚拟地址空间也为 0~4G 字节。Linux 内核将这 4G 字节的空间分为两部分。将最高
的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为“内核空间”。而将
较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个进程使用,称为“用户空
间”。因为每个进程可以通过系统调用进入内核,因此,Linux 内核由系统内的所有进程共享。
于是,从具体进程的角度来看,每个进程可以拥有 4G 字节的虚拟空间。图 6.3 给出了进程
虚拟空间示意图。
在这里插入图片描述
Linux 使用两级保护机制:0 级供内核使用,3 级供用户程序使用。从图 6.3 中可以看出,
每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高
的 1G 字节虚拟内核空间则为所有进程以及内核所共享。
1.虚拟内核空间到物理空间的映射
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和
数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。读者会问,系统启动时,内
核的代码和数据不是被装入到物理内存吗?它们为什么也处于虚拟内存中呢?这和编译程序
有关,后面我们通过具体讨论就会明白这一点。
虽然内核空间占据了每个虚拟空间中的最高 1G 字节,但映射到物理内存却总是从最低
地址(0x00000000)开始。如图 6.4 所示,对内核空间来说,其地址映射是很简单的线性映
射,0xC0000000就是物理地址与线性地址之间的位移量,在 Linux代码中就叫做 PAGE_OFFSET。
在这里插入图片描述
我们来看一下在 include/asm/i386/page.h 中对内核空间中地址映射的说明及定义:

/* 
* This handles the memory map.. We could make this a config 
* option, but too many people screw it up, and too few need 
* it. 
* 
* A __PAGE_OFFSET of 0xC0000000 means that the kernel has 
* a virtual address space of one gigabyte, which limits the 
* amount of physical memory you can use to about 950MB. 
* 
* If you want more physical memory than this then see the CONFIG_HIGHMEM4G 
* and CONFIG_HIGHMEM64G options in the kernel configuration. 
*/ 
#define __PAGE_OFFSET (0xC0000000) 
…… 
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) 
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) 
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) 

源代码的注释中说明,如果你的物理内存大于 950MB,那么在编译内核时就需要加
CONFIG_HIGHMEM4G 和 CONFIG_HIGHMEM64G 选项,这种情况我们暂不考虑。如果物理内存小于
950MB,则对于内核空间而言,给定一个虚地址 x,其物理地址为“x- PAGE_OFFSET”,给定
一个物理地址 x,其虚地址为“x+ PAGE_OFFSET”。
这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于
用户空间,用户空间的地址映射要复杂得多。
2.内核映像
在下面的描述中,我们把内核的代码和数据就叫内核映像(Kernel Image)。当系统启
动时,Linux 内核映像被安装在物理地址 0x00100000 开始的地方,即 1MB 开始的区间(第 1M
留作它用)。然而,在正常运行时, 整个内核映像应该在虚拟内核空间中,因此,连接程序
在连接内核映像时,在所有的符号地址上加一个偏移量 PAGE_OFFSET,这样,内核映像在内
核空间的起始地址就为 0xC0100000。
例如,进程的页目录 PGD(属于内核数据结构)就处于内核空间中。在进程切换时,要
将寄存器 CR3 设置成指向新进程的页目录 PGD,而该目录的起始地址在内核空间中是虚地址,
但 CR3 所需要的是物理地址,这时候就要用__pa()进行地址转换。在 mm_context.h 中就有这
么一行语句:
asm volatile(“movl %0,%%cr3”: :”r” (__pa(next->pgd));
这是一行嵌入式汇编代码,其含义是将下一个进程的页目录起始地址 next_pgd,通过
__pa()转换成物理地址,存放在某个寄存器中,然后用 mov 指令将其写入 CR3 寄存器中。经
过这行语句的处理,CR3 就指向新进程 next 的页目录表 PGD 了。

6.1.3 虚拟内存实现机制间的关系
Linux 虚拟内存的实现需要各种机制的支持,因此,本章我们将对内存的初始化进行描
述以后,围绕以下几种实现机制进行介绍:
• 内存分配和回收机制;
• 地址映射机制;
• 缓存和刷新机制;
• 请页机制;
• 交换机制;
• 内存共享机制。
这几种机制的关系如图 6.5 所示。
在这里插入图片描述
首先内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址,在用户程序运
行时如果发现程序中要用的虚地址没有对应的物理内存时,就发出了请页要求①;如果有空
闲的内存可供分配,就请求分配内存②(于是用到了内存的分配和回收),并把正在使用的物
理页记录在页缓存中③(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换
机制,腾出一部分内存④⑤。另外在地址映射中要通过 TLB(翻译后援存储器)来寻找物理
页⑧;交换机制中也要用到交换缓存⑥,并且把物理页内容交换到交换文件中后也要修改页
表来映射文件地址⑦。

6.2 Linux 内存管理的初始化

在对内存管理(MM)的各种机制介绍之前,首先需要对 MM 的初始化过程有所了解,以
下介绍的内容是以第二章分段和分页机制为基础的,因此,读者应该先温习一下相关内容。
因为在嵌入式操作系统的开发中,内存的初始化是重点关注的内容之一,因此本节将对内存
的初始化给予详细描述。
6.2.1 启用分页机制
当 Linux 启动时,首先运行在实模式下,随后就要转到保护模式下运行。因为在第二章
段机制中,我们已经介绍了 Linux 对段的设置,在此我们主要讨论与分页机制相关的问题。
Linux 内核代码的入口点就是/arch/i386/kernel/head.S 中的 startup_32。
1.页表的初步初始化

/* 
 * The page tables are initialized to only 8MB here - the final page 
 * tables are set up later depending on memory size. 
 */ 
.org 0x2000 
ENTRY(pg0) 
.org 0x3000 
ENTRY(pg1) 
/* 
 * empty_zero_page must immediately follow the page tables ! (The 
 * initialization loop counts until empty_zero_page) 
 */ 
.org 0x4000 
ENTRY(empty_zero_page) 
 /* 
 * Initialize page tables 
 */
 movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */ 
 movl $007,%eax /* "007" doesn't mean with right to kill, but 
 PRESENT+RW+USER */ 
 2: stosl 
 add $0x1000,%eax 
 cmp $empty_zero_page-__PAGE_OFFSET,%edi 
 jne 2b 

内核的这段代码执行时,因为页机制还没有启用,还没有进入保护模式,因此指令寄存
器 EIP 中的地址还是物理地址,但因为 pg0 中存放的是虚拟地址(gcc 编译内核以后形成的
符号地址都是虚拟地址),因此,“$pg0-__PAGE_OFFSET ”获得 pg0 的物理地址,可见 pg0
存放在相对于内核代码起点为 0x2000 的地方,即物理地址为 0x00102000,而 pg1 的物理地址
则为 0x00103000。Pg0 和 pg1 这个两个页表中的表项则依次被设置为 0x007、0x1007、0x2007
等。其中最低的 3 位均为 1,表示这两个页为用户页,可写,且页的内容在内存中(参见图
2.24)。所映射的物理页的基地址则为 0x0、0x1000、0x2000 等,也就是物理内存中的页面 0、
1、2、3 等等,共映射 2K 个页面,即 8MB 的存储空间。由此可以看出,Linux 内核对物理内
存的最低要求为 8MB。紧接着存放的是 empty_zero_page 页(即零页),零页存放的是系统启
动参数和命令行参数,具体内容参见第十三章。
2.启用分页机制

/* 
 * This is initialized to create an identity-mapping at 0-8M (for bootup 
 * purposes) and another mapping of the 0-8M area at virtual address 
 * PAGE_OFFSET. 
 */ 
.org 0x1000 
ENTRY(swapper_pg_dir) 
 .long 0x00102007 
 .long 0x00103007 
 .fill BOOT_USER_PGD_PTRS-2,4,0 
 /* default: 766 entries */ 
 .long 0x00102007 
 .long 0x00103007 
 /* default: 254 entries */ 
 .fill BOOT_KERNEL_PGD_PTRS-2,4,0 
/* 
 * Enable paging 
 */ 

3:

 movl $swapper_pg_dir-__PAGE_OFFSET,%eax 
 movl %eax,%cr3 /* set the page table pointer.. */ 
 movl %cr0,%eax 
 orl $0x80000000,%eax 
 movl %eax,%cr0 /* ..and set paging (PG) bit */ 
 jmp 1f /* flush the prefetch-queue */ 
1: 
 movl $1f,%eax
 jmp *%eax /* make sure eip is relocated */ 
1:

我们先来看这段代码的功能。这段代码就是把页目录 swapper_pg_dir 的物理地址装入
控制寄存器 cr3,并把 cr0 中的最高位置成 1,这就开启了分页机制。

但是,启用了分页机制,并不说明 Linux 内核真正进入了保护模式,因为此时,指令寄
存器 EIP 中的地址还是物理地址,而不是虚地址。“jmp 1f”指令从逻辑上说不起什么作用,
但是,从功能上说它起到丢弃指令流水线中内容的作用(这是 Intel 在 i386 技术资料中所建
议的),因为这是一个短跳转,EIP 中还是物理地址。紧接着的 mov 和 jmp 指令把第 2 个标号
为 1 的地址装入 EAX 寄存器并跳转到那儿。在这两条指令执行的过程中, EIP 还是指向物理
地址“1MB+某处”。因为编译程序使所有的符号地址都在虚拟内存空间中,因此,第 2 个标
号 1 的地址就在虚拟内存空间的某处(PAGE_OFFSET+某处),于是,jmp 指令执行以后,EIP
就指向虚拟内核空间的某个地址,这就使 CPU 转入了内核空间,从而完成了从实模式到保护
模式的平稳过渡。

然后再看页目录 swapper_pg_dir 中的内容。从前面的讨论我们知道 pg0 和 pg1 这两个
页表的起始物理地址分别为 0x00102000 和 0x00103000。从图 2.22 可知,页目录项的最低 12
位用来描述页表的属性。因此,在 swapper_pg_dir 中的第 0 和第 1 个目录项 0x00102007、
0x00103007,就表示 pg0 和 pg1 这两个页表是用户页表、可写且页表的内容在内存。
接着,把 swapper_pg_dir 中的第 2~767 共 766 个目录项全部置为 0。因为一个页表的
大小为 4KB,每个表项占 4 个字节,即每个页表含有 1024 个表项,每个页的大小也为 4KB,
因此这 768 个目录项所映射的虚拟空间为 768×1024×4K=3G,也就是 swapper_pg_dir 表中的
前 768 个目录项映射的是用户空间。

最后,在第 768 和 769 个目录项中又存放 pg0 和 pg1 这两个页表的地址和属性,而把第
770~1023 共 254 个目录项置 0。这 256 个目录项所映射的虚拟地址空间为 256×1024×4K=1G,
也就是 swapper_pg_dir 表中的后 256 个目录项映射的是内核空间。

由此可以看出,在初始的页目录 swapper_pg_dir 中,用户空间和内核空间都只映射了
开头的两个目录项,即 8MB 的空间,而且有着相同的映射,如图 6.6 所示。
在这里插入图片描述
读者会问,内核开始运行后运行在内核空间,那么,为什么把用户空间的低区(8M)也
进行映射,而且与内核空间低区的映射相同?简而言之,是为了从实模式到保护模式的平稳
过渡。具体地说,当 CPU 进入内核代码的起点 startup_32 后,是以物理地址来取指令的。在
这种情况下,如果页目录只映射内核空间,而不映射用户空间的低区,则一旦开启页映射机
制以后就不能继续执行了,这是因为,此时 CPU 中的指令寄存器 EIP 仍指向低区,仍会以物
理地址取指令,直到以某个符号地址为目标作绝对转移或调用子程序为止。所以,Linux 内
核就采取了上述的解决办法。
但是,在 CPU 转入内核空间以后,应该把用户空间低区的映射清除掉。后面读者将会看
到,页目录 swapper_pg_dir 经扩充后就成为所有内核线程的页目录。在内核线程的正常运行
中,处于内核态的 CPU 是不应该通过用户空间的虚拟地址访问内存的。清除了低区的映射以
后,如果发生 CPU 在内核中通过用户空间的虚拟地址访问内存,就可以因为产生页面异常而
捕获这个错误。

3.物理内存的初始分布
经过这个阶段的初始化,初始化阶段页目录及几个页表在物理空间中的位置如图 6.7 所
示。
在这里插入图片描述
其中 empty_zero_page 中存放的是在操作系统的引导过程中所收集的一些数据,叫做引
导参数。因为这个页面开始的内容全为 0,所以叫做“零页”,代码中常常通过宏定义 ZERO_PAGE
来引用这个页面。不过,这个页面要到初始化完成,系统转入正常运行时才会用到。为了后
面内容介绍的方便,我们看一下复制到这个页面中的命令行参数和引导参数。这里假定这些
参数已被复制到“零页”,在 setup.c 中定义了引用这些参数的宏:

 /* 
 * This is set up by the setup-routine at boot-time 
 */ 
#define PARAM ((unsigned char *)empty_zero_page) 
#define SCREEN_INFO (*(struct screen_info *) (PARAM+0))
#define EXT_MEM_K (*(unsigned short *) (PARAM+2)) 
#define ALT_MEM_K (*(unsigned long *) (PARAM+0x1e0)) 
#define E820_MAP_NR (*(char*) (PARAM+E820NR)) 
#define E820_MAP ((struct e820entry *) (PARAM+E820MAP)) 
#define APM_BIOS_INFO (*(struct apm_bios_info *) (PARAM+0x40)) 
#define DRIVE_INFO (*(struct drive_info_struct *) (PARAM+0x80))
#define SYS_DESC_TABLE (*(struct sys_desc_table_struct*)(PARAM+0xa0)) 
#define MOUNT_ROOT_RDONLY (*(unsigned short *) (PARAM+0x1F2)) 
#define RAMDISK_FLAGS (*(unsigned short *) (PARAM+0x1F8)) 
#define ORIG_ROOT_DEV (*(unsigned short *) (PARAM+0x1FC)) 
#define AUX_DEVICE_INFO (*(unsigned char *) (PARAM+0x1FF)) 
#define LOADER_TYPE (*(unsigned char *) (PARAM+0x210)) 
#define KERNEL_START (*(unsigned long *) (PARAM+0x214)) 
#define INITRD_START (*(unsigned long *) (PARAM+0x218)) 
#define INITRD_SIZE (*(unsigned long *) (PARAM+0x21c)) 
#define COMMAND_LINE ((char *) (PARAM+2048)) 
#define COMMAND_LINE_SIZE 256 

其中宏 PARAM 就是 empty_zero_page 的起始位置,随着代码的阅读,读者会逐渐理解这
些参数的用途。这里要特别对宏 E820_MAP 进行说明。E820_MAP 是个 struct e820entry 数据
结 构 的 指 针 , 存 放 在 参 数 块 中 位 移 为 0x2d0 的 地 方 。 这 个 数 据 结 构 定 义 在
include/i386/e820.h 中:

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[E820MAX]; 
}; 
extern struct e820map e820; 

其中,E820MAX 被定义为 32。从这个数据结构的定义可以看出,每个 e820entry 都是对
一个物理区间的描述,并且一个物理区间必须是同一类型。如果有一片地址连续的物理内存
空间,其一部分是 RAM,而另一部分是 ROM,那就要分成两个区间。即使同属 RAM,如果其中
一部分要保留用于特殊目的,那也属于不同的分区。在 e820.h 文件中定义了 4 种不同的类型:

#define E820_RAM 1 
#define E820_RESERVED 2 
#define E820_ACPI 3 /* usable as RAM once ACPI tables have been read */ 
#define E820_NVS 4 
#define HIGH_MEMORY (1024*1024) 

其中 E820_NVS 表示“Non-Volatile Storage”,即“不挥发”存储器,包括 ROM、EPROM、
Flash 存储器等。
在 PC 中,对于最初 1MB 存储空间的使用是特殊的。开头 640KB(0x0~0x9FFFF)为 RAM,
从 0xA0000 开始的空间则用于 CGA、EGA、VGA 等图形卡。现在已经很少使用这些图形卡,但
是不管是什么图形卡,开机时总是工作于 EGA 或 VGA 模式。从 0xF0000 开始到 0xFFFFF,即
最高的 4KB,就是在 EPROM 或 Flash 存储器中的 BIOS。所以,只要有 BIOS 存在,就至少有两
个区间,如果 nr_map 小于 2,那就一定出错了。由于 BIOS 的存在,本来连续的 RAM 空间就
不连续了。当然,现在已经不存在这样的存储结构了。1MB 的边界早已被突破,但因为历史
的原因,把 1MB 以上的空间定义为“HIGH_MEMORY”,这个称呼一直沿用到现在,于是代码中
的常数 HIGH_MEMORY 就定义为“1024×1024”。现在,配备了 128MB 的内存已经是很普遍了。
但是,为了保持兼容,就得留出最初 1MB 的空间。
这个阶段初始化后,物理内存中内核映像的分布如图 6.8 所示。
在这里插入图片描述
符号_text 对应物理地址 0x00100000,表示内核代码的第一个字节的地址。内核代码的
结束位置用另一个类似的符号_etext 表示。内核数据被分为两组:初始化过的数据和未初始
化过的数据。初始化过的数据在_etext 后开始,在_edata 处结束,紧接着是未初始化过的数
据,其结束符号为_end,这也是整个内核映像的结束符号。
图中出现的符号是由编译程序在编译内核时产生的。你可以在 System.map 文件中找到
这些符号的线性地址(或叫虚拟地址),System.map 是编译内核以后所创建的。

6.2.2 物理内存的探测
我们知道,BIOS 不仅能引导操作系统,还担负着加电自检和对资源的扫描探测,其中就
包括了对物理内存的自检和扫描(你刚开机时所看到的信息就是此阶段 BIOS 显示的信息)。
对于这个阶段中获得的内存信息可以通过 BIOS 调用“int 0x15”加以检查。由于 Linux 内核
不能作 BIOS 调用,因此内核本身就得代为检查,并根据获得的信息生成一幅物理内存构成图,
这就是上面所介绍的 e820 图,然后通过上面提到的参数块传给内核。使得内核能知道系统中
内存资源的配置。之所以称为 e820 图,是因为在通过”int 0x15”查询内存的构成时要把调
用参数之一设置成 0xe820。
分页机制启用以后,与内存管理相关的操作就是调用 init/main.c 中的 start_kernel
()函数,start_kernel()函数要调用一个叫 setup_arch()的函数,setup_arch()位于
arch/i386/kernel/setup.c 文件中,我们所关注的与物理内存探测相关的内容就在这个函数
中。

1.setup_arch()函数
这个函数比较繁琐和冗长,下面我们只对 setup_arch()中与内存相关的内容给予描述。
首先调用 setup_memory_region()函数,这个函数处理内存构成图(map),并把内存的
分布信息存放在全局变量 e820 中,后面会对此函数进行具体描述。
调用 parse_mem_cmdline(cmdline_p)函数。在特殊的情况下,有的系统可能有特殊的
RAM 空间结构,此时可以通过引导命令行中的选择项来改变存储空间的逻辑结构,使其正确
反映内存的物理结构。此函数的作用就是分析命令行中的选择项,并据此对数据结构 e820
中的内容作出修正,其代码也在 setup.c 中。
宏定义:

 #define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT) 
 #define PFN_DOWN(x) ((x) >> PAGE_SHIFT) 
 #define PFN_PHYS(x) ((x) << PAGE_SHIFT) 

PFN_UP() 和 PFN_DOWN()都是将地址 x 转换为页面号(PFN 即 Page Frame Number
的缩写),二者之间的区别为:PFN_UP()返回大于 x 的第 1 个页面号,而 PFN_DOWN()返
回小于 x 的第 1 个页面号。宏 PFN_PHYS()返回页面号 x 的物理地址。
宏定义

 /* 
 * 128MB for vmalloc and initrd 
 */ 
 #define VMALLOC_RESERVE (unsigned long)(128 << 20) 
 #define MAXMEM (unsigned long)(-PAGE_OFFSET-VMALLOC_RESERVE) 
 #define MAXMEM_PFN PFN_DOWN(MAXMEM) 
 #define MAX_NONPAE_PFN (1 << 20) 

对这几个宏描述如下:
• VMALLOC_RESERVE:为 vmalloc()函数访问内核空间所保留的内存区,大小为 128MB。
• MAXMEM:内核能够直接映射的最大 RAM 容量,为 1GB-128MB=896MB(-PAGE_OFFSET
就等于 1GB)
• MAXMEM_PFN:返回由内核能直接映射的最大物理页面数。
• MAX_NONPAE_PFN:给出在 4GB 之上第 1 个页面的页面号。当页面扩充(PAE)功能启
用时,才能访问 4GB 以上的内存。
获得内核映像之后的起始页面号:

 /* 
 * partially used pages are not usable - thus 
 * we are rounding upwards: 
 */ 
 start_pfn = PFN_UP(__pa(&_end)); 

在上一节已说明,宏__pa()返回给定虚拟地址的物理地址。其中标识符_end 表示内核
映像在内核空间的结束位置。因此,存放在变量 start_pfn 中的值就是紧接着内核映像之后
的页面号。
找出可用的最高页面号:

/* 
 * Find the highest page frame number we have available 
 */ 
 max_pfn = 0; 
 for (i = 0; i < e820.nr_map; i++) { 
 unsigned long start, end; 
 /* RAM? */ 
 if (e820.map[i].type != E820_RAM) 
 continue; 
 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; 
 } 

上面这段代码循环查找类型为 E820_RAM(可用 RAM)的内存区,并把最后一个页面的页
面号存放在 max_pfn 中。
确定最高和最低内存范围:

 /* 
 * Determine low and high memory ranges: 
 */ 
 max_low_pfn = max_pfn; 
 if (max_low_pfn > MAXMEM_PFN) { 
 max_low_pfn = MAXMEM_PFN; 
 #ifndef CONFIG_HIGHMEM 
 /* Maximum memory usable is what is directly addressable */ 
 printk(KERN_WARNING "Warning only %ldMB will be used.\n", 
 MAXMEM>>20); 
 if (max_pfn > MAX_NONPAE_PFN) 
 printk(KERN_WARNING "Use a PAE enabled kernel.\n"; 
 else 
 printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n"; 
 #else /* !CONFIG_HIGHMEM */ 
 #ifndef CONFIG_X86_PAE 
 if (max_pfn > MAX_NONPAE_PFN) { 
 max_pfn = MAX_NONPAE_PFN; 
 printk(KERN_WARNING "Warning only 4GB will be used.\n"; 
 printk(KERN_WARNING "Use a PAE enabled kernel.\n"; 
 } 
 #endif /* !CONFIG_X86_PAE */ 
 #endif /* !CONFIG_HIGHMEM */ 
 } 

有两种情况:
• 如果物理内存 RAM 大于 896MB,而小于 4GB,则选用 CONFIG_HIGHMEM 选项来进行访问;
• 如果物理内存 RAM 大于 4GB,则选用 CONFIG_X86_PAE(启用 PAE 模式)来进行访问。
上面这段代码检查了这两种情况,并显示适当的警告信息。

 #ifdef CONFIG_HIGHMEM 
 highstart_pfn = highend_pfn = max_pfn;
 if (max_pfn > MAXMEM_PFN) { 
 highstart_pfn = MAXMEM_PFN; 
 printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", 
 pages_to_mb(highend_pfn - highstart_pfn)); 
 } 
 #endif 

如果使用了 CONFIG_HIGHMEM 选项,上面这段代码仅仅打印出大于 896MB 的可用物理内
存数量。
初始化引导时的分配器

 * Initialize the boot-time allocator (with low memory only): 
 */ 
 bootmap_size = init_bootmem(start_pfn, max_low_pfn); 

通过调用 init_bootmem()函数,为物理内存页面管理机制的建立做初步准备,为整个
物理内存建立起一个页面位图。这个位图建立在从 start_pfn 开始的地方,也就是说,把内
核映像终点_end 上方的若干页面用作物理页面位图。在前面的代码中已经搞清楚了物理内存
顶点所在的页面号为 max_low_pfn,所以物理内存的页面号一定在 0~max_low_pfn 之间。可
是,在这个范围内可能有空洞(hole),另一方面,并不是所有的物理内存页面都可以动态分
配。建立这个位图的目的就是要搞清楚哪一些物理内存页面可以动态分配的。后面会具体描
述 bootmem 分配器。
用 bootmem 分配器,登记全部低区(0~896MB)的可用 RAM 页面

 /* 
 * Register fully available low RAM pages with the 
 * bootmem allocator. 
 */ 
 for (i = 0; i < e820.nr_map; i++) { 
 unsigned long curr_pfn, last_pfn, size; 
 /* 
 * Reserve usable low memory 
 */ 
 if (e820.map[i].type != E820_RAM) 
 continue; 
 /* 
 * We are rounding up the start address of usable memory: 
 */ 
 curr_pfn = PFN_UP(e820.map[i].addr); 
 if (curr_pfn >= max_low_pfn) 
 continue; 
 /* 
 * ... and at the end of the usable range downwards: 
 */ 
 last_pfn = PFN_DOWN(e820.map[i].addr + e820.map[i].size); 
 if (last_pfn > max_low_pfn) 
 last_pfn = max_low_pfn; 
 /* 
 * .. finally, did all the rounding and playing 
 * around just make the area go away? 
 */ 
 if (last_pfn <= curr_pfn)
  = last_pfn - curr_pfn; 
 free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size)); 
 } 

这个循环仔细检查所有可以使用的 RAM,并调用 free_bootmem()函数把这些可用 RAM 标
记为可用。这个函数调用以后,只有类型为 1(可用 RAM)的内存被标记为可用的,参看后面
对这个函数的具体描述。
保留内存:

 /* 
 * Reserve the bootmem bitmap itself as well. We do this in two 
 * steps (first step was init_bootmem()) because this catches 
 * the (very unlikely) case of us accidentally initializing the 
 * bootmem allocator with an invalid RAM area. 
 */ 

reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));
这个函数把内核和 bootmem 位图所占的内存标记为“保留”。 HIGH_MEMORY 为 1MB,即
内核开始的地方,后面还要对这个函数进行具体描述。
分页机制的初始化
paging_init();
这个函数初始化分页内存管理所需要的数据结构,参见后面的详细描述。

2.setup_memory_region() 函数
这个函数用来处理 BIOS 的内存构成图,并把这个构成图拷贝到全局变量 e820 中。如果
操作失败,就创建一个伪内存构成图。这个函数的主要操作如下所述。
• 调用 sanitize_e820_map()函数,以删除内存构成图中任何重叠的部分,因为 BIOS
所报告的内存构成图可能有重叠。
• 调用 copy_e820_map()进行实际的拷贝。
• 如果操作失败,创建一个伪内存构成图,这个伪构成图有两部分:0 到 640K 及 1M 到
最大物理内存。
• 打印最终的内存构成图。

3.copy_e820_map() 函数
函数原型为:
static int __init sanitize_e820_map(struct e820entry * biosmap, char * pnr_map)
其主要操作如下概述。
(1)如果物理内存区间小于 2,那肯定出错。因为 BIOS 至少和 RAM 属于不同的物理区间。
if (nr_map < 2)
return -1;
(2)从 BIOS 构成图中读出一项。

 do { 
 unsigned long long start = biosmap->addr; 
 unsigned long long size = biosmap->size; 
 unsigned long long end = start + size;
 unsigned long type = biosmap->type; 

(3)进行检查。

 /* Overflow in 64 bits? Ignore the memory map. */ 
 if (start > end) 
 return -1; 

(4)一些 BIOS 把 640KB~1MB 之间的区间作为 RAM 来用,这是不符合常规的。因为从
0xA0000 开始的空间用于图形卡,因此,在内存构成图中要进行修正。如果一个区的起点在
0xA0000 以下,而终点在 1MB 之上,就要将这个区间拆开成两个区间,中间跳过从 0xA0000
到 1MB 边界之间的那一部分。

/* 
 * Some BIOSes claim RAM in the 640k - 1M region. 
 * Not right. Fix it up. 
 */ 
 if (type == E820_RAM) { 
 if (start < 0x100000ULL && end > 0xA0000ULL) { 
 if (start < 0xA0000ULL) 
 add_memory_region(start, 0xA0000ULL-start, type) 
 if (end <= 0x100000ULL) 
 continue; 
 start = 0x100000ULL; 
 size = end - start; 
 } 
 } 
 
 add_memory_region(start, size, type); 
 } while (biosmap++,--nr_map); 
 return 0; 

4.add_memory_region() 函数
这个函数的功能就是在 e820 中增加一项,其主要操作如下所述。
(1)获得已追加在 e820 中的内存区数。
int x = e820.nr_map;
(2)如果数目已达到最大(32),则显示一个警告信息并返回。

 if (x == E820MAX) { 
 printk(KERN_ERR "Oops! Too many entries in 
 the memory map!\n"; 
 return; 
 } 

(3)在 e820 中增加一项,并给 nr_map 加 1。

 e820.map[x].addr = start; 
 e820.map[x].size = size; 
 e820.map[x].type = type; 
 e820.nr_map++; 

5.print_memory_map() 函数
这个函数把内存构成图在控制台上输出,函数本身比较简单,在此给出一个运行实例。
例如函数的输出为(BIOS 所提供的物理 RAM 区间):
BIOS-e820: 0000000000000000 - 00000000000a0000 (usable)
BIOS-e820: 00000000000f0000 - 0000000000100000 (reserved)
BIOS-e820: 0000000000100000 - 000000000c000000 (usable)
BIOS-e820: 00000000ffff0000 - 0000000100000000 (reserved)
6.2.3 物理内存的描述
为了对内存的初始化内容进行进一步的讨论,我们首先要了解 Linux 对物理内存的描述
机制。
1.一致存储结构(UMA)和非一致存储结构(NUMA)
在传统的计算机结构中,整个物理内存都是均匀一致的,CPU 访问这个空间中的任何一
个地址所需要的时间都相同,所以把这种内存称为“一致存储结构(Uniform Memory
Architecture)”,简称 UMA。可是,在一些新的系统结构中,特别是多 CPU 结构的系统中,
物理存储空间在这方面的一致性却成了问题。这是因为,在多 CPU 结构中,系统中只有一条
总线(例如,PCI 总线),有多个 CPU 模块连接在系统总线上,每个 CPU 模块都有本地的物理
内存,但是也可以通过系统总线访问其他 CPU 模块上的内存。另外,系统总线上还连接着一
个公用的存储模块,所有的 CPU 模块都可以通过系统总线来访问它。因此,所有这些物理内
存的地址可以互相连续而形成一个连续的物理地址空间。

显然,就某个特定的 CPU 而言,访问其本地的存储器速度是最快的,而穿过系统总线访
问公用存储模块或其他 CPU 模块上的存储器就比较慢,而且还面临因可能的竞争而引起的不
确定性。也就是说,在这样的系统中,其物理存储空间虽然地址连续,但因为所处“位置”
不同而导致的存取速度不一致,所以称为“非一致存储结构( Non-Uniform Memory
Architecture),简称 NUMA。

事实上,严格意义上的 UMA 结构几乎不存在。就拿配置最简单的单 CPU 来说,其物理存
储空间就包括了 RAM、ROM(用于 BIOS),还有图形卡上的静态 RAM。但是,在 UMA 中,除主
存 RAM 之外的存储器空间都很小,因此可以把它们放在特殊的地址上,在编程时加以特别注
意就行,那么,可以认为以 RAM 为主体的主存是 UMA 结构。

由于 NUMA 的引入,就需要存储管理机制的支持,因此,Linux 内核从 2.4 版本开始就提
供了对 NUMA 的支持(作为一个编译可选项)。为了对 NUMA 进行描述,引入一个新的概念—
“存储节点(或叫节点)”,把访问时间相同的存储空间就叫做一个“存储节点”。一般来说,连
续的物理页面应该分配在相同的存储节点上。例如,如果 CPU 模块 1 要求分配 5 个页面,但
是由于本模块上的存储空间已经不够,只能分配 3 个页面,那么此时,是把另外两个页面分
配在其他 CPU 模块上呢,还是把 5 个页面干脆分配在一个模块上?显然,合理的分配方式因
该是将这 5 个页面都分配在公用模块上。

Linux把物理内存划分为 3个层次来管理:存储节点(Node)、管理区(Zone)和页面(Page),
并用 3 个相应的数据结构来描述。
2.页面(Page)数据结构
对一个物理页面的描述在/include/linux/mm.h 中:

/* 
 * Each physical page in the system has a struct page associated with 
 * it to keep track of whatever it is we are using the page for at the 
 * moment. Note that we have no way to track which tasks are using 
 * a page. 
 * 
 * Try to keep the most commonly accessed fields in single cache lines 
 * here (16 bytes or greater). This ordering should be particularly 
 * beneficial on 32-bit processors. 
 * 
 * The first line is data used in page cache lookup, the second line 
 * is used for linear searches (eg. clock algorithm scans). 
 * 
 * TODO: make this structure smaller, it could be as small as 32 bytes. 
 */ 
typedef struct page { 
 struct list_head list; /* ->mapping has some page lists. */ 
 struct address_space *mapping; /* The inode (or ...) we belong to. */ 
 unsigned long index; /* Our offset within mapping. */ 
 struct page *next_hash; /* Next page sharing our hash bucket in 
 the pagecache hash table. */ 
 atomic_t count; /* Usage count, see below. */ 
 unsigned long flags; /* atomic flags, some possibly 
 updated asynchronously */ 
 struct list_head lru; /* Pageout list, eg. active_list; 
 protected by pagemap_lru_lock !! */ 
 wait_queue_head_t wait; /* Page locked? Stand in line... */ 
 struct page **pprev_hash; /* Complement to *next_hash. */ 
 struct buffer_head * buffers; /* Buffer maps us to a disk block. */ 
 void *virtual; /* Kernel virtual address (NULL if 
 not kmapped, ie. highmem) */ 
 struct zone_struct *zone; /* Memory zone we are in. */ 
} mem_map_t; 
extern mem_map_t * mem_map;

源代码的注释中对这个数据结构给出了一定的说明,从中我们可以对此结构有一定的理
解,后面还要对此结构中的每个域给出具体的解释。
内核中用来表示这个数据结构的变量常常是 page 或 map。
当页面的数据来自一个文件时,index 代表着该页面中的数据在文件中的偏移量;当页
面的内容被换出到交换设备上,则 index 指明了页面的去向。结构中各个成分的次序是有讲
究的,尽量使得联系紧密的若干域存放在一起,这样当这个数据结构被装入到高速缓存中时,
联系紧密的域就可以存放在同一缓冲行(Cache Line)中。因为同一缓冲行(其大小为 16
字节)中的内容几乎可以同时存取,因此,代码注释中希望这个数据结构尽量地小到用 32
个字节可以描述。
系统中的每个物理页面都有一个 Page(或 mem_map_t)结构。系统在初始化阶段根据内
存的大小建立起一个 Page 结构的数组 mem_map,数组的下标就是内存中物理页面的序号。

3.管理区 Zone
为了对物理页面进行有效的管理,Linux 又把物理页面划分为 3 个区:
• 专供 DMA 使用的 ZONE_DMA 区(小于 16MB);
• 常规的 ZONE_NORMAL 区(大于 16MB 小于 896MB);
• 内核不能直接映射的区 ZONE_HIGME 区(大于 896MB)。
这里进一步说明为什么对 DMA 要单独设置管理区。首先,DMA 使用的页面是磁盘 I/O 所
需的,如果在页面的分配过程中,所有的页面全被分配完,那么页面及盘区的交换就无法进
行了,这是操作系统决不允许出现的现象。另外,在 i386 CPU 中,页式存储管理的硬件支持
是在 CPU 内部实现的,而不像有些 CPU 那样由一个单独的 MMU 来提供,所以 DMA 对内存的访
问不经过 MMU 提供的地址映射。这样,外部设备就要直接访问物理页面的地址。可是,有些
外设(特别是插在 ISA 总线上的外设接口卡)在这方面往往有些限制,要求用于 DMA 的物理
地址不能过高。另一方面,当 DMA 所需的缓冲区超过一个物理页面的大小时,就要求两个物
理页面在物理上是连续的,但因为此时 DMA 控制器不能依靠 CPU 内部的 MMU 将连续的虚存页
面映射到物理上也连续的页面上,因此,用于 DMA 的物理页面必须加以单独管理。
关于管理区的数据结构 zone_struct(或 zone_t)将在后面进行描述。

4.存储节点(Node)的数据结构
存储节点的数据结构为 pglist_data,定义于 Include/linux/mmzone.h 中:

 typedef struct pglist_data { 
 zone_t node_zones[MAX_NR_ZONES]; 
 zonelist_t node_zonelists[GFP_ZONEMASK+1]; 
 int nr_zones; 
 struct page *node_mem_map; 
 unsigned long *valid_addr_bitmap; 
 struct bootmem_data *bdata; 
 unsigned long node_start_paddr; 
 unsigned long node_start_mapnr; 
 unsigned long node_size; 
 int node_id; 
 struct pglist_data *node_next; 
 } pg_data_t; 

显然,若干存储节点的 pglist_data 数据结构可以通过 node_next 形成一个单链表队列。
每个结构中的 node_mem_map 指向具体节点的 page 结构数组,而数组 node_zone[]就是该节
点的最多 3 个页面管理区。
在 pglist_data 结构里设置了一个 node_zonelists 数组,其类型定义也在同一文件中:

 typedef struct zonelist_struct { 
 zone_t *zone[MAX_NR_ZONE+1]; //NULL delimited 
 Int gfp_mast; 
 } zonelist_t 

这里的 zone[]是个指针数组,各个元素按特定的次序指向具体的页面管理区,表示分配
页面时先试 zone[0]所指向的管理区,如果不能满足要求就试 zone[1]所指向的管理区,等等。
这些管理区可以属于不同的存储节点。关于管理区的分配可以有很多种策略,例如,CPU 模
块 1 需要分配 5 个用于 DMA 的页面,可是它的 ZONE_DMA 只有 3 个页面,于是就从公用模块的
ZONE_DMA 中分配 5 个页面。就是说,每个 zonelist_t 规定了一种分配策略。然而,每个存
储节点不应该只有一种分配策略,所以在 pglist_data 中提供的是一个 zonelist_t 数组,数
组的大小 NR_GFPINDEX 为 100。

每日分享15分钟技术摘要选读,关注一波,一起保持学习动力!

在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第一章 走进linux 1.1 GNU与Linux的成长 1.2 Linux的开发模式和运作机制 1.3走进Linux内核 1.4 分析Linux内核的意义 1.5 Linux内核结构 1.6 Linux内核源代码 1.7 Linux内核源代码分析工具 第二章 Linux运行的硬件基础 2.1 i386的寄存器 2.2 内存地址 2.3 段机制和描述符 2.4 分页机制 2.5 Linux中的分页机制 2.6 Linux中的汇编语言 第三章中断机制 3.1 中断基本知识 3.2中断描述符表的初始化 3.3异常处理 3.4 中断处理 3.5中断的后半部分处理机制 第四章 进程描述 4.1 进程和程序(Process and Program) 4.2 Linux中的进程概述 4.3 task_struct结构描述 4.4 task_struct结构在内存中的存放 4.5 进程组织的方式 4.6 内核线程 4.7 进程的权能 4.8 内核同步 第五章进程调度 5.1 Linux时间系统 5.2 时钟中断 5.3 Linux的调度程序-Schedule( ) 5.4 进程切换 第六章 Linux内存管理 6.1 Linux内存管理概述 6.2 Linux内存管理的初始化 6.3 内存的分配和回收 6.4 地址映射机制 6.5 请页机制 6.6 交换机制 6.7 缓存和刷新机制 6.8 进程的创建和执行 第七章 进程间通信 7.1 管道 7.2 信号(signal) 7.3 System V 的IPC机制 第八章 虚拟文件系统 8.1 概述 8.2 VFS中的数据结构 8.3 高速缓存 8.4 文件系统的注册、安装与拆卸 8.5 限额机制 8.6 具体文件系统举例 8.7 文件系统的系统调用 8 .8 Linux2.4文件系统的移植问题 第九章 Ext2文件系统 9.1 基本概念 9.2 Ext2的磁盘布局和数据结构 9.3 文件的访问权限和安全 9.4 链接文件 9.5 分配策略 第十章 模块机制 10.1 概述 10.2 实现机制 10.3 模块的装入和卸载 10.4 内核版本 10.5 编写内核模块 第十一章 设备驱动程序 11.1 概述 11.2 设备驱动基础 11.3 块设备驱动程序 11.4 字符设备驱动程序 第十二章 网络 12.1 概述 12.2 网络协议 12.3 套接字(socket) 12.4 套接字缓冲区(sk_buff) 12.5 网络设备接口 第十三章 启动系统 13.1 初始化流程 13.2 初始化的任务 13.3 Linux 的Boot Loarder 13.4 进入操作系统 13.5 main.c中的初始化 13.6 建立init进程 附录: 1 Linux 2.4内核API 2.1 驱动程序的基本函数 2.2 双向循环链表的操作 2.3 基本C库函数 2.4 Linux内存管理中Slab缓冲区 2.5 Linux中的VFS 2.6 Linux的连网 2.7 网络设备支持 2.8 模块支持 2.9 硬件接口 2.10 块设备 2.11 USB 设备

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值