内存
虚拟内存
Linux 采用的是虚拟内存
机制,每个进程都有自己的虚拟内存地址空间,仅当实际使用内存的时候才会映射到物理内存地址之上。这种设计提供了物理内存的超额分配,Linux 中的内存管理机制包括页换出守护进程(page out daemon)、物理换页设备(swap device),以及在极端情况下直接杀掉内存溢出的进程(OOM Killer)。
内存分配器
虚拟内存地址空间内部由分为内核空间
和用户空间
两部分,不同字长(单个 CPU 指令可以处理数据的最大长度,如32位处理器和64位处理器)的处理器,地址空间的范围也不同。
对于使用 libc 内存分配器的进程来说,存储在进程虚拟内存地址空间的一段动态区间中的内存,被称为堆内存(Heap)。libc 提供了一系列内存分配的函数,包括 malloc() 和 free() 等。释放内存 libc 会将被释放的内存地址记录下来,以便提供给未来的 malloc() 使用。只有在内存用尽时 libc 才需要扩展堆内存。一般来说,libc 没有必要缩小堆内存,因为这些都是虚拟内存,而不是真正的内存。
内存映射
内存映射其实就是将虚拟内存地址映射到物理内存地址,为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟机地址与物理地址的映射关系。页表实际存储在 CPU 的内存管理单元 MMU 中,在正常情况下处理器可以通过硬件找到要访问的内存。
TLB
TLB 位于 CPU 之内,是一块高速缓存。TLB 根据虚拟地址查找缓存,虚拟地址首先发往 TLB 确认是否命中缓存,如果命中则直接返回物理地址。否则一级一级查找页表获取物理地址。并将虚拟地址和物理地址的映射关系缓存到TLB中。
内存页
MMU 并不是以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是 4KB 大小。这样的话每次内存映射只需要关联 4KB 或者 4KB 整数倍的空间即可。
但是由于页的大小只有 4KB,将会导致页表非常巨大。为了解决这个问题,Linux 提供了两种机制,即多级页表和巨页(HugePage)。
缺页中断
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页中断,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
内存页和换页机制
内存页的生命周期
- 应用程序发起内存分配请求(libc malloc())。
- 应用程序库代码要么直接从空闲列表中相应请求,要么先扩展虚拟内存地址空间在分配。根据内存分配库的不同实现,由两种不同选项:
- 利用 brk() 系统调用来扩展堆的尺寸。
- 利用 mmap() 系统调用来创建一个新的内存段地址。
- 内存分配之后,应用程序试图使用 store/load 指令来使用之前分配的内存地址。这时会调用到 CPU 内部的内存管理单元(MMU)来进行虚拟地址到物理地址到转换。这时虚拟地址并没有被映射到对应的物理地址。此时操作系统将会产生一个缺页中断。
- 缺页中断由系统内核进行处理。在对应的处理函数中,内核会在物理内存空闲列表中找到一个空闲地址并映射到该虚拟地址。接下来内存会通知 MMU 刷新页表以便未来直接查找该映射。
- 现在用户进程就占据了一个新到物理内存页。进程所使用的全部内存数量被称为常驻集大小(Resident set Size,即 RSS)。
- 当系统内存需求超过一定水平是,内核中的换页守护进程(kswapd)就开始寻找可以释放的内存页。kswapd 会释放以下三种内存页之一:
- 有磁盘备份的页:从磁盘中读出且没有经过修改过的页,这些页可以被立即释放。包括应用程序可执行代码、数据、文件系统源数据等。
- 被修改过的文件系统页:这些页被称为
脏页
,这些页需要先写回磁盘才能被释放。 - 应用程序内存页:这些页被称为
匿名页
,因为这些页不是来源于某个文件等。如果系统中有换页设备(swap device),那么这些页可以先存入换页设备,在被释放。将内存页写入换页设备的操作被称为换页。
brk() 与 mmap() 系统调用
对于小块内存(小于 128K),一般会使用 brk() 来进行分配,也就是将堆顶指针向高地址推(堆是从低地址向高地址扩展),并且只是分配虚拟内存空间,只有当真正访问(读/写)数据时,才会触发缺页中断。
由于低地址内存必须在高地址内存释放之后才能得到的释放(这就是内存碎片的原因之一),否则高地址内存处于 brk() 之后,会被当作未分配内存而被重写覆盖。当然未释放的低地址内存会被标记为空闲区,若再来一个刚好等于此个低地址内存空间的请求 malloc() 可能直接将其分配出去。
对于大块内存(大于 128K),则会使用 mmap() 来进行分配,也就是在虚拟内存映射段找一块空闲的内存分配出去。与 brk() 分配内存不同的是 mmap() 分配的内存可以单独释放。
页换出守护进程
页换出守护进程(kswapd)会被定期唤醒,它会批量扫描活跃页的 LRU(最近最少使用算法)列表和非活跃页的 LRU 列表以寻找可以释放的内存。当空闲内存低于某个阈值时,该进程被唤醒,当空闲内存高于另一个阈值时才会休息。
kswapd 负责协调在后台进行页换出操作;除非 CPU 和磁盘 I/O 极为紧张,否则这些操作不会影响应用程序的性能。
如果 kswapd 释放内存的速度不够快,导致页数量低于系统中配置的最低页数量,那么它就会切换为直接回收模式;在这种模式下,页回收会直接在前台运行,直接释放内存以便应对新的内存分配请求。在这种模式中内存分配会被阻塞直到有新的也被释放为止。
在直接回收模式下,kswapd 可以直接调用内核模块的收缩(shrinker)函数,这些函数释放的内存很有可能来自内核的缓存区域,包括内核中的 slab 缓存。
换页设备(Swap)
当系统内存不够时,换页设备允许系统以一种降级模式运行。进程可以继续申请内存,但是不经常使用的页将会被换出到对应的换页设备上,这通常会导致应用程序运行速度大幅下降。
在关键应用程序所在的系统上通常不会配置换页设备,当一个没有配置换页设备的系统出现内存不足时,内核会调用内存溢出进程终止程序杀掉某个进程。
内存溢出进程终止程序(OOM Killer)
Linux 的内存溢出进程终止程序是释放内存的最后一道防线,该程序使用预定规则来选择要 kill 的进程,并执行 kill 操作。预定规则中定了将除内核关键任务和 init 进程之外的占用内存最多的进程 kill 掉。Linux 提供了调整 OOM Killer 的行为的全局和每进程的配置参数。
页压缩
随着时间的推移,释放的内存会逐渐碎片化,这样使内核分配一个较大的连续空间越来越难。内核中有一个压缩程序来移动内存页,以便扩大连续区间。
文件系统缓存和缓存区
Linux 会借用空闲内存作为文件系统缓存,这种设计会导致系统启动之后空闲内存不断变小。但实际上这些内存都作了文件系统的缓存,同时文件系统也需要一定内存作为写回缓冲区。在系统空闲内存紧张时,Linux 回释放掉一部分缓存,以确保操作系统有足够的内存来应对新的内存申请。
传统工具
工具 | 介绍 |
dmesg | OOM Killer 事件的详细信息 |
swapon/swapoff | 换页设备管理 |
free | 显示系统内存用量信息 |
ps | 按进程展示系统资源使用情况 |
pmap | 按进程统计信息,包括内存用量 |
vmstat | 系统整体资源使用情况 |
top | 按进程展示系统资源使用情况 |
sar | 可以显示换页错误和也扫描频率 |
perf | 内存相关的 PMC 统计信息和事件采样信息 |
dmesg
# dmesg 查看系统中的 oom 事件
$ dmesg | grep -i oom
[255083.947041] a.out invoked oom-killer: gfp_mask=0x1100dca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO), order=0, oom_score_adj=0
[255083.947143] oom_kill_process.cold+0xb/0x10
[255083.947301] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
[255083.947369] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=user.slice,mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/session-487.scope,task=a.out,pid=8204,uid=0
[255083.947389] Out of memory: Killed process 8204 (a.out) total-vm:7946846728kB, anon-rss:3880048kB, file-rss:1152kB, shmem-rss:0kB, UID:0 pgtables:3910580kB oom_score_adj:0
swapon
# swapon 查看当前系统配置的换页设备和开启指定的换页设备
$ swapon
NAME TYPE SIZE USED PRIO
/swap.img file 8G 101.3M -2
# swapoff 关闭指定的换页设备
# -a 关闭所有的换页设备
$ swapoff -a
# -a 打开所有的换页设备
$ swapon -a
free
# free 显示系统中的内存用量
$ free
total used free shared buff/cache available
Mem: 32167 9280 14839 47 8047 22554
Swap: 0 0 0
ps
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.0 225544 9328 ? Ss Nov07 11:15 /sbin/init maybe-ubiquity
pmap
# pmap 命令可以按地址空间段展示进程内存用量
$ pmap -x 838
838: sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
Address Kbytes RSS Dirty Mode Mapping
000055d64c975000 44 0 0 r---- sshd
000055d64c980000 548 0 0 r-x-- sshd
000055d64ca09000 288 0 0 r---- sshd
000055d64ca51000 16 16 16 r---- sshd
......
---------------- ------- ------- -------
total kB 15556 4856 1732
vmstat
$ vmstat -Sm 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 8011 3 92 0 0 1 1 8 12 0 0 100 0 0
0 0 0 8011 3 92 0 0 0 0 43 53 0 0 100 0 0
sar
# sar 是一个可以打印不同目标、不同监控指标的复合工具
# -B 打印页统计信息
$ sar -B 1
Linux 5.15.0-48-generic (ebpf) 11/11/22 _x86_64_ (8 CPU)
16:26:00 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
16:26:01 0.00 0.00 5.00 0.00 0.00 0.00 0.00 0.00 0.00
16:26:02 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
工具输出单位解释
单位 | 来源 | 描述 |
total | | 物理内存总大小 |
used | free | 已使用内存大小,包括共享内存 |
free | free | 空闲内存大小 |
shared | free | 共享内存大小 |
buff/cache | free | buff:内核缓冲去用到的内存,对应 /proc/meminfo 中 Buffers 的值。 cache:内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 和 SReclaimable 之和。Buffers 是对原始磁盘块的临时,也就是用来缓存磁盘数据,通常不会特别大(20MB左右)。这样,内核就可以把分散的写集合起来,统一优化磁盘的写入,比如把多次小的写合并成单次大的写。Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样访问这些文件数据时,就可以直接从内存中读取,而不需要再访问缓慢的磁盘。SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分使用 SReclaimable 记录;不可回收部分使用 SUnreclaim 记录 |
available | free | 系统中的可用内存大小(随时可以释放以响应新的内存申请请求) |
MEM | | 进程使用的物理内存占系统全部内存的比例 |
VSZ | ps | 虚拟内存大小 |
RSS | ps | 常驻集大小,即进程实际使用的物理内存 |
pgpgin/s | | 每秒换入的内存大小,单位 KB。越大表明物理内存越紧张需要频繁的从换页设备换入换出,对性能的影响也越大 |
pgpgout/s | sar | 每秒换出的内存大小,单位 KB。越大表明物理内存越紧张需要频繁的从换页设备换入换出,对性能的影响也越大 |
bpf 工具
工具 | 来源 | 目标 | 描述 |
cachestat | BCC | 文件系统缓存 | |
cachetop | BCC | 文件系统缓存 | |
oomkill | BCC | OOM | 展示 OOM Killer 事件的详细信息 |
memleak | BCC | 调度 | 展示可能有内存泄露的代码路径 |
shmsnoop | BCC | 系统调用 | 跟踪共享内存相关的调用信息 |
swapin | BCC | VM | 按进程展示页换入信息 |
cachestat
# 输出文件系统缓存信息
# 每秒输出一次
$ cachestat-bpfcc 1
HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
0 0 0 0.00% 12 1223
192 0 0 100.00% 12 879
291 0 54404 100.00% 12 435
591 16 59466 97.36% 12 668
248 0 68608 100.00% 12 936
246 0 69120 100.00% 12 1206
305 8 4352 97.44% 12 1223
911 0 16 100.00% 12 1223
0 0 0 0.00% 12 1223
0 0 0 0.00% 12 1223
单位 | 描述 |
HITS | 缓存命中的次数 |
MISSES | 缓存未命中的次数 |
DIRTIES | 新增到缓存中的脏页数 |
HITRATIO | 命中率 |
BUFFERS_MB | Buffers 的大小,单位为 MB 为 |
CACHED_MB | Cached 的大小,单位为 MB 为 |
cachetop
# 以 top 方式按进程实时输出文件系统缓存信息
# 每秒刷新一次
$ cachetop-bpfcc 1
17:57:19 Buffers MB: 12 / Cached MB: 1180 / Sort: HITS / Order: descending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
8429 root dd 6529 6154 3072 27.3% 24.3%
8279 root kworker/u8:3 90 0 0 100.0% 0.0%
8327 root cachetop-bpfcc 4 0 0 100.0% 0.0%
oomkill
# 跟踪 oom 事件
$ oomkill-bpfcc
Tracing OOM kills... Ctrl-C to stop.
18:18:10 Triggered by PID 8548 ("a.out"), OOM kill of PID 8548 ("a.out"), 2035069 pages, loadavg: 0.08 0.02 0.01 5/171 8548
memleak
# memleak 用于跟踪内存分配和释放事件对应的调用栈信息
$ memleak
1024 bytes in 4 allocations from stack
kmem_cache_alloc+0x26f [kernel]
kmem_cache_alloc+0x26f [kernel]
__alloc_file+0x28 [kernel]
alloc_empty_file+0x45 [kernel]
alloc_file+0x2b [kernel]
alloc_file_pseudo+0x97 [kernel]
__anon_inode_getfile+0x8b [kernel]
anon_inode_getfd+0x43 [kernel]
bpf_prog_load+0x60a [kernel]
__sys_bpf+0x1af [kernel]
__x64_sys_bpf+0x1a [kernel]
do_syscall_64+0x59 [kernel]
entry_SYSCALL_64_after_hwframe+0x61 [kernel]
shmsnoop
# shmsnoop 显示共享内存用量信息
$ shmsnoop-bpfcc
PID COMM SYS RET ARGs
9003 a.out SHMGET 1 key: 0x12, size: 100, shmflg: 0x7b6 (IPC_CREAT|IPC_EXCL|0666)
9003 a.out SHMAT 7ff4d23fd000 shmid: 0x1, shmaddr: 0x0, shmflg: 0x0
9003 a.out SHMDT 0 shmaddr: 0x7ff4d23fd000
9003 a.out SHMCTL 0 shmid: 0x1, cmd: 0, buf: 0x0
swapin
# swapin 展示了那个进程正在从换页设备中换入页标签:缓存,0.00,换页,内存,Linux,进程,内核 From: https://blog.51cto.com/hongchen99/5845370
$ swapin-bpfcc
20:17:47
COMM PID COUNT
java 8825 7
......