目录

虚拟地址空间划分(用户空间)

  • 32位系统虚拟地址空间分配
  • 64位系统虚拟地址空间分配

内存管理

内核布局虚拟地址空间

  • 虚拟内存区域在内核中组织
  • 内存访问权限
  • 调用malloc 申请内存,

虚拟地址空间布局(内核)

  • 直接映射区:范围地址3G-3G+896M
  • 高端内存896M以上,ZONE_HIGHMEM,
  • 虚拟内存 vmalloc 动态映射区
  • 虚拟内存 永久映射区
  • 虚拟内存 固定映射区
  • 临时映射区:

虚拟地址空间划分(用户空间)

1:代码段
存放二进制文件机器码的虚拟内存

2:数据段
在代码中被我们制定了初始值的全局变量和静态变量在虚拟内存中的存储区
没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域叫做BSS段;这些未初始化的全局变量被加载进内存知乎会被初始化为0.

上面介绍的全局变量和静态变量都是在编译期间确定的;

3:堆
程序运行时申请的内存,所以在虚拟内存中需要一块区域存放动态申请的内存,叫做堆。

4:文件映射与匿名映射区
动态链接库.so glibc 这些动态链接库也有自己对应的代码段,数据段BSS段 也需要被加载内存
文件映射mmap 映射的内存
5、栈
调用函数以及过程中的局部变量和函数参数也需要一块区域保存。

32位系统虚拟地址空间分配

寻址范围2^32 ,虚拟内存空间4GB 0x0000 0000 - 0xFFFF FFFF

其中用户态虚拟地址空间为3GB,虚拟内存范围0x0000 0000 - 0xC000 000
内核态地址空间1GB ,虚拟内存范围0XC000 000 -0xFFFF FFFF

用户态地址不是从0x0000 0000 开始 而是从0x08048000地址开始,因为大多数操作系统中数值比较小被认为是一个不合法的地址,不允许访问小地址。

内核使用start_brk标识堆的起始地址,brk标识当前堆的结束位置。当堆申请新的内存空间时,只需要将brk指针增加对应的大小,回收地址时减少对应的大小即可。如malloc申请内存就是通过改变brk位置实现。

文件映射区域的地址增长方向 从高地址向低地址增长;

栈空间的地址增长方向从高地址向低地址增长,每次进行申请新的栈地址时,其地址在减少。Start_stack标识起始地址,RSP 寄存器保存栈顶指针stack pointer,RBP寄存器中保存栈基地址。

64位系统虚拟地址空间分配

2^64 虚拟内存空间为16EB
内核空间与用户空间有空洞 叫做 canonical address空洞

内存管理

内核的结构体 task_struct -> mm_struct

在do_fork中 copy_mm 完成子进程虚拟内存空间mm_struct结构的创建和初始化
dup_mm函数将父进程的虚拟内存空间以及相关页表拷贝到子进程的mm_struct中,再赋值给task_struct

如果设置CLONE_VM,将父进程的虚拟内存空间以及相关页表直接赋值给子进程,父子进程共享

内核线程和用户线程区别,内核线程没有mm_struct,内核线程之间调度不涉及地址空间切换。

内核和用户地址空间划分 分界线

TASK_SIZE 大小计算 :task_size_max 1 左移47位 减 PAGE_SIZE (默认为4k) 就是
0x00007FFFFFFFF000,共 128T

内核空间的起始地址0xFFFF 8000 0000 0000

struct mm_struct {
    unsigned long task_size; /* size of task vm space */
...
}

task_size 定义了用户态地址空间与内核态地址空间之间的分界线

  • 32 位系统中用户地址空间和内核地址空间的分界线在 0xC000 000 地址处,task_size 为 0xC000 000。
#define TASK_SIZE  __PAGE_OFFSET
  • 64 位系统中用户地址空间和内核地址空间的分界线在 0x0000 7FFF FFFF F000 地址处,task_size 为0x0000 7FFF FFFF F000

/arch/x86/include/asm/page_64_types.h

#define TASK_SIZE  (test_thread_flag(TIF_ADDR32) ? \
     IA32_PAGE_OFFSET : TASK_SIZE_MAX)

#define TASK_SIZE_MAX  task_size_max()

#define task_size_max()  ((_AC(1,UL) << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)

#define __VIRTUAL_MASK_SHIFT 47

计算 TASK_SIZ与页大小有关,在 task_size_max() 的计算逻辑中 1 左移 47 位得到的地址是 0x0000800000000000,然后减去一个 PAGE_SIZE (默认为 4K),就是 0x00007FFFFFFFF000,共 128T。
所以在 64 位系统中的 TASK_SIZE 为 0x00007FFFFFFFF000

PAGE_SIZE 定义在 /arch/x86/include/asm/page_types.h文件中

/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT  12
#define PAGE_SIZE  (_AC(1,UL) << PAGE_SHIFT)

内核布局虚拟地址空间

task_struct->mm_struct

struct mm_struct {
    unsigned long task_size;    /* size of task vm space */
    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;
    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 */

       ..............
}
  • task_size 来划分用户态虚拟内存和内核态虚拟内存空间。
  • total_vm 表示进程虚拟内存空间中与物理内存映射的页的总数,只是将虚拟内存与物理内存建立管理关系,并不代表真正的分配物理内存。当内存吃紧的时候,有些页可以换出到硬盘上,有些不能换出。
  • Locked_vm 就是被锁定不能换出的内存页总数;
  • pinned_vm表示既不能换出,也不能移动的内存页总数
  • data_vm 表示数据段中映射的内存页数目,
  • exec_vm是代码段中存放可执行文件内存的页数目。
  • stack_vm是栈中映射内存数目

虚拟内存区域在内核中组织

struct vm_area_struct, 每个结构体对应于虚拟内存空间中的唯一虚拟内存区域VMA [vm_start,vm_end)

内存访问权限

vm_flags 访问权限
VM_READ 可读
VM_WRITE 可写
VM_EXEC 可执行
VM_SHARD 可多进程之间共享
VM_IO 可映射至设备 IO 空间
VM_RESERVED 内存区域不可被换出
VM_SEQ_READ 内存区域可能被顺序访问
VM_RAND_READ 内存区域可能被随机访问
  • VM_SHARD 用于指定这块虚拟内存区域映射的物理内存是否可以在多进程之间共享,以便完成进程间通讯。设置这个值即为 mmap 的共享映射,不设置的话则为私有映射。这个等后面我们讲到 mmap 的相关实现时还会再次提起。
  • VM_IO 的设置表示这块虚拟内存区域可以映射至设备 IO 空间中。通常在设备驱动程序执行 mmap 进行 IO 空间映射时才会被设置。
  • VM_RESERVED 的设置表示在内存紧张的时候,这块虚拟内存区域非常重要,不能被换出到磁盘中。
  • VM_SEQ_READ 的设置用来暗示内核,应用程序对这块虚拟内存区域的读取是会采用顺序读的方式进行,内核会根据实际情况决定预读后续的内存页数,以便加快下次顺序访问速度。
  • VM_RAND_READ 的设置会暗示内核,应用程序会对这块虚拟内存区域进行随机读取,内核则会根据实际情况减少预读的内存页数甚至停止预读。
    我们可以通过 posix_fadvise,madvise 系统调用来暗示内核是否对相关内存区域进行顺序读取或者随机读取。

虚拟内存映射可以映射到文件上,也可以映射到物理内存上。

映射到物理内存上称之为 匿名映射,映射到文件中 称之为 文件映射。

调用malloc 申请内存,

如果申请的是小块内存(低于128k),则会调用do_brk(),通过调整堆中的brk指针大小来增加或者回收堆内存;

如果申请的是大块内存(大于128k),调用mmap,在文件映射与匿名映射区域中创建一块VMA内存区域。这块映射区域用 struct anon_vma 表示。

当调用 mmap 进行文件映射时,vm_file 属性就用来关联被映射的文件。这样一来虚拟内存区域就与映射文件关联了起来。vm_pgoff 则表示映射进虚拟内存中的文件内容,在文件中的偏移。

  • 对虚拟内存区域操作 vm_ops
  • 虚拟内存在内核中的组织方式:双向链表与红黑树
  • 红黑树用于查找特定内存区域
  • 载入elf文件,在内核中完成这个映射过程的函数是 load_elf_binary

    虚拟地址空间布局(内核空间)

    32位体系结构,1G虚拟内存空间

直接映射区:范围地址3G-3G+896M

这块连续的虚拟内存地址会映射到物理地址0~896M这块连续的物理内存上。

  • 前1M 已经在系统启动的时候被系统占用;
  • 1M之后的物理内存存放的是内核代码段,数据段,BSS段。(ELF文件)可以通过cat /proc/iomem命令查看具体物理内存布局情况。
  • 在使用fork创建进程时,会创建task_struct, mm_struct, vm_area_struct 这些数据结构会放在896M内存中,会被映射到内核虚拟地址,3G到3G+896M;
  • 进程创建好,在内核运行过程,涉及内核栈的分配,内核为每个进程分配一个固定大小的内核栈,也是在直接映射区;

物理内存直接映射区的前16M,ZONE_DMA:用于为DMA分配内存

16M-896M,ZONE_NORMAL

高端内存896M以上,ZONE_HIGHMEM,

32位4G 高端内存区域4G-896M = 3200M,这块区域如何映射到内核虚拟内存中?

内核虚拟内存空间 1G - 896M = -128M,两者大小不一致不能通过直接映射

需要采用动态映射

虚拟内存 vmalloc 动态映射区

VMALLOC_START到VMALLOC_END之间的这块区域成为动态映射区,动态方式映射到物理内存中的高端内存。

用户使用malloc 申请内存,在内核中使用vmalloc 进行内存分配。vmalloc分配的内存在虚拟地址上是连续的,但是在物理内存上是不连续的。

虚拟内存 永久映射区

PKMAP_BASE到PKMAP_START之间的这段空间成为永久映射区。允许这段虚拟内存映射到物理高端内存长期映射关系。 如通过alloc_pages函数在物理内存高端区域申请到物理页,这些物理内存通过调用kmap映射到永久映射区中。

页数限制 PKMAP_BASE

虚拟内存 固定映射区

范围:FIXADDR_START 到 FIXMAP_TOP

此处虚拟地址可以自由映射到物理内存的高端地址上,虚拟地址是固定的,而被映射的物理地址是可以改变的。

作用:在内核启动过程中,有些模块需要使用虚拟内存并映射到指定的物理地址上,而且这些模块也没有办法等待完整的内存管理模块初始化后再进行地址映射。因此,内核固定分配了一些虚拟地址,这些地址有固定用途,使用该地址的模块在初始化的时候,将这些固定分配的虚拟地址映射到指定的物理地址上去。

临时映射区:

数据拷贝过程,kmap_atomic 创建映射;kunmap_atomic 解除映射

学习链接:
kenel学习链接

参考

https://course.0voice.com/v1/course/intro?courseId=2&agentId=0