分类: LINUX
2013-12-10 14:38:39
原文地址:Linux进程地址空间FAQ 作者:zyd_cu
1. 内核与普通进程获取内存时有何不同?
内核是操作系统中优先级最高的成分,如果某个内核函数请求动态内存,则其必定有正当的理由发出那个请求,内核的内存请求应该立即被满足;内核信任自己,所有的内核函数都被假定是没有错误的,内核函数不必考虑编程错误的保护措施。
而当用户态进程请求分配内存时,进程对动态内存的请求被认为是不紧迫的,当进程的可执行文件被装入时,进程并不一定立即对所有的代码页进行访问,同样的,当进程调用malloc获得动态内存时,也不意味着进程很快就会访问所有所获得的内存,故内核总是尽量推迟给用户态进程分配动态内存;由于用户进程是不可信任的,故内核必须随时准备捕获用户态进程所引起的所有寻址错误。
2. 什么是进程的地址空间?
进程的地址空间由允许进程使用的全部线性地址组成,每个进程所看到的线性地址集合是不同的。访问权限相同的连续线性地址构成一个线性区,进程的地址空间通常包含多个线性区,由于效率原因,线性区的起始地址和长度都必须是4096的倍数,以便每个线性区所识别的数据完全填满分配给它的页框。
用户进程可访问的地址空间为0-3G,为何不将整个空间交给进程直接使用,而要将其以线性区的形式组织起来,个人觉得主要是考虑安全因素,进程使用的区域必须向内核报告,内核知道所有用户态进程的内存使用情况,从而对其行为进行有效的控制,如大片内存的malloc导致失败,如果0-3G的所有内存都能不加申请的使用,则用户程序中的漏洞经常会导致物理内存的耗尽。
3. 进程何时会获得新的线性区?
(1) 当创建一个新的进程时,一个全新的地址空间被分配给了新的进程。
(2) 正在运行的进程装入新的程序(exec)时,旧的线性区被释放,新的线性区被分配给进程。
(3) 进程持续向用户态堆栈增加数据。
(4) 进程通过malloc扩展动态区。
(5) 进程创建一个IPC共享线性区与其他合作进程共享数据。
与创建、删除线性区相关的系统调用主要包括brk(), execve(), _exit(), fork(), mmap(), mmap2(), mremap(), munmap(), shmat(), shmdt()等。
4. 内核如何描述线性区?
内核使用struct vm_area_struct描述符表示线性区,其主要包括:
(1) 线性区的起始地址与结束地址
(2) 线性区的访问权限
(3) 线性区的的链接信息(链表与红黑树)
5. 内核如何描述进程的地址空间?
内核使用struct mm_struct描述符描述进程的地址空间,其主要包括:
(1) 指向线性区的链表头
(2) 指向线性区对象的红黑树
(3) 指向进程页全局目录的指针
(4) 线性区的个数
(5) 线性空间大小(页数)
(6) 分配给进程的页框数
(7) 可执行代码的起始与结束地址
(8) 已初始化数据的起始与结束地址
(9) 堆的起始与结束地址
(10)用户态堆栈的起始地址
(11)命令行参数的起始与结束地址
(12)环境变量的起始与结束地址
6. 线性区如何组织?
进程所拥有的所有线性区是通过链表按内存地址升序链接起来,内核通过遍历链表能方便的扫描整个线性区集合;同时,由于内核频繁执行查找包含指定线性地址的线性区的操作,故为了提高查找效率,线性区被组织成一棵红黑树,以提供对数级的查找效率。
7. 何时为进程分配页框?
只有当进程访问实际实际数据时,才为线性区分配实际页框,为此,进程会产生一个缺页异常。linux的缺页异常可能由编程错误或是引用属于进程地址空间但尚未分配物理页框的页引起,内核通过将引起缺页异常的线性地址与当前进程的线性区进行比较,如果访问权限与进程线性区相匹配,则属于合法访问,应该给该线性地址所在的线性区分配一个新的页框(alloc_page),并更新内核页表。对于其它情况,则属于非法访问,内核需进行相应的错误处理。
8. 栈向下增长,如何映射到线性区?
栈所在的线性区,其VM_GROWSDOWN标志被设置,vm_end字段保持不变,vm_start的值可能被减小。线性区的边界包括但不严格限定用户态堆栈的当前大小,栈所在的线性区的vm_start字段只可能减小,永远不可能增加(即使进程执行一系列pop指令时)。当进程填满了给它的堆栈分配的最后一个页框后,push会导致堆栈线性区的扩展,expand_stack会被调用,vm_start被减小(同时必须为页面大小4096的倍数)。
9. 写时赋值与进程地址空间的联系?
最初的linux系统实现了傻瓜式的进程创建,当发出fork系统调用时,内核原样复制父进程的整个地址空间并把赋值的那一份分配给子进程,这就需要为子进程的页表分配页框,为子进程的页分配页框,初始化子进程的页表,把父进程的页复制到子进程相应的页中。 现在的linux采用更为高效的方法,即写时复制。父进程与新创建的子进程共享页框而不是赋值页框,父进程或子进程需要对页框的数据进行修改时,内核就赋值一个新的页框。
copy_mm函数把父进程的地址空间给子进程,如果带有CLONE_VM标志,则子进程共享父进程的mm_struct;如果该标志没有被设置,copy_mm函数必须创建一个新的地址空间(申请并初始化新的mm_struct结构,并赋值给子进程的mm字段)。
10. 如何管理进程的堆?
内存描述符的start_brk与brk字段分别限定了进程堆区的开始地址与结束地址。
与进程对相关的库函数或系统调用包括:
malloc(size) 从堆中请求size个字节的动态内存
calloc(n, size) 请求n个大小为size的元素的数组空间,并将数组元素清0
realloc(ptr, size) 改变由前面malloc或calloc分配的内存区的大小
free(ptr) 释放有malloc或calloc分配的内存区
brk(addr) 直接修改堆的大小,brk字段会被调整为页面大小的整数倍,根据当前堆的大小调用do_mmap或do_munmap扩展或缩小线性区(在不超过限制范围的前提下)
sbrk(increment) 增加或减少堆大小