在linux操作系统下,每个进程或者线程都用一个task_struct来描述,这其中有一个mm_struct结构体来管理用户态进程的内存,我们称之为内存描述符,该结构体包含了用户态进程地址空间相关的全部信息。task_struct 中还有一个很重要的元素为 active_mm (上一个被调用的用户态进程的mm指针),主要用于内核线程访问内核页全局目录,事实上,内核线程并没有相关的内存描述符,对应的task_struct
中的mm
指针为空。
include/linux/mm_types.h
struct mm_struct {
struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb; /*VMA形成的红黑树*/
u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
......
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd; /*页全局目录*/
......
#ifdef CONFIG_MMU
atomic_long_t pgtables_bytes; /* PTE page table pages */
#endif
int map_count; /* number of VMAs */
......
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 data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
unsigned long def_flags;
spinlock_t arg_lock; /* protect the below fields */
unsigned long start_code, end_code, start_data, end_data; /*text段(可执行代码)的开始和结束地址,data段(已初始化数据)的开始和结束地址*/
unsigned long start_brk, brk, start_stack; /*heap的起始地址和当前的结束地址,stack的起始地址*/
unsigned long arg_start, arg_end, env_start, env_end;/*命令行参数的开始和结束地址,环境变量的开始和结束地址,都位于stack中最高地址的地方*/
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;
struct linux_binfmt *binfmt;
/* Architecture-specific MM context */
mm_context_t context;
。。。。。。
};
我们知道,用户态进程的虚拟地址空间很大,不可能都有真实的物理内存对应,mm_struct结构体里total_vm是总共映射的页的数量。data_vm是存放数据的页的数量,exec_vm是存放可执行文件的页的数量,stack_vm是栈占用的页的数量。
start_code, end_code, start_data, end_data分别为text段的开始和结束地址,data段的开始和结束地址。start_brk, brk, start_stack, mmap_base分别为heap的起始地址和当前的结束地址,stack的起始地址,虚拟空间中用于内存映射的起始地址。除了这些代码段的位置信息,mm_struct里还有 struct vm_area_struct 的单链表mmap以及红黑树mm_rb方便快速查找一个内存区域,vm_area_struct 描述了一段连续的具有相同属性的虚拟空间,是进程虚拟地址空间管理的基本单元,最终的内存映射图形如下。
根据此图,我们再来看各个代码段的位置信息,start_code和end_code是由elf文件传入的,通过exec运行一个二进制程序的时候,除了解析ELF格式还会建立内存映射,调用顺序为 __do_execve_file->exec_binprm->search_binary_handler->load_binary load_binary是一个hook函数,最终调用load_elf_binary,这个函数完成的事情主要如下
static int load_elf_binary(struct linux_binprm *bprm)
{
....................................
setup_new_exec(bprm); //设置内存映射区mmap_base
....................................
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack); //设置栈的vm_area_struct
....................................
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size); //将ELF文件中的代码部分映射到内存中
....................................
retval = set_brk(elf_bss, elf_brk, bss_prot); //设置heap的vm_area_struct
....................................
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) { //bss段清零
retval = -EFAULT; /* Nobody gets to see this, but.. */
goto out_free_dentry;
}
....................................
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata); //将动态.so文件映射到内存映射区域
....................................
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
....................................
start_thread(regs, elf_entry, bprm->p); //设置应用程序的入口,当内核操作结束,返回用户态的时候,接下来执行的就是该程序了。
....................................
}
至此, start_stack, mmap_base, brk, start_brk, end_data, start_data, end_code, start_code 都设置好了,bss段也已经清零,最后调用start_thread设置程序返回用户态时的入口。动态库ld.so,libc.so等会映射到Memory Mapping Segment,并且设置vm_area_struct结构体变量vm_flags为VM_READ | VM_EXEC表示只读可执行。
这些映射好的段地址空间什么时候会改变呢?从图中箭头可以看出,stack区,memory mapping区,heap区会沿着箭头方向扩展,随着函数的调用,函数栈的栈顶指针会改变,当我们要进行文件映射或者匿名映射的时候memory mapping区会改变,用户态调用libc接口malloc申请一块内存空间时,如果申请的内存小于128KB会按页对齐将brk往上推,如果申请的内存较大则调用mmap。
上面说了运行elf文件产生的进程,那么用fork创建的进程是如何配置进程空间的呢?fork -> _do_fork -> copy_process -> copy_mm -> dup_mm 这个调用流程里dump_mm函数会创建一个新的内存描述符,将父进程的mm结构体整个copy过去,
mm_init初始化内存相关的模块(
复制内核空间的页表),
copy_page_range复制页表空间。