转载请注明出处,并保留以上所有对文章内容、图片、表格的来源的描述。
一、ASLR的问题
ASLR(Address Space Layout Randomization),可以通过/proc/sys/kernel/randomize_va_space修改。但是较新的内核版本该值默认为2(在3.2.0如此),老版本为1(在2.6.18如此)。至少可以知道为0的时候是关闭,为1和为2有什么差别还不知道。
可以在Documentation/sysctl/kernel.txt中找到如下一段话:
==============================================================
randomize_va_space:
This option can be used to select the type of process address
space randomization that is used in the system, for architectures
that support this feature.
0 - Turn the process address space randomization off. This is the
default for architectures that do not support this feature anyways,
and kernels that are booted with the "norandmaps" parameter.
1 - Make the addresses of mmap base, stack and VDSO page randomized.
This, among other things, implies that shared libraries will be
loaded to random addresses. Also for PIE-linked binaries, the
location of code start is randomized. This is the default if the
CONFIG_COMPAT_BRK option is enabled.
2 - Additionally enable heap randomization. This is the default if
CONFIG_COMPAT_BRK is disabled.
There are a few legacy applications out there (such as some ancient
versions of libc.so.5 from 1996) that assume that brk area starts
just after the end of the code+bss. These applications break when
start of the brk area is randomized. There are however no known
non-legacy applications that would be broken this way, so for most
systems it is safe to choose full randomization.
Systems with ancient and/or broken binaries should be configured
with CONFIG_COMPAT_BRK enabled, which excludes the heap from process
address space randomization.
==============================================================
这段话中有几个名词需要解释:
- VDSO page randomized:Virtual Dynamically linked Shared Objects。是一种在用户态调用内核态的方法。参考:http://en.wikipedia.org/wiki/VDSO
- PIE-linked binaries:PIE(Position-Independent-Executable),是一种介于共享库和普通可执行程序之间的一种可执行文件。参考资料:http://www.linuxfromscratch.org/~manuel/hlfs-book/glibc-2.4/chapter02/pie.html
- CONFIG_COMPAT_BRK:内核中brk相关的变量很多指的都是堆(heap),这个配置选项 “CONFIG_COMPAT_BRK=y means that heap randomization is turned off, so it's *always* a safe choice. I assume the help text is trying to say that if one does not run ancient binaries, then enabling heap randomization is safe.”所以该配置=y指的是关闭堆地址空间随机化技术来支持一些老的binary(COMPAT选项一般都是向后兼容的选项)。
所以,在/proc/sys/kernel/randomize_va_space中的值如果为0则表示关闭所有的随机化,如果为1,表示打开mmap base、栈、VDSO页面随机化,如果为2则表示在1的基础上进一步打开堆地址随机化。在打开堆地址随机化之前,堆的起始位置是紧接着应用程序bss段之后的。
二、匿名页
There are two type of pages: anonymous pages and file-backed pages. A file-backed page originates from mmap()-ing a file in disk, whereas an anonymous page is the kind you get when doing malloc(). It has no relationship with any files at all. When the RAM becomes tight, the kernel swaps out anonymous pages to swap space and flushes file-backed pages to the file to give room for current requests. In other words, anonymous pages may consume swap area while file-backed pages don't. The only exception is for files mmap()-ed using the MAP_PRIVATE flag. In this case, file modification occurs in RAM only.
From: http://linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html
Linux进程虚拟地址空间
Linux进程虚拟地址空间的是Linux内存管理的另外一个重要的部分。之前说过Linux对物理内存的管理,对于用户进程的内存访问,Linux提供了一套另外一套更加复杂的模式,这种模式通过页表来访问物理内存,而这种访问模式目前也被大部分CPU体系结构所支持。
一、内存虚拟空间的概述
内存虚拟空间的布局
我们知道,在IA-32体系结构中,任何一个进程都能够访问4GB的内存空间;在这4GB内存空间中,高1GB是内核的空间,这一部分的管理已经在之前讲过了。而低3GB的内存空间(我们成为用户虚拟内存空间)中,也需要通过一定的布局来进行管理。用户虚拟内存空间至少需要分为三个部分:堆空间、栈空间以及MMAP空间。栈空间和堆空间大家都已经很熟了,MMAP空间主要是将文件映射到进程的虚拟内存空间时,对应虚拟内存空间的位置。注意这里指的“文件”是Linux中宽泛概念的文件,包括块设备(硬盘等)上的文件、设备文件、虚拟文件系统的文件等等。
在IA-32架构中,Linux传统的的布局空间如下:
一般来说,IA-32体系结构中进程地址空间的的代码段(.text)从0x08048000,这与最低可用地址有128MB的间距,用户捕获NULL指针。其他体系结构也有类似的缺口,UltraSparc使用0x10000000作为代码段起点,AMD64则使用0x0000000000400000。在代码段之上是数据段和bss段。再之上是堆空间,并向高地址增长。MMAP空间用于内存映射,起始于mm_struct->mmap_base,通常设置为TASK_UNMAPPED_BASE,每个体系结构有自己不同的定义,但是几乎所有情况下其值都是TASK_SIZE/3。栈空间是从用户虚拟地址空间的最高点(一般0xBFFFFFFF向下)向下增长。
这里面存在一个问题,TASK_UNMAPPED_BASE在IA-32中的值只有为0x40000000,也就是说堆空间只有1GB可以使用,为了能够扩展堆空间的大小,在内核版本2.6.7开发期间为IA-32计算机引入一个新的虚拟地址空间:
我们可以看到唯一的差别是MMAP区域的增长方向。新的布局导致了栈空间的固定,而堆空间和MMAP区域公用一段空间,这在很大程度上增长了堆空间的大小。
进程虚拟空间的数据结构
在Linux内核中,每一个进程都有一个自己的数据结构(可能是内核中最大的数据结构)struct task_struct,该结构中有一个struct mm_struct数据结构,该结构则保存了进程的内存管理信息。该结构的摘要如下:
<mm_types.h>
struct mm_struct {
...
unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff,unsigned long flags);
...
unsigned long mmap_base; /* base of mmap area */
unsigned long task_size; /* size of task vm space */
...
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
...
}
该结构中get_unmapped_area函数用于在虚拟空间中获得未被映射的空间,mmap_base是上文中MMAP区域的基地址,task_size是进程地址空间的大小,start_code和end_code是进程代码段的起止地址,start_data和end_data是进程数据段的起止地址,start_brk和堆空间的起始地址,start_stack是栈空间的起始地址,brk表示堆区域当前的结束地址(为什么栈空间没有当前的结束地址呢?想想esp寄存器...),arg_start和arg_end表示进程参数列表,env_start和env_end表示环境变量,这两个区域都位于栈中最高的区域。
地址空间布局随机化
Linux Kernel引入了地址空间布局随机化的概念,该概念的提出是出于安全考虑。试想如果堆栈空间的地址都是确定的,那么恶意代码就很容易通过内存溢出的代码来访问堆栈空间的内容,地址空间布局随机化就是使得进程虚拟空间的布局(主要是各个部分的起始地址)位于随机的位置,以此来降低被攻击的可能性。
地址空间布局随机化(ASLR,Address Space Layout Randomization)可以通过/proc/sys/kernel/randomize_va_space修改。randomize_va_space的可能值有三种,可以在Documentation/sysctl/kernel.txt中找到如下一段话:
==============================================================
randomize_va_space:
This option can be used to select the type of process address
space randomization that is used in the system, for architectures
that support this feature.
0 - Turn the process address space randomization off. This is the
default for architectures that do not support this feature anyways,
and kernels that are booted with the "norandmaps" parameter.
1 - Make the addresses of mmap base, stack and VDSO page randomized.
This, among other things, implies that shared libraries will be
loaded to random addresses. Also for PIE-linked binaries, the
location of code start is randomized. This is the default if the
CONFIG_COMPAT_BRK option is enabled.
2 - Additionally enable heap randomization. This is the default if
CONFIG_COMPAT_BRK is disabled.
There are a few legacy applications out there (such as some ancient
versions of libc.so.5 from 1996) that assume that brk area starts
just after the end of the code+bss. These applications break when
start of the brk area is randomized. There are however no known
non-legacy applications that would be broken this way, so for most
systems it is safe to choose full randomization.
Systems with ancient and/or broken binaries should be configured
with CONFIG_COMPAT_BRK enabled, which excludes the heap from process
address space randomization.
==============================================================
具体我就不翻译了,只是解释其中几个不容易理解的名词。
- VDSO page randomized:Virtual Dynamically linked Shared Objects。是一种在用户态调用内核态的方法。参考:http://en.wikipedia.org/wiki/VDSO
- PIE-linked binaries:PIE(Position-Independent-Executable),是一种介于共享库和普通可执行程序之间的一种可执行文件。参考资料:http://www.linuxfromscratch.org/~manuel/hlfs-book/glibc-2.4/chapter02/pie.html
- CONFIG_COMPAT_BRK:内核中brk相关的变量很多指的都是堆(heap),这个配置选项 “CONFIG_COMPAT_BRK=y means that heap randomization is turned off, so it's *always* a safe choice. I assume the help text is trying to say that if one does not run ancient binaries, then enabling heap randomization is safe.”所以该配置=y指的是关闭堆地址空间随机化技术来支持一些老的binary(带有COMPAT的配置选项一般都是向后兼容的选项)。
所以,在/proc/sys/kernel/randomize_va_space中的值如果为0则表示关闭所有的随机化,如果为1,表示打开mmap base、栈、VDSO页面随机化,如果为2则表示在1的基础上进一步打开堆地址随机化。在打开堆地址随机化之前,堆的起始位置是紧接着应用程序bss段之后的。
二、内存映射的原理
内存映射的原理很简单,本质上就是将需要的数据映射到进程的虚拟地址空间中,这里面的数据可以是硬盘上的文件,也可以是内核中的数据,甚至堆栈都是使用内存映射来实现的。如图:
当然图示是很简化的,因为文件数据在硬盘上的存储通常不是连续的,而是分布到若干的区域。内核利用address_space数据结构,提供一组方法从后备存储器(比如硬盘)读取数据,因此address_space行程一个辅助层,将映射的数据表示为连续的线性区域,提供给内存管理子系统。
在Linux内存管理中有两个很重要的概念:按需调页(demand paging)和按需分配(demand allocation)。对于在后备存储器上的数据,Linux并非将所有需要的数据都在执行前载入内存中,而是按照需要来载入,这种机制成为按需调页;对于堆栈空间对物理内存的使用,也是根据程序运行时的需求来进行分配,这是按需分配。使用的各种数据结构如图:
过程如下:
- 进程试图访问用户虚拟地址空间的某个地址,但是该地址没有和物理内存关联。
- 处理器触发缺页中断,发送到内核
- 内核处理该中断,找到适当的后备存储器
- 分配物理内存页,并从后备存储器读取所需数据填充物理内存
- 更新用户进程页表,建立物理地址与用户虚拟地址的联系,恢复进程的执行
三、数据结构
基本数据结构
我们知道struct mm_struct很重要,该结构提供了进程在内存布局中的所有信息。另外它还包括下列成员,用于管理用户进程在虚拟地址空间中的所有内存区域。
<mm_types.h>
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
...
}
对于进程的内存区域,每个区域都通过一个vm_area_struct实例来描述,所有的vm_area_struct通过两个数据结构来管理:mmap用一个单链表来管理,mm_rb通过一个红黑树来管理。结构如图:
注意这里的红黑树只是一个简单的表示,真实的结构远比此复杂。
每个区域表示一个vm_area_struct实例,定义的简化形式如下:
<mm_types.h>
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, listed below. */
struct rb_node vm_rb;
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
/*
* A file’s MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
struct vm_operations_struct * vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
};
其中:
- vm_mm是一个反向指针,指向该区域所属的mm_struct实例
- vm_start和vm_end指向了该区域在用户空间中的起始和结束地址
- vm_next是一个单链表,根据地址递增序来组织
- vm_rb是红黑树的一个节点,用来与红黑树的集成
- vm_page_prot存储区域的访问权限
- shared联合体反映了“共享映射”,共享映射是指文件与进程的虚拟地址空间双向的映射,即通过进程的虚拟地址空间能够找到在文件对应的位置,也能够通过给定一个文件的区间,内核能够知道该区间映射到的所有进程。后者叫做反向映射。为了实现反向映射,使用了该联合体来实现了一个优先搜索树(Priority Search Tree)。优先搜索树将在后文详细说明。
- anon_vma_node和anon_vma用于管理匿名映射(anonymous mapping)。指向相同的页的映射都通过anon_vma_node组织在一个双链表上,内核中有若干此类链表,anon_vma成员是一个指向与各个链表关联的管理结构的指针,该管理结构包含一个表头和相关的锁。有关匿名映射的内容马上就会涉及到。
- vm_ops是一个指向struct vm_operation_struct的指针,该结构中的方法用于在区域上执行各种标准操作,包括open、close、fault、nopage等。创建和删除区域时会使用open/close;fault是在处理缺页中断时调用;nopage是被内核抛弃的缺页中断处理方法,新的代码中不应该使用。
- vm_pgoffset指定了文件映射的偏移量。该值只用于映射了一部分文件内容的情况,如果映射了整个文件则该值为0。
- vm_file指向file实例。后文在介绍优先查找树时会具体介绍,此外file实例是VFS文件系统中的重要的数据结构,想要细致的了解请参考文件系统相关内容。
- vm_private_data可用于指定存储的私有数据,不由通用的内存管理例程操作。只有少数声音和视频驱动程序使用了该选项。
- vm_flags存储了定义区域性质的标志,可以从<mm.h>中生命的VM_xxx预处理常数定义。
匿名页和文件页
There are two type of pages: anonymous pages and file-backed pages. A file-backed page originates from mmap()-ing a file in disk, whereas an anonymous page is the kind you get when doing malloc(). It has no relationship with any files at all. When the RAM becomes tight, the kernel swaps out anonymous pages to swap space and flushes file-backed pages to the file to give room for current requests. In other words, anonymous pages may consume swap area while file-backed pages don't. The only exception is for files mmap()-ed using the MAP_PRIVATE flag. In this case, file modification occurs in RAM only.
From: http://linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html
我是实在懒得翻译了,只是需要说明匿名页在进程虚拟地址空间中的堆、栈的实现有着重要用处,而由文件映射来的内存都是通过文件页来管理的。
四、优先搜索树(Priority Search Tree, PST)
我们已经知道,如果进程虚拟空间的某一个内存区域(vm_area_struct)已经和后备存储器中的某个文件的某个区域建立了映射,我们很容易通过通过页表的机制来获得该虚拟内存域与物理内存域的关联信息,进而找到对应的文件区域。对于动态链接库在系统中的实现,我们需要将同一个动态链接库映射到调用该库的不同的进程地址空间中,这就需要追踪文件的某个区域都被哪些进程的地址空间映射。这个映射过程叫做“反向映射”,Linux内核中使用的方式是优先搜索树的数据结构。
附加的数据结构
在这里需要简单介绍几个附加的数据结构:
<fs.h>
struct address_space {
struct inode *host; /* owner: inode, block_device */
...
struct prio_tree_root i_mmap; /* tree of private and shared mappings */
struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
...
}
<fs.h>
struct file {
...
struct address_space *f_mapping;
...
}
<fs.h>
struct inode {
...
struct address_space *i_mapping;
...
}
下面简单介绍上述几个数据结构的用途。对于进程来说,如果打开一个文件,则需要维护一个struct file的实例,该结构包含了一个指向struct address_space对象的指针。对于每个文件和块设备,在kernel中都会表示成一个struct inode实例,这个实例对应于ext文件系统中的inode。struct file是通过open系统调用在VFS层的文件的抽象,而inode表示文件系统自身中的对象。上述三个数据结构中,struct address_space是优先搜索树(prio tree)的关键结构。
对于这些结构的关联,我们可以通过下图来表示:
可以看到不同的集成打开同一文件时会生成不同的struct file实例,该实例会通过struct address_space将file->i_mapping赋值为inode->i_mapping,这使得多个进程可以同时访问一个文件而不互相影响。struct address_space结构体中有两个重要的变量,i_mmap结构对应的是private and shared mapping,就是那个优先搜索树,我们可以看到从inode可以找到其对应的address_space,从该address_space的这棵树中能够找到该inode所有偏移对应的struct vm_area_struct,前文我们已经知道,vm_area_struct是进程虚拟内存管理的基本结构,而其中有指向其所属进程的mm_struct的指针。所以通过这个路径就能够知道一个文件中某区域在所有进程中的映射情况,实现了反向映射。
优先搜索树
优先搜索树在很多情况下又称作Radix Priority Search Tree,因为其结构很像基数树的结构。对于该优先搜索树的操作可以在mm/prio_tree.c中找到,在此不讲述具体的代码,只是简单的介绍一下树的结构。
首先我们需要明确不同进程对同一个文件不同/相同区域映射的模型,如图:
首先将vm_area_struct中与优先搜索树相关的结构再列一遍:
<mm_types.h>
struct vm_area_struct {
...
struct vm_area_struct *vm_next;
...
struct rb_node vm_rb;
...
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
...
};
可以看到进程对文件的映射通过两个值可以确定区域:映射区域的开始和结束。我们将映射区域的开始称作radix_index,结束称作heap_index,二者的差即映射区域的大小称作size_index。所以一个区域可以表示成(radix_index, size_index, heap_index),其实去掉size_index也可以,只是kernel的文档中这样表示,为了和之后的图对应,在这里就这样表示了。
优先搜索树满足如下性质:
- 当前节点的heap_index大于或者等于其子节点(左右两个子节点)的heap_index
- 如果当前节点的heap_index和某个子节点的heap_index相同,则其radix_index小于该子节点的radix_index
- 对于heap_index和radix_index都相同的节点,则其映射到同样的区域中。这些节点被struct vm_area_struct中shared联合体中的vm_set组织在一起。
我们可以看到,优先搜索树的根是address_space中的struct prio_tree_root i_mmap,对于每个在该搜索树中的struct vm_area_struct,都通过该区域数据结构中shared联合体中的struct raw_prio_tree_node prio_tree_node来集成到优先搜索树中。对于映射区域完全重叠的vm_area_struct,则通过shared联合体中的vm_set结构的list进行连接。另外shared联合体中的vm_set结构体还被用来映射在address_space中的非线性映射区(address_space->i_mmap_nonlinear中表示)。对于非线性映射的概念后面会讲到,这里只需要提到非线性映射的vm_area_struct不会同时出现在优先搜索树中,所以使用同一个联合体的结构不会产生冲突。
下面给出一个优先搜索树的实例,由于排版原因,完全使用了屏幕截图的方式:
可以看到0~4层是优先搜索树的结构,而4~8层是根据prio_tree_root->index_bits值进行优化的结果,上半部分叫做Regular radix priority search tree,是heap-and-radix indexed的,即用heap_index值和radix_index值来进行建树;下半部分叫做Overflow-sub-trees,是根据heap_index和size_index来进行建树。
上面这张图和之前的优先搜索树的定义均来自kernel的文档,可以在kernel源码的Documentation/prio_tree.txt中找到更为详细的信息,包括Overflow-sub-trees的说明等。在此就不再说明了。
五、对区域的操作
内核提供了各种函数来对vm_area_struct进行操作,一些典型的操作如下图所示:
图的上半部分表示三种类型的增加区域的操作,对于增加的区域来说,能够和原有的区域合并为同一个区域,而三种增加区域的操作得到的新的区域又能够表示成同一个区域。所以在进行操作之后,系统将进行优化,只保存一个区域。图的下半部分表示两种删除操作,第一种删除之后剩下一个区域,第二种删除操作事实上相当于新增了一个区域。
所以内核提供了如下的函数来对区域进行操作:
- struct vm_area_struct *find_vma(struct mm_struct * mm, unsigned long addr):查找用户地址空间中结束地址在给定地址之后的第一个区域,即满足addr < vm_area_struct->vm_end条件的第一个区域。该函数有助于将虚拟地址关联到区域。
- 函数vma_merge提供将一个新区域与周边区域合并的功能。
- 函数insert_vm_struct是内核用于插入新区域的标准函数。
- 函数get_unmapped_area是内核在插入新的内存区域之前,确认虚拟地址空间中有足够的空闲空间的函数,相当于是创建区域的函数。
对区域的操作在此就不详细说明了,详细内容可以参考代码中的函数注释说明。
六、内存映射
我们以上所描述的若干机制,最后都是要为进行进程地址空间内存映射服务的。我们知道,C标准库提供了mmap函数建立映射(关于mmap的用法请google或者man mmap),在内核一端,提供了两个系统调用mmap和mmap2,某些体系结构实现了两个版本,例如IA-64和Sparc(64),其他的只实现了第一个(AMD64)或第二个(IA-32)。具体的函数声明就不在这里给出,请自行查看源代码,这里指给出执行过程。
创建映射
创建映射就是通过mmap或者mmap2系统调用实现的,下面只讨论sys_mmap2,在系统调用mmap2的处理过程中,将所有的工作委托给do_mmap2.内核在其中提供文件描述符找到file实例,以及所处理文件的所有特征数据。剩余的工作委托给do_mmap_pgoff。该函数是一个重要的函数,与体系结构无关,定义在mm/mmap.c中,下图给出了相关代码的流程图:
do_mmap_pgoff曾经是内核中最长的函数,现在被分成了两个部分。get_unmapped_area上文中已经说明,即创建一个内存区。之后需要计算该映射的flags。之后将所有的工作交给函数mmap_region。mmap_region调用find_vma_prepare函数,来查找前一个和后一个区域的vm_area_struct实例,以及红黑树中节点对应的数据。如果在指定的映射位置已经存在一个映射,则通过do_munmap删除它。之后内核检查内存空闲是否满足要求,之后创建新的vm_area_struct实例,并用特定于文件的函数file->f_op->mmap创建映射。如果设置了VM_LOCKED,或者通过系统调用的标志参数显示的传递进来,或者通过mlockall机制隐形设置,内核都会调用make_pages_present一次扫描映射中各页,对每一页出发缺页中断以便读入其数据。之后返回映射的起始地址。
删除映射
删除映射的操作很简单,使用munmap系统调用,它需要两个参数:接触映射区域的起始地址和长度。按照惯例将工作交给do_munmap处理,流程如下:
find_vma_prev找到接触映射区域的vm_area_struct实例,如果解除映射区域的起始地址与找到的区域的起始地址不同,则需要将找到的区域的后端分裂出来,调用split_vma函数。如果解除映射的部分区域的末端与原区域不重合,那么原区域后部仍然有一部分未接触映射,因此需要对这部分重复上述处理过程。
内核接下来调用detach_vmas_to_be_unmapped,列出所有需要解除映射的区域,之后调用unmap_region从页表中删除与映射相关的所有项,此外内核还必须确保将相关的项从TLB移除或使之失效。最后用remove_vma_list释放vm_area_struct实例占用的空间。
非线性映射
在上文中,我们提到了struct address_space中的i_mmap_nonlinear,该部分对应的是非线性映射。相对于非线性映射,普通的映射将文件中一个连续的部分映射到虚拟内存中一个同样连续的部分,但如果需要将文件的不同部分以不同顺序映射到虚拟内存的连续区域中,通常必须使用几个映射。从消耗的资源来看,代价比较昂贵(特别是需要分配的vm_area_struct数量)。实现同样效果的一个更简单的方法是使用非线性映射。该特性在内核2.5版本中引入。
mm/fremap.c
long sys_remap_file_pages(unsigned long start, unsigned long size,
unsigned long __prot, unsigned long pgoff, unsigned long flags)
该系统调用允许重拍映射中的页,是的内存与文件中的顺序不再等价。实现该特性无需移动内存中的数据,只通过操作进程的页表就可以实现。该函数可以将现存映射(位置pgoff,长度size)移动到虚拟内存中的一个新位置。start标志了移动的目标映射,因而必须落入某个现存映射的地址范围。它还指定了由pgoff和size标识的页移动的目标位置。
对于所有的非线性映射区域,维护在struct address_space的i_mmap_nonlinear为表头的链表中,链表中的各个vm_area_struct实例采用shared.vm_set.list作为链表元素。原因如同上文所说,在标准的优先搜索树中不存在非线性映射区域。
所属区域对应的页表项用一些特殊的项来填充,使得这些页表项看起来像是对应于不存在的页,但其中包含附加信息,将其标识为非线性映射的页表项。在访问此类页表项描述的页时,会产生一个缺页中断,从而读入正确的页。
反向映射
内核利用此前讨论的结构,已经可以建立虚拟和物理地址之间的联系(通过页表),以及进程的一个内存区域预期虚拟内存页地址之间的关联。仍然确实的一个联系是,物理内存页和所有使用该页的进程的对应页表项之间的联系。在物理页面换出时,正好需要此关联,以便更新所有涉及的进程的页表项。
为此,内存使用了一些附加的数据结构和函数,实现了一种反向映射的机制。
反向映射使用了简介的数据结构,即在struct page结构中包含了一个用于实现反向映射的成员:
<mm.h>
struct page {
....
atomic_t _mapcount; /* Count of ptes mapped in mms,
* to show when page is mapped
* & limit reverse map searches.
*/
...
};
_mapcount表明共享该页的位置的数目。计数器初值为-1,在页插入到你想映射数据结构时,计数器赋值为0.页每次增加一个使用者时计数器加1。此外,在struct vm_area_struct数据结构中(参考上文),我们维护了优先搜索树,该树中嵌入了所有非匿名映射的区域以及指向内存中同一页的匿名区域的链表。这样,内核可以根据物理页面找到该页面对应的vm_area_struct,从而找到包含该页的所有使用者。该方法又名基于对象的反向映射(object-based reverse mapping),因为没有存储页和使用者之间的直接关联,而是在两者之间插入一个对象(该页所在的区域struct vm_area_struct)。
在建立反向映射时,需要对匿名页和基于文件映射的页分别处理,这是因为管理这两种选项的数据结构不同。对匿名页简历反向映射的函数是page_add_anon_rmap,对基于文件映射的页的函数是page_add_file_rmap。
反向映射在页交换中非常有用,此外内核定义的try_to_unmap函数也依赖该技术,并且也大量涉及到了页交换的细节。所以在此就不讨论了。
堆空间的管理
对是进程用于动态分配空间的内存区域,最重要的函数是malloc来分配任意长度的内存区。malloc和内核之间的经典接口是brk系统调用,负责扩展/收缩堆。brk系统调用只需要一个参数,用于指定堆在虚拟地址空间中新的结束位置。调用流程如下:
brk机制是基于匿名映射实现的,对于堆的扩展与收缩实际上就是匿名映射的处理,在此就不详细说明了。
七、缺页中断
缺页中断也叫缺页异常,主要差别是看发生在用户空间还是内核空间。我们在这里不区分两者的差别,统一都叫缺页中断。
下面给出了缺页中断的一般处理流程:
缺页中断分为内核态和用户态两方面,处于哪一态的中断主要取决于发生缺页中断的虚拟地址落在哪一部分。内核态的缺页中断只需要检查当前是否在执行内核态代码。内核态中断不需要其它的检查,因为内核是相信自己的,发生内核中断时内核一定能够在对应的位置找到缺页并且填充该页面,而不会产生异常状况。对于用户态缺页中断,首先需要检查映射是否存在,其次要检查权限,最后才能够处理缺页中断。其中任何一个环节出错都会导致Segmentation Fault。到这里大家知道了Seg Fault的原因了吧?大部分都是因为访问地址出错引起的。
缺页中断是灰常灰常复杂的一个机制,而且非常依赖体系结构。在此我们只讨论IA-32体系结构上的方法。arch/x86/kernel/entry_32.S中的一个汇编例程是缺页中断的入口,但是其立刻调用了arch/x86/mm/fault_32.c中的C函数do_page_fault。代码流程如下:
do_page_fault只需要两个参数:发生中断时使用中的寄存器集合,提供错误原因的错误代码(long error_code)。目前error_code只使用了前五个比特位,语义如下表:
比特位 置位(1) 未置位(0)
0 缺页 保护异常(没有足够的访问权限)
1 读访问 写访问
2 核心态 用户态
3 表示检测到使用了保留位
4 表示缺页异常是在取指令时出现的
另外需要明确的是,对于缺页中断的恢复地址通过read_cr2()函数获得,即保存在cr2寄存器中。
用户空间缺页中断
在确定缺页中断是在允许的地址触发之后,内核必须确定将所需数据读入物理内存的适当方法。该任务交给handle_mm_fault,它不依赖底层体系结构。该函数确认在各级页目录中,通向对应于一场地址的页表项的各个页目录都存在。handle_pte_fault函数分析缺页异常的原因,pte指向相关页表项(pte_t)的指针。
mm/memory.c
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, int write_access)
如果页不在物理内存中,该函数的流程如下:
- 如果没有对应的页表项,则内核必须从头开始加载该页。对匿名映射成为按需分配(demand allocation),对基于文件的映射,则称之为按需调页(demand paging)。
- 如果该页标记为不存在,而页表中保存了相关信息,则意味着该页已经换出(swap out),因而需要从系统的某个交换区换入。
- 非线性映射已经换出的部分不能像普通页那样换入,因为必须正确地恢复非线性关联。pte_file函数用于检查页表项是否属于非线性映射,do_nolinear_fault用户处理该类已成。
如果该页存在于物理内存中,但是该区域对页授予了写权限,而硬件的存取机制没有授予。这种情况下出发的异常,需要调用do_wp_page函数创建该页的副本并插入到进程的页表中。该机制成为写时复制(Copy on write, COW)。fork之后会大量触发此类异常。
按需分配/调页
按需分配页的工作交给do_linear_fault函数(定义在mm/memory.c中),在转换一些参数后,其余工作交给__do_fault。该函数是缺页中断处理的核心之一,代码流程如图:
总的来说,该部分处理具体的方法依赖于映射到发生异常的地址空间(address_space)中的文件,因此需要调用特定于文件的方法来获取数据。通常该方法保存在vm->vm_ops->fault。由于较早的版本约定使用nopage,则如果没有注册fault方法,使用旧的vm->vm_ops->nopage。
对于给定涉及的区域vm_area_struct,内核选择何种方法读取页面?
- 使用vm_area_struct->vm_file找到映射的file对象
- 在file->f_mapping中找到指向映像自身的指针。
- 每个地址空间都有特定的地址空间操作,从中选择readpage方法。使用mapping->a_ops->readpage(file, page)从文件中将数据传输到物理内存。
如果需要写访问,内核必须区分共享和私有映射。对私有映射,必须准备页的一份副本。
对于匿名页使用匿名页添加新映射的方法来处理(前文提到过),并且需要将其添加到缓存中;对于基于文件映射的页需要采用page_add_file_rmap来处理。
此外,在需要使用新的物理页面时,内核倾向使用用户高端内存中分配页面,如下:
page=alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
最后必须更新处理器的MMU缓存,因为页表已经修改。
写时复制
写时复制的处理函数是do_wp_page,流程如下:
内核首先调用vm_normal_page,通过页表项找到struct page实例。在page_cache_get获取页之后,接下来anon_vma_prepare准备好反向映射机制的数据结构,以接受一个新的匿名区域。由于缺页中断的来源是需要将一个充满有用数据的页复制到新页,因此内核调用alloc_page_vma分配一个新页。cow_user_page将异常页的数据复制到新页。然后使用page_remove_rmap删除原来的只读页的你想映射,最后使用lru_cache_add_active将新分配的页放到LRU缓存的活动列表上,并通过page_add_anon_rmap将其插入到你想映射的数据结构。
内核缺页中断
在访问内核地址空间时,缺页中断可能被下列条件触发:
- 内核设计错误导致访问错误地址
- 内核通过用户空间传递的系统调用参数,访问了无效地址
- 访问使用vmalloc分配的区域,触发缺页中断
前两种情况是真正的错误,内核必须对此进行额外的检查。vmalloc的情况是合理的,需要加以矫正。我们在Figure 4-18中看到大量的fixup_exception函数就在用来搜索异常表(exception_table_entry),异常表项struct exception_table_entry实例和fixup_exception函数如下:
<include/asm-x86/uaccess_32.h>
struct exception_table_entry
{
unsigned long insn, fixup;
};
arch/x86/mm/extable_32.c
int fixup_exception(struct pt_regs *regs)
{
const struct exception_table_entry *fixup;
fixup = search_exception_tables(regs->eip);
if (fixup) {
regs->eip = fixup->fixup;
return 1;
}
return 0;
}
EIP寄存器在IA-32处理器上包含了出发中断的代码段地址。search_exception_tables扫描异常表,查找合适的匹配项。如果在异常表中找到了对应的修正例程,则执行该例程;如果没有找到,表明出现了一个真正的内核异常,将调用do_page_fault来处理该异常,并且最终导致内核进入oops状态,并强制使用SIGKILL结束当前进程,做最后的垂死挣扎,不过大部分情况下到这里内核就挂掉了。
八、内核和用户空间之间的数据传递
内核并不能直接使用用户空间的数据,用户空间也不能直接调用内核空间的数据。所以数据的传递在内核与用户空间之间有一套通用的接口,该接口在系统调用过程中和对device文件操作时经常被调用。见下表:
大多数函数都有两个版本,没有双下划线的版本会调用access_user对用户空间地址进行检查。
这些函数主要是使用汇编语言实现的,由于调用非常频繁,对性能要求极高,因此还必须使用GNU C用于嵌入汇编的复杂构造和代码中的链接指令将异常代码也集成进来,才能达到较好的性能。在内核2.5开发期间,编译过程增加了一个检查工具。该工具分析源代码,检查用户空间的指针是否能够直接解引用,而不实用上述函数。所以源自用户空间的指针必须用关键字__user标记,以便工具分辨所需检查的指针。例如:
<fs/open.c>
asmlinkage long sys_chroot(const char __user * filename) {
...
}
终于总结完了!!!完全晕菜了有木有!
总的来说还是总结了《深入Linux内核架构》一书(Professional Linux Kernel Architecture),其中有一些部分来自Kernel源码自带的Documentations,还有一些来源于Kernel Git库的changelog,其他的来自于谷歌。文中所有的图片和表格都来自于《深入Linux内核架构》。
这部分的复杂程度堪比Linux对物理内存的管理,尤其是两个反向映射(inode映射到所有使用的进程、物理页面映射到所有使用该页的虚拟地址的页表!尼玛说起来这这么长!)加上一个优先搜索树的结构,啃了好久看了好多资料才慢慢明白。其实可以从用户空间总结一下整个过程(只包含内存管理部分):
- 用户空间调用某个命令(创建进程)
- 准备进程的基本内存,此时命令的代码段和数据段以及动态链接库都没有载入到内存中。
- 进程执行第一条指令,触发缺页中断
- 缺页中断进入到用户态,发生按需调页
- 进程使用malloc分配大内存
- 堆空间不够大,发生缺页中断,用户态按需分配匿名页面
- 进程从硬盘中读文件
- 缺页中断,在MMAP区域映射后备存储器中的文件
- 进程使用remap_file_pages系统调用
- 内核建立非线性映射
- 进程操作某device driver,该驱动使用vmalloc分配内存
- 发生内核态缺页中断,处理vmalloc缺页中断
- 该device driver写的很渣渣,其中错误访问了0号页面
- 发生内核态缺页中断,调用fixup_exception,发生错误
- Device driver崩溃,内核可能也挂了
以上基本能够囊括整个进程虚拟地址空间的管理(不包括初始化部分),希望能给大家一个整体的印象。