认识 Linux 内存构成:Linux 内存调优之内存分配机制和换页行为认知

99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式

写在前面


  • 博文内容涉及 Linux 中内存分配和换页机制的基本认知
  • 理解不足小伙伴帮忙指正 :),生活加油

99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式


前面的文章和小伙伴们分享了 Linux 虚拟内存,物理内存,以及页表,TLB,大页认知,今天我们来看看具体的内存分配以及换页行为

内存分配机制和换页行为认知

内存分配机制

前面的博文我们有讲到,Linux 系统中进程内存的使用是通过申请虚拟内存,按需分配物理内存的方式内存页是内存的基本单位,Linux 一个标准的内存页一般为 4kb, 具体要由 CPU 确定,虚拟内存地址物理内存地址通过页表来建立映射关系,页表是由多个页表项构成,一个内存页对应一个页表项,所以映射会造成会有一个巨大的页表,所以一般系统会使用多级页表的方式按需建立映射关系,类似文章的四级目录一样,进程在每次访问内存需要查询页表,找到物理地址加上偏移量,获取实际的物理地址,在多级页表的情况下因为会查询多个表,所以会有延迟,所以会通过TLB( CPU 硬件缓存)来缓存高频的页表项,每次访问页表先查询 TLB,所以TLB的命中率会直接影响查询效率。

同时,Linux 提供大页支持,包含标准大页透明大页,透明大页是内核自动管理,用于合并多个标准内存页,标准大页需要手动预分配连续的物理内存,用于减少页表层级。提高查询效率,当页表查询没有映射物理内存页时,会触发缺页异常,缺页异常会分配物理内存(按需分配)写入页表建立映射关系,同时写入TLB

今天我们来看一下系统中进程 申请内存是如何发生的

下面是一个典型的内存页的生命周期

在这里插入图片描述

1.进程申请虚拟内存的过程是内存分配器与内核协同完成的机制,对使用 glibc 内存分配器的进程来说,glibc 提供了一系列内存分配的函数,包括 malloc() 和 free()等,应用程序发起内存分配请求会调用相关的内核方法(例如,libc malloc())。

在Linux系统中,malloc()、free()、realloc()和calloc()是常用的内存分配和释放函数,主要用于动态内存管理。

  • malloc()用于分配指定大小的内存块,并返回指向该内存的指针。如果分配失败,则返回NULL。
  • free()用于释放之前通过malloc()、calloc()或realloc()分配的内存,以避免内存泄漏。
  • realloc()用于调整已分配内存块的大小,可以扩大或缩小内存块,若无法满足新大小的要求,可能会返回NULL。
  • calloc()不仅分配内存,还会将其初始化为零,适用于需要初始化内存的场景

这些函数通常依赖于底层的系统调用如brk()和mmap()来管理虚拟内存,当进程调用 malloc()等函数时,glibc首先在进程的​​用户虚拟地址空间​​中划分一块连续的虚拟内存区域此时仅进行逻辑分配,不涉及物理内存占用,当实际需要物理内存时,会通过触发缺页异常映射物理内存。

2.这块连续的虚拟内存区域,对于小内存分配​​,优先从 brk() 扩展的堆区分配,利用空闲链表优化碎片。对于大内存分配​,直接调用 mmap() 映射独立,避免堆区碎片化

小内存分配​​:优先从 brk()

对于 brk() 的方式,会直接从空闲列表中响应请求,在应用程序启动时,动态内存分配库会根据堆内存的初始状态初始化空闲链表,首次分配时​​,若程序未显式初始化堆,首次调用 malloc()calloc() 时会通过 brk() 扩展堆空间,并初始化空闲链表管理堆内存块。某些场景下,需要显示初始化,需手动初始化链表头指针为空(head = NULL),表示空链表,例如 C 语言中的链表构造

空闲列表(Free List)​​用于动态管理堆内存中的空闲内存块。其本质是通过链表形式记录所有未被占用的内存区域,实现内存的高效分配与回收,内核为每个 CPU 和 DRAM 组维护一组空闲内存列表(freelist)

在这里插入图片描述

同时,内核软件本身的内存分配需求也从这个空闲内存列表直接获取,一般通过内核内存分配器进行,例如,slab 分配器

只有在内存用尽时才需要扩展堆内存,当空闲列表耗尽时,通过 brk 系统调用上移堆顶指针,扩大虚拟地址空间,所以 brk 的优点是批量扩展堆空间,可以减少用户态与内核态的切换,缺点是

释放内存时会将被释放内存的地址记录下来,以便提供给未来的malloc() 使用。通过空闲列表缓存释放的内存块,避免频繁调用系统调用。

对于小块内存(<128KB):释放后挂入 fastbinssmall bins,供后续 malloc 快速复用。fastbins 不合并相邻块以提升效率,而 small bins 则按固定大小分类管理(fastbins等为空闲链表数据结构)。

默认仅在堆顶存在连续空闲内存且超过阈值(默认128KB)时,调用 malloc_trim 通过 brk 下移堆顶归还内存。频繁收缩会因系统调用开销影响性能,因此倾向于保留虚拟内存

虚拟内存的惰性回收,管理的堆内存本质上是进程的虚拟地址空间,其释放仅标记为可复用,不立即归还物理内存。物理内存的回收由内核的页框管理算法决定,例如通过缺页中断按需分配、通过 kswapd 内核线程异步回收。保留虚拟内存池避免了重复的 brk 调用,减少了用户态与内核态的切换开销,尤其在高并发场景下显著提升效率。

如果希望强制内存归还,调用 malloc_trim(0) 可立即触发堆收缩,将空闲内存归还操作系统。这在需要严格控制内存占用的场景(如嵌入式系统)中尤为重要。

大内存分配​​:直接调用 mmap()

​​匿名内存映射​​,mmap() 创建独立的内存段(如 MAP_ANONYMOUS),不依赖堆区直接映射到进程虚拟地址空间,一般直接按页对齐分配(对应内存页页的整数倍),直接建立虚拟地址到物理内存的映射。

mmap() 既可以使用标准的 ​​4KB 内存页​​,也可以使用 ​​静态大页(HugePages)​,如果进程使用的是 mmap()系统函数调用,则必须挂载-个 hugetlbfs 文件系统。

1
2
3
4
┌──[root@liruilongs.github.io]-[~]
└─$mkdir /largepage
┌──[root@liruilongs.github.io]-[~]
└─$mount -t hugetlbfs none /largepage

mmap 创建的内存段可单独释放(通过 munmap),避免碎片,归还操作系统。

3.内存分配之后,应用程序试图使用 store/load 指令来使用之前分配的内存地址,这就要调用 CPU 内部的内存管理单元(MMU)来进行虚拟地址到物理地址的转换。会发现页表里面没有对应的页表项,MMU 触发缺页错误(page fault)。

4.缺页错误由系统内核处理。在对应的处理函数中,内核会在物理内存空闲列表中找到一个空闲地址并映射到该虚拟地址。会把映射关系写入 MMU,并且更新TLB,

到这里该用户进程就占据了一个新的物理内存页。进程所使用的全部物理内存数量称为常驻集大小(Resident set Size,RSS)

ps 命令的 RSS 列

1
2
3
4
5
6
7
8
9
10
11
12
┌──[root@liruilongs.github.io]-[~]
└─$ps -e -o pid,vsz,rss,comm | awk '$2 > 0 {print}'
PID VSZ RSS COMMAND
1 170808 14208 systemd
704 26824 11520 systemd-journal
719 34968 12160 systemd-udevd
892 18156 4540 auditd
913 10796 4736 dbus-broker-lau
。。。。。。。。。。
4177 6664 3712 awk
┌──[root@liruilongs.github.io]-[~]
└─$

top 命令的 RES 列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──[root@liruilongs.github.io]-[~]
└─$top
top - 11:46:44 up 8 min, 1 user, load average: 6.52, 17.45, 10.38
Tasks: 449 total, 1 running, 448 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.9 us, 1.1 sy, 0.0 ni, 96.8 id, 0.0 wa, 0.6 hi, 0.5 si, 0.0 st
MiB Mem : 15730.5 total, 7503.6 free, 7147.4 used, 1079.5 buff/cache
MiB Swap: 2068.0 total, 2068.0 free, 0.0 used. 8177.7 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1947 42418 20 0 555196 132972 20272 S 1.3 0.8 0:12.86 heat-engine
10320 42436 20 0 540276 122200 17492 S 1.3 0.8 0:08.95 nova-scheduler
949 root 20 0 694072 31640 16876 S 1.0 0.2 0:16.58 tuned
1945 42415 20 0 749212 142564 34988 S 1.0 0.9 0:12.38 glance-api
1965 42436 20 0 540120 121848 17324 S 1.0 0.8 0:12.16 nova-conductor
15567 root 20 0 269540 5228 4160 R 1.0 0.0 0:00.05 top
..........................................

换页机制

当系统内存需求超过一定水平时,内核中的页换出守护进程(kswapd)就开始寻找可以释放的内存页. 类似一些编程语言的垃圾回收

1
2
3
[root@liruilongs.github.io ~]# ps -eaf | grep ksw | grep -v col
root 113 2 0 15:02 ? 00:00:00 [kswapd0]
[root@liruilongs.github.io ~]#

当系统内存剩余量低于低水位阈值(pages_low)时,内核的页换出守护进程(kswapd)会被唤醒并开始回收内存。

守护进程(kswapd)会释放以下列出的三种内存页之一:

  • 文件系统页:从磁盘中读出并且是没有修改过的页(术语为有磁盘备份的页(backed by disk),这些页可以立即被释放,等需要的时候可以再读取回来这些页包括应用程序可执行代码、数据,以及文件系统的元数据等。
  • 被修改过的文件系统页:这些页被称为脏页,这些页需要先写回磁盘才能被释放
  • 应用程序内存页:这些页被称为匿名页(anonymous memory),因为这些页不是来源于某个文件的。如果系统中有换页设备(swap device),那么这些页可以先存入换页设备,再被释放。将内存页写入换页设备(在 linux系统上)称为换页

这一过程的触发水位和调控机制与以下核心概念和参数相关:

在这里插入图片描述

Linux 内核通过三个 内存水位阈值(watermark) 衡量内存压力,并决定是否触发 kswapd 的回收行为

  1. 页高阈值(pages_high):当剩余内存(pages_free)高于此值,表示内存充足,kswapd 处于休眠状态。
  2. 页低阈值(pages_low):当剩余内存 介于页低阈值和页高阈值之间,内存有一定压力,但 kswapd 不会主动回收。当剩余内存 低于此值,kswapd 被唤醒并开始回收内存,直到剩余内存超过页高阈值。
  3. 页最小阈值(pages_min),当剩余内存 低于此值,系统内存极度紧张,触发 直接内存回收(同步阻塞进程)或 OOM Killer 杀死进程。

这三个阈值通过以下公式动态计算(基于 min_free_kbytes):

[
\text{pages_low} = \text{pages_min} \times \frac{5}{4}, \quad \text{pages_high} = \text{pages_min} \times \frac{3}{2}
]

min_free_kbytes(页最小阈值的直接控制): 设置系统保留的最小空闲内存(单位为 KB),直接影响 pages_min 的值。

1
echo 65536 > /proc/sys/vm/min_free_kbytes  # 设置为 64MB

注意事项:

  • 值过高会导致内存浪费,过低可能频繁触发直接内存回收。
  • 默认值通常为系统总内存的 0.1%~3%,需根据实际负载调整。

可以通过 /proc/zoneinfo 查看各内存区域的 minlowhigh 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@liruilongs.github.io ~]# cat /proc/zoneinfo | grep -A 5 'pages free'
pages free 703937
min 4674
low 5842
high 7010
spanned 786432
present 786432
............
--
pages free 676939
min 6589
low 8236
high 9883
spanned 1310720
present 1310720
...........
[root@liruilongs.github.io ~]#

守护进程(kswapd)会被定期唤醒,它会批量扫描活跃页的LRU列表非活跃页的LRU列表以寻找可以释放的内存。当空闲内存低于某个阈值的时候,该进程就会被唤醒,当空闲内存高于另外一个阌值时才会休息,

kswapd 负责协调在后台进行页换出操作;除非CPU和磁盘IO极为紧张,否则这些操作不会影响应用程序的性能。如果kswapd释放内存的速度不够快,导致页数量低于系统中配置的最低页数量,那么它就会切换到直接回收模式

在这种模式下,页回收会直接在前台运行,直接释放内存以便应对新的内存分配请求。在这种模式中,内存分配会阻塞直到有新的页被释放为止

在直接回收模式下,kswapd 可以直接调用内核模块的收缩(shrinker)函数,这些函数释放的内存很有可能来自内核的缓存区域,包括内核中的slab缓存

对于上面应用程序的换页操作,交换分区的使用一般会导致应用程序运行速度大幅下降。所以一般的生产系统根本不会配置换页设备,在没有配置换页设备的系统出现内存不足的情况时,内核会调用内存溢出进程终止程序杀掉某个进程。为了避免被杀掉,应用程序应该配置为避免超出系统内存的上限(Cgroup)

缓存/缓冲区机制

当内存不够时,Linux 会进行换页操作,在极端情况下会直接杀掉内存溢出进程的 OOM Killer,在内存充足的时候, Linux 会使用空闲的内存来作为文件系统的缓存cache 解决读延迟,buffer 解决写的延迟。

虽然被使用了,但是这部分内存实际是可以使用的,可用内存: available ≈ free + buffers + cache(可回收部分)

在 Linux 中,可以通过 换页操作(vm.swappniess)来调整是优先释放文件系统缓存还是优先进行其他的内存释放操作。

1
echo 0 > /proc/sys/vm/swappiness

决定内核优先回收 ​​匿名页(交换到 Swap)​​ 还是 ​​文件页(直接释放)​​ 的倾向。0(优先回收文件页)到 100(积极使用 Swap)。默认值:60,建议对延迟敏感场景设为 0(k8s 部署会禁用交换分区)

也可以通过命令主动释放缓存和缓冲的内存占用,需要注意的是在释放需要把数据写回磁盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ cat drop_caches #缓存处理
0
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ sync
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ free -m
total used free shared buff/cache available
Mem: 3935 212 3357 16 366 3440
Swap: 10239 0 10239
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ echo 3 > /proc/sys/vm/drop_caches
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ free -m
total used free shared buff/cache available
Mem: 3935 200 3575 16 159 3504
Swap: 10239 0 10239
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)


《BPF Performance Tools》

《性能之巅 系统、企业与云可观测性(第2版)》

《 Red Hat Performance Tuning 442 》


© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

发布于

2024-06-16

更新于

2025-05-12

许可协议

评论
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×