分类:
2012-10-30 23:05:05
原文地址:读书笔记之linux内核设计与实现(13)进程地址空间 作者:garyybl
内核除了管理本身的内存外,还必须管理进程的地址空间——也就是系统中每个用户空间进程所看到的内存。Linux操作系统采用虚拟内存技术,因此,系统中 的所有进程之间以虚拟方式共享内存。对每个进程来说,它们好像都可以访问整个系统的所有物理内存。更重要的是,即使单独一个进程,它拥有的地址空间也可以 远远大于系统物理内存。
进程地址空间由每个进程中的线性地址区组成,而且更为重要的特点是内 核允许进程使用该空间中的地址。每个进程都有一个32位的平坦(flat)地址空间。“平坦”描述的地址空间范围是一个独立的连续区间。通常情况下,每个 进程都有唯一的这种平坦地址空间,而且进程地址空间之间彼此互不相干。两个不同的进程可以在他们各自地址空间的相同地址内存存放不同的数据。但是进程之间 也可以选择共享地址空间,我们称这样的进程为线程。
在地址空间中,我们往往更为关心的是进程有权访问的虚拟内存地址空间,比如08048000-0804c000。这些可被访问的合法地址区间被称为内存区域(memory area),通过内核,进程可以给自己的地址空间动态的添加或减少内存区域。 进程只能访问有效范围内的内存地址。每个内存区域也具有相应进程必须遵循的特定访问属性,如只读、只写、可执行等属性。
内存区域可以包含各种内存对象:
可执行文件代码的内存映射,成为代码段(text section)。
可执行文件的已初始化全局变量的内存映射,称为数据段(data section)。
包含未初始化全局变量,也就是bss段的零页的内存映射。
用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射。
每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间。
任何内存映射文件。
任何共享内存段。
任何匿名的内存映射,比如由malloc()分配的内存。
14.1 内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构体包含了和进程地址空间有关的全部信息。内存描述符由mm_struct结构体表示,定义在
mm_users域记录正在使用该地址的进程数目。mm_count域是mm_struct结构体的主引用计数。只要前者不为0,那么后者为1。当 mm_count的值为0时,说明已经没有任何指向该mm_struct结构体的引用了,这时该结构体会被销毁。mmap和mm_rb这两个不同的数据结 构描述的对象是相同的:该地址空间中的全部内存区域。所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是 init_mm内存描述符,它代表了init进程的地址空间。
14.1.1 分配内存描述符
在进程的进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程中的内存描述符。fork()函数利用 copy_mm()函数复制父进程的内存描述符,也就是current->mm域给其子进程,而子进程中的mm_struct结构体实际上是通过文 件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的。通常每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。
如果父进程希望和其子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们把这样的进程叫做线程。
14.2 内存区域
我们先来看一下内存区域和线程地址空间的关系,前面提到,每个进程都有一个32位的平坦地址空间,在这个地址空间中,我们往往更为关心的是进程有权访问的 虚拟内存地址空间。这些可被访问的合法地址区间被称为内存区域。也就是说,内存区域是进程所能够访问的有效范围内的地址区间。注意,一个进程地址空间可以 有多个内存区域。
内存区域由vm_area_struct结构体描述,定义在文件
每个VMA描述符都对应于进程地址空间中的唯一区间。vm_start域指向区间的首地址,vm_end域指向区间的尾地址之后的第一个字节,因 此,vm_end-vm_start的大小便是内存区域的长度。注意,在同一个地址空间中,不同的内存区域不能重叠。
vm_mm域指向和VMA相关的mm_struct结构体,注意每个VMA对其相关的mm_struct结构体来说是唯一的。所以即使连个独立的进程将同 一个文件映射到各自的地址空间,它们分别都有一个vm_area_struct结构体来标志自己的内存区域。
14.2.1 VMA标志
VMA标志是一种位标志,定义于
我们来了解一下其中有趣和重要的标志,VM_READ、VM_WRITE和VM_EXEC标志了内存区域中页面的读、写和执行权限。VM_SHARE指明 了内存区域包含的映射是否可以在多进程间共享,如果该标志被设置,则我们称其为共享映射;否则称之为私有映射。VM_IO标志内存区域中包含有对设备 I/O空间的映射。该标志通常在设备驱动程序执行mmap()函数进行I/O空间映射时才被设置。
14.2.2 VMA操作
vm_area_struct结构体中的vm_ops域指向与指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。vm_area_struct作为通用对象代表了任何类型的内存区域,而操作表描述了针对特定的对象实例的特定方法。操作函数表由vm_operations_struct结构体表示,定义在文件
struct vm_operations_struct {
void (* open)(struct vm_area_struct *);
void (* close)(struct vm_area_struct *);
struct page * (*nopage)(struct vm_area_struct *, unsigned long, int);
int ( *populate)(struct vm_area_struct *, unsigned long, unsigned long, pgprot_t,
unsigned long, int);
};
void open(struct vm_area_struct *area) 当指定的内存区域被加入到一个地址空间时,该函数被调用。
void close(struct vm_area_struct *area) 当指定的内存区域从地址空间被删除时,该函数被调用。
14.2.3 实际使用中的内存区域
可以使用/proc文件系统和pmap(1)工具查看给定进程的内存空间可其中所含的内存区域。下面列出了进程号为1764的进程的地址空间中的全部区域:
$pmap 1764
xinetd(1764)
08048000 (132 KB) r-xp (08:02 392673) /usr/sbin/xinetd
08069000 (4 KB) rw-p (08:02 392673) /usr/sbin/xinetd
0806a000 (60 KB) rwxp (00:00 0)
40000000 (84 KB) r-xp (08:02 1062912) /lib/ld-2.3.2.so
40015000 (4 KB) rw-p (08:02 1062912) /lib/ld-2.3.2.so
40016000 (4 KB) rw-p (00:00 0)
40029000 (28 KB) r-xp (08:02 359889) /usr/lib/libwrap.so.0.7.6
40030000 (4 KB) rw-p (08:02 359889) /usr/lib/libwrap.so.0.7.6
40031000 (4 KB) rw-p (00:00 0)
40032000 (72 KB) r-xp (08:02 1062927) /lib/libnsl-2.3.2.so
40044000 (4 KB) rw-p (08:02 1062927) /lib/libnsl-2.3.2.so
40045000 (8 KB) rw-p (00:00 0)
40047000 (132 KB) r-xp (08:02 768584) /lib/tls/libm-2.3.2.so
40068000 (4 KB) rw-p (08:02 768584) /lib/tls/libm-2.3.2.so
40069000 (20 KB) r-xp (08:02 1062921) /lib/libcrypt-2.3.2.so
4006e000 (4 KB) rw-p (08:02 1062921) /lib/libcrypt-2.3.2.so
4006f000 (160 KB) rw-p (00:00 0)
40097000 (44 KB) r-xp (08:02 1062933) /lib/libnss_files-2.3.2.so
400a2000 (4 KB) rw-p (08:02 1062933) /lib/libnss_files-2.3.2.so
42000000 (1208 KB) r-xp (08:02 768582) /lib/tls/libc-2.3.2.so
4212e000 (12 KB) rw-p (08:02 768582) /lib/tls/libc-2.3.2.so
42131000 (8 KB) rw-p (00:00 0)
bfffb000 (20 KB) rwxp (00:00 0)
mapped: 2024 KB writable/private: 304 KB shared: 0 KB
前三行对应源程序的代码段、数据段和bss段,接下来的三行为动态连接程序ld.so的代码段、数据段和bss段;最后一行为进程的栈。
没有映射文件的内存区域的设备标志为00:00,索引节点标志也为0,这个区域就是0页——零页映射的内容全为0。如果将零页映射到可写的内存区域,那么该区域将被初始化为全0.这是零页的一个重要用处,而bss段需要的就是全0的内存区域。
14.3 操作内存区域
14.4 mmap()和do_mmap():创建地址区间
内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已存在的地址区间
相邻且他们具有相同的访问权限的话,那么两个区间将合并为一个。不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况,do_mmap()函数
都会将一个地址区间加入到进程的地址空间中——无论是扩展已存在的内存区域还是创建一个新的区域。do_mmap()函数定义在文
件
unsigned long do_mmap( struct file *file, unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flag, unsigned long offset)
该函数映射由file指定的文件,具体映射的是文件中从偏移offset处开始,长度为len字节的范围内的数据。如果file参数为NULL且 offset参数为0,那么就代表这次映射没有和文件相关,该情况被称作匿名映射。若指定了文件名和偏移量,那么该映射被称为文件映射。adr是可选参 数,指定搜索空闲区域的起始地址。
prot参数指定内存区域中页面的访问权限。访问权限标志定义在文件
PROT_READ 对新建区间中页的要求 对应于VM_READ
PROT_WRITE 对应于VM_WRITE
PROT_EXEC 对应于VM_EXEC
PROT_NONE 页不可被访问
系统调用会在虚拟内存中分配一个合适的新内存区域。如有可能,将新区域和临近区域进行合并,否则内核从vm_area_cachep长字节(slab)中 缓存中分配一个vm_area_struct结构体,并且使用vma_link()函数将新分配的内存区域添加到地址空间的内存区域链表和红——黑树中, 随后还要更新内促描述符中的total_vm域,然后才返回新分配的地址区间的初始地址。
mmap()系统调用
在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能。mmpa()系统调用定义如下:
void *mmap2(void *start, size_t length, int prot, int flags, int fd, off_t pgoff)
14.5 munmap()和do_munmap():删除地址区间
14.6 页表
Linux中使用三级页表完成虚拟地址到物理地址的转换。linux对所有体系结构,包括对那些不支持三级页表的体系结构都使用三级页表管理。
顶级页表是页全局目录(PGD)。PGD包含了一个pgd_t类型的数组,多数体系结构中pgd_t类型等同于无符号长整型类型。PGD中的表项指向二级页目录中的表项:PMD。
二级页表是中间页目录(PMD)。它是一个pmd_t类型数组,其中的表项指向PTE中的表项。
最后一级的页表简称页表,其中包含了pte_t类型的页表项,该页表项指向物理页面。
每个进程都有自己的页表。内存描述符的pgd域指向的就是进程的页全局目录。
页表对应的结构体依赖于具体的体系结构,所以定义在文件
由于几乎每次对虚拟内存中的页面访问都必须先解析它,从而得到物理内存中的对应地址,所以页面操作的性能非常关键。但不幸的是,搜索内存中的物理地址速度 很有限,因此为了加快搜索,多数体系结构都实现了一个翻译后缓冲器(translation lookaside buffer, TLB)。TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映 射,若在缓存中直接命中,物理地址立刻返回。