linux用户态进程地址空间管理

    在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 描述了一段连续的具有相同属性的虚拟空间,是进程虚拟地址空间管理的基本单元,最终的内存映射图形如下。

linux用户态进程地址空间管理_第1张图片

    根据此图,我们再来看各个代码段的位置信息,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复制页表空间。

 

 

你可能感兴趣的:(linux用户态进程地址空间管理)