1、地址空间
进程的地址空间由进程可寻址的虚拟内存组成。每个进程都有自己独立的地址空间,两个进程之间没有关系,互不影响。这些虚拟内存区域包含各种对象,常见的有如下:
代码段:可执行文件代码的内存映射
数据段:可执行文件的已初始化全局变量的内存映射
Bss段:包含未初始化的全局变量,也称bss段零页的内存映射
用户进程用户空间栈的零页内存映射,栈大小编译时指定
每个共享库的代码段、数据段和bss段也会被载入进程的地址空间
任何内存映射文件
任何共享内存段
任何匿名的内存映射,比如malloc
进程地址空间中任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。
2、内存描述符
(1)数据结构
内核使用内存描述符结构体标识进程的地址空间,该数据结构包含了和进行地址空间相关的全部信息。
struct task_struct {
…
struct mm_struct *mm,*active_mm; //进程任务描述符指向内存描述符,mm为进程地址空间所有信息,active_mm则为当前执行的地址空间信息
…
}
struct mm_struct {
struct vm_area_struct * mmap; //内存区域链表
struct rb_root mm_rb; //内存区域形成的红黑树
struct vm_area_struct * mmap_cache; //最近使用的内存区域
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr,unsigned long len,
unsigned long pgoff, unsignedlong flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
unsigned long mmap_base; /*base of mmap area */
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; /* first hole of size cached_hole_size or larger */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
atomic_t mm_users; /* Howmany users with user space? */
atomic_t mm_count; /* Howmany references to "struct mm_struct" (users count as 1) */
int map_count; /*number of VMAs */
spinlock_t page_table_lock; /*Protects page tables and some counters */
struct rw_semaphore mmap_sem;
struct list_headmmlist; /* List of maybe swappedmm's. These are globally strung
* together off init_mm.mmlist, andare protected
* by mmlist_lock
*/
unsigned long hiwater_rss; /*High-watermark of RSS usage */
unsigned long hiwater_vm; /*High-water virtual memory usage */
unsigned long total_vm; /*Total pages mapped */
unsigned long locked_vm; /*Pages that have PG_mlocked set */
unsigned long pinned_vm; /*Refcount permanently increased */
unsigned long shared_vm; /*Shared pages (files) */
unsigned long exec_vm; /*VM_EXEC & ~VM_WRITE */
unsigned long stack_vm; /*VM_GROWSUP/DOWN */
unsigned long def_flags;
unsigned long nr_ptes; /*Page table pages */
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 */
/*
* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
struct mm_rss_stat rss_stat;
structlinux_binfmt *binfmt;
cpumask_var_t cpu_vm_mask_var;
/* Architecture-specific MM context */
mm_context_t context;
unsigned long flags; /* Must use atomic bitops to access the bits */
structcore_state *core_state; /* coredumping support */
…
}
mmap和mm_rb指向的是相同数据对象(vm_area_stuct虚拟内存区域),只是组织方式不一样。mmap以链表形式存放,利于简单、高效的遍历所有元素;mm_rb以红黑树的形式存放,更适合搜索指定元素。
pgd为页全局目录,实现虚拟地址和物理内存的映射,在switch_mm时将pgd首地址放入CPU寄存器供mmu(硬件实现)使用。TLB则是pgd的一个子集,存放已经使用过的映射,加速地址映射查找。
mm_users和mm_count:mm_users记录正在使用该内存的进程数目;mm_count域是mm_struct结构体的主引用计数。区别:mm_users一个进程创建多个线程时会增加,mm_count当内核线程共享进程地址空间时增加,初始化是1。
所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表进程0的地址空间。
见后面关联图说明。
(2)分配与撤销
A.分配
在进程创建一节提到过,do_fork的copy_process()完成大部分工作。copy_process()中的copy_mm()实现复制父进程的内存描述符,源码解析如下:
static intcopy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
/* 把子进程初始化成NULL */
tsk->mm = NULL;
tsk->active_mm = NULL;
/* 由于内核线程没有独立的地址空间mm=NULL,所以当前current(即父进程)的mm为NULL,则表示父进程是内核线程,子进程上面已经初始化为NULL,这里直接返回 */
oldmm = current->mm;
if (!oldmm)
return 0;
/* 如果设置了CLONE_VM共享地址空间标识,则子进程直接指向父进程的内存描述符,使用计数+1,一般用户创建线程使用 */
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
/* 其他情况则是通过allocate_mm()申请自己的mm_struct,拷贝父进程的内容,再进行自己独立的初始化(包括pgd页表申请等)
allocate_mm()是kmem_cache_alloc的宏,从mm_cashep slab缓存中分配得到 */
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
B.撤销
当进程退出时,内核会调用exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量。其中,该函数会调用mmput减少mm_users计数,如果到0,调用mmdrop函数减少mm_count计数,如果到0,那么调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中。
(3)内核线程mm_struct
上面分配一节已经看到,内核线程对应的进程描述符中mm域为空,这是因为内核线程在用户空间中没有任何页,所以不需要有自己的内存描述符和页表,所有内核线程共享内核地址空间。
尽管mm域为空,即使访问内核内存,内核线程也还是需要使用一些数据的。为了避免内核线程为内存描述符和页表浪费内存,避免向新地址空间进程切换浪费处理器周期,内核线程将直接使用前一个进程的内存描述符,进程切换源码如下:
static inline void context_switch(struct rq*rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
…
mm = next->mm; //下一个将执行进程的mm
oldmm = prev->active_mm; //当前执行进程的active_mm
/*mm=NULL则为内核线程,内核线程的active_mm指向当前执行进程的active_mm,同时mm_count+1
mm != NULL则为用户进程,通过switch_mm实现内存描述符的切换,主要完成将pgd首地址放入CPU寄存器供mmu(硬件实现)使用
*/
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
}else
switch_mm(oldmm, mm, next);
/*如果当前的进程为内核线程,则将active_mm=NULL,下次运行再重新赋值*/
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
…
}
3、虚拟内存区域
(1)数据结构
虚拟内存区域(VMA)由vm_area_struct结构体描述,指定地址空间内连续区间上的一个独立内存范围。每一个VMA作为一个单独的内存对象管理,具有一致的属性(比如访问权限等),因此,一个VMA就代表了一种类型的内存区域(如内存映射文件、进程用户空间栈等)。结构体如下:
struct vm_area_struct {
unsignedlong vm_start; //内存区间的收地址
unsigned longvm_end; //内存区间的尾地址
//VMA双向链表,按照地址顺序排序
struct vm_area_struct *vm_next, *vm_prev;
struct rb_nodevm_rb; //放在红黑树上的节点
unsigned longrb_subtree_gap;
struct mm_struct *vm_mm; //指向内存描述符
pgprot_t vm_page_prot; //访问权限
unsigned long vm_flags; //标志
union {
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear;
const char __user *anon_name;
} shared;
struct list_head anon_vma_chain; //匿名VMA对象链表
struct anon_vma *anon_vma; //匿名VMA对象
const structvm_operations_struct *vm_ops; //相关的操作表
unsigned long vm_pgoff; //文件中的偏移量
struct file * vm_file; //被映射的文件
void * vm_private_data; //私有数据
#ifndefCONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
};
在同一个地址空间内的不同内存区间不能重叠。
struct vm_operations_struct {
void(*open)(struct vm_area_struct * area);
当指定的内存区域被加入到一个地址空间时,该函数被调用
void(*close)(struct vm_area_struct * area);
当指定的内存区域从地址空间删除时,该函数被调用
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用
int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
当某个页面为只读页面时,该函数被页面故障处理调用
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
当get_user_pages()函数调用失败时,该函数被access_process_vm()函数调用
int(*remap_pages)(struct vm_area_struct *vma, unsigned long addr,
unsigned long size, pgoff_tpgoff);
};
(2)相关操作
内核时常需要在某个内存区域上执行一些操作,比如某个指定地址是否包含在某个内存区域中。这类操作非常频繁,也是mmap例程的基础。为了方便执行这类对内存区域的操作,内核定义了许多的辅助函数。
A. find_vma
structvm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
为了找到一个给定的内存地址属于哪一个内存区域。该函数在指定的地址空间搜索第一个vm_end大于addr的内存区域。如果没有发现这样的区域,返回NULL,否则指向匹配的内存区域vm_area_struct结构体指针。返回的结果被缓存在内存描述符mmap_cache域中,有相当好的命中率,提高速度。该函数通过红黑树查找。
B.find_vma_prev
和find_vma类似,但是它返回第一个小于addr的VMA。
C.find_vma_intersection
返回第一个和指定地址区间相交的VMA,也是调用find_vma
D.mmap
创建一个新的线性地址区间。如果新创建的VMA和已经存在的地址区间相邻,并且相同的访问权限,两个区域将合并为一个;如果不能合并就算是一个新的VMA。创建之后,会将该地址空间加入到进程的地址空间中。
Mmap系统调用对应的内核函数do_mmap_pgoff:
unsigned long do_mmap_pgoff(struct file*file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff,
unsigned long *populate)
file为NULL且pgoff为0,代表没有和文件相关,称为匿名映射
如果指定了文件名和偏移量,称为文件映射
E.munmap
从特定的进程地址空间中删除指定地址空间,munmap系统调用对应的内核函数do_munmap:
int do_munmap(struct mm_struct *mm,unsigned long start, size_t len)
4、缺页异常处理
Linux缺页异常处理程序必须区分两种情况:
由编程错误引起的异常
由引用属于进程地址空间但还尚未分配物理页框的页所引起的异常。
内核由static int __kprobes do_page_fault(unsigned long addr, unsigned intfsr, struct pt_regs *regs)实现。
总体方案如下图:
5、关联图
进程、线程之间内存描述符关系
进程A和B是独立进程,进程B由A创建,T1是进程A的一个线程。上图展示说明如下:
A.进程A fork进程B,进程B会复制父进程A的task_struct和mm_struct,进行相应的初始化,有各自的页表目录。
B.进程A pthread_create 线程T1,复制父进程A的task_struct,但是共享mm_struct,使用同一个页表目录
C.进程A和进程B的虚拟地址通过mm_struct的pgd页表映射到实际物理内存,对于可读页(如C库等),两个进程是共享实际内存,对于可写页(如堆栈、变量等)则有各自的物理内存
D.进程A和线程T1共享mm_struct,因此也共享pgd页表,当线程需要自己的可写页时,就会加入到进程A的共享页表里。
进程的虚拟内存管理
VMA:虚拟内存区域
mm_rb:为红黑树,图中用链表示意,表示mmap和mm_rb都指向虚拟内存区域
每个进程都有一个task_struct,所有进程通过链表连接起来,链表头为init_task。每个task_struct又都有一个mm_struct结构,所有的mm_struct通过mmlist连接在一个双向链表中,该链表的首元素是init_mm。每个进程的各个虚拟内存区域通过mmap和mm_rb组织起来,然后通过pgd页表,映射到实际的物理内存。