内存管理概览

NUMA

非一致性内存架构(Non-uniform Memory Architecture)是为了解决传统的对称多处理(Symmetric Multi-processor)系统中的可扩展性问题而诞生的。

NUMA系统拥有多条内存总线,于是将几个处理器通过内存总线与一块内存相连构成一个组,这样整个庞大的系统就可以被分为若干个组,这个组的概念在NUMA系统中被称为节点(node)

很多大型机器都采用NUMA架构,将内存和CPU分为很多组,每一组称为一个节点(node)。节点与节点之间的互相访问,会因为“距离”的不同导致不同的开销。Linux通过struct pglist_data这个结构体来描述节点

buddy伙伴系统

Linux内核内存管理的一项重要工作就是如何在频繁申请释放内存的情况下,避免碎片的产生。Linux采用伙伴系统解决外部碎片的问题,采用slab解决内部碎片的问题

Buddy算法最主要的的特点任何时候区域里的空闲内存都能以2的n次方进行拆分或合并。

伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求。在最初,只有一个块,也就是整个内存,假如为1M大小,而允许的最小块为64K,那么当我们申请一块200K大小的内存时,就要先将1M的块分裂成两等分,各为512K,这两分之间的关系就称为伙伴,然后再将第一个512K的内存块分裂成两等分,各位256K,将第一个256K的内存块分配给内存,这样就是一个分配的过程。

cat /proc/buddyinfo

Node 0, zone Normal 8323 14676 6207 1328 237 209 40 9 0 0 0
8323说明Normal Zone里面1页空闲的还有8323个
14676说明Normal Zone里面2页空闲的还有14676个

Zone内存区域

为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点

  • 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为pg_data_t的实例. 系统中每个节点被链接到一个以NULL结尾的pgdat_list链表中,而其中的每个节点利用pg_data_tnode_next字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构.
  • 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射.
kernel/msm-5.4/include/linux/mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES

};
管理内存域描述
ZONE_DMA在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。标记了适合DMA的内存域. 该区域的长度依赖于处理器类型. 这是由于古老的ISA设备强加的边界. 但是为了兼容性, 现代的计算机也可能受此影响
ZONE_DMA32标记了使用32位地址字可寻址, 适合DMA的内存域. 显然, 只有在53位系统中ZONE_DMA32才和ZONE_DMA有区别, 在32位系统中, 本区域是空的, 即长度为0MB, 在Alpha和AMD64系统上, 该内存的长度可能是从0到4GB
ZONE_NORMAL标记了可直接映射到内存段的普通内存域. 这是在所有体系结构上保证会存在的唯一内存区域, 但无法保证该地址范围对应了实际的物理地址. 例如, 如果AMD64系统只有两2G内存, 那么所有的内存都属于ZONE_DMA32范围, 而ZONE_NORMAL则为空
ZONE_HIGHMEM保留给系统使用,是系统中预留的可用内存空间。标记了超出内核虚拟地址空间的物理内存段, 因此这段地址不能被内核直接映射
ZONE_MOVABLE内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用
ZONE_DEVICE为支持热插拔设备而分配的Non Volatile Memory非易失性内存
MAX_NR_ZONES充当结束标记, 在内核中想要迭代系统中所有内存域, 会用到该常亮

在64位系统中, 并不需要高端内存, 因为AM64的linux采用4级页表,支持的最大物理内存为64TB, 对于虚拟地址空间的划分,将0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000这128T地址用于用户空间;而0xffff,8000,0000,0000以上的128T为系统空间地址, 这远大于当前我们系统中的内存空间, 因此所有的物理地址都可以直接映射到内核中, 不需要高端内存的特殊映射

详情Zone和Watermark

文件页和匿名页

  • 文件页(file-backed pages):有文件背景的页面,如代码段,要交换的话,可以直接和硬盘对应的文件进行交换
  • 匿名页(anonymous pages):如堆,栈,数据段等,不是以文件形式存在,因此无法和磁盘文件交换,但可以通过硬盘上划分额外的swap交换分区或使用交换文件进行交换。即上面wap作为名词的意思。Swap分区可以将不活跃的页交换到硬盘中,缓解内存紧张。

通过free命令可以看到当前page cache占用内存的大小,free命令中会打印buffers和cached(有的版本free命令将二者放到一起了)。通过文件系统来访问文件(挂载文件系统,通过文件名打开文件)产生的缓存就由cached记录,而直接操作裸盘(打开/dev/sda设备去读写)产生的缓存就由buffers记录。

“内部碎片”,是指系统已经分配给用户使用、用户自己没有用到的那部分存储空间;
“外部碎片”,是指系统无法把它分配出去供用户使用的那部分存储空间。

在这里插入图片描述

slub算法

内核管理页面使用了2个算法:伙伴算法和slub算法,伙伴算法以页为单位管理内存,但在大多数情况下,程序需要的并不是一整页,而是几个、几十个字节的小内存。于是需要另外一套系统来完成对小内存的管理,这就是slub系统。slub系统运行在伙伴系统之上,为内核提供小内存管理的功能。

物理页按照对象(object)大小组织成单向链表,对象大小时候objsize指定的。例如16字节的对象大小,每个object就是16字节,每个object包含指向下一个object的指针,该指针的位置是每个object的起始地址+offset。

在这里插入图片描述

SLAB

slab与Buddy的关系:

  • slab与Buddy都是内存分配器。
  • slab的内存来自Buddy
  • slab与Buddy在算法上级别对等。Buddy把内存条当作一个池子来管理,slab是把从Buddy拿到的内存当作一个池子来管理的。

SLAB 作用

对从Buddy拿到的内存进行二次管理,以更小的单位进行分配和回收(注意,是回收而不是释放),防止了空间的浪费。
让频繁使用的对象尽量分配在同一块内存区间并保留基本数据结构,提高程序效率。

所有的内存申请最终都来自Buddy,但malloc/free及kmalloc/kfree都不与Buddy一一对应,libc和slab都相当于二级分配器。
在这里插入图片描述

内核内存申请和回收流程

回收有两种不同的机制:

  • 一个是使用 kswapd进程对内存进行周期检查 ,以保证平常状态下剩余内存尽可能够用。
    参见mm/vmscan.c中的kswapd()主逻辑
  • 另一个是 直接内存回收(directpagereclaim) ,就是当内存分配时没有空闲内存可以满足要求时,触发直接内存回收。
    参见内核代码中的mm/page_alloc.c中的__alloc_pages_slowpath方法

内存回收操作主要针对的就是内存中的文件页(file cache)和匿名页
关于活跃(active)还是不活跃(inactive)的判断内核会使用lru算法进行处理并进行标记

整个扫描的过程分几个循环:

  • 首先扫描每个zone上的cgroup组;
  • 然后再以cgroup的内存为单元进行page链表的扫描;
  • 内核会先扫描anon的active链表,将不频繁的放进inactive链表中,然后扫描inactive链表,将里面活跃的移回active中;
  • 进行swap的时候,先对inactive的页进行换出;
  • 如果是file的文件映射page页,则判断其是否为脏数据,如果是脏数据就写回,不是脏数据可以直接释放。

内存回收这个行为会对两种内存的使用进行回收:

一种是anon的匿名页内存,主要回收手段是swap;
另一种是file-backed的文件映射页,主要的释放手段是写回和清空。

cat /proc/meminfo看到的active和inactive的内存就是指lru算法里面去评估的一个页面的使用情况(有没有被访问过),inactive的页面中最inactive的页面最先被回收。如果inactive的页都回收了但内存仍然不够,也会从active的页中回收相对最不活跃的页面。
所以我们就知道,如果lowmem被使用殆尽,触及low或min水位,内核的普通kmalloc就申请不到内存了,就会触发cache/buffers的回收和匿名页swap,再不行就OOM了。

  • Swappiness
    当系统内存不足时,可以从filebacked pages或者anonymous pages回收内存,不论哪个被回收,再次被加载进内存一定都会影响程序的效率。具体从哪里回收,Linux提供swappiness值作为衡量标准。
    定义在/proc/sys/vm/swappiness ,Swappiness越大,越倾向于回收匿名页;swappiness越小,越倾向于回收file-backed的页面。当然,它们的回收方法都是一样的LRU算。
    如果这个值为0,那么内存在free和file-backed使用的页面总量小于高水位标记(high water mark)之前,不会发生交换。如果swappiness设置为100,那么匿名页和文件将用同样的优先级进行回收
    如果file-backed中的数据不是脏数据的话,那么可以不用写回,这样就没有IO发生,而一旦进行交换,就一定会造成IO。
    Linux内核对这部分逻辑的实现代码在 get_scan_count() 这个方法中,这个方法被 shrink_lruvec() 调用
    在这里插入图片描述
  • zRamswap
    对于嵌入式设备,它的磁盘是SD卡,MMC,一方面速度较慢,另一方面,有使用寿命的问题,不太适合做swap分区。嵌入式设备一般会从内存中拿出一小部分当作虚拟内存,这个就是zRam。但是这样直接用又没有什么意义,因为虚拟内存的目的就是在内存不足时“扩展内存”,现在内存还是那片内存就是换了个说法,所以为了“扩展内存”,当系统把内存交换到这个虚拟内存,通常是以压缩的方式存储,当swap in时在解压。这样就某种程度的“扩展了内存”,但是缺点是增加了CPU的压力,需要进行压缩和解压缩。

kswapd回收内存原理

kswapd进程要周期对内存进行检测,达到一定阈值的时候开始进行内存回收。
Linux内核使用水位标记(watermark)的概念来描述这个压力情况。三种内存水位标记:high、low、min。他们 所标记的含义分别为:

  • 剩余内存在high以上表示内存剩余较多,目前内存使用压力不大;
  • high-low的范围表示目前剩余内存存在一定压力;
  • low-min表示内存开始有较大使用压力,剩余内存不多了;
  • min是最小的水位标记,当剩余内存达到这个状态时,就说明内存面临很大压力。
  • 小于min这部分内存,内核是保留给特定情况下使用的,一般不会分配。

当系统剩余内存低于watermark[low]的时候,内核的kswapd开始起作用,进行内存回收。直到剩余内存达到watermark[high]的时候停止

如果内存消耗导致剩余内存达到了或超过了watermark[min]时,就会触发直接回收(direct reclaim)

zonefile + zonefree <= high_wmark_pages(zone)

如果zonefile还有的话,就可以尽量通过清空文件缓存获得部分内存,而不必只使用swap方式对anon的内存进行交换。
在全局回收的状态下(有global_reclaim(sc)标记),如果当前的文件映射内存总量+剩余内存总量的值评估小于等于watermark[high]标记的时候,就可以进行直接swap了

writeback异步回收内存原理

ulmkd kill进程原理

TLB

为了提高CPU对内存的访问效率,在CPU第一次访问内存之前,加了一个快速缓冲区寄存器TLB(Translation Lookaside Buffer),TLB是MMU的核心部件,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache,俗称“快表”。当TLB中没有缓冲对应的地址转换关系时,需要通过对内存中转换表(大多数处理器的转换表为多级页表)的访问来获得虚拟地址和物理地址的对应关系,引出MMU的另一核心部件TTW(TranslationTable walk)。TTW成功后,结果应写入TLB中。TLB里面存放了近期访问过的页表项。当CPU发起一次访问时,先到TLB中查询是否存在对应的页表项,如果有就直接返回了。整个过程只需要访问一次内存

kmalloc、vmalloc、malloc比较

  • kmalloc函数是基于slab算法的,从物理内存的low mem获取内存,并线性映射到物理内存映射区(映射过程开机就已经完成了),由于是线性映射,物理地址和虚拟地址存在简单的转换关系(物理地址和虚拟地址的值只是相差了一个固定的偏移),所以使用kmalloc分配内存是十分高效的。

  • vmalloc函数分配内存的过程需要先通过alloc_pages函数获取内存(获取范围是整个内存条),然后在通过复杂的逻辑转换(注意,vmalloc并不是简单的线性映射,它获取的内存并不是连续的),把物理内存映射到vmalloc映射区。这个过程比较复杂,所以,如果只是使用vmalloc分配很小的内存空间是不合适的。

  • malloc是标准C库的函数,C库对申请的内存做二次管理,类似Slab。但是注意一点,当我们使用malloc函数申请一片内存时,实际上是从C库获取的内存,就是说,调用malloc返回后,系统未必给你一片真正的内存,分两种情况

  • C库还持有足够的内存,那么malloc就可以直接分配到C库现有的内存
  • C库没有足够的内存,malloc返回时,系统只是把要申请的内存大小的虚拟地址空间全部映射到同一块已经清零的物理内存,当我们实际要写这片内存的时候,才通过brk/mmap系统调用分配真实的物理内存,并改写进程页表。
  • 另外有一点值得一提,vmalloc映射区,除了vmalloc函数分配的内存会映射在该区域,设备的寄存器也同样会通过ioremap映射到该区域。

zone_reclaim_mode

zone_reclaim_mode模式是在2.6版本后期开始加入内核的一种模式,可以用来管理当一个内存区域(zone)内部的内存耗尽时,是从其内部进行内存回收还是可以从其他zone进行回收的选项,我们可以通过 /proc/sys/vm/zone_reclaim_mode 文件对这个参数进行调整。

在申请内存时(内核的get_page_from_freelist()方法中),内核在当前zone内没有足够内存可用的情况下,会根据zone_reclaim_mode的设置来决策是从下一个zone找空闲内存还是在zone内部进行回收。这个值为0时表示可以从下一个zone找可用内存,非0表示在本地回收。

这个文件可以设置的值及其含义如下:

  • echo 0 > /proc/sys/vm/zone_reclaim_mode:意味着关闭zone_reclaim模式,可以从其他zone或NUMA节点回收内存。这在很多应用场景下可以提高效率,比如文件服务器,或者依赖内存中cache比较多的应用场景。
  • echo 1 > /proc/sys/vm/zone_reclaim_mode:表示打开zone_reclaim模式,这样内存回收只会发生在本地节点内。
  • echo 2 > /proc/sys/vm/zone_reclaim_mode:在本地回收内存时,可以将cache中的脏数据写回硬盘,以回收内存。
  • echo 4 > /proc/sys/vm/zone_reclaim_mode:可以用swap方式回收内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值