在前面的系列博文中我们已经看到,内核中的函数以相当直截了当的方式获得动态内存:__get_free_pages()或alloc_pages()通过伙伴算法从分区页框分配器中获得页框,kmem_cache_alloc()或kmalloc()使用slab分配器为专用或通用对象分配内存,而vmalloc()或vmalloc_32()获得一块非连续的内存区。如果所请求的内存区得以满足,这些函数都返回一个页描述符或线性地址(即所分配动态内存区的起始地址)。
使用这些简单方法是基于以下两个原因:
(1)内核是操作系统中优先级最高的成分。如果某个内核函数请求动态内存,那么,必定有正当的理由发出那个请求,因此,没有道理试图推迟这个请求。
(2)内核信任自己。所有的内核函数都被假定是没有错误的,因此内核函数不必插入针对编程错误的任何保护措施。
但是,给用户态进程分配内存时,情况完全不同:
进程对动态内存的请求被认为是不紧迫的。例如,当进程的可执行文件被装入内存时,进程并不一定立即对所有的代码页进行访问。类似地,当进程调用malloc()以获得请求的动态内存时,也并不意味着进程很快就会访问所有所获得的内存。因此,一般来说,内核总是尽量推迟给用户态进程分配动态内存。
由于用户进程是不可信任的,因此,内核必须能随时准备捕获用户态进程引起的所有寻址错误,这里面最重要,最典型的错误就是缺页异常。
这里,我们要重点讨论,内核使用一种新的资源成功实现了对进程动态内存的推迟分配。当用户态进程请求动态内存时,并没有获得请求的页框,而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址区间就成为进程地址空间的一部分。
这一区间叫做“线性区”(memory region)(其实际含义就是通常所指的虚拟内存中的一个区间,可以称为“虚存区”(Virtual Memory Area, VMA))。
进程的地址空间(address space)由允许进程使用的全部线性地址组成。每个进程所看到的线性地址集合是不同的,一个进程所使用的地址与另外一个进程所使用的地址之间没有什么关系。后面我们会看到,内核可以通过增加或删除某些线性地址区间来动态修改进程的地址空间。
内核通过所谓线性区的资源来表示线性地址区间,线性区是由起始线性地址、长度和一些访问权限来描述的。为了效率起见,起始地址和线性区的长度都必须是4096的倍数,以便每个线性区所识别的数据完全填满分配给它的页框。
我们会在“缺页异常处理程序”博文中看到,确定一个进程当前所拥有的线性区(即进程的地址空间)是内核的基本任务,因为这可以让缺页异常处理程序有效地区分引发这个异常处理程序的两种不同类型的无效线性地址:
- 由编程错误引发的无效线性地址。
- 由缺页引发的无效线性地址;即使这个线性地址属于进程的地址空间,但是对应于这个地址的页框仍然有待分配。
从进程的观点来看,后一种地址不是无效的,内核要利用这种缺页以实现请求调页:内核通过提供页框来处理这种缺页,并让进程继续执行。
与进程地址空间有关的全部信息都包含在一个叫做内存描述符(memory descriptor)的数据结构中,这个结构的类型为mm_struct,进程描述符的mm字段就指向这个结构。内存描述符的字段如下所示(位于include/linux/Sched.h):
struct mm_struct {
struct vm_area_struct * mmap; /* 指向线性区对象VMA的链表头 */
struct rb_root mm_rb; /* 指向线性区对象的红-黑树的根 */
struct vm_area_struct * mmap_cache; /* 指向最后一个引用的线性区对象 */
unsigned long (*get_unmapped_area) (struct file *filp, /* 在进程地址空间中搜索有效线性地址区间的方法 */
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
unsigned long (*get_unmapped_exec_area) (struct file *filp, /* */
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr); /* 释放线性地址区间时调用的方法 */
unsigned long mmap_base; /* 标识第一个分配的匿名线性区或文件内存映射的线性地址 */
unsigned long task_size; /* size of task vm space */
unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */
unsigned long free_area_cache; /* 内核从这个地址开始搜索进程地址空间中线性地址的空闲区间 */
pgd_t * pgd; /* 指向页全局目录 */
atomic_t mm_users; /* How many users with user space? 次使用计数器 */
atomic_t mm_count; /* 主使用计数器(users count as 1) */
int map_count; /* 线性区的个数 */
struct rw_semaphore mmap_sem; /* 线性区的读/写信号量 */
spinlock_t page_table_lock; /* 线性区的自旋锁和页表的自旋锁 */
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
*/
/* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
mm_counter_t _file_rss;
mm_counter_t _anon_rss;
unsigned long hiwater_rss; /* High-watermark of RSS usage 进程所拥有的最大页框数 */
unsigned long hiwater_vm; /* High-water virtual memory usage 进程线性区中的最大页数 */
unsigned long total_vm, locked_vm, shared_vm, exec_vm; /* 进程地址空间的大小(页数)、
* “锁住”而不能换出的页的个数、
* 共享文件内存映射中的页数、
* 可执行内存映射中的页数 */
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes; /* 用户态堆栈中的页数、
* 在保留区中的页数或在特殊线性区中的页数、
* 线性区默认的访问标志、
* 本进程的页表数 */
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 saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv(开始执行ELF程序时使用) */
unsigned dumpable:2; /* 表示是否可以产生内存信息转储的标志 */
cpumask_t cpu_vm_mask; /* 用于懒惰TLB交换的位掩码 */
/* Architecture-specific MM context 指向有关特定体系结构信息的表(例如,在80x86平台上的LDT地址)*/
mm_context_t context;
/* Token based thrashing protection. 进程在这个时间将有资格获得交换标记*/
unsigned long swap_token_time;
char recent_pagein; /* 如果最近发生了主缺页,则设置该标志 */
#if defined(CONFIG_MMU_NOTIFIER) && !defined(__GENKSYMS__)
unsigned short mmu_notifier_idx;
#endif
/* coredumping support 正在把进程地址空间的内容卸载到转储文件中的轻量级进程的数量 */
int core_waiters;
struct completion *core_startup_done, core_done; /* 指向创建内存转储文件时的补充原语、
* 创建内存转储文件时使用的补充原语 */
/* aio bits */
rwlock_t ioctx_list_lock; /* 用于保护异步I/O上下文链表的锁 */
struct kioctx *ioctx_list; /* 异步I/O上下文链表 */
};
所有的内存描述符存放在一个双向链表中。每个描述符在mmlist段存放链表相邻元素的地址。链表的第一个元素是init_mm的mmlist字段,init_mm是初始化阶段进程0所使用的内存描述符。mmlist_lock自旋锁保护多处理器系统对链表的同时访问(同样是位于include/linux/Sched.h):
extern spinlock_t mmlist_lock;
mm_users字段存放共享mm_struct数据结构的轻进程的个数。mm_count字段是内存描述符的主是使用计数器,在mm_users次使用计数器中的所有用户在mm_count中只作为一个单位。每当mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内存描述符,因为不再有用户使用它。
我们用一个例子来解释mm_users和mm_count之间的不同。如果一个内存描述符由两个轻量级进程共享。它的mm_users字段通常存放的值为2,而mm_count字段存放的值为1(两个所有者进程算作一个)。
如果把内存描述符暂时借给一个内核线程,那么,内核就增加mm_count。这样,即使两个轻量级进程都死亡,且mm_users字段变为0,这个内存描述符也不被释放,直到内核线程使用完为止,因为mm_count字段仍然大于0。
但是,如果内核想确保内存描述符在一个长操作的中间不被释放,那么,就应该增加mm_users字段而不是mm_coont字段的值。最终的结果是相同的,因为mm_users的增加确保了mm_count不变为0,即使拥有这个内存描述符的所有轻进程全部死亡。
mm_alloc()函数用来获得一个新的内存描述符。由于这些描述符被保存在slab分配器高速缓存中,因此,mm_alloc()调用kmem_cache_alloc()来初始化新的内存描述符,并把mm_count和mm_users字段都置为1(位于kernel/Fork.c):
struct mm_struct * mm_alloc(void)
{
struct mm_struct * mm;
mm = allocate_mm();
if (mm) {
memset(mm, 0, sizeof(*mm));
mm = mm_init(mm);
}
return mm;
}
#define allocate_mm() (kmem_cache_alloc(mm_cachep, SLAB_KERNEL))
static struct mm_struct * mm_init(struct mm_struct * mm)
{
unsigned long mm_flags;
atomic_set(&mm->mm_users, 1);
atomic_set(&mm->mm_count, 1);
init_rwsem(&mm->mmap_sem);
INIT_LIST_HEAD(&mm->mmlist);
mm->core_waiters = 0;
mm->nr_ptes = 0;
set_mm_counter(mm, file_rss, 0);
set_mm_counter(mm, anon_rss, 0);
spin_lock_init(&mm->page_table_lock);
rwlock_init(&mm->ioctx_list_lock);
mm->ioctx_list = NULL;
mm->free_area_cache = TASK_UNMAPPED_BASE;
mm->cached_hole_size = ~0UL;
mm_flags = get_mm_flags(current->mm);
if (mm_flags != MMF_DUMP_FILTER_DEFAULT) {
if (unlikely(set_mm_flags(mm, mm_flags, 0) < 0))
goto fail_nomem;
}
if (likely(!mm_alloc_pgd(mm))) {
mm->def_flags = 0;
mmu_notifier_mm_init(mm);
return mm;
}
if (mm_flags != MMF_DUMP_FILTER_DEFAULT)
free_mm_flags(mm);
fail_nomem:
free_mm(mm);
return NULL;
}
相反,mmput()函数递减内存描述符的mm_users字段。如果该字段变为0,这个函数就释放线性区描述符(exit_mmap(mm))、执行内存描述符链表脱链(list_del(&mm->mmlist)),并调用mmdrop()。后一个函数把mm_count字段减1,如果该字段变为0,就释放内存描述符所引用的页表,最后释放mm_struct数据结构(同样是位于kernel/Fork.c):
void mmput(struct mm_struct *mm)
{
might_sleep();
if (atomic_dec_and_test(&mm->mm_users)) {
exit_aio(mm);
exit_mmap(mm);
if (!list_empty(&mm->mmlist)) {
spin_lock(&mmlist_lock);
list_del(&mm->mmlist);
spin_unlock(&mmlist_lock);
}
put_swap_token(mm);
mmdrop(mm);
}
}
static inline void mmdrop(struct mm_struct * mm)
{
if (atomic_dec_and_test(&mm->mm_count))
__mmdrop(mm);
}
void fastcall __mmdrop(struct mm_struct *mm)
{
BUG_ON(mm == &init_mm);
free_mm_flags(mm);
mm_free_pgd(mm);
destroy_context(mm);
mmu_notifier_mm_destroy(mm);
free_mm(mm);
}
#define free_mm(mm) (kmem_cache_free(mm_cachep, (mm)))
内核线程仅运行在内核态,因此,它们永远不会访问低于TASK_SIZE(等于PAGE_OFFSET,通常为0xc0000000,即768MB)的地址。与普通进程相反,内核线程不用线性区(vm_area_struct),因此,内存描述符的很多字段对内核线程是没有意义的。
因为大于TASK_SIZE线性地址的相应页表项都应该总是相同的,因此,一个内核线程到底使用什么样的页表集根本就没有什么关系。为了避免无用的TLB和高速缓存刷新,内核线程使用一组最近运行的普通进程的页表。所以,我们在每个进程描述符中包含了两种内存描述符的指针:mm和active_mm。
进程描述符中的mm字段指向进程所拥有的内存描述符,而active_mm字段指向进程运行时所使用的内存描述符。对于普通进程而言,这两个字段存放相同的指针。但是,内核线程不拥有任何内存描述符,因此,它们的mm字段总是为NULL。当内核线程得以运行时,他的active_mm字段被初始化为前一个运行进程的active_mm值(“schedule()函数”博文中有提到)。
然而,事情有点复杂。只要处于内核态的一个进程为“高端”线性地址(高于TASK_SIZE)修改了页表项,那么,它就也应当更新系统中所有进程页表集合中的相应表项。事实上,一旦内核态的一个进程进行了设置,那么,映射应该对内核态的其他所有进程都有效。触及所有进程的页表集合是相当费时的操作,因此,Linux采用一种延迟方式。
我们在“非连续内存区管理”一博已经提到这种延迟方式:每当一个高端地址必须被重新映射时(一般是通过vmalloc()或vfree()),内核就更新根目录在swapper_pg_dir主内核页全局目录中的常规页表集合。这个页全局目录由主内存描述符(master memory descriptor)的pgd字段所指向,而主内存描述符存放于init_mm变量。我们在前面的博文中提到中提到,swapper内核线程在初始化阶段使用init_mm。但是,一旦初始化完成,swapper再不使用这个内存描述符了。
struct mm_struct init_mm = INIT_MM(init_mm);
#define INIT_MM(name) /
{ /
.mm_rb = RB_ROOT, /
.pgd = swapper_pg_dir, /
.mm_users = ATOMIC_INIT(2), /
.mm_count = ATOMIC_INIT(1), /
.mmap_sem = __RWSEM_INITIALIZER(name.mmap_sem), /
.page_table_lock = __SPIN_LOCK_UNLOCKED(name.page_table_lock), /
.mmlist = LIST_HEAD_INIT(name.mmlist), /
.cpu_vm_mask = CPU_MASK_ALL, /
}