内核和处理器负责将虚拟内存映射到物理内存。为了提高效率,会在称为页面的内存组中创建内存映射,其中每个页面的大小是处理器的详细信息。尽管大多数处理器也支持更大的容量,但通常有4 KB,Linux称其为 hugepage大页面。内核可以从其自己的空闲列表中为物理内存页面请求提供服务,内核为每个DRAM组和CPU维护这些请求以提高效率。内核自己的软件也通常通过内核分配器(例如slab分配器)从这些空闲列表中消耗内存。
内存页和交换
典型的用户内存页面的生命周期如图7-2所示,其中列举了以下步骤:
1. 应用程序从内存分配请求开始(例如,libc malloc() )。
2. 分配库可以从其自己的空闲列表中为内存请求提供服务,或者它可能需要扩展虚拟内存来容纳。根据分配库,它将:
1. 通过调用brk() syscall并将堆内存用于分配来扩展堆的大小。
2. 通过mmap() 系统调用创建一个新的内存段。
3. 稍后,应用程序尝试通过存储和加载指令使用分配的内存范围,这涉及调用处理器内存管理单元(MMU)进行虚拟到物理地址的转换。至此,虚拟内存的谎言就暴露出来了:该地址没有映射!这会导致称为页面错误的MMU错误。
4. 页面错误由内核处理,内核建立从其物理内存可用列表到虚拟内存的映射,然后将该映射通知MMU以供以后查找。现在,该过程占用了额外的物理内存页面。进程使用的物理内存量称为其驻留集大小(RSS)。
5. 当系统上的内存需求过多时,内核页面输出守护程序(kswapd)可能会寻找可用的内存页面。它将释放三种类型的内存中的一种(尽管只有(c)如图7-2所示,因为它显示了用户内存页面的生命周期):
1. 从磁盘读取但未修改的文件系统页面(称为“由磁盘支持”):可以立即释放这些页面,并在需要时简单地重新读取。这些页面是应用程序可执行的文本,数据和文件系统元数据。
2. 已修改的文件系统页面:这些是“脏”的,必须先写入磁盘,然后才能释放它们。
3. 应用程序内存页面:由于它们没有文件来源,因此被称为匿名内存。如果正在使用交换设备,则可以先将它们存储在交换设备上来释放它们。将页面写到交换设备称为交换(在Linux上)。
内存分配请求通常是频繁的活动:对于繁忙的应用程序,用户级别的分配每秒可能发生数百万次。加载和存储指令以及MMU查找更加频繁。它们每秒可能发生数十亿次。在图7-2中,这些箭头以粗体显示。其他活动相对较少:brk()和mmap()调用,页面错误和页面退出(较亮的箭头)。
page-out daemon页面输出守护程序
定期激活页面输出守护程序(kswapd)以扫描非活动和活动页面的LRU列表,以寻找可用的内存。如图7-3所示,当空闲内存越过低阈值时它将被唤醒,而当空闲内存越过高阈值时将回到睡眠状态。
kswapd协调后台页面调出;除了CPU和磁盘I/O争用外,这些不应直接损害应用程序性能。如果kswapd无法足够快地释放内存,则会超过可调的最小页面阈值,并使用直接回收;这是释放内存以满足分配条件的前台模式。在这种模式下,分配阻塞(停顿)并同步等待页面被释放。
直接回收可以调用内核模块收缩器函数:这些释放的内存可能保留在缓存中的内存,包括内核slab缓存。
swap devices交换设备
交换设备为内存不足的系统提供了降级的操作模式:进程可以继续分配,但是现在将不常使用的页面移入和移出交换设备,这通常会使应用程序运行得慢得多。
一些生产系统无需交换即可运行;这样做的理由是,对于那些关键系统来说,降级的操作模式是永远无法接受的,因为这些关键系统可能有许多冗余(且运行状况良好)服务器,比开始交换的服务器要好用得多。(例如,对于Netflix云实例,通常就是这种情况。)
如果无交换系统的内存不足,则内核oom killer会牺牲一个进程。为了避免这种情况,将应用程序配置为永不超过系统的内存限制。
oom killer
Linux内存不足杀手是释放内存的最后手段:它将使用启发式方法找到受害者进程,并通过杀死它们来牺牲它们。启发式寻找将释放许多页面的最大受害者,并且这不是关键任务,例如内核线程或init(PID 1)。Linux提供了在整个系统和每个进程中调整OOM杀手的行为的方法。
page compaction页面压缩
随着时间的流逝,释放的页面变得碎片化,从而使内核很难根据需要分配较大的连续块。内核使用压缩程序来移动页面,从而释放连续区域。
file system caching and buffering文件系统缓存和缓冲
Linux借用空闲内存进行文件系统缓存,并在有需求时将其恢复为空闲状态。这种借用的结果是,在Linux启动之后,系统报告的可用内存趋向于零,这可能使用户担心系统实际上只是在预热其文件系统缓存时会耗尽内存。此外,文件系统使用内存进行回写缓冲(write-back buffering)。
可以将Linux调整为更喜欢从文件系统缓存中释放或通过交换释放内存(通过调整参数vm.swappiness)。
传统的分析工具
传统的性能工具提供了许多基于容量的内存使用情况统计信息,包括每个进程和系统范围内使用了多少虚拟和物理内存,以及某些细分,例如按流程段或面板。分析内存使用率超出基本知识,例如页面错误率,分配库,运行时或应用程序对每个分配都需要内置的工具;或者可以使用像Valgrind这样的虚拟机分析器;后一种方法可能会导致目标应用程序在检测时运行速度慢10倍以上。BPF工具效率更高,开销也更小。
Tool | Type | Description |
---|---|---|
dmesg | Kernel log | OOM killer event details |
swapon | Kernel statistics | Swap device usage |
free | Kernel statistics | System-wide memory usage |
ps | Kernel statistics | Process statistics, including memory usage |
pmap | Kernel statistics | Process memory usage by segment |
vmstat | Kernel statistics | Various statistics, including memory |
sar | Kernel statistics | Can show page fault and page scanner rates |
perf | Software events, hardware statistics, hardware sampling | Memory-related PMC statistics and event sampling |
用于内存分析相关的BPF工具
内存相关的工具:
Tool | Source | Target | Description |
---|---|---|---|
oomkill | BCC/BT | OOM | Shows extra info on OOM kill events显示oom相关的事件 |
memleak | BCC | Sched | Shows possible memory leak code paths显示可能的内存泄漏代码路径 |
mmapsnoop | Book | Syscalls | Traces mmap(2) calls system-wide跟踪系统范围内的mmap调用 |
brkstack | Book | Syscalls | Shows brk() calls with user stack traces显示带有用户堆栈跟踪的brk()调用 |
shmsnoop | BCC | Syscalls | Traces shared memory calls with details跟踪共享内存调用的详细信息 |
faults | Book | Faults | Shows page faults, by user stack trace通过用户堆栈跟踪显示页面错误 |
ffaults | Book | Faults | Shows page faults, by filename通过文件名显示页面错误 |
vmscan | Book | VM | Measures VM scanner shrink and reclaim times测量vm scaner的收缩和回收时间 |
drsnoop | BCC | VM | Traces direct reclaim events, showing latency跟踪直接回收事件,显示延迟 |
swapin | Book | VM | Shows swap-ins by process按进程显示swap情况 |
hfaults | Book | Faults | Shows huge page faults, by process按进程显示巨页错误情况 |
此外,还有几个用于内存分析的BPF工具: kmem 、kpages 、 slabratetop 、 numamove
oomkill是一个BCC和bpftrace工具,用于跟踪内存不足杀手事件并打印详细信息(例如平均负载)。平均负载为OOM时的系统状态提供了一些额外的上下文,显示了系统是否正在变得忙碌或稳定。
此输出表明PID 18601(perl)需要内存,这触发了PID 1165(java)的OOM终止。PID 1165的内存占用已达到18006224个pages;这些通常每页4 KB,具体取决于处理器和进程内存设置。loadavg平均负载表明,在OOM终止时,系统变得更加繁忙。
该工具通过使用kprobes跟踪oom_kill_process() 函数并打印各种细节来工作。在这种情况下,只需读取/proc/loadavg即可获取平均负载。调试OOM事件时,可以根据需要增强此工具以打印其他详细信息。此外,此工具尚未使用可以显示有关如何选择任务的更多详细信息的oom跟踪点。
memleak是一个BCC工具,可跟踪内存分配和空闲事件以及分配堆栈跟踪。随着时间的流逝,它可以显示长期幸存者-尚未释放的分配。
此示例显示了在bash shell进程上运行的memleak:
仅memleak不能告诉您这些分配是否是真正的内存泄漏(内存泄漏:指的是没有引用并且永远不会释放的已分配内存),内存增长还是长期分配。为了区分它们,需要研究和理解代码路径。
当然,还有很多内存分析的小工具。这里就不一一列举了。具体可以看brendangregg大佬的新书。