一步一步学linux操作系统: 19 内存管理_进程的虚拟内存空间的管理

用户态和内核态的划分

struct mm_struct 结构来管理内存,是struct task_struct中的一个成员变量
\include\linux\sched.h

struct mm_struct    *mm;

在这里插入图片描述
task_sizestruct mm_struct 里面的一个成员变量

\include\linux\mm_types.h

unsigned long task_size;    /* size of task vm space */

在这里插入图片描述

整个虚拟内存空间要一分为二,一部分是用户态地址空间,一部分是内核态地址空间,那这两部分的分界线就要是用 task_size 来定义

TASK_SIZE 定义

\arch\x86\include\asm\processor.h


#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;

32 位的系统

  • 最大能够寻址 2^32=4G,其中用户态虚拟地址空间是 3G,内核态是 1G。

64 位系统
虚拟地址只使用了 48 位

  • 1 左移了 47 位,就相当于 48 位地址空间一半的位置,0x0000800000000000
#define TASK_SIZE_MAX  ((1UL << 47) - PAGE_SIZE)
  • 然后减去一个页,就是 0x00007FFFFFFFF000,共 128T。
  • 内核空间也是 128T。内核空间和用户空间之间隔着很大的空隙,以此来进行隔离。

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

用户态布局

用户态虚拟空间里面的几类数据

struct mm_struct 里面
\include\linux\mm_types.h

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;

在这里插入图片描述

  • total_vm
    总共映射的页的数目
  • locked_vm
    就是被锁定不能换出
  • pinned_vm
    不能换出,也不能移动
  • data_vm
    存放数据的页的数目
  • exec_vm
    存放可执行文件的页的数目
  • stack_vm
    栈所占的页的数目
  • start_code
    可执行代码的开始位置
  • end_code
    可执行代码的结束位置
  • start_data
    已初始化数据的开始位置
  • end_data
    已初始化数据的结束位置
  • start_brk
    堆的起始位置
  • brk
    堆当前的结束位置,malloc 申请一小块内存的话,就是通过改变 brk 位置实现的
  • start_stack
    栈的起始位置,栈的结束位置在寄存器的栈顶指针中
  • arg_start 和 arg_end
    参数列表的位置
  • env_start 和 env_end
    环境变量的位置
  • mmap_base
    虚拟地址空间中用于内存映射的起始地址

布局图
图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

虽然 32 位和 64 位的空间相差很大,但是区域的类别和布局是相似的

区域的属性描述

使用 vm_area_struct 结构
\include\linux\mm_types.h

struct vm_area_struct *mmap;    /* list of VMAs */
struct rb_root mm_rb;

在这里插入图片描述
vm_area_struct 定义

\include\linux\mm_types.h

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 中anoy 是 anonymous,匿名的意思,匿名映射
  • vm_file 指定被映射的文件

vm_area_struct 是如何和内存区域几类数据关联

虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射,anon_vma 中,anoy 就是 anonymous,匿名的意思,映射到文件就需要有 vm_file 指定被映射的文件。

映射是在load_elf_binary里面实现的,加载内核的是它,启动第一个用户态进程 init 的是它,fork 完了以后,调用 exec 运行一个二进制程序的也是它,exec 运行一个二进制程序的时候,除了解析 ELF 的格式之外,另外一个重要的事情就是建立内存映射。

load_elf_binary 函数

\fs\binfmt_elf.c


static int load_elf_binary(struct linux_binprm *bprm)
{
......
  setup_new_exec(bprm);
......
  retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
         executable_stack);
......
  error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
        elf_prot, elf_flags, total_size);
......
  retval = set_brk(elf_bss, elf_brk, bss_prot);
......
  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;
......
}
	

在这里插入图片描述
load_elf_binary 做了以下的事情

  • 调用 setup_new_exec,设置内存映射区 mmap_base;
  • 调用 setup_arg_pages,设置栈的 vm_area_struct,这里面设置了 mm->arg_start 是指向栈底的,current->mm->start_stack 就是栈底;
  • elf_map 会将 ELF 文件中的代码部分映射到内存中来;
  • set_brk 设置了堆的 vm_area_struct,这里面设置了 current->mm->start_brk = current->mm->brk,也即堆里面还是空的;
  • load_elf_interp 将依赖的 so 映射到内存中的内存映射区域。

形成下面这个内存映射图

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

映射完毕后,什么情况下会修改呢?

  • 第一种情况是函数的调用,涉及函数栈的改变,主要是改变栈顶指针。
  • 第二种情况是通过 malloc 申请一个堆内的空间,当然底层要么执行 brk,要么执行 mmap。

内核态的布局

内核态的虚拟空间和某一个进程没有关系,所有进程通过系统调用进入到内核之后,看到的虚拟地址空间都是一样的。

在内核态,由于 32 位内核态空间太小了,32 位和 64 位的布局差别比较大

32 位的内核态的布局

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

32 位的内核态虚拟地址空间一共就 1G,占绝大部分的前 896M,称为直接映射区

直接映射区

所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去 3G,就得到物理内存的位置。(虚拟地址 - 3G = 物理地址)

直接映射区也需要建立页表, 通过虚拟地址访问(除了内存管理模块)

直接映射区组成: 1MB 启动时占用; 然后是内核代码/全局变量/BSS等,即 内核 ELF文件内容; 进程task_struct 即内核栈也在其中

896M 这个值在内核中被定义为 high_memory,在此之上常称为“高端内存”

两个宏

__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)

\arch\x86\include\asm\page.h
在这里插入图片描述
\arch\x86\include\asm\page_32.h

在这里插入图片描述

剩余虚拟空间组成
  • 8MB 空余
    在 896M 到 VMALLOC_START 之间有 8M 的空间
  • 内核动态映射空间
    VMALLOC_START 到 VMALLOC_END 之间称为内核动态映射空间,动态分配内存, 映射放在内核页表中
  • 持久内存映射
    PKMAP_BASE 到 FIXADDR_START 的空间称为持久内核映射。储存物理页信息
  • 固定内存映射
    FIXADDR_START 到 FIXADDR_TOP(0xFFFF F000) 的空间,称为固定映射区域,主要用于满足特殊需求。
  • 临时内存映射
    在最后一个区域可以通过 kmap_atomic 实现临时内核映射,例如为进程映射文件时使用

64 位的内核布局

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

  • 8T 空余
    从 0xffff800000000000 开始就是内核的部分,不过一开始有 8T 的空档区域
  • 64T直接映射区域
    从__PAGE_OFFSET_BASE(0xffff880000000000) 开始的 64T 的虚拟地址空间是直接映射区域,减去 PAGE_OFFSET 就是物理地址。大部分情况下还是会通过建立页表的方式进行映射
  • 32T动态映射
    从 VMALLOC_START(0xffffc90000000000)开始到 VMALLOC_END(0xffffe90000000000)的 - 32T 的空间
  • 1T物理页描述结构 struct page
    从 VMEMMAP_START(0xffffea0000000000)开始的 1T 空间用于存放物理页面的描述结构 struct page 的
  • 512MB存放内核代码段、全局变量、BSS 等
    从 __START_KERNEL_map(0xffffffff80000000)开始的 512M。减去 __START_KERNEL_map 就能得到物理内存的地址。

总结

内存结构

  • 用户态:
    • 代码段、全局变量、BSS
    • 函数栈
    • 内存映射区
  • 内核态:
    • 内核的代码、全局变量、BSS
    • 内核数据结构例如 task_struct
    • 内核栈
    • 内核中动态分配的内存

32 位的对应关系
图片来自极客时间趣谈linux操作系统

图片来自极客时间趣谈linux操作系统

64 位的对应关系
图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

参考资料:

趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值