《奔跑吧Linux内核(第二版)》第九章笔记

从硬件角度看内存管理

硬件的发展影响着软件的实现。

最早为了同时运行多个进程,内存管理出现了固定分区和动态分区两种技术。但无论是固定分区还是静态分区都会造成内存碎片问题。除此以外还存在 :

  • 进程地址空间保护问题。所有的用户进程都可以访问全部的物理内存,所以恶意的程序可以修改其他程序的内存数据,这使得进程一直处于危险和担惊受怕的状态下。即使系统里所有的进程都不是悉意进程,但是进程A依然可能不小心修改了进程B的数据,从而导致进程B运行崩溃。这明显违背了“进程地址空间需要保护”的原则,也就是地址空间要相对独立。因此每个进程的地址空间都应该受到保护,以免被其他进程有意或者无意地伤害。
  • 内存使用效率低。如果即将要运行的进程所需要的内存空间不足,就需要选择一个进程进行整体换出,这种机制导致有大量的数据需要换出和换入,效率非常低下。
  • 程序运行地址重定位问题。进程在每次换出换入时运行的地址都是不固定的,这给程序的编写带来一定的麻烦,因为访问数据和指令跳转时的目标地址通常是固定的,这就需要重定位技术了。

这些问题的解决依靠操作系统层面已无能为力,必须在处理器层面才能解决。

如果站在内存使用的角度看,进程大概在三个地方用到内存:

  • 进程本身会占用内存,比如代码段以及数据段用来存储程序本身需要的数据。
  • 栈空间。程序运行时需要分配内存空间来保存函数调用关系、局部变量、函数参数以及函数返回值等内容,这些也是需要消耗内存空间的。
  • 堆空间。程序运行时需要动态分配程序需要使用的内存,比如存储程序需要使用的数据等。

进程地址空间的抽象

后来,设计人员对内存建立了抽象,把上述了种用到的内存抽象成进程地址空间或虚拟内存。对于进程来说,它不用关心分配的内存在哪个地址,它只管分配使用。最终由处理器来处理进程对内存的请求,中间做转换,把进程请求的虚拟地址转换成物理地址。进程运行时看到的地址是虛拟地址,然后需要通过 CPU 提供的地址映射方法,把虚拟地址转换成实际的物理地址。这样多个进程在同时运行时,就可以保证每个进程的虛拟内存空间是相互隔离的,操作系统只需要维护虚拟地址到物理地址的映射关系。

虚拟地址到物理地址的转换

在使能了分页机制的系统中,处理器直接寻址虚拟地址,这个地址不会直接发给内存控制器,而是先发给内存管理单元MMU,MMU负责虚拟地址到物理地址的转换。在虚拟地址空间里可按照固定大小来分页。而在物理内存中,空间也是分成和虚拟地址空间大小相同的块,称为页帧。

VPN:虚拟地址空间第几页
PFN:物理地址空间第几页

页表用来存储VPN—>PFN的映射关系,页表类似于数组,VPN类似于数组下标。页表存放于主内存中,通过页表基地址寄存器来指向页表的起始地址。

一级页表地址转换过程如下:
在这里插入图片描述

由于只用一级页表占用内存过大,所以采用多级页表,使用时只把顶级页表加载到内存使用即可,根据需要加载其他级别的页表。

从软件角度看内存管理

free命令

交换分区的概念:交换分区主要在内存不够用的时候,将部分内存中的数据交换到swap空间上,以便让系统不会因内存不足而导致OOM或更致命的情况出现。

malloc()函数

malloc()函数分配出来的是进程地址空间中的虚拟内存,什么时候分配物理内存呢?分两种情况:1、当使用这段内存时,CPU去查询页表,发现页表为空,CPU触发缺页异常,然后再缺页异常里一页一页的分配内存,需要一页就给一页。2、直接分配已经对应好物理内存并建立好页表映射的虚拟内存。

虚拟内存空间

ARM64 架构处理器采用 48位物理寻址机制,最多可以寻找256TB 的物理地址空间。对于目前的应用来说已经足够了,不需要扩展到 64 位的物理寻址。虚拉地址同样最多支持 48 位寻址,所以在处理器架构的设计上,把虛拟地址空间划分为两个空间,每个空间最多支持 256TB。

Linux 内核在大多数架构中把虚拟地址空间划分为用户空间和内核空间。

  • 用户空间:0x0000000000000000~0x0000ffffffffffff。
  • 内核空间:0xffff000000000000~0xffffffffffffffff。

请添加图片描述

内核空间也是通过页表访问的,因为现代CPU寻址不能绕过MMU,不过内核空间一般不做swap,也就没有page_fault,而且它一般不会将连续的虚拟地址空间映射成不连续的物理空间,一般只做一个offset。

每个进程都有一个单独的页表,内核页表只是进程页表的一部分,也就是说每个进程页表都有一个内核页表的拷贝。CPU在用户态使用进程页表,在内核态使用内核页表。对于进程页表和内核页表不做区分,使用时根据虚拟地址去判断是内核空间还是用户空间。

下图展示了虚拟地址空间中用户空间的详细分布:
请添加图片描述

内核对物理内存的管理

page

管理物理内存最小的单位是页。Linux内核内存管理的实现以page数据结构为核心。page数据结构定义在include/linux/mm_types.h头文件中。

Linux内核使用page数据结构来描述物理页面,该数据结构主要包括如下信息:

  • 内核需要知道当前这个页面的状态(通过 flags 字段)。
  • 内核需要知道一个页面是否空闲,即有没有被分配出去,有多少个进程或者内存路径使用了这个页面(使用_count 和_mapcount 引用计数)。
  • 内核需要知道谁在使用这个页面,比如使用者是用户空间进程的匿名页面,还是page cache(通过 mapping 字段)。
  • 内核需要知道这个页面是否被slab 机制使用 (通过Iru、s_mem 等字段)。
  • 内核需要知道这个页面是否属于线性映射(通过 virtual 字段)。

内核可以通过 struct page 数据结构的字段知道很多东西。但是我们发现没有描述具体是哪个物理页面,比如页面的物理地址?

其实 Linux 内核为每个物理页面都分配了一个struct page 数据结构,采用 mem_map[]数组的形式来存放这些 struct page 数据结构,并且它们和物理页面是一对一的映射关系,如下图所示。因此,struct page 数据结构里面就不需要有一个成员来描述这个物理页面的起始物理地址了。struct page 数据结构的 mem_map[]数组定义在mm/memory.c 文件中。
请添加图片描述
由于每个物理页面都需要一个page数据结构来跟踪和管理它的使用情况所以管理成本很高。

page 数据结构可以分成4部分,如图所示。

请添加图片描述

  • 8字节的标志位。
  • 5个字(5个字在32 位处理器上是20 字节,在64位处理器上是40字节)的联合体,用于匿名页面和文件映射页面 slab/slub/slob分配器以及混合页面等。
  • 4字节的联合体,用来管理_mapcount 等引用计数。(_mapcount 表示页面被进程映射的个数,即已经映射了多少个 PTE。每个用户进程都拥有各自独立的虛拟空间(256TB)和一份独立的页表,所以有可能出现多个用户进程地址空间同时映射到一个物理页面的情況。若mapcount 等于-1,表示没有pte页表映射到页面中。若mapcount 等于 0,表示只有父进程映射了页面。匿名页面刚分配时,mapcount初始化为 0。大于0表示除了父进程子进程的映射个数。)
  • 4字节的_refcount(表示内核中引用页面的次数,值为0表示空闲或即将被释放的页面,值大于0表示页面已经被分配且内核正在使用)

zone

Linux使用“内存管理区”来管理一整块内存(一段连续的物理页)。在32位系统中,虚拟地址空间和物理地址空间的对应关系如下图所示:

在这里插入图片描述
因为内核的虚拟地址空间只有1GB,但它需要访问整个4GB的物理空间,因此从物理地址0~896MB的部分(ZONE_DMA+ZONE_NORMAL),直接加上3GB的偏移(在Linux中用PAGE_OFFSET表示),就得到了对应的虚拟地址,这种映射方式被称为线性/直接映射(Direct Map)。

而896M~4GB的物理地址部分(ZONE_HIGHMEM)需要映射到(3G+896M)~4GB这128MB的虚拟地址空间,显然也按线性映射是不行的。

采用的是做法是,ZONE_HIGHMEM中的某段物理内存和这128M中的某段虚拟空间建立映射,完成所需操作后,需要断开与这部分虚拟空间的映射关系,以便ZONE_HIGHMEM中其他的物理内存可以继续往这个区域映射,即动态映射的方式。

用户空间的进程只能访问整个虚拟地址空间的0~3GB部分,不能直接访问3G~4GB的内核空间部分,但出于对性能方面的考虑,Linux中内核使用的地址也是映射到进程地址空间的(被所有进程共享),因此进程的虚拟地址空间可视为整个4GB(虽然实际只有3GB)。

  • ZONE_DMA: 用于执行DMA 操作,只适用于 Intel x86架构,ARM 架构没有这个内存管理区。
  • ZONE_NORMAL: 用于线性映射物理内存。
  • ZONE_HIGHMEM:用于管理高端内存,这些高端内存是不能线性映射到内核地址空间的。注意,在64位的处理器中不需要这个内存管理区。

Linux内核抽象了一种数据结构来描述这些内存管理区,称为内存管理区描述符。使用的数据结构是zone,该数据结构的主要成员如下:
请添加图片描述

分配和释放页面

伙伴系统是操作系统中最常用的动态存储管理方法之一。当用户提出申请时,伙伴系统分配一块大小合适的内存块给用户,反之在用户释放内存块时回收。在伙伴系统中,内存块是2的order 次幂。Linux 内核中order 的最大值用 MAX_ORDER 来表示,通常是11,也就是把所有的空闲页面分组成 11 个内存块链表,这些内存块链表分别包含 1, 2, 4,8,16,32,1024 个连续的页面。1024 个页面对应着一块 4MB 大小的连续物理内存。

早期的伙伴系统的空闲页块的管理实现比较简单,如图所示。内存管理区数据结构中有一个free_area 数组,数组的大小是 MAX_ORDER,数组中有一个链表,链表的成员是2的order 次方大小的空闲内存块。
请添加图片描述
Linux内核提供的页面分配函数:

static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)

alloc_pages()函数用来分配 2 的order 次个连续的物理页面,返回值是第一个物理页面的page 数据结构。第一个参数是gfp_mask 分配掩码,用来控制页面分配的流程;第二个参数是 order值,请求的 order不能大于 MAX_ORDER, MAX_ORDER 通常是 11。

对于内核开发者或者驱动开发者来说,要正确使用这些标志位是一件很困难的事情,因此定义一些常用的分配掩码的组合,叫作类型标志 (Type Flag),如表所示。类型标志提供了内核开发中常用的分配掩码的组合,推荐开发者使用这些类型标志。

请添加图片描述
请添加图片描述
请添加图片描述

Linux内核提供的页面释放函数:

void __free_pages(struct page *page, unsigned int order);

释放时需要特别注意参数,传递错误的 page 指针或者错误的 order 值会引起系统崩溃。free_pages()函数的第一个参数是待释放页面的 struct page 指针,第二个参数是 order值。__free_page()函数用来释放单个页面。

内存碎片化

内存碎片化是内存管理中一个比较难的课题。Linux 内核在采用伙伴系统算法时考虑了如何减少内存碎片化。在伙伴系统算法中,什么样的内存块可以成为伙伴呢?其实伙伴系统算法有如下3个基本条件。

1)两个块大小相同。
2)两个块地址连续。
3)两个块必须是同一个大的内存块中分离出来的。
请添加图片描述
学术上常用的解决外碎片化的技术叫作内存规整 (Memory Compaction),也就是利用移动页面的位置让空闲页面连成一片。如果从页面的迁移属性来看,用户进程分配使用的内存是可以迁移的,但是内核本身使用的内存页面是不能随便迁移的。

为什么内核本身使用的页面不能被迁移呢?因为要迁移这个页面,首先需要把物理页面映射关系断开,然后重新去建立映射关系。在这个断开映射关系的过程中,如果内核继续访问这个物理页面,就会访问不正确的指针和内存,导致内核出现 oops 错误,甚至导致系统崩溃(Crash)。内核是一个敏感区域,它必须保证使用的内存是安全的。这和用户进程不太一样,用户进程使用的页面在断开映射关系之后,如果用户进程继续访问这个页面,就会产生一个缺页异常。在缺页异常处理中,会去重新分配一个物理页面,然后和虚拟内存建立映射关系。这个过程对于用户进程来说是安全的。

分配小块内存(远小于一个页面的大小)

当内核需要分配几十字节的小块内存,若使用页面分配器分配一个页面,就显得很浪费资源了,因此必须有一种用来管理小块内存的新分配机制——slab机制。

slab机制的基本思想:拿mm_struct举例,我们建立一个mm_struct 的对象缓存池,在内存不紧张时,我们去创建这个基于mm_struct 的对象缓存池,并预先分配好若干个空闲对象,这样当内核需要时就可以慷慨地把空闲对象拿出来了。

slab算法有如下新特性。

  • 把分配的内存块当作对象 (object) 看待。可以自定义构造的数 (constructor) 和析构函数 (destractor)来初始化和释放对象的内容。
  • slab 对象释放之后不会马上被丢弃,而是继续保留在内存中,有可能稍后会马上被用到,这样就不需要重新向伙伴系统申请内存。
  • slab算法不仅可以根据特定大小的内存块来创建 slab 描述符,比如内存中常见的数据结构、打开文件对象等,这样可以有效避免内存碎片的产生,还可以快速获得频繁访问的数据结构。slab 算法也支持基于2的n次幂字节大小的分配模式。
  • slab 算法创建了多层的缓冲池,充分利用了空间换时间的思想,未丽绸缪,有效解决了效率问题。
  • 每个 CPU 都有本地对象缓冲池,避免了多核之间的锁争用问题
  • 每个内存节点都有共享对象缓冲池。

请添加图片描述
所谓的本地对象缓沖池,就是在创建每个slab 描述符时为每个 CPU 创建一个本地缓存池,这样当需要从 slab 描达符中分配空闲对象时,就可以优先从当前 CPU 的本地缓存池中分配内存。所谓的本地缓存池,就是本地 CPU 可以访问的缓冲池。给每个CPU 创建一个本地缓冲池是一个很棒的主意,这样可以减少多核 CPU 之间的锁的竞争,本地缓冲池只属于本地的 CPU,其他 CPU 不能使用。

共享对象缓冲池由所有 CPU 共享。当本地对象缓存池里没有空闲对象时,就会从共享对象缓冲池中取一批空闲对象搬移到本地对象缓冲池中。

slab机制

slab描述符(kmem_cache)slab是两种数据结构。
slab描述符是用来管理一种对象的众多slab的数据结构,而一个slab包含多个空闲对象,使用时,需要将slab中的空闲对象放入共享缓冲池或本地对象缓冲池中使用。

请添加图片描述
请添加图片描述

slab描述符(kmem_cache)
请添加图片描述

请添加图片描述
slab 机制分两步完成。第一步是使用 kmem_cache_create()函数创建一个slab 描述符,使用struct kmem_cache 数据结构来描述。struct kmem_cache 数据结构里有几个主要的成员,一个是指向本地缓存池的指针,另一个是指向 slab 节点的node 指针。每个内存节点有一个slab节点,通常 ARM 只有一个内存节点,这里就假设系统只有一个slab 节点。其他是描述这个slab 描述符的信息,比如这个slab 的对象的大小、名字以及 align 等信息。

这个slab 节点里有3个链表,分别是slab 空闲链表、slab 满链表和 slab partial链表。这些链表的成员是slab,不是对象。另外,该节点里有一个指针指向一个共享缓存池,它和本地缓存池是相对的。

第二步是从这个slab 描述符中去分配空闲对象。一个CPU 要从这个slab 描述符中分配对象,它首先去访问当前 CPU 对应的这个slab 描述符里的本地缓存池。如果本地缓冲池里有空闲对象,就直接获取,没有其他 CPU 过来竞争。如果本地缓冲池里没有空闲对象,那么需要去共享缓冲池里查询是否有空闲对象。如果有,就从共享缓存池里搬移几个空闲对象到自己的缓存池中。

可是刚创建 slab 描述符时,本地缓冲池和共享对象缓冲池里都是空的,没有空闲对象,那 slab 是怎么建立的呢?

建立 slab 所使用的物理页面需要向页面分配器申请,这个过程可能会睡眠。如图所示,建好一个slab之后,会把这个slab 添加到slab 节点的slab 空闲链表里,所以slab 中的3个链表的成员是slab,而不是对象。这里是通过 slab 的第一个页面的Iru 成员挂入链表中的。另外,空闲的对象会搬移到共享缓冲池和本地缓冲池,供分配器使用。

请添加图片描述

进程对虚拟内存的管理

VMA与mm_struct

Linux内核使用vm_area_struct(简称VMA)数据结构来描述一段虚拟内存区域。

内存区域包含内容如下。

  • 代码段映射,可执行文件中包含只读并可执行的程序头,如代码段和 .init 段等。
  • 数据段映射,可执行文件中包含可读可写的程序头,如数据段和 bss 段等。
  • 用户进程的栈。通常是在用户空间的最高地址,从上往下延伸。它包含栈帧,里面包含了局部变量和函数调用参数等。注意不要和内核栈混淆,进程的内核栈独立存在并有内核维护,主要用于上下文切换。
  • MMAP 映射区域。位于用户进程栈下面,主要用于mmap 系统调用,比如映射一个文件的内容到进程地址空间等。
  • 堆映射区域。malloc()函数分配的进程虚拟地址就是这段区域。

进程地址空间中每个内存区域相互不能重叠。

Linux 内核需要管理每个进程所有的内存区域以及它们对应的页表映射,所以必须抽象出一个数据结构,这就是mm_struct 数据结构。进程的进程控制块(PCB)数据结构 task struct中有一个指针mm 指向这个mm_struct 数据结构。

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

请添加图片描述
请添加图片描述
作为进程地址空间中的区间,VMA 是有属性的,比如可读可写、共享等属性。 vm_flags成员描达了这些属性,涉及 VMA 的全部页面信息,包括如何映射页面、如何访问每个页面的权限等信息,VMA 属性可以任意组合,但是最终仍要落实到硬件机制上,即落实到页表项的属性中,如图所示。vm_area_struct 数据结构中有两个成员与此相关:
一个是vm_flags成员,用于描述 VMA 的属性;另一个是vm_page_prot
成员,用于把 VMA 属性标志位转换成处理器相关的页表项的属性,这和具体架构相关。在创建新的 VMA 时,使用 vm_get_page_prot()函数可以把 vm_flags 标志位转换成具体的页表项的硬件标志位。请添加图片描述

与虚拟内存分配相关的两个系统调用

brk

用户进程的可执行文件由代码段和数据段组成,数据段包括所有的静态分配的数据空间,例如全局变量和静态局部变量等。在可执行文件装载时,内核就已分配好空间,包括虚拟地址和物理页面,并建立好二者的映射关系。用户进程的用户栈从 TASK_SIZE(0x1 0000 0000 0000)指定的虚拟空间的顶部开始,由顶向下延伸,而brk 分配的空间是从数据段的顶部 end_data 到用户栈的底部。所以动态分配空间时会从进程的end_data 开始,每分配一块空间,就把这个边界往上推进一段,同时内核和进程都会记录当前边界的位置。
请添加图片描述

mmap

在 Linux 内核中映射可以分 成匿名映射和文件映射:

  • 匿名映射:没有对应的相关文件,这种映射的内存区域的内容会被初始化为 0。
  • 文件映射:映射和实际文件相关联,通常是把文件的内容映射到进程地址空间,这样应用程序就可以像操作进程地址空间一样读写文件,如图所示。

最后,根据文件关联性和映射区域是否共享等属性,映射又可以分成如下4种情况。

  • 私有磨名映射,通常用于内存分配。
  • 私有文件映射,通常用于加载动态库。
  • 共享匿名映射,通常用于进程问共享内存。
  • 共享文件映射,通常用于内存映射 I/O 和进程间通信。
    请添加图片描述

磁盘与内存的映射就是文件映射,说这个问题之前我们先说下swap,因为这个问题让我很容易想起swap,linux swap 是交换分区的意思,在内存不够的情况下,操作系统先把内存与磁盘的swap区进行一个“映射”,然后把这些内存解放出来放入内存中,为之后的进程的腾出一块内存空间,等到自己的进程再次被唤醒时候,再把磁盘里面的内存换进来。这里有文件和内存之间的映射奥,可是mmap与swap设计思想上是完全不同的,一个针对的物理内存,一个针对的是虚拟内存。

在说mmap之前我们先说一下普通的读写文件的原理,进程调用read或是write后会陷入内核,因为这两个函数都是系统调用,进入系统调用后,内核开始读写文件,假设内核在读取文件,内核首先把文件读入自己的内核空间,读完之后进程在内核回归用户态,内核把读入内核内存的数据再copy进入进程的用户态内存空间。实际上我们同一份文件内容相当于读了两次,先读入内核空间,再从内核空间读入用户空间。

mmap是系统调用,mmap的作用是将进程的虚拟地址空间和文件在磁盘的位置做一一映射,做映射之后,读写文件虽然同样是调用read和write但是触发的机制已经不一样了(mmap是file_operations中的成员这么一说是不是了然了),实际上我们这么说是不太合理的因为一一映射并不是mmap一开始就全部完成映射的。

mmap只会返回来一个指针,指向进程逻辑地址空间的一个位置。这个时候的过程是这样的,首先read会改写为读内存操作,读内存的时候,系统发现该地址对应的物理内存是空的,触发缺页机制,缺页机制先在swap寻找对应的页面,发现没有然后再去通过mmap建立的映射关系,从硬盘上将文件读入物理内存。也就是说mmap把文件直接映射到了用户空间,没有经历内核空间。

mmap可以映射文件进入用户的虚拟内存,实际上,他也可以把设备的内存映射入用户的虚拟内存,因为我们一般都需要内核去读写设备,如果把设备的物理内存直接映射入空间就跟上述一样,省去一次的内核copy。

对比

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

缺页异常

请添加图片描述

内存短缺

页面回收

在Linux 系统中,当内存有盈余时,内核会尽量多地使用内存作为文件缓存,从而提高系统的性能。文件缓存页面会加入文件类型的 LRU 链表中,当系统内存紧张时,文件缓存页面会被丢弃,或者被修改的文件缓存会被回写到存储设备中,与块设备同步之后便可释放出物理内存。现在的应用程序越来越转向内存密集型,无论系统中有多少物理内存都是不够用的,因此 Linux 系统会使用存储设备当作交换分区,内核将很少使用的内存换出到交换分区,以便释放出物理内存,这个机制称为页交换(Swapping),这些处理机制统称为页面回收 (Page Reclaim )。

OOM killer

请添加图片描述
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值