整个虚拟内存空间一分为二,一部分是用户态地址空间,一部分是内核态地址空间,这两部分的分界线由 task_size 来定义。
struct task_struct => struct mm_struct *mm; => unsigned long task_size; /* size of task vm space */ => #ifdef CONFIG_X86_32 /* * User space process size: 3GB (default). */ #define TASK_SIZE PAGE_OFFSET #define TASK_SIZE_MAX TASK_SIZE /* config PAGE_OFFSET hex default 0xC0000000 depends on X86_32 */ #else /* * User space process size. 47bits minus one guard page. */ #define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE) #define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? IA32_PAGE_OFFSET : TASK_SIZE_MAX) ...... // 当执行一个新的进程的时候,会设置 current->mm->task_size = TASK_SIZE;
用户态布局
struct mm_struct
mmap_base:
malloc 申请一大块内存的时候,就是通过 mmap 在这里映射一块区域到物理内存;
加载动态链接库 so 文件,也是在这个区域里面,映射一块区域到 so 文件。
// 用户态的堆、栈、内存映射区等区域的统计信息和位置 unsigned long mmap_base; /* base of mmap area 虚拟地址空间中用于内存映射的起始地址,从高地址到低地址增长 */ 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 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; /* 参数列表, 环境变量的位置,位于栈中最高地址的地方 */ // 各区域的属性 struct vm_area_struct *mmap; /* list of VMAs 用于将各区域串起来 */ struct rb_root mm_rb; // 红黑树,快速查找、修改一个内存区域
struct vm_area_struct { /* The first cache line has the info for VMA tree walking. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; struct rb_node vm_rb; struct mm_struct *vm_mm; /* The address space we belong to. */ struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock */ struct anon_vma *anon_vma; /* Serialized by page_table_lock */ /* Function pointers to deal with this struct. */ const struct vm_operations_struct *vm_ops; struct file * vm_file; /* File we map to (can be NULL). */ void * vm_private_data; /* was vm_pte (shared mem) */ } __randomize_layout;
vm_start 和 vm_end 指定了该区域在用户空间中的起始和结束地址。
vm_next 和 vm_prev 将这个区域串在链表上。
vm_rb 将这个区域放在红黑树上。vm_ops 里面是对这个内存区域可以做的操作的定义。
虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射。
anon_vma 中,anon 即 anonymous 匿名,映射到文件就需要有 vm_file 指定被映射的文件。
那这些 vm_area_struct 是如何和上面的内存区域关联的呢?这个事情是在 load_elf_binary 里面实现的。
没错,就是它。加载内核的是它,启动第一个用户态进程 init 的是它,fork 完了以后,调用 exec 运行一个二进制程序的也是它。
当 exec 运行一个二进制程序的时候,除了解析 ELF 的格式之外,另外一个重要的事情就是建立内存映射。
static int load_elf_binary(struct linux_binprm *bprm) { ...... // 设置内存映射区 mmap_base setup_new_exec(bprm); ...... // 设置栈的 vm_area_struct, current->mm->arg_start = current->mm->start_stack指向栈底 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); ...... // 将 ELF 文件中的代码部分映射到内存中来 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); ...... // 设置堆的 vm_area_struct,current->mm->start_brk = current->mm->brk,即堆里面还是空的 retval = set_brk(elf_bss, elf_brk, bss_prot); ...... // 将依赖的 so 映射到内存中的内存映射区域。 elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); ...... 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; ...... }
映射完毕后,什么情况下会修改呢?
第一种情况是函数调用,涉及函数栈的改变,主要是改变栈顶指针。
第二种情况是通过 malloc 申请一个堆内的空间,当然底层要么执行 brk,要么执行 mmap。
brk 系统调用实现的入口是 sys_brk 函数。
SYSCALL_DEFINE1(brk, unsigned long, brk) { unsigned long retval; unsigned long newbrk, oldbrk; struct mm_struct *mm = current->mm; struct vm_area_struct *next; ...... newbrk = PAGE_ALIGN(brk); // brk新的堆顶位置 oldbrk = PAGE_ALIGN(mm->brk); // mm->brk原来堆顶的位置 if (oldbrk == newbrk) goto set_brk; /* Always allow shrinking brk. */ if (brk <= mm->brk) { if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf)) goto set_brk; goto out; } /* Check against existing mmap mappings. */ next = find_vma(mm, oldbrk); if (next && newbrk + PAGE_SIZE > vm_start_gap(next)) goto out; /* Ok, looks good - let it rip. */ if (do_brk(oldbrk, newbrk-oldbrk, &uf) < 0) goto out; set_brk: mm->brk = brk; ...... return brk; out: retval = mm->brk; return retval }
堆是从低地址向高地址增长的,首先要将原来的堆顶和现在的堆顶,都按照页对齐地址,然后比较大小。如果两者相同,说明这次增加的堆的量很小,还在一个页里面,不需要另行分配页,直接跳到 set_brk 那里,设置 mm->brk 为新的 brk 就可以了。
如果发现新旧堆顶不在一个页里面,说明要跨页。如果新堆顶小于旧堆顶,说明是释放内存,至少要释放一页,于是调用 do_munmap 将这一页的内存映射去掉。
如果堆将要扩大,就要调用 find_vma。如果打开这个函数,看到的是对红黑树的查找,找到的是原堆顶所在的 vm_area_struct 的下一个 vm_area_struct,看当前的堆顶和下一个 vm_area_struct 之间还能不能分配一个完整的页。如果不能,没办法只好直接退出返回,内存空间都被占满了。如果还有空间,就调用 do_brk 进一步分配堆空间,从旧堆顶开始,分配计算出的新旧堆顶之间的页数。
static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf) { return do_brk_flags(addr, len, 0, uf); } static int do_brk_flags(unsigned long addr, unsigned long request, unsigned long flags, struct list_head *uf) { struct mm_struct *mm = current->mm; struct vm_area_struct *vma, *prev; unsigned long len; struct rb_node **rb_link, *rb_parent; pgoff_t pgoff = addr >> PAGE_SHIFT; int error; len = PAGE_ALIGN(request); ...... find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent); ...... vma = vma_merge(mm, prev, addr, addr + len, flags, NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX); if (vma) goto out; ...... vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); INIT_LIST_HEAD(&vma->anon_vma_chain); vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_pgoff = pgoff; vma->vm_flags = flags; vma->vm_page_prot = vm_get_page_prot(flags); vma_link(mm, vma, prev, rb_link, rb_parent); out: perf_event_mmap(vma); mm->total_vm += len >> PAGE_SHIFT; mm->data_vm += len >> PAGE_SHIFT; if (flags & VM_LOCKED) mm->locked_vm += (len >> PAGE_SHIFT); vma->vm_flags |= VM_SOFTDIRTY; return 0; }
在 do_brk 中,调用 find_vma_links 找到将来的 vm_area_struct 节点在红黑树的位置,找到它的父节点、前序节点。
接下来调用 vma_merge,看这个新节点是否能够和现有树中的节点合并。
如果地址是连着的,能够合并,则不用创建新的 vm_area_struct 了,直接跳到 out,更新统计值即可。
如果不能合并,则创建新的 vm_area_struct,既加到 anon_vma_chain 链表中,也加到红黑树中。
内核态布局
在内核里面,有两个宏:
__pa(vaddr) 返回与虚拟地址 vaddr 相关的物理地址;
__va(paddr) 计算出对应于物理地址 paddr 的虚拟地址。
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) #define __pa(x) __phys_addr((unsigned long)(x)) #define __phys_addr(x) __phys_addr_nodebug(x) #define __phys_addr_nodebug(x) ((x) - PAGE_OFFSET)