深入理解Linux内存管理

  • 1.1 内存管理的意义
  • 1.2 原始内存管理
  • 1.3 分段内存管理
  • 1.4 分页内存管理
  • 1.5 内存管理的目标
  • 1.6 Linux内存管理体系
  • 2.1 物理内存节点
  • 2.2 物理内存区域
  • 2.3 物理内存页面
  • 2.4 物理内存模型
  • 2.5 三级区划关系
  • 3.1 Buddy System
    • 3.1.1 伙伴系统的内存来源
    • 3.1.2 伙伴系统的管理数据结构
    • 3.1.3 伙伴系统的算法逻辑
    • 3.1.4 伙伴系统的接口
    • 3.1.5 伙伴系统的实现
  • 3.2 Slab Allocator
    • 3.2.1 Slab接口
    • 3.2.2 Slab实现
    • 3.2.3 Slob实现
    • 3.2.4 Slub实现
  • 3.3 Kmalloc
  • 3.4 Vmalloc
  • 3.5 CMA
  • 4.1 内存规整
  • 4.2 页帧回收
  • 4.3 交换区
  • 4.4 OOM Killer
  • 5.1 ZRAM
  • 5.2 ZSwap
  • 5.3 ZCache
  • 6.1 页表
  • 6.2 MMU
  • 6.3 缺页异常
  • 7.1 内核空间
  • 7.2 用户空间
  • 8.1 总体统计
  • 8.2 进程统计

一、内存管理概览

内存是计算机最重要的资源之一,内存管理是操作系统最重要的任务之一。内存管理并不是简单地管理一下内存而已,它还直接影响着操作系统的风格以及用户空间编程的模式。可以说内存管理的方式是一个系统刻入DNA的秉性。既然内存管理那么重要,那么今天我们就来全面系统地讲一讲Linux内存管理。

1.1 内存管理的意义

外存是程序存储的地方,内存是进程运行的地方。外存相当于是军营,内存相当于是战场。选择一个良好的战场才有利于军队打胜仗,实现一个完善的内存管理机制才能让进程多快好省地运行。如何更好地实现内存管理一直是操作系统发展的一大主题。在此过程中内存管理的基本模式也经历了好几代的发展,下面我们就来看一下。

1.2 原始内存管理

最初的时候,内存管理是十分的简陋,大家都运行在物理内存上,内核和进程运行在一个空间中,内存分配算法有首次适应算法(FirstFit)、最佳适应算法(BestFit)、最差适应算法(WorstFit)等。显然,这样的内存管理方式问题是很明显的。内核与进程之间没有做隔离,进程可以随意访问(干扰、窃取)内核的数据。而且进程和内核没有权限的区分,进程可以随意做一些敏感操作。还有一个问题就是当时的物理内存非常少,能同时运行的进程比较少,运行进程的吞吐量比较少。

1.3 分段内存管理

于是第二代内存管理方式,分段内存管理诞生了。分段内存管理需要硬件的支持和软件的配合。在分段内存中,软件可以把物理内存分成一个一个的段,每个段都有段基址和段限长,还有段类型和段权限。段基址和段限长确定一个段的范围,可以防止内存访问越界。段与段之间也可以互相访问,但是不能随便访问,有一定的规则限制。段类型分为代码段和数据段,正好对应程序的代码和数据,代码段是只读和可执行的,数据段有只读数据段和读写数据段。代码段是不可写的,只读数据段也是不可写,数据段是不可执行的,这样又增加了一层安全性。段权限分为有特权(内核权限)和无特权(用户权限),内核的代码段和数据段都设置为特权段,进程的代码段和数据段都设置为用户段,这样进程就不能随意访问内核了。当CPU执行特权段代码的时候会把自己设置为特权模式,此时CPU可以执行所以的指令。当CPU执行用户段代码的时候会把自己设置为用户模式,此时CPU只能执行普通指令,不能执行敏感指令。

至此,分段内存管理完美解决了原始内存管理存在的大部分问题:进程与内核之间的隔离实现了,进程不能随意访问内核了;CPU特权级实现了,进程无法再执行敏感指令了;内存访问的安全性提高了,越界访问和野指针问题得到了一定程度的遏制。但是分段内存管理还有一个严重的问题没有解决,那就是当时的物理内存非常少的问题。为此当时想的办法是用软件方法来解决,而且是进程自己解决。程序员在编写程序的时候就要想好,把程序分成几个模块,关联不大的模块,它们占用相同的物理地址。然后再编写一个overlay manager,在程序运行的时候,动态地加载即将会运行的模块,覆盖掉暂时不用的模块。这样一个程序占用较少的物理内存,也能顺利地运行下去。显然这样的方法很麻烦,每个程序都要写overlay manager也不太优雅。

1.4 分页内存管理

于是第三代内存管理方式,虚拟内存管理(分页内存管理)诞生了。虚拟内存管理也是需要硬件的支持和软件的配合。在虚拟内存中,CPU访问任何内存都是通过虚拟内存地址来访问的,但是实际上最终访问内存还是得用物理内存地址。所以在CPU中存在一个MMU,负责把虚拟地址转化为物理地址,然后再去访问内存。而MMU把虚拟地址转化为物理的过程需要页表的支持,页表是由内核负责创建和维护的。一套页表可以用来表达一个虚拟内存空间,不同的进程可以用不同的页表集,页表集是可以不停地切换的,哪个进程正在运行就切换到哪个进程的页表集。于是一个进程就只能访问自己的虚拟内存空间,而访问不了别人的虚拟内存空间,这样就实现了进程之间的隔离。一个虚拟内存空间又分为两部分,内核空间和用户空间,内核空间只有一个,用户空间有N个,所有的虚拟内存空间都共享同一个内核空间。内核运行在内核空间,进程运行在用户空间,内核空间有特权,用户空间无特权,用户空间不能随意访问内核空间。这样进程和内核之间的隔离就形成了。内核空间的代码运行的时候,CPU会把自己设置为特权模式,可以执行所有的指令。用户空间运行的时候,CPU会把自己设置为用户模式,只能执行普通指令,不能执行敏感指令。

至此,分段内存实现的功能,虚拟内存都做到了,下面就是虚拟内存如何解决物理内存不足的问题了。系统刚启动的时候还是运行在物理内存上的,内核也被全部加载到了物理内存。然后内核建立页表体系并开启分页机制,内核的物理内存和虚拟内存就建立映射了,整个系统就运行在虚拟内存上了。后面运行进程的时候就不是这样了,内核会记录进程的虚拟内存分配情况,但是并不会马上分配物理内存建立页表映射,而是让进程先运行着。进程运行的时候,CPU都是通过MMU访问虚拟内存地址的,MMU会用页表去解析虚拟内存,如果找到了其对应的物理地址就直接访问,如果页表项是空的,就会触发缺页异常,在缺页异常中会去分配物理内存并建立页表映射。然后再重新执行刚才的那条指令,然后CPU还是通过MMU访问内存,由于页表建立好了,这下就可以访问到物理内存了。当物理内存不足的时候,内核还会把一部分物理内存解除映射,把其内容存放到外存中,等其再次需要的时候再加载回来。这样,一个进程运行的时候并不需要立马加载其全部内容到物理内存,进程只需要少量的物理内存就能顺利地运行,于是系统运行进程的吞吐量就大大提高了。

分页内存管理不仅实现了分段内存管理的功能,还有额外的优点,于是分段内存管理就没有存在的意义了。但是这里面还有一个历史包袱问题。对于那些比较新的CPU,比如ARM、RISC-V,它们没有历史包袱,直接实现的就是分页内存管理,根本不存在分段机制。但是对于x86就不一样了,x86是从直接物理内存、分段内存、分页内存一步一步走过来的,有着沉重的历史包袱。在x86 32上,分段机制和分页机制是并存的,系统可以选择只使用分段机制或者两种机制都使用。Linux的选择是使用分页机制,并在逻辑上屏蔽分段机制,因为分段机制是不能禁用的。逻辑上屏蔽分段机制的方法是,所有段的段基址都是0,段限长都是最大值,这样就相当于是不分段了。分段机制无法禁用的原因是因为CPU特权级是在分段机制中实现的,分页机制没有单独的CPU特权级机制。所以Linux创建了4个段,__KERNEL_CS、__KERNEL_DS用于内核空间,__USER_CS、__USER_DS用于用户空间,它们在会空间切换时自动切换,这样CPU特权级就跟着切换了。对于x86 64,从硬件上基本屏蔽了分段,因为硬件规定CS、DS、ES、SS这些段的段基址必须是0,段限长必须是最大值,软件设置其它值也没用。

因此我们在这里要强调一句,分段机制早就是历史了,x86 64已经从硬件上屏蔽了分段机制,Linux早就从软件上屏蔽了分段机制。X86 CPU的寄存器CS、DS、ES、FS和内核的__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS,已经不具有分段的意义了,它们的作用是为了实现CPU特权级的切换。

1.5 内存管理的目标

内存管理的目标除了前面所说的进程之间的隔离、进程与内核之间的隔离、减少物理内存并发使用\\\\\\的数量之外,还有以下几个目标。

1.减少内存碎片,包括外部碎片和内部碎片。外部碎片是指还在内存分配器中的内存,但是由于比较分散,无法满足用户大块连续内存分配的申请。内部碎片是指你申请了5个字节的内存,分配器给你分配了8个字节的内存,其中3个字节的内存是内部碎片。内存管理要尽量同时减少外部碎片和内部碎片。

内存分配接口要灵活多样,同时满足多种不同的内存分配需求。既要满足大块连续内存分配的需求,又能满足小块零碎内存分配的需求。

3.内存分配效率要高。内存分配要尽量快地完成,比如说你设计了一种算法,能完全解决内存碎片问题,但是内存算法实现得特别复杂,每次分配都需要1毫秒的时间,这就不可取了。

4.提高物理内存的利用率。比如及时回收物理内存、对内存进行压缩。

1.6 Linux内存管理体系

Linux内存管理的整体模式是虚拟内存管理(分页内存管理),并在此基础上建立了一个庞大的内存管理体系。我们先来看一下总体结构图。

整个体系分为3部分,左边是物理内存,右边是虚拟内存,中间是虚拟内存映射(分页机制)。我们先从物理内存说起,内存管理的基础还是物理内存的管理。

物理内存那么大,应该怎么管理呢?首先要对物理内存进行层级区划,其原理可以类比于我国的行政区划管理。我国幅员辽阔,国家直接管理个人肯定是不行的,我国采取的是省县乡三级管理体系。把整个国家按照一定的规则和历史原因分成若干个省,每个省由省长管理。每个省再分成若干个县,每个县由县长管理。每个县再分成若干个乡,每个乡由乡长管理,乡长直接管理个人。(注意,类比是理解工具,不是论证工具)。对应的,物理内存也是采用类似的三级区域划分的方式来管理的,三个层级分别叫做节点(node)、区域(zone)、页面(page),对应到省、县、乡。系统首先把整个物理内存划分为N个节点,内存节点只是叫节点,大家不能把它看成一个点,要把它看成是相当于一个省的大区域。每个节点都有一个节点描述符,相当于是省长。节点下面再划分区域,每个区域都有区域描述符,相当于是县长。区域下面再划分页面,每个页面都有页面描述符,相当于是乡长。页面再下面就是字节了,相当于是个人。

对物理内存建立三级区域划分之后,就可以在其基础之上建立分配体系了。物理内存的分配体系可以类比于一个公司的销售体系,有工厂直接进行大额销售,有批发公司进行大量批发,有小卖部进行日常零售。物理内存的三级分配体系分别是buddy system、slab allocator和kmalloc。buddy system相当于是工厂销售,slab allocator相当于是批发公司,kmalloc相当于是小卖部,分别满足人们不同规模的需求。

物理内存有分配也有释放,但是当分配速度大于释放速度的时候,物理内存就会逐渐变得不够用了。此时我们就要进行内存回收了。内存回收首先考虑的是内存规整,也就是内存碎片整理,因为有可能我们不是可用内存不足了,而是内存太分散了,没法分配连续的内存。内存规整之后如果还是分配不到内存的话,就会进行页帧回收。内核的物理内存是不换页的,所以内核只会进行缓存回收。用户空间的物理内存是可以换页的,所以会对用户空间的物理内存进行换页以便回收其物理内存。用户空间的物理内存分为文件页和匿名页。对于文件页,如果其是clean的,可以直接丢弃内容,回收其物理内存,如果其是dirty的,则会先把其内容写回到文件,然后再回收内存。对于匿名页,如果系统配置的有swap区的话,则会把其内容先写入swap区,然后再回收,如果系统没有swap区的话则不会进行回收。把进程占用的但是当前并不在使用的物理内存进行回收,并分配给新的进程来使用的过程就叫做换页。进程被换页的物理内存后面如果再被使用到的话,还会通过缺页异常再换入内存。如果页帧回收之后还没有得到足够的物理内存,内核将会使用最后一招,OOM Killer。OOM Killer会按照一定的规则选择一个进程将其杀死,然后其物理内存就被释放了。

内核还有三个内存压缩技术zram、zswap、zcache,图里并没有画出来。它们产生的原因并不相同,zram和zswap产生的原因是因为把匿名页写入swap区是IO操作,是非常耗时的,使用zram和zswap可以达到用空间换时间的效果。zcache产生的原因是因为内核一般都有大量的pagecache,pagecache是对文件的缓存,有些文件缓存暂时用不到,可以对它们进行压缩,以节省内存空间,到用的时候再解压缩,以达到用时间换空间的效果。

物理内存的这些操作都是在内核里进行的,但是CPU访问内存用的并不是物理内存地址,而是虚拟内存地址。内核需要建立页表把虚拟内存映射到物理内存上,然后CPU就可以通过MMU用虚拟地址来访问物理内存了。虚拟内存地址空间分为两部分,内核空间和用户空间。内核空间只有一个,其页表映射是在内核启动的早期就建立的。用户空间有N个,用户空间是随着进程的创建而建立的,但是其页表映射并不是马上建立,而是在程序的运行过程中通过缺页异常逐步建立的。内核页表建立好了之后就不会再取消了,所以内核是不换页的,用户页表建立之后可能会因为内存回收而取消,所以用户空间是换页的。内核页表是在内核启动时建立的,所以内核空间的映射是线性映射,用户空间的页表是在运行时动态创建的,不可能做到线性映射,所以是随机映射。

有些书上会说用户空间是分页的,内核是不分页的,这是对英语paging的错误翻译,paging在这里不是分页的意思,而是换页的意思。分页是指整个分页机制,换页是内存回收中的操作,两者的含义是完全不同的。

现在我们对Linux内存管理体系已经有了宏观上的了解,下面我们就来对每个模块进行具体地分析。

二、物理内存区划

内核对物理内存进行了三级区划。为什么要进行三级区划,具体怎么划分的呢?这个不是软件随意决定的,而是和硬件因素有关。下面我们来看一下每一层级划分的原因,以及软件上是如果描述的。

2.1 物理内存节点

我国的省为什么要按照现在的这个形状来划分呢,主要是依据山川地形还有民俗风情等历史原因。那么物理内存划分为节点的原因是什么呢?这就要从UMA、NUMA说起了。我们用三个图来看一下。

图中的CPU都是物理CPU。当一个系统中的CPU越来越多、内存越来越多的时候,内存总线就会成为一个系统的瓶颈。如果大家都还挤在同一个总线上,速度必然很慢。于是我们可以采取一种方法,把一部分CPU和一部分内存直连在一起,构成一个节点,不同节点之间CPU访问内存采用间接方式。节点内的内存访问速度就会很快,节点之间的内存访问速度虽然很慢,但是我们可以尽量减少节点之间的内存访问,这样系统总的内存访问速度就会很快。

Linux中的代码对UMA和NUMA是统一处理的,因为UMA可以看成是只有一个节点的NUMA。如果编译内核时配置了CONFIG_NUMA,内核支持NUMA架构的计算机,内核中会定义节点指针数组来表示各个node。如果编译内核时没有配置CONFIG_NUMA,则内核只支持UMA架构的计算机,内核中会定义一个内存节点。这样所有其它的代码都可以统一处理了。

下面我们先来看一下节点描述符的定义。
linux-src/include/linux/mmzone.h

typedef struct pglist_data { /*  * node_zones contains just the zo
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值