• 进程空间管理


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

     

  • 相关阅读:
    如何把this指针转换成boost的shared_ptr
    字符指针数组 和 字符指针的指针 即 char ** arr VS char * arr[]
    latex图片自动浮动到最后一页单独占用一页
    Android首次开发的经历
    jvm内存问题诊断1
    性能优化从删除子查询做起
    资治通鉴故事止谤莫如自修
    Forward框架的逆袭:解析Forward+渲染
    FFT镜头效果解析
    最先进的开源游戏引擎KlayGE 4.1发布
  • 原文地址:https://www.cnblogs.com/sunnycindy/p/14908170.html
Copyright © 2020-2023  润新知