Linux 操作系统原理 — 物理内存

目录

物理内存的内核映射

在 Linux 系统中通过分段和分页机制,把物理内存划分 4K 大小的内存页(Page),也称作页框(Page Frame),物理内存的分配和回收都是基于内存页进行,把物理内存分页管理有很多好处。

假如系统请求小块内存,可以预先分配一页给它,避免了反复的申请和释放小块内存带来频繁的系统开销。假如系统需要大块内存,则可以用多页内存拼凑,而不必要求大块连续内存。

注意,如果就直接这样把内存分页使用,不再加额外的管理还是存在一些问题,下面我们来看下,系统在多次分配和释放物理页的时候会遇到哪些问题。

在内核态申请内存比在用户态申请内存要更为直接,它没有采用用户态那种延迟分配内存技术。内核认为一旦有内核函数申请内存,那么就必须立刻满足该申请内存的请求,并且这个请求一定是正确合理的。相反,对于用户态申请内存的请求,内核总是尽量延后分配物理内存,用户进程总是先获得一个虚拟内存区的使用权,最终通过缺页异常获得一块真正的物理内存。

IA32 架构中内核虚拟地址空间只有 1GB 大小(从 3GB 到 4GB),因此可以直接将 1GB 大小的物理内存(即常规内存)映射到内核地址空间,但超出 1GB 大小的物理内存(即高端内存)就不能映射到内核空间。为此,内核采取了下面的方法使得内核可以使用所有的物理内存。

  1. 高端内存不能全部映射到内核空间,也就是说这些物理内存没有对应的线性地址。不过,内核为每个物理页框都分配了对应的页框描述符,所有的页框描述符都保存在 mem_map 数组中,因此每个页框描述符的线性地址都是固定存在的。内核此时可以使用 alloc_pages() 和 alloc_page() 来分配高端内存,因为这些函数返回页框描述符的线性地址。
  2. 内核地址空间的后 128MB 专门用于映射高端内存,否则,没有线性地址的高端内存不能被内核所访问。这些高端内存的内核映射显然是暂时映射的,否则也只能映射 128MB 的高端内存。当内核需要访问高端内存时就临时在这个区域进行地址映射,使用完毕之后再用来进行其他高端内存的映射。

由于要进行高端内存的内核映射,因此直接能够映射的物理内存大小只有 896MB,该值保存在 high_memory 中。内核地址空间的线性地址区间如下图所示:

在这里插入图片描述

从图中可以看出,内核采用了三种机制将高端内存映射到内核空间:永久内核映射、固定映射和 vmalloc 机制。

物理内存管理

基于物理内存在内核空间中的映射原理,物理内存的管理方式也有所不同。

物理页管理面临问题:物理内存页分配会出现外部碎片和内部碎片问题,所谓的内部和外部是针对 “页框内外” 而言的,一个页框内的内存碎片是内部碎片,多个页框间的碎片是外部碎片。

  • 外部碎片:当需要分配大块内存的时候,要用好几页组合起来才够,而系统分配物理内存页的时候会尽量分配连续的内存页面,频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间,形成外部碎片。
    在这里插入图片描述
  • 内部碎片:物理内存是按页来分配的,这样当实际只需要很小内存的时候,也会分配至少是 4K 大小的页面,而内核中有很多需要以字节为单位分配内存的场景,这样本来只想要几个字节而已却不得不分配一页内存,除去用掉的字节剩下的就形成了内部碎片。
    在这里插入图片描述

内核中物理内存的管理机制主要有伙伴算法,Slab 高速缓存和 vmalloc 机制。其中伙伴算法和 Slab 高速缓存都在物理内存映射区分配物理内存,而 vmalloc 机制则在高端内存映射区分配物理内存。

Buddy(伙伴)分配算法

伙伴系统算法(Buddy system),顾名思义,就是把相同大小的页框块用链表串起来,页框块就像手拉手的好伙伴,也是这个算法名字的由来。伙伴算法负责大块连续物理内存的分配和释放,以页框为基本单位。该机制可以避免外部碎片。

具体的,所有的空闲页框分组为 11 个块链表,每个块链表分别包含大小为 1,2,4,8,16,32,64,128,256,512 和 1024 个连续页框的页框块。最大可以申请 1024 个连续页框,对应 4MB 大小的连续内存。

在这里插入图片描述
因为任何正整数都可以由 2^n 的和组成,所以总能找到合适大小的内存块分配出去,减少了外部碎片产生 。

如此的,假设需要申请 4 个页框,但是长度为 4 个连续页框块链表没有空闲的页框块,伙伴系统会从连续 8 个页框块的链表获取一个,并将其拆分为两个连续 4 个页框块,取其中一个,另外一个放入连续 4 个页框块的空闲链表中。释放的时候会检查,释放的这几个页框前后的页框是否空闲,能否组成下一级长度的块。

命令查看:

[root@c-dev ~]# cat /proc/buddyinfo
Node 0, zone      DMA      1      0      0      0      2      1      1      0      1      1      3
Node 0, zone    DMA32    189    209    119     80     38     17     11      1      1      2    627
Node 0, zone   Normal   1298   1768   1859    661    743    461    275    133     68     61   2752

Slab 分配器

一般来说,内核对象的生命周期是:分配内存 -> 初始化 -> 释放内存,内核中有大量的小对象,比如:文件描述结构对象、任务描述结构对象,如果按照伙伴系统按页分配和释放内存,就会对小对象频繁的执行分配内存 -> 初始化 -> 释放内存的过程,会非常消耗性能。

伙伴系统分配出去的内存还是以页框为单位,而对于内核的很多场景都是分配小片内存,远用不到一页内存大小的空间。Slab 分配器最初是为了解决物理内存的内部碎片而提出的,它将内核中常用的数据结构看做对象。Slab 分配器为每一种对象建立高速缓存,通过将内存按使用对象不同再划分成不同大小的空间,应用于内核对象的缓存,内核对该对象的分配和释放均是在这块高速缓存中操作。

可见,Slab 内存分配器是对伙伴分配算法的补充。Slab 缓存负责小块物理内存的分配,并且它也作为高速缓存,主要针对内核中经常分配并释放的对象。

在这里插入图片描述

对于每个内核中的相同类型的对象,如:task_struct、file_struct 等需要重复使用的小型内核数据对象,都会有个 Slab 缓存池,缓存住大量常用的「已经初始化」的对象,每当要申请这种类型的对象时,就从缓存池的 Slab 列表中分配一个出去;而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片,同时也大大提高了内存分配性能。

主要优点:

  • Slab 内存管理基于内核小对象,不用每次都分配一页内存,充分利用内存空间,避免内部碎片。
  • Slab 对内核中频繁创建和释放的小对象做缓存,重复利用一些相同的对象,减少内存分配次数。

在这里插入图片描述
kmem_cache 是一个cache_chain 的链表组成节点,代表的是一个内核中的相同类型的「对象高速缓存」,每个kmem_cache 通常是一段连续的内存块,包含了三种类型的 slabs 链表:

  • slabs_full:完全分配的 slab 链表
  • slabs_partial:部分分配的 slab 链表
  • slabs_empty:没有被分配对象的 slab 链表

kmem_cache 中有个重要的结构体 kmem_list3 包含了以上三个数据结构的声明。

在这里插入图片描述

slab 是 Slab 分配器的最小单位,在实现上一个 slab 由一个或多个连续的物理页组成(通常只有一页)。单个 slab 可以在 slab 链表之间移动,例如:如果一个半满 slabs_partial 链表被分配了对象后变满了,就要从 slabs_partial 中删除,同时插入到全满 slabs_full 链表中去。内核 slab 对象的分配过程是这样的:

在这里插入图片描述

  1. 如果 slabs_partial 链表还有未分配的空间,分配对象,若分配之后变满,移动 slab 到 slabs_full 链表。
  2. 如果 slabs_partial 链表没有未分配的空间,进入下一步。
  3. 如果 slabs_empty 链表还有未分配的空间,分配对象,同时移动 slab 进入 slabs_partial 链表。
  4. 如果 slabs_empty 为空,请求伙伴系统分页,创建一个新的空闲 slab, 按步骤 3 分配对象。

查看 Slab 内存信息:

[root@c-dev ~]# cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
isofs_inode_cache     50     50    640   25    4 : tunables    0    0    0 : slabdata      2      2      0
kvm_async_pf           0      0    136   30    1 : tunables    0    0    0 : slabdata      0      0      0
kvm_vcpu               0      0  14976    2    8 : tunables    0    0    0 : slabdata      0      0      0
xfs_dqtrx              0      0    528   31    4 : tunables    0    0    0 : slabdata      0      0      0
xfs_dquot              0      0    488   33    4 : tunables    0    0    0 : slabdata      0      0      0
xfs_ili            40296  40296    168   24    1 : tunables    0    0    0 : slabdata   1679   1679      0
xfs_inode          41591  41616    960   34    8 : tunables    0    0    0 : slabdata   1224   1224      0
xfs_efd_item        1053   1053    416   39    4 : tunables    0    0    0 : slabdata     27     27      0

实时显示内核中的 Slab 内存缓存信息:

$ slabtop

Slab 高速缓存分为以下两类:

  1. 通用高速缓存:slab 分配器中用 kmem_cache 来描述高速缓存的结构,它本身也需要 slab 分配器对其进行高速缓存。cache_cache 保存着对高速缓存描述符的高速缓存,是一种通用高速缓存,保存在 cache_chain 链表中的第一个元素。另外,slab 分配器所提供的小块连续内存的分配,也是通用高速缓存实现的。通用高速缓存所提供的对象具有几何分布的大小,范围为 32 到 131072 字节。内核中提供了 kmalloc() 和 kfree() 两个接口分别进行内存的申请和释放。
  2. 专用高速缓存:内核为专用高速缓存的申请和释放提供了一套完整的接口,根据所传入的参数为指定的对象分配 Slab 缓存。

分区页框分配器

分区页框分配器(Zoned Page Frame Allocator),处理对连续页框的内存分配请求。分区页框管理器分为两大部分:前端的管理区分配器和伙伴系统,如下图:

在这里插入图片描述
管理区分配器负责搜索一个能满足请求页框块大小的管理区。在每个管理区中,具体的页框分配工作由伙伴系统负责。为了达到更好的系统性能,单个页框的申请工作直接通过 per-CPU 页框高速缓存完成。

per-CPU 页框高速缓存:内核经常请求和释放单个页框,该缓存包含预先分配的页框,用于满足本地 CPU 发出的单一页框请求。

该分配器通过几个函数和宏来请求页框,它们之间的封装关系如下图所示。

在这里插入图片描述
这些函数和宏将核心的分配函数 __alloc_pages_nodemask() 封装,形成满足不同分配需求的分配函数。其中,alloc_pages() 系列函数返回物理内存首页框描述符,__get_free_pages() 系列函数返回内存的线性地址。

非连续内存区内存的分配

内核通过 vmalloc() 来申请非连续的物理内存,若申请成功,该函数返回连续内存区的起始地址,否则,返回 NULL。vmalloc() 和 kmalloc() 申请的内存有所不同,kmalloc() 所申请内存的线性地址与物理地址都是连续的,而 vmalloc() 所申请的内存线性地址连续而物理地址则是离散的,两个地址之间通过内核页表进行映射。

vmalloc 机制使得内核通过连续的线性地址来访问非连续的物理页框,这样可以最大限度的使用高端物理内存。

vmalloc() 的工作方式理解起来很简单:

  1. 寻找一个新的连续线性地址空间;
  2. 依次分配一组非连续的页框;
  3. 为线性地址空间和非连续页框建立映射关系,即修改内核页表;

vmalloc() 的内存分配原理与用户态的内存分配相似,都是通过连续的虚拟内存来访问离散的物理内存,并且虚拟地址和物理地址之间是通过页表进行连接的,通过这种方式可以有效的使用物理内存。但是应该注意的是,vmalloc() 申请物理内存时是立即分配的,因为内核认为这种内存分配请求是正当而且紧急的;相反,用户态有内存请求时,内核总是尽可能的延后,毕竟用户态跟内核态不在一个特权级。

原创文章 592 获赞 1483 访问量 197万+

猜你喜欢

转载自blog.csdn.net/Jmilk/article/details/100583098