整个虚拟内存空间一分为二,一部分是用户态地址空间,一部分是内核态地址空间,这两部分的分界线由 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)

 

 

posted on 2021-06-20 22:56  jingmojing  阅读(169)  评论(0编辑  收藏  举报