【原理剖析】a-Linux 内存管理
概述:主要讲述 Linux 操作系统中的内存管理方式及存储器管理的基本概念及原理,从中说明 Linux 操作系统对内存的管理模式以及在 Linux 操作系统中运行的进程的内存管理情况和内存读取、调用的最底层实现方式
- Linux 分页管理机制
- 虚拟内存段的组织与管理
- 内存的共享和保护
- 内存空间管理
- 空闲内存管理
- 内核态内存的申请与释放
- 用户态内存的申请与释放
- Linux 系统交换空间
- 存储管理系统的缓冲机制
0x01 Linux分页管理机制
Linux 采用“按需调页”算法,支持三层管理策略。由于 Intel CPU 在硬件级提供了段式存储管理和二层页式存储管理功能,Linux 操作系统作为一种软件,必须与之兼容。Linux 根据 Intel 处理器的要求,最低限度地设置与段相关的结构和初始化程序,但实质上是放弃了段式存储管理。Intel 微型计算机上的 Linux 系统考虑到 CPU 的限制,将第二层的页式管理(pmd
)与第一层的页式管理(pgd
)合并,因此真正发挥作用的是以页目录和页表为中心的数据结构和函数。
在 Linux 中,每个用户进程都可以访问 4GB 的线性虚拟内存空间。其中,03GB 的虚拟地址空间是用户空间,用户进程可以直接对其进行访问;3GB4GB 的虚拟内存地址空间为内核态空间,存放仅供内核态访问的代码和数据,用户态进程不可访问。当用户进程通过中断或系统调用访问内核态空间时,就会触发处理特权级转换,即从操作系统的用户态转换到内核态。
Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分, 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
所有进程从 3GB 到 4GB 的虚拟空间是一样的,有相同的页目录项和页表,对应相同的物理内存段。Linux 以此方式使得内核态进程共享代码段和数据段。
内核态虚拟空间从 3GB 到 3GB+4MB 的一段被映射到物理空间 04MB。因此,进程处于内核态时,只要通过虚拟空间 3GB3GB+4MB 段即可访问物理空间 0~4MB 段。
既然对于用户空间来说访问的内存地址都是连续的 4GB 线性虚拟地址,那么就需要知道 Linux 是如何划分虚拟空间。Linux 采用“按需调页”技术管理虚拟内存。标准 Linux 的虚拟页表分为三级页表,依次为页目录(PGD
Page Directory)、中间页目录(PMD
Page Middle Directory)和页表(PTE
PageTable),如下图所示:
![图片描述](a-Linux 内存管理/1.png)
在 Intel CPU 上,Linux 的页表结构实际上为两级。IA-32 体系结构的页管理机制中的页目录是 PGD,页表是 PTE,而 PGD 和 PMD 实际上合二为一。在用户进程中用到的与内存管理有关的数据结构是 mm_struct
结构,此结构中包含了用户进程中与存储有关的信息,具体结构定义如下:
1 |
|
每一个进程都有一个页目录,存储该进程所使用的内存页面情况。Linux 按照“按需调页”的原则,只分配必需的内存页面,从而避免了页表过多占用存储空间的情况出现。例如,系统调用 fork
分配内存页面的情况如下:
- 内核态堆栈 1 页;
- 页目录 1 页;
- 页表几页;
而系统调用 exec
分配内存页面的情况则:
- 可执行文件的文件头 1 页;
- 用户堆栈 1 页或几页;
这样,当进程开始运行时,如果执行代码不在内存中,将产生第一次缺页中断,让操作系统分配内存页面,并将执行代码装入内存中。此后,按需要逐渐分配更多的内存页面,并参与页面调度。当系统内存不足时,由操作系统决定是否将该进程的一部分页面换出到磁盘交换区或交换文件中。进程终止时,操作系统释放所有该进程占用的资源,包括内存页面。
0x02 虚拟内存段的组织与管理
用户程序总共拥有 4GB 的虚拟空间,但是并不是 4GB 空间都可以让用户进程读写或申请使用。用户进程实际上可以申请的虚拟空间为 0 ~ 3GB 。在用户进程创建时,已由系统调用 fork()
的执行函数 do_fork()
将内核的代码段和数据段映射到 3GB 以后的虚拟空间,供内核态进程访问。所有进程的 3GB~4GB 的虚拟控件的映像都是相同的,并以此方式共享代码段和数据段。
为了能够以自然的方式管理进程虚拟控件,Linux 定义了虚拟段(vma
virtual memory area),一个 vma
段是某个进程的一段连续的虚拟控件,在这段虚拟空间的所有单元拥有相同的特征。例如,属于同一个进程具有相同的访问权限,将同时被锁定、受保护等。
虚拟段的数据结构如下:
1 |
|
结合前面的内存管理数据结构,可以绘制如下的 Linux 内存管理框图:
![图片描述](a-Linux 内存管理/2.png)
0x03 内存的共享和保护
Linux 中内存共享以页表的形式实现,共享该页的各进程的页表表项直接指向共享页,如下图所示。这种结构不需要设立共享页表,节约内存的占用,但是效率较低。当共享页状态发生变化时,共享该页的各进程的页表均需要修改,并要多次访问页表。
![图片描述](a-Linux 内存管理/3.png)
Linux 可以对虚拟段中的任意部分加锁或保护。对进程的虚拟地址加锁,其实质就是对 vma
段的 vm_flags
属性与 VM_LOCKED(0X2000)
进行“或”操作,虚拟内存加锁后,它对应的物理页面驻留内存不再被页面置换程序换出。加锁操作有三种:
- Mlock:对指定的一段虚拟空间加锁;
- Munlock:对指定的一段虚拟空间解锁;
- Mlockall:对进程的所有虚拟空间解锁;
对进程的虚拟地址施行地址保护措施就是重新设置 vma
段的访问权限,其实质就是对 vma
段的 vm_flags
属性重置 PORT_READ(0X1)
、PORT_WRITE(0X2)
、PROT_EXEC(0X4)
,重新设定 vm_page_port
属性,与此同时,对虚拟地址范围内的所有页表项,其访问权限也相应调整,保护操作由系统调用 mprotect
实施。
虚拟段加锁、保护操作可以有以下几种方式:
- 对整个虚拟段加锁或保护
- 对虚拟段前部加锁或保护
- 对虚拟段后部加锁或保护
- 对虚拟段中部加锁或保护
0x04 内存空间管理
尽管 Linux 采用虚拟存储管理策略,有些申请仍然需要直接分配物理空间。例如,为刚创建的进程分配页目录,为装入进程的代码段分配空间,为 I/O 操作准备缓冲区等。物理内存以页帧为单位,页帧的长度固定,等于页长,对于 Intel CPU 默认为 4KB。
Linux 对物理内存的管理通过 mem_map
表描述。mem_map
在系统初始化时,由 free_area_init()
函数创建,如下图所示:
![图片描述](a-Linux 内存管理/4.png)
0x05 空闲内存管理
在物理内存低端,紧跟 mem_map
表的 bitmap
表以位示图方式记录了有有物理内存的空闲情况。与 mem_map
一样,bitmap
表在系统初始化时由 free_area_init()
函数创建,如下图所示:
![图片描述](a-Linux 内存管理/5.png)
0x06 内核态的内存的申请与释放
内核态内存是用来存放 Linux 内核系统数据结构的内存区域,处于进程虚拟空间的 3GB~4GB 范围内。以 Intel CPU 为例,申请的内存块大小 blocksize
有 32、64、128、512 等。在 blocksize
中能够申请到的块的大小近似于 2 的次幂,所差的恰好是管理数据结构所占用的字节数,如 16 是 struct page_dsecriptor
结构的长度。
管理内核空间空闲块的数据结构及相互关系如下图所示:
![图片描述](a-Linux 内存管理/6.png)
0x07 用户态内存的申请与释放
Linux 用 kmalloc()
函数和 kfree()
函数提供内核内存申请、释放的接口,它还实现另一种虚拟空间的申请、释放接口,就是 vmalloc()
和 vfree()
。
由 vmalloc()
分配的存储空间在进程的虚拟空间是连续的,当它对应的物理内存扔需要经过缺页中断后由缺页中断服务程序分配,所分配的物理页帧不是连续的。这些特征和访问用户内存相似,所以不妨把 vmalloc()
和 vfree()
成为用户态内存的申请和释放界面。
可分配的虚拟空间在 3GB+high_memory+HOLE_8MB
以上的高端,由 vmlist
链表管理。3GB
是内核态赖以访问物理内存的起始地址。high_memory
是安装在计算机中实际可用的物理内存的最高地址。 因此 3GB+high_memory
也是物理内存的上界。 HOLE_8MB
则是长度为 8MB 的“隔离带”,起越界保护作用。这样,vmlist
管辖的虚拟空间既不与进程用户态0~3GB
的虚拟空间冲突,也不与进程内核态映射的 3GB~3GB+high_memory
的虚拟空间冲突。
vmlist
链表的节点类型 vm_struct
具体内容如下:
1 |
|
起始时,vmlist
只有一个节点,vmlist_addr
置为 VMALLOC_START
(段地址 3GB,偏移量 high_memory+8MB
)。动态管理过程中,vmlist
的虚拟内存块按起始地址从小到大排序,每个虚拟内存块之后都有一个 4KB
大小的“隔离带”,用来检查指针的越界错误,用户申请大块连续空间可用 vmalloc()
,如下图所示:
![图片描述](a-Linux 内存管理/7.png)
0x08 交换空间
计算机的物理内存空间总是影响机器性能。内存太小时,操作系统采用交换的方式。1970 年以后,按需调页算法得到了应用,是 Linux 操作系统采用的虚拟存储器的策略。换页操作时,Linux 区分两种不同的内存数据。一部分没有写权限的进程空间在换页时无需存入交换空间,直接丢弃即可。那些修改过的页面,换页时,其内容必须保存,保存的位置属于交换空间的某个页面。
Linux 采用两种方式保存换出的页面,一种是整个块设备,如磁盘的一个分区,称为交换设备;另一种是文件系统的固定长度的文件,称为交换文件。交换设备和交换文件统称为交换空间。
尽管交换空间有两种不同的方式,当它们的内部格式是一致的。一个交换空间最多可容纳 32687
个页面。如果一个交换空间不够用,Linux 允许并行管理多个交换空间,交换设备远比交换文件有效。在交换设备中,属于同一个页面的数据块总是连续的,第一个数据块地址确定,后续的数据库可以按顺序读出或写入,而在交换文件中,属于同一个页面的数据块虽然在逻辑上是连续的,但是数据块的实际位置可能是零散的。
当交换进程 kswapd
尝试换出页面时,调用测试进程 try_to_swap_out()
测试页面的存在时间。如果某个物理页面可以换出,则调用 get_swap_page()
申请交换空间的页面,得到交换进程的入口地址 entry
,将要换出的物理页面换到 entry
指定的交换空间的某个页面中。
0x09 页交换进程与页面交换
当物理页面不够时,Linux 存储管理系统必须释放部分物理页面,将它们的内容写到交换空间。实现这个功能的就是内核态交换程序 kswwapd
。
kswapd
属于一种特殊的进程,成为内核态进程。Linux 的内核态进程没有虚拟存储空间进程,它们运行在内核态,直接使用物理地址空间。它不仅能够将页面换出交换空间,而且保证系统中有足够的空闲页面,保证存储系统高效运行。
kswapd
在系统初始启动时由内核态进程 init
创建,其初始化程序段以调用 init_swap_timer()
函数结束,进而转入 while(1)
循环,并马上睡眠。
内核交换进程依照 3 种方式缩减系统使用的物理页面,
page cache
和buffer cache
- 换出
SYSTEM V
共享内存占用的页面 - 换出或丢弃进程占用的页面
0x0a 缺页中断和页面交换
磁盘中的可执行文件映像 Image
一旦被映射到一个进程的虚拟空间就可以开始执行。由于只有该映像区的开始部分调入内存,因此,进程迟早会执行到那些尚未调入内存的代码。当一个进程访问了一个还没有有效页表项的虚拟地址时(即页表项的 P 位为 0),处理器将产生缺页中断,通知操作系统,并将出现缺页的虚拟地址(在 CR2 寄存器中)和缺页时访问虚拟内存的模式,并传递给 Linux 的缺页中断服务程序。
在系统初始化时,设定了缺页中断服务程序为 do_page_fault()
:
1 |
|
根据控制寄存器 CR2
传递的缺页地址,Linux 必须找到用来表示缺页的虚拟存储区的 vm_area_struct
结构。在搜索进程的 vm_area_struct
结构时,对搜索时间应有严格的限制。为了有效地处理搜索工作,Linux 将所有的 vm_area_struct
结构通过 AVL 平衡树连接起来。如果没用找到与缺页相对应的 vm_area_struct
结构,那么说明进程访问了一个非法存储器,Linux 向进程发送信号 GIGSEGV
,如果进程没用处理该信号的函数,该进程将被终止。
Linux 接着检测缺页时访问模式是否合法。如果进程对该页的访问超越权限,例如试图对只允许读操作的页面进行写操作,系统也将向该进程发送一个信号,通知进程的存储访问出错。经过以上两步检查,可以确定肯定是正常的缺页中断。
Linux 还区分产生缺页中断的页面是在交换空间,还是在磁盘中作为某一可执行文件映像的一部分,进而做出不同的处理,这一点通过页表项中的位来区分。如果该页面所对应的页表项是无效的(p=0
),但是非空,说明缺页在交换空间中。否则,页面是某一个可执行文件映像(image
)的一部分。
并不是所有的 vm_area_struct
结构变量都有完整的一套虚拟存储操作,有些甚至没有 nopage
操作函数指针。在这种情况下,Linux 将使用缺省的操作函数为该虚拟页面找到物理页帧,同时为其设置一页表项。如果 vm_area_struct
结构变量中有 nopage
操作函数,Linux 使用该操作函数。
Linux 的 nopage
函数通常用来调入已被存储映射的可执行磁盘映像,而且它是利用 page cache
来将所需映像页调入内存的。
0x0b 存储管理系统的缓冲机制
存储管理系统的缓冲机制主要包括 kmalloc cache
、swap cache
和 page cache Kmalloc cache
在 “内核态内存的申请与释放”已做了介绍,本节介绍另外两种 cache
Swap cache
如果以前被调出到交换空间的页面由于进程再次访问而调入物理内存,只要该页调入后没有被修改过,那么它的内容与交换空间中的内容是一样的。在这种情况下,交换空间中的备份还是有效的。因此在该页再度换出时,就没必要执行写操作。Linux 采用swap_cache
表描述的swap cache
来实现这种思想。swap cache
实质上是关于页表项的一个列表,swap_cache
表位于mem_map
表之前。
每一物理页面都在 swap cache
中占有一表项,swap_cache
表项的总数就是物理页面总数,若该物理页面的内容是新创建的,或虽然曾经换出过,但换出后,该物理页面已经被修改时,则该表项清零。内容非零的表现,正好是某个进程的页表项,描述了页面在交换空间中的位置。
当 Linux 将一物理页面调出到交换空间时,它先查询 swap cache
,如果其中有与该页面对应的有效页表项,那就不需要将该页写出,因为原交换空间中的内容与待换出的页面内容是一致的。
Page Cache
Linux 的page cache
的作用是加快对磁盘的访问速度,文件被映射到内存中,每次读取一页,而这些页就保存于page cache
中。
每当需要读取文件的一页时,总是首先通过 page cache
读取。如果所需页面在 page cache
中,就返回指向表示该页面的 mem-map-1
的指针。否则必须从文件系统中调入。接着,Linux 申请一物理页,将该页从磁盘文件中调入内存。
如果有可能的话,Linux 还发出读取当前页面后一个页面的读操作请求。这种预读一页的思想来自局部原理,即在进程读当前页时,它的后一页也可能被进程用到。
随着越来越多的文件页面被读取、执行,page cache
将会变得越来越大。进程不再需要的页面应从 page cache
中删除。当 Linux 在使用内存过程中发现屋里页面渐变稀少时,将缩减 page cache
。