主要内容:
*内核如何实现对进程动态内存的推迟分配?
——使用一种新的资源(线性地址区)
当用户进程请求动态内存时,并没有获得请求的页框,而是获得了一个新的线性地址的使用权。这线性地址区域就成为进程地址空间的一部分。
*为什么要推迟分配?
*进程怎样看待动态内存。
*进程地址空间的基本组成。
*缺页异常处理程序在推迟给进程分配页框中所起的作用。
*内核怎样创建和删除进程的整个地址空间?
*与进程的地址空间管理有关的API和系统调用。
====>>>>>
1. 进程地址空间
2. 内存描述符
3. 线性区
4. 缺页异常处理程序
5. 创建和删除进程的地址空间
6. 堆的管理
进程地址空间
进程的地址空间(Address Space)由允许进程使用的全部线性地址组成。一个进程使用的进程地址空间与另一个进程使用的进程地址空间之间没有关系。如果进程之间共享相同的地址空间,则被称为线程。
内核可以通过增加或删除某些线性地址区间来动态修改进程的地址空间。
内核通过所谓线性区的资源来表示线性地址空间,线性地址空间由起始线性地址、长度以及相应访问权限来描述。
起始地址和长度都是4096(一页)的整数倍——提高效率。
*进程创建新的线性区的典型情况(六种):
程序执行
exec()函数
缺页异常处理程序
内存映射
IPC共享内存
malloc()函数—heap
*与创建、删除线性区相关的系统调用:
系统调用 | 说明 |
brk() | 改变进程堆的大小 |
execve() | 装入可执行文件,从而改变进程的地址空间 |
_exit() | 结束当前进程并撤销它的进程地址空间 |
fork() | 创建一个新的进程,并创建新的地址空间。 |
mmap(),mmap2() | 为文件创建一个内存映射,从而扩大进程的地址空间 |
mremap() | 扩大或缩小线性区 |
remap_file_pages() | 为文件创建非线性映射 |
munmap() | 撤销对文件的内存映射,从而缩小进程的地址空间 |
shmat() | 创建一个共享线性区 |
shmdt() | 撤销一个共享线性区 |
确定进程所拥有的线性地址区是内核的任务,这使得缺页处理程序能够有效的处理以下两种无效的线性地址:
1. 由编程错误引发的非法地址访问(如数组越界、非法指针);
2. 由缺页(物理页)引发的无效线性地址,即使这个线性地址属于进程地址空间,但是内核还未分配物理页。——请求调页。
内存描述符
与进程地址空间的全部信息都包含在一个叫做内存描述符(Memory Descriptor)的数据结构中,这个结构的类型是mm_struct,进程描述符的mm字段指向此结构。
无论是内核线程还是用户进程,对于内核来说,都是task_struct这个数据结构的一个实例,task_struct被称为进程描述符(process descriptor),因为它记录了这个进程所有的上下文(context)。其中有一个被称为“内存描述符”(memory descriptor)的数据结构 mm_struct,该结构抽象并描述了Linux视角下管理进程地址空间的所有信息。
[start_code,end_code)表示代码段的地址空间范围。
[start_data,end_data)表示数据段的地址空间范围。
[start_brk,brk)分别表示heap段的起始空间和当前的heap指针。
[start_stack,end_stack)表示stack段的地址空间范围。
mmap_base表示memory mapping段的起始地址。
具体结构图:
内存描述符:
mm_strcut定义:
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 */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct vm_area_struct *area);
unsigned long mmap_base; /* base of mmap area */
unsigned long free_area_cache; /* first hole */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables, mm->rss, mm->anon_rss */
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
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;
unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;
unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long saved_auxv[42]; /* for /proc/PID/auxv */
unsigned dumpable:1;
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
mm_context_t context;
/* Token based thrashing protection. */
unsigned long swap_token_time;
char recent_pagein;
/* coredumping support */
int core_waiters;
struct completion *core_startup_done, core_done;
/* aio bits */
rwlock_t ioctx_list_lock;
struct kioctx *ioctx_list;
struct kioctx default_kioctx;
unsigned long hiwater_rss; /* High-water RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
};
mm_struct字段:
类型 | 字段 | 说明 |
struct vm_area_struct* | mmap | 指向线性区域对象的链表头 |
struct rb_root | mm_rb | 指向线性区对象的红黑树的根 |
struct vm_area_struct* | mmap_cache | 指向最后一个引用的线性区对象 |
unsigned long (*) () | get_unmapped_area | 在进程地址空间中搜索有效线性地址区 |
void (*) () | unmap_area | 释放线性地址区间时调用的方法 |
unsigned long | free_area_cache | 内核从这个地址开始搜索进程地址空间中线性地址的空闲区域 |
pgd_t * | pdg | 指向页全局目录 |
atomic_t | mm_users | 次使用计数器 |
atomic_t | mm_count | 主使用计数器 |
int | map_count | 线性区的个数 |
struct rw_semaphore | mmap_sem | 线性区的读/写信号量 |
spinlock_t | page_table_lock | 线性区的自旋所和页表的自旋锁 |
struct list_head | mmlist | 指向内存描述符链表中的相邻元素 |
unsigned long | start_code | 可执行代码的起始地址 |
unsigned long | end_code | 可执行代码的最后地址 |
unsigned long | start_data | 已初始化数据的起始地址 |
unsigned long | end_data | 已初始化数据的最后地址 |
unsigned long | start_brk | 堆的起始地址 |
unsigned long | brk | 堆的当前最后地址 |
unsigned long | start_stack | 用户堆栈的起始地址 |
unsigned long | arg_start | 命令行参数的起始地址 |
unsigned long | arg_end | 命令行参数的最后地址 |
unsigned long | env_start | 环境变量的起始地址 |
unsigned long | env_end | 环境变量的最后地址 |
unsigned long | rss | 分配给进程的页框数 |
unsigned long | anon_rss | 非配给匿名内存映射的页框数 |
unsigned long | total_vm | 进程地址空间的大小(页数) |
unsigned long | locked_vm | 锁住而不能换出的页的个数 |
unsigned long | shared_vm | 共享文件内存映射中的页数 |
unsigned long | exec_vm | 可执行内存映射中的页数 |
unsigned long | stack_vm | 用户堆栈中的页数 |
unsigned long | reserved_vm | 在保留区中的页数或在特殊线性区中的页数 |
unsigned long | def_flags | 线性区默认的访问标志 |
unsigned long | nr_ptes | 进程的页表数 |
unsigned long [] | saved_auxv | 开始执行ELF程序时使用 |
unsigned int | dumpable | 表示是否可以产生内存转储信息的标志 |
cpumask_t | cpu_vm_mask | 用于惰性TLB交换的位掩码 |
mm_context_t | context | 指向有关特定体系结构信息的表 |
unsigned long | swap_token_time | 进程在这个时间将有资格获得交换标志 |
char | recent_pagein | 最近发生了主缺页,则设置该标志 |
int | core_waiters | 正在把进程地址空间的内容转储到core文件中的轻量级进程的数目 |
struct completion * | core_startup_done | 指向创建内存转储文件的补充原语 |
struct completion | core_done | 指向创建内存转储文件的补充原语 |
rwlock_t | ioctx_list_lock | 用于保护异步I/O上下文链表的锁 |
struct kioctx * | ioctx_list | 异步I/O上下文链表 |
struct kioctx | default_kioctx | 默认的异步I/O上下文 |
unsigned long | hiwater_rss | 进程所拥有的最大页框数 |
unsigned long | hiwater_vm | 进程线性区中的最大页数 |
所有的内存描述符存放在一个双向链表中。每个内存描述符在mmlist字段存放链表相邻元素的地址。链表的第一个元素是init_mm的mmlist字段,init_mm是初始化阶段进程0使用的内存描述符。
注意理解和比较两个字段:mm_users和mm_count字段。
# mm_users字段存放共享mm_struct数据结构的轻量级进程的个数。
# mm_count字段是内存描述符的主使用计数器,在mm_users次使用计数器中的所有用户在mm_count中只作为一个单元。每当mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内存描述符,因为不再有用户使用它。
===>为什么要设置mm_count字段?见下文。
内核线程的内存描述符
内核线程就能运行在内核态,因此,它们永远不会访问低于TASK_SIZE(等于PAGE_OFFSET)的地址。与普通进程相反,内核线程不用线性区。因此描述符的很多字段对内核线程是没有意义的。
内核线程使用前一个进程的内存描述符。
在每个进程描述符中包含了两种内存描述符指针:mm和active_mm。
进程描述符中的mm字段指向进程所拥有的内存描述符,而active_mm字段指向进程运行时所使用的内存描述符。对于普通进程,这两个字段存放相同的指针。但是,对于内核线程,由于内核线程不拥有任何内存描述符,因此,它们的mm字段总是NULL。当内核线程运行时,它的active_mm字段被初始化为前一个运行进程的active_mm值。
这里,可以解释上面mm_users字段和mm_count字段的区别:
<<<====>>>
mm_users字段记录共享该内存描述符的普通进程的个数,mm_count的目的在于支持内核线程级别。
对Linux来说,用户进程和内核线程(kernel thread)都是task_struct的实例(tsk),唯一的区别是内核线程是没有进程地址空间的,内核线程也没有mm内存描述符,所以内核线程的tsk->mm域(其中,struct task_struct * tsk)是空(NULL)。当内核scheduler在执行进程上下文切换(context switching)时,会根据tsk->mm判断即将调度的进程是用户进程还是内核线程。虽然内核线程不访问用户进程地址空间,但是仍然需要通过page table来访问内核线程的内核地址空间。幸运的是,对于任何用户进程来说,它们的内核空间都是完全相同的(3G-4G),所以内核可以“借用”上一个被调用的用户进程的内存描述符mm中的页表来访问内核地址,并将此mm记录在active_mm。简而言之,对于用户进程,tsk->mm == tsk->active_mm;而对于内核线程,tsk->mm == NULL表示自己内核线程的身份,而tsk->active_mm是使用上一个用户进程的mm,通过此mm的page table来访问内核空间。
为了支持内核线程级别,mm_struct里面引入了另外一个counter,主使用计数器mm_count。前面有mm_users表示这个进程地址空间被多少普通进程共享或者引用,而mm_count则表示这个地址空间被内核线程引用的次数+1(这里的1,指的是在mm_users次使用计数器中的所有用户在mm_count中只作为一个单元)。内核不会因为mm_users == 0而销毁此mm_struct,内核只会当mm_count == 0时才会释放mm_struct,因为这个时候既没有用户进程使用这个地址空间,也没有内核线程引用这个地址空间。