一. 前言
从本文开始,我们进入内存部分的学习。首先会接着前面的任务task_struct
讲解任务空间管理结构体mm_struct
,并简单介绍物理内存和虚拟内存的相关知识,关于详细的基础知识和概念可以参照CSAPP一书,这里不会做过多的赘述,而是默认在已了解其映射关系的基础上进行的学习。在后文中,会继续介绍物理内存的管理以及用户态和内核态的内存映射。
二. 基本概念梳理
- CPU、缓存、内存、主存的架构是源于越快的设备越贵,因此出于节约(qiong)考虑设计了多层架构,CPU中有了MMU
- 物理内存有限,多进程共享物理内存存在安全问题,因此出现了虚拟内存的设计
- 虚拟内存根据ELF的结构进行了相应的设计,存在堆、映射区、栈、数据段等部分
- 考虑到虚拟内存的结构,出现了堆的申请即动态内存
- 虚拟内存为每个进程分配单独的地址空间,映射到物理内存上执行,因此有了物理内存和虚拟内存的映射方法:页
- 为了管理虚拟内存,出现了页表和多级页表
- 为了加速映射,出现了CPU中的TLB
- 为了满足共享的需求,出现了内存映射中的共享内存
- 由于内存碎片的存在,出现了碎片管理的设计以及垃圾回收器
三. 进程内存管理
对于一个进程来说,需要考虑用户态和内核态两部分需要存储在内核内的各个结构
用户态包括
- 代码段
- 全局变量
- 常量字符串
- 函数栈,包括函数调用,局部变量,函数参数等
- 堆:malloc 分配的内存等
- 内存映射,如
glibc
的调用,glibc
的代码是以 so 文件的形式存在的,也需要放在内存里面。
内核态包括
- 内核部分的代码
- 内核中全局变量
- task_struct
- 内核栈
- 在内核里面也有动态分配的内存
- 虚拟地址到物理地址的映射表
进程在内核态中通过task_struct
管理,而task_struct
中关于内存有如下成员变量
1 | struct mm_struct *mm; |
其中mm_struct
结构体也较为复杂,我们将分步介绍。首先我们来看看内核态和用户态的地址划分。这里highest_vm_end
存储当前虚拟内存地址的最大地址,而task_size
则是用户态的大小。
1 | struct mm_struct { |
task_size
定义如下,从注释可见用户态分配了4G虚拟内存中的3G空间,而64位因为空间巨大因此在内核态和用户态中间还保留了空闲区域进行隔离,用户态仅使用47位,即128TB。内核态同样分配128TB,位于最高位。
1 |
|
3.1 用户态内存结构
在用户态,mm_struct
有着以下成员变量
mmap_base
:内存映射的起始地址mmap_legacy_base
:表示映射的基址,在32位中为固定的TASK_UNMAPPED_BASE
,而在64位中,存在一个虚拟地址随机映射机制,因此为TASK_UNMAPPED_BASE + mmap_rnd()
hiwater_rss
:RSS的高水位使用情况hiwater_vm
:高水位虚拟内存使用情况total_vm
:映射的总页数locked_vm
:被锁定不能换出的页数pinned_vm
:不能换出也不能移动的页数data_vm
:存放数据的页数exec_vm
:存放可执行文件的页数stack_vm
:存放栈的页数arg_lock
:引入spin_lock
用于保护对下面区域变量们的并行访问start_code 和 end_code
: 可执行代码的开始和结束位置start_data 和 end_data
:已初始化数据的开始位置和结束位置start_brk
:堆的起始位置brk
:堆当前的结束位置start_stack
:栈的起始位置,栈的结束位置在寄存器的栈顶指针中arg_start 和 arg_end
:参数列表的位置,位于栈中最高地址的地方。env_start 和 env_end
:环境变量的位置,位于栈中最高地址的地方。
1 | struct mm_struct { |
根据这些成员变量,我们可以规划出用户态中各个部分的位置,但是我们还需要一个结构体描述这些区域的属性,即vm_area_struct
1 | struct mm_struct { |
vm_area_struct
的具体结构体定义如下所示,实际是通过vm_next
和vm_prev
组合而成的双向链表,即通过一系列的vm_area_struct
来表述一个进程在用户态分配的各个区域的内容。
vm_start
和vm_end
表述该块区域的开始和结束为止vm_rb
对应一颗红黑树,这颗红黑树将所有vm_area_struct
组合起来,便于增删查找。rb_subtree_gap
存储当前区域和上个区域之间的间隔,用于后续分配使用。vm_mm
指向该结构体所属的vm_struct
vm_page_prot
管理该页的接入权限,vm_flags
为标记位rb
和rb_subtree_last
:有空余位置的区间树结构ano_vma 和 ano_vma_chain
:匿名映射。虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射,映射到文件需要vm_file
指定被映射文件,vm_pgoff
存储偏移量。vm_opts
:指向该结构体的函数指针,用于处理该结构体vm_private_data
:私有数据存储
1 | /* |
对一个mm_struct
来说,其众多的vm_area_struct
会在ELF文件加载,即load_elf_binary()
时构造。该函数在解析ELF文件格式后,就会进行内存映射的建立,主要包括
- 调用
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
映射到内存中的内存映射区域
1 |
|
3.2 内核态结构
由于32位和64位系统空间大小差距过大,因此结构上也有一些区别。我们这里分别讨论二者的结构。
3.2.1 32位内核态结构
内核态的虚拟空间和进程是无关的,即所有进程通过系统调用进入内核后,看到的虚拟地址空间是一样的。如下图所示为32位内核态虚拟空间分布图。
直接映射区
前896M为直接映射区,该区域用于和物理内存进行直接映射。虚拟内存地址减去 3G,就得到对应的物理内存的位置。在内核里面,有两个宏:
__pa(vaddr)
返回与虚拟地址vaddr
相关的物理地址;__va(paddr)
则计算出对应于物理地址paddr
的虚拟地址。
对于该部分虚拟地址的访问,同样采取分页的方式进行,但是页表地址比较简单,直接一一对应即可。
在系统启动的时候,物理内存的前 1M 已经被占用了,从 1M 开始加载内核代码段,然后就是内核的全局变量、BSS 等,也是 ELF 里面涵盖的。这样内核的代码段,全局变量,BSS 也就会被映射到 3G 后的虚拟地址空间里面。具体的物理内存布局可以查看 /proc/iomem
,具体会因为每个人的系统、配置等产生区别。
- high_memory
高端内存的名字来源于x86架构中将物理地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存。
高端内存是内存管理模块看待物理内存的称谓,指的也即896M直接映射区上面的区域。内核中除了内存管理模块外,其余均操作虚拟地址。而内存管理模块会直接操作物理地址,进行虚拟地址的分配和映射。其存在的意义是以32位系统有限的内核空间去访问无限的物理内存空间:借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核页表),临时用一会,用完后归还。
- 内核动态映射空间(noncontiguous memory allocation)
在VMALLOC_START
和VMALLOC_END
之间的区域称之为内核动态映射空间,对应于用户态进程malloc
申请内存一样,在内核态可以通过vmalloc
来申请。内核态有单独的页表管理,和用户态分开。
- 持久内核映射区(permanent kernel mapping)
PKMAP_BASE
到 FIXADDR_START
的空间称为持久内核映射,这个地址范围是 4G-8M 到 4G-4M 之间。使用 alloc_pages()
函数的时候,在物理内存的高端内存得到 struct page
结构,可以调用 kmap()
将其映射到这个区域。因为允许永久映射的数量有限,当不再需要高端内存时,应该解除映射,这可以通过kunmap()
函数来完成。
- 固定映射区
FIXADDR_START
到 FIXADDR_TOP(0xFFFF F000)
的空间,称为固定映射区域,主要用于满足特殊需求。
- 临时映射区(temporary kernel mapping)
临时内核映射通过kmap_atomic
和kunmap_atomic
实现,主要用于当需要写入物理内存或主存时的操作,如写入文件时使用。
3.2.2 64位内核态结构
64位内核态因为空间巨大,所以不需要像32位一样精打细算,直接分出很多的空闲区域做保护,结构如下图所示
- 从 0xffff800000000000 开始就是内核的部分,只不过一开始有 8T 的空档区域。
- 从
__PAGE_OFFSET_BASE(0xffff880000000000)
开始的 64T 的虚拟地址空间是直接映射区域,也就是减去PAGE_OFFSET
就是物理地址。虚拟地址和物理地址之间的映射在大部分情况下还是会通过建立页表的方式进行映射。 - 从
VMALLOC_START(0xffffc90000000000)
开始到VMALLOC_END(0xffffe90000000000)
的 32T 的空间是给vmalloc
的。 - 从
VMEMMAP_START(0xffffea0000000000)
开始的 1T 空间用于存放物理页面的描述结构struct page
的。 - 从
__START_KERNEL_map(0xffffffff80000000)
开始的 512M 用于存放内核代码段、全局变量、BSS 等。这里对应到物理内存开始的位置,减去__START_KERNEL_map
就能得到物理内存的地址。这里和直接映射区有点像,但是不矛盾,因为直接映射区之前有 8T 的空当区域,早就过了内核代码在物理内存中加载的位置。
总结
本文比较详细的分析了内存在用户态和内核态的结构,以此为基础,后文可以开始分析内存的管理、映射了。
代码资料
[1] linux/include/linux/mm_types.h
参考资料
[1] wiki
[3] woboq
[4] Linux-insides
[5] 深入理解Linux内核
[6] Linux内核设计的艺术
[7] 极客时间 趣谈Linux操作系统