帝游网提供最新手游APP下载和游戏攻略!

Linux内核源代码深度解析:内存管理技术详解(第一部分)

发布时间:2024-09-20浏览:35

大家好,感谢邀请,今天来为大家分享一下Linux内核源代码深度解析:内存管理技术详解(第一部分)的问题,以及和的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!

————零声学院

存储器是一种必须仔细管理的重要资源。在理想的情况下,每个程序员都喜欢无穷大、

快速并且内容不易变(即掉电后内容不会丢失)的存储器,同时又希望它是廉价的。但不幸

的是,当前技术没有能够提供这样的存储器,因此大部分的计算机都有一个存储器层次结构,

即少量、快速、昂贵、易变的高速缓存(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 0x2000ENTRY(pg0).org 0x3000ENTRY(pg1)/* * empty_zero_page must immediately follow the page tables ! (The * initialization loop counts until empty_zero_page) */.org 0x4000ENTRY(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 0x1000ENTRY(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,%eaxjmp *%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

来引用这个页面。不过,这个页面要到初始化完成,系统转入正常运行时才会用到。为了后

/* * 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_HIGHMEMhighstart_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 数组,数

用户评论

淡抹烟熏妆丶

这篇文章简直太棒了!终于有人把Linux内存管理说得这么清楚,我之前总弄不明白很多概念,读完这篇博文算是彻底了解了,准备好好学习一下linux内核源码了。

    有14位网友表示赞同!

来自火星的我

最近在学Linux内核驱动开发,刚好遇到了内存管理这个部分,看到这篇文章感觉很及时。作者分析得很深入,特别是关于虚拟内存的概念讲解得非常棒。

    有10位网友表示赞同!

铁树不曾开花

我对Linux内核源代码一直比较感兴趣,但入门确实有点困难,这篇博文讲解的很好,让我对linux内存管理有了更深的认识。希望能后续还有更多相关技术分享!

    有5位网友表示赞同!

微信名字

我觉得这篇文章写的有些深奥了,很多地方我没完全看懂,可能是我的基础不够扎实吧。不过文章的思路很清晰,对于有Linux内核经验的人应该很容易理解。

    有13位网友表示赞同!

命里缺他

虚拟内存太复杂了吧,我阅读完这篇文章还是一头雾水,感觉作者把一些比较高级的概念都给忽略了,也许可以针对初学者进行更详细的讲解?

    有5位网友表示赞同!

仅有的余温

博文内容不错,介绍的也很全面,但希望能增加更多图解和案例,这样更容易理解。

    有9位网友表示赞同!

岁岁年年

内存管理是Linux内核的核心部分,这篇博文做了一次很好的梳理,对新手入门很有帮助,希望后续能继续深入浅出地讲解其他模块。

    有5位网友表示赞同!

坏小子不坏

说实在的这篇文章太专业了,我感觉自己要看十遍才能勉强懂其中一些概念。有没有更适合初学者的教程呢?

    有17位网友表示赞同!

灵魂摆渡人

学习Linux内核源代码需要扎实的底知识,这篇博文对内存管理的讲解非常到位,可以帮助我们更好的理解内核的工作原理。

    有13位网友表示赞同!

一样剩余

我比较关注linux安全方面的内容,希望作者能对内存管理中的安全机制进行更深入的分析。

    有12位网友表示赞同!

肆忌

我对Linux内核源代码和虚拟内存都很感兴趣,这篇文章让我看到了学习的方向,准备去系统地学习一下相关的知识!

    有18位网友表示赞同!

眷恋

文章解读比较清晰,内容也十分丰富,帮助我理解了Linux内核内存管理的一些基本原理。

    有19位网友表示赞同!

请在乎我1秒

作为一名嵌入式开发工程师,我对linux 内存管理有着深入的了解,这篇文章让我看到了很多新想法和新的学习方向。

    有17位网友表示赞同!

抚涟i

写这篇文章的人真厉害,把复杂的概念讲解得非常清晰易懂!对于像我这样的新手来说,确实是一份宝贵资料。

    有14位网友表示赞同!

寻鱼水之欢

感觉写的有点太理论了,缺少一些实用的案例分析,希望作者能够在后续的文章中加入更多实践内容。

    有13位网友表示赞同!

繁华若梦

对嵌入式系统开发人员来说,理解Linux内核内存管理非常重要,这篇博文很好的总结了相关知识点,值得参考学习!

    有20位网友表示赞同!

久爱不厌

文章的标题很吸引人,内容也确实不错,但是篇幅有点短,希望能补充一些更具体的案例和代码示例。

    有10位网友表示赞同!

没过试用期的爱~

想要深入理解Linux内核源代码,内存管理无疑是需要重点关注的部分,这篇文章对我的理解做了很好的补充!

    有17位网友表示赞同!

热点资讯