【本文正在参加金石计划附加挑战赛------第四期命题】
Linux 内核是如何管理应用内存的?
Linux 使用虚拟内存子系统作为应用程序内存请求与物理内存(RAM)之间的逻辑层. 这种类型的抽象向应用程序隐藏了平台特定物理内存实现的复杂性.
当应用程序访问由 Linux 虚拟内存子系统导出的虚拟地址时, 硬件 MMU 会引发一个事件, 告诉内核发生了对没有物理内存映射的内存区域的访问. 该事件会导致异常, 称为Page Fault, Linux 内核会通过将故障虚拟地址映射到物理内存页来处理该异常.
虚拟地址到物理内存的映射
虚拟地址通过硬件(MMU, 内存管理单元)和软件(页表)的协作透明地映射到物理内存. 虚拟到物理映射信息也缓存在硬件中, 称为 TLB(转换旁路缓冲区), 供以后参考, 以便快速查找物理内存位置.
页是物理内存中一组连续的线性地址. 在 x86 平台上, 页大小为 4 KB
虚拟机抽象有几个好处:
- 程序员无需了解平台的物理内存架构. 虚拟机将其隐藏起来, 允许编写独立于架构的代码.
- 无论物理内存如何碎片化, 进程在其地址空间中总能看到线性连续的字节范围.
例如: 当应用程序分配 10 MB 内存时, Linux 内核会在进程地址空间中保留 10 MB 的连续虚拟地址范围. 映射这些虚拟地址范围的物理内存位置可能并不连续. 物理内存中唯一保证连续的部分是页面大小(4 KB).
- 由于部分加载, 启动速度更快. 需求分页会在指令被引用时加载指令.
- 内存共享. 物理内存中的单一库/程序副本可映射到多个进程地址空间. 允许有效使用物理内存.
使用
pmap -X <pid>
可以查找哪些进程驻留内存被其他进程共享或私有.
- 内存足迹大于物理内存的多个程序可以同时运行. 后台内核会以透明方式将最近访问次数最少的页面重定位到磁盘(交换).
- 进程被隔离到自己的虚拟地址空间, 因此不会影响或破坏其他进程的内存.
两个进程可能使用相同的虚拟地址, 但这些虚拟地址被映射到不同的物理内存位置. 附加到相同共享内存(SHM) 段的进程, 其虚拟地址将映射到相同的物理内存位置.
32 位地址空间限制为 4GB, 而 64 位地址空间则为数百 TB. 进程地址空间的大小限制了应用程序可使用的物理内存量.
进程虚拟地址空间由各种类型的内存段组成: 进程虚拟地址空间由以下类型的内存段组成: 文本, 数据, 堆, 栈, 共享内存(SHM)和 mmap. 进程地址空间被定义为虚拟内存地址的范围, 作为进程的环境输出给进程.
每个内存段都由具有起始地址和终止地址的线性虚拟地址范围组成, 并由某些后备存储(如文件系统或交换)提供支持.
页故障通过从后备存储区填充物理内存页来解决. 在内存不足时, 缓存在物理内存页中的数据会迁移到其后备存储区. 进程文本
内存段由文件系统中的可执行文件提供支持. 堆栈, 堆, COW(写入时复制)和共享内存页称为匿名(Anon)页, 由 swap(磁盘分区或文件)备份.
当未配置
swap
时, 匿名页无法释放, 因此会被连接到物理内存中, 在内存不足时无法从这些物理页中迁移数据.
当进程调用 malloc()
或 sbrk()
时, 内核会在进程地址空间创建一个新的堆段, 并保留可合法访问的进程虚拟地址范围. 对保留地址范围之外的虚拟地址的任何引用都会导致违反分段规定, 并杀死进程. 物理内存分配会延迟, 直到进程访问了新创建内存段内的虚拟地址.
应用程序执行 50GB 的大容量
malloc
并只接触(页面故障)10 MB 范围的虚拟地址时, 将只消耗 10 MB 的物理内存.
虚拟内存/物理内存使用情况可通过ps
, pidstat
或 top
查看. SIZE 列表示虚拟内存段的大小, RSS列显示分配的物理内存.
用于程序文本和缓存文件系统数据的物理内存页(称为页面缓存)可以在内存不足时快速释放, 因为数据总是可以从后备存储(文件系统)中检索到. 不过, 要释放匿名页, 需要先将数据写入交换设备, 然后才能释放.
Linux 内存分配政策
进程内存分配由 Linux 内存分配策略控制. Linux 提供三种不同的内存分配模式, 具体取决于 vm.overcommit_memory
的可调值设置.
- 启发式超量分配 (vm.overcommit_memory=0): Linux 默认模式允许进程超量使用由内部启发式方法决定的
合理
内存, 其中考虑到:可用内存和可用交换内存. 除此之外, 通过缩减文件系统缓存和内核板块缓存(内核驱动程序和子系统使用)而释放的内存也在考虑之列.
优点: 使用宽松的记账规则, 对于通常请求内存大于实际使用内存的程序很有用. 只要有足够的可用内存和/或交换空间来满足请求, 进程就能继续运行.
缺点: 除非进程触及(访问)内存段中的所有虚拟地址, 否则 Linux 内核不会尝试为进程保留物理内存.
- 始终超量提交(vm.overcommit_memory=1): 允许进程尽可能多地超量调用内存, 而且总是成功.
优点: 考虑到对空闲内存或交换没有限制, 允许随意分配.
缺点: 与启发式超量分配一样. 应用程序可以在只有几 GB 物理内存的系统上
malloc()
TB 内存. 在所有页面都被触及并触发 OOM Killer 之前, 不会出现故障.
- 严格超量提交(vm.overcommit_memory=2): 通过保留虚拟内存范围和物理内存来防止超量提交. 没有超量占用意味着没有 OOM Killer. 使用严格超量提交, 内核会跟踪, 保留或已提交的物理内存量.
由于严格超量占用策略不考虑空闲内存和交换, 因此不应依赖空闲内存或交换指标(由
free
和vmstat
报告)来发现可用内存. 相反, 应使用cat /proc/meminfo
指标: CommitLimit, Committed_AS 来估算可供分配的内存.
要计算当前的超量提交或分配限制, 应使用以下公式:CommitLimit - Committed_AS
.
内核可调参数 vm.overcommit_ratio
为该模式设置超量分配限制. 超量提交限制设置为 物理内存 x overcommit_ratio + swap . 可以通过将 vm.overcommit_ratio 可调参数设置为更大的值来提高超量占用限制(默认值为物理内存的 50%).
优点 禁用 OOM Killer . 与在提供生产负载时被 OOM Killer 杀死相比, 启动时发生故障对生产的影响较小. Solaris 操作系统只提供这种模式. 严格超量委托不使用空闲内存/交换计算超量委托限制.
缺点 : 不允许超量提交. 保留但未使用的内存不能被其他应用程序使用. 即使系统报告有大量可用内存, 新程序也可能无法分配内存. 这是由于代表现有进程保留了物理内存.
使用严格超量提交策略监控可用内存变得非常棘手. Linux 中的应用程序通常不会处理内存分配失败. 无法检查内存故障可能会导致内存损坏和难以调试的随机故障.
注意: 对于启发式和严格超量分配, 内核都会为根保留一定量的内存. 在启发式模式下, 是可用物理内存的 1/32. 在严格超量占用模式下, 是你设置的实际内存百分比的 1/32. 这是内核中的硬编码, 无法调整. 这意味着拥有 64GB 内存的系统将为 root 用户保留 2GB 内存.
OOM Killer
当系统内存不足达到以下极端情况时: 文件系统缓存已被缩减; 所有可能的内存页面已被回收. 如果内存需求持续居高不下, 最终将耗尽所有可用内存. 为了应对这种情况, 内核会选择可以杀死的进程来满足内存分配需求. 内核的这种无奈之举被称为 OOM Killer .
用于查找候选进程的标准有时会杀死最关键的进程. 处理 OOM Killer 的方法有以下几种:
将内核内存分配策略更改为严格超量提交, 禁用 OOM Killer.
$sudo sysctl vm.overcommit_memory=2 | $sudo sysctl vm.overcommit_ratio=80
从 OOM Killer 中选择退出关键进程. 退出关键服务器进程有时可能不足以保证系统正常运行. 内核仍需杀死进程以释放内存. 在某些情况下, 自动重启服务器来处理 OOM Killer 可能是唯一的选择.
$sudo sysctl vm.panic_on_oom=1
$sudo sysctl kernel.panic="number_of_seconds_too_wait_before_reboot"
文件系统缓存的好处
Linux 将未使用的空闲内存用于缓存文件系统页面和磁盘块.
文件系统缓存使用的内存被算作空闲内存, 在需要时供应用程序使用. Linux 工具
free
会将文件系统缓存内存报告为可用内存.
文件系统缓存的好处是提高应用程序的读写性能:
读取: 应用程序读取文件时, 内核会执行物理 IO 从磁盘读取数据块. 数据缓存在文件系统缓存中, 供以后使用, 以避免物理读取. 当应用程序请求相同的数据块时, 只需要逻辑 IO(从文件系统页面缓存中读取), 这就提高了应用程序的性能. 此外, 当检测到顺序 IO 模式时, 文件系统会预取(提前读取)区块, 以防应用程序请求下一个相邻的区块. 这也有助于减少 IO 延迟.
写入: 当应用程序向文件写入数据时, 内核会将数据缓存到页面缓存并确认完成(称为缓冲区写入). 在内核安排将脏页面写入磁盘之前, 文件系统缓存中的文件数据可以在内存中多次更新(称为写入取消).
文件系统缓存中的脏页由 flusher
(旧称 pdflush
) 内核线程写入. 当内存中的脏缓冲区比例超过虚拟内存阈值(内核可调)时, 脏页面会被定期刷新.
文件系统缓存通过隐藏存储延迟来提高应用程序的 IO 性能.
HugeTLB 或 HugePage 的优势
Linux HugeTLB 功能允许应用程序使用巨大或大型页面: 2 MB, 1 GB, 而不是默认的 4 KB 大小. TLB(转化查找缓冲区)是缓存虚拟到物理翻译的硬件组件. 当在 TLB 中找不到转换时, 称为 TLB 未命中, 结果是需要花费大量时间到内存驻留页表中查找虚拟到物理内存的转换.
由于 CPU 和内存的速度和内存密度差距越来越大, TLB 缓存命中率变得越来越重要. 频繁错过 TLB 可能会对应用程序性能产生负面影响.
TLB 是 CPU 芯片上的稀缺资源, Linux 内核试图充分利用有限的 TLB 缓存项. 每个 TLB 缓存项都可编程为访问不同大小的连续物理内存地址: 4 KB, 2 MB 或 1 GB.
TLB 有 64 个插槽. 如果将插槽编程为缓存更大大小的页面, 就能改善物理内存与 TLB 的连接. 4 KB 页面: 64x4 + 1024x4 = 4 MB > 4 KB 页面:64x4 + 1024x4 = 4 MB 2 MB 页面: 32x2048 +1024x2048 = 2 GB > 2 MB 页面:32x2048 +1024x2048 = 2 GB 1 GB 页面: 4 GB
优点:
- HugeTLB 可以覆盖更大的进程地址空间, 有助于减少 TLB 未命中.
- 页面大小越大, 所需的页表条目就越少, 级别也就越浅. 这样可以减少内存延迟, 因为只需访问 2 级而不是 4 级页表, 而且页表转换使用的是物理内存.
- 大页面被锁定在内存中, 因此在内存不足时不会被分页.
- 降低页面故障率. 与 4 KB 相比, 每个页面故障可占用 2 MB 或 1GB 物理内存. 因此, 应用程序的预热速度更快.
- 如果应用程序的访问模式具有数据位置性, HugeTLB 将有所帮助. 从随机位置读取数据或仅从每页中读取几个字节(大型哈希表查找)将受益于 4 KB 的页面大小, 而不是大型页面.
- 大页面无需在 4K 边界重新启动预取操作, 从而改进了内存预取操作.
缺点:
- 大页面需要预先保留. 系统管理员需要将内核可调参数设置为所需的 HugePage 数量:
vm.nr_hugepages=<number_of_pages>
Linux Transparent HugePage(THP)功能没有预付费用.
- 应用程序应具有 HugePage 意识.
要利用 HugePage 的优势, 应使用
-XX=+UseLargePages
选项启动 java 应用程序, 以便为 java 堆使用大页面. 否则, 分配的页面可能无法用于任何目的. 可以监控监控 Hugepages 的使用情况:
cat /proc/meminfo|grep PageTables
- HugePages 需要 2 MB 和 1GB 大小的连续物理内存. 如果系统运行时间较长, 且大部分内存被降级为 4 KB 块, 则大页面请求可能会失败.
总结一下
今天主要分享了 Linux 内核是如何管理应用程序内存的. 从虚拟地址, 物理地址和交换区间, 再到内存分配策略, OOM Killer, 文件系统缓存和 HugePages.
好吧, 今天的内容就分享到这里啦!
一家之言, 欢迎拍砖!
Happy Coding! Stay GOLDEN!