内核栈溢出

在Linux系统上,进程运行分为用户态与内核态,进入内核态之后使用的是内核栈,作为基本的安全机制,用户程序不能直接访问内核栈,所以尽管内核栈属于进程的地址空间,但与用户栈是分开的。Linux的内核栈大小是固定的,从2.6.32-520开始缺省大小是16KB,之前的kernel版本缺省大小是8KB。内核栈的大小可以修改,但要通过重新编译内核实现。以下文件定义了它的大小:

arch/x86/include/asm/page_64_types.h
8KB:
#define THREAD_ORDER 1
16KB:
#define THREAD_ORDER 2

由于内核栈的大小是有限的,就会有发生溢出的可能,比如调用嵌套太多、参数太多都会导致内核栈的使用超出设定的大小。内核栈溢出的结果往往是系统崩溃,因为溢出会覆盖掉本不该触碰的数据,首当其冲的就是thread_info — 它就在内核栈的底部,内核栈是从高地址往低地址生长的,一旦溢出首先就破坏了thread_info,thread_info里存放着指向进程的指针等关键数据,迟早会被访问到,那时系统崩溃就是必然的事。kstack-smash

【小知识】:把thread_info放在内核栈的底部是一个精巧的设计,在高端CPU中比如PowerPC、Itanium往往都保留了一个专门的寄存器来存放当前进程的指针,因为这个指针的使用率极高,然而x86的寄存器太少了,专门分配一个寄存器实在太奢侈,所以Linux巧妙地利用了栈寄存器,把thread_info放在内核栈的底部,这样通过栈寄存器里的指针可以很方便地算出thread_info的地址,而thread_info的第一个字段就是进程的指针。

内核栈溢出导致的系统崩溃有时会被直接报出来,比如你可能会看到:

但更多的情况是不直接报错,而是各种奇怪的panic。在分析vmcore的时候,它们的共同点是thread_info被破坏了。以下是一个实例,注意在task_struct中stack字段直接指向内核栈底部也就是thread_info的位置,我们看到thread_info显然被破坏了:cpu的值大得离谱,而且指向task的指针与task_struct的实际地址不匹配:

作为一种分析故障的手段,可以监控内核栈的大小和深度,方法如下:

然后检查下列数值,可以看到迄今为止内核栈使用的峰值和对应的backtrace:

你可以写个脚本定时收集上述数据,有助于找到导致溢出的代码。下面是一个输出结果的实例:

 

为什么手工drop_caches之后cache值并未减少?

在Linux系统上查看内存使用状况最常用的命令是”free”,其中buffers和cache通常被认为是可以回收的:

当内存紧张的时候,有一个常用的手段就是使用下面的命令来手工回收cache:

注:drop_caches接受以下三种值:

  • To free pagecache:
    echo 1 > /proc/sys/vm/drop_caches
  • To free reclaimable slab objects (includes dentries and inodes):
    echo 2 > /proc/sys/vm/drop_caches
  • To free slab objects and pagecache:
    echo 3 > /proc/sys/vm/drop_caches

当我们考虑有多少cache可供回收的时候,首先要知道的是:不同版本的”free”命令计算cache值的算法不同,据不完全统计举例如下:

  1. 版本:procps-3.2.8-36
    cache值等于/proc/meminfo中的”Cached”;
  2. 版本:procps-3.3.9-10.1
    cache值等于/proc/meminfo的 [Cached + SReclaimable];
  3. 版本:procps-ng-3.3.10-3
    cache值等于/proc/meminfo的 [Cached + Slab]。

注:
/proc/meminfo中的”Cached”表示page cache所占用的内存大小;
“Slab”表示内核Slab所占用的内存大小,slab有的可回收有的不可回收,其中可回收的通过”SReclaimable”表示,不可回收的通过”SUnreclaim”表示。
所以,对上述第2、3版本的”free”命令,”echo 1 > /proc/sys/vm/drop_caches”对其中的SReclaimable或Slab部分是不起作用的。

即便仅考虑page cache (对应于 /proc/meminfo 的”Cached”),也并不是所有的页面都可以回收的:

首先,drop_caches只回收clean pages,不回收dirty pages,参见https://www.kernel.org/doc/Documentation/sysctl/vm.txt
所以如果想回收更多的cache,应该在drop_caches之前先执行”sync”命令,把dirty pages变成clean pages。

其次,即使提前执行了sync命令,drop_cache操作也不可能把cache值降到0,甚至有时候cache值几乎没有下降,这是为什么呢?因为page cache中包含的tmpfs和共享内存是不能通过drop_caches回收的。

Page cache用于缓存文件里的数据,不仅包括普通的磁盘文件,还包括了tmpfs文件,tmpfs文件系统是将一部分内存空间模拟成文件系统,由于背后并没有对应着磁盘,无法进行paging(换页),只能进行swapping(交换),在执行drop_cache操作的时候tmpfs对应的page cache并不会回收。

我们通过实验来观察tmpfs文件对free命令的影响:

结论:
tmpfs占用的page cache是不能通过drop_caches操作回收的,tmpfs占用的page cache同时也算进了”shared”中,也就是说,被视为共享内存。

Linux kernel利用tmpfs实现共享内存(shared memory),参见:
https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt所以共享内存也和tmpfs一样,属于page cache,但又不能被drop_caches回收。这里所说的共享内存包括:

  • SysV shared memory
    是通过shmget申请的共享内存,用”ipcs -m”或”cat /proc/sysvipc/shm”查看;
  • POSIX shared memory
    是通过shm_open申请的共享内存,用”ls /dev/shm”查看;
  • shared anonymous mmap
    通过mmap(…MAP_ANONYMOUS|MAP_SHARED…)申请的内存,可以用”pmap -x”或者”cat /proc/<PID>/maps”查看;
    注:mmap调用参数如果不是MAP_ANONYMOUS|MAP_SHARED,则不属于tmpfs,比如MAP_ANONYMOUS|MAP_PRIVATE根本不属于page cache而是属于AnonPages,MAP_SHARED属于普通文件,对应的page cache可以回写硬盘并回收。

我们通过一个实验来观察共享内存对free命令的影响。以下程序通过shared anonymous mmap方式申请256MB大小的内存:

实验过程如下:

结论:cache值包含了共享内存的大小,然而drop_caches是不能回收它的。
注:上例是shared anonymous mmap,如果是SysV shared memory或IPC shared memory,结果是一样的。

总结:为什么drop_caches操作不能回收所有的page cache?因为

  • dirty pages不能回收;
  • 共享内存和tmpfs不能回收(注意观察free命令显示的shared值);
  • 不同版本的free命令有不同的计算cache值的方法,有的包含了slab或SReclaimable,”echo 1 > /proc/sys/vm/drop_caches”是不能回收slab的,”echo 3 > /proc/sys/vm/drop_caches”也只是回收slab中的SReclaimable部分。

内核如何检测soft lockup与hard lockup?

所谓lockup,是指某段内核代码占着CPU不放。Lockup严重的情况下会导致整个系统失去响应。Lockup有几个特点:

  • 首先只有内核代码才能引起lockup,因为用户代码是可以被抢占的,不可能形成lockup(只有一种情况例外,就是SCHED_FIFO优先级为99的实时进程即使在用户态也可能使[watchdog/x]内核线程抢不到CPU而形成soft lock,参见《Real-Time进程会导致系统Lockup吗?》)
  • 其次内核代码必须处于禁止内核抢占的状态(preemption disabled),因为Linux是可抢占式的内核,只在某些特定的代码区才禁止抢占,在这些代码区才有可能形成lockup。

Lockup分为两种:soft lockup 和 hard lockup,它们的区别是 hard lockup 发生在CPU屏蔽中断的情况下。

  • Soft lockup是指CPU被内核代码占据,以至于无法执行其它进程。检测soft lockup的原理是给每个CPU分配一个定时执行的内核线程[watchdog/x],如果该线程在设定的期限内没有得到执行的话就意味着发生了soft lockup,[watchdog/x]是SCHED_FIFO实时进程,优先级为最高的99,拥有优先运行的特权。
  • Hard lockup比soft lockup更加严重,CPU不仅无法执行其它进程,而且不再响应中断。检测hard lockup的原理利用了PMU的NMI perf event,因为NMI中断是不可屏蔽的,在CPU不再响应中断的情况下仍然可以得到执行,它再去检查时钟中断的计数器hrtimer_interrupts是否在保持递增,如果停滞就意味着时钟中断未得到响应,也就是发生了hard lockup。

Linux kernel设计了一个检测lockup的机制,称为NMI Watchdog,是利用NMI中断实现的,用NMI是因为lockup有可能发生在中断被屏蔽的状态下,这时唯一能把CPU抢下来的方法就是通过NMI,因为NMI中断是不可屏蔽的。NMI Watchdog 中包含 soft lockup detector 和 hard lockup detector,2.6之后的内核的实现方法如下。

NMI Watchdog 的触发机制包括两部分:

  1. 一个高精度计时器(hrtimer),对应的中断处理例程是kernel/watchdog.c: watchdog_timer_fn(),在该例程中:
    • 要递增计数器hrtimer_interrupts,这个计数器供hard lockup detector用于判断CPU是否响应中断;
    • 还要唤醒[watchdog/x]内核线程,该线程的任务是更新一个时间戳;
    • soft lock detector检查时间戳,如果超过soft lockup threshold一直未更新,说明[watchdog/x]未得到运行机会,意味着CPU被霸占,也就是发生了soft lockup。
  2. 基于PMU的NMI perf event,当PMU的计数器溢出时会触发NMI中断,对应的中断处理例程是 kernel/watchdog.c: watchdog_overflow_callback(),hard lockup detector就在其中,它会检查上述hrtimer的中断次数(hrtimer_interrupts)是否在保持递增,如果停滞则表明hrtimer中断未得到响应,也就是发生了hard lockup。

hrtimer的周期是:softlockup_thresh/5。
注:

  • 在2.6内核中:
    softlockup_thresh的值等于内核参数kernel.watchdog_thresh,默认60秒;
  • 而到3.10内核中:
    内核参数kernel.watchdog_thresh名称未变,但含义变成了hard lockup threshold,默认10秒;
    soft lockup threshold则等于(2*kernel.watchdog_thresh),即默认20秒。

NMI perf event是基于PMU的,触发周期(hard lockup threshold)在2.6内核里是固定的60秒,不可手工调整;在3.10内核里可以手工调整,因为直接对应着内核参数kernel.watchdog_thresh,默认值10秒。

检测到 lockup 之后怎么办?可以自动panic,也可输出条信息就算完了,这是可以通过内核参数来定义的:

  • kernel.softlockup_panic: 决定了检测到soft lockup时是否自动panic,缺省值是0;
  • kernel.nmi_watchdog: 定义是否开启nmi watchdog、以及hard lockup是否导致panic,该内核参数的格式是”=[panic,][nopanic,][num]”.
    (注:最新的kernel引入了新的内核参数kernel.hardlockup_panic,可以通过检查是否存在 /proc/sys/kernel/hardlockup_panic来判断你的内核是否支持。)

参考资料:

Softlockup detector and hardlockup detector (aka nmi_watchdog)

kernel/watchdog.c:
设置PMU NMI perf event的代码 wachdog_nmi_enable()
响应NMI perf overflow中断的代码 watchdog_overflow_callback()
[watchdog/x]内核线程 watchdog()
响应hrtimer中断的代码 watchdog_timer_fn()

Real-Time进程会导致系统lockup吗?

Linux kernel支持两种实时(real-time)调度策略(scheduling policy):SCHED_FIFO和SCHED_RR,无论是哪一种,实时进程的优先级范围[0~99]都高于普通进程[100~139],始终优先于普通进程得到运行。如果实时进程是CPU消耗型的,会不会导致其它进程得不到运行机会,造成系统lockup呢?

这实际上是两个问题,不能混为一谈,第一个问题是会不会造成系统lockup,第二个问题是会不会导致其它进程得不到运行机会。我们一个一个分别来谈。

实时进程会不会造成系统lockup?

Lockup分为soft lockup和hard lockup,我在《内核如何检测SOFT LOCKUP与HARD LOCKUP》一文中解释了Linux kernel检测lockup的原理。

Hard lockup发生在CPU中断被屏蔽的情况下,因为实时进程本身并不会屏蔽CPU中断,hrtimer时钟中断是可以得到响应的,所以不会导致hard lockup

Soft lockup发生在内核线程[watchdog/x]得不到运行的情况下,理论上如果实时进程占着CPU不放,确实有可能导致[watchdog/x]得不到运行而发生soft lockup,然而这个可能性并不大,因为[watchdog/x]本身也是实时进程,调度策略为SCHED_FIFO,优先级已经是最高的99:

如果占着CPU不放的实时进程也是SCHED_FIFO并且优先级为99,就有可能导致soft lockup。为什么呢?我们看一下实时进程的调度策略就明白了:

  • 在多个实时进程之间,优先级更高的会抢先运行
    (注:实时进程的优先级数字越大则优先级越高,99最高,0最低;而普通进程正好相反,优先级数字越大则优先级越低,139最低,100最高);
  • 优先级相同的实时进程之间,不会互相抢占,只能等对方主动释放CPU;
  • SCHED_FIFO调度策略的特点是,进程会一直保持运行直到发生以下情况之一:
    1. 进程主动调用sched_yield(2)放弃运行,自动排到运行队列的队尾,等到相同优先级的其它进程运行之后才有机会再运行;
    2. 进程进入睡眠状态(比如由于等待I/O的原因),唤醒后自动排到运行队列的队尾,等到相同优先级的其它进程运行之后才有机会再运行;
    3. 被优先级更高的实时进程抢占,这种情况下会自动排到运行队列的队首,下次运行的机会排在相同优先级的其它进程的前面。
  • SCHED_RR进程与SCHED_FIFO唯一不同的是,实时进程的运行时间是分为一段一段的,在相同优先级的进程之间轮流运行,每个进程运行完一个时间段之后,必须让给下一个进程(强调:仅对相同优先级而言,不同优先级的进程之间仍然会互相抢占)。

所以,如果占着CPU不放的实时进程的调度策略是SCHED_FIFO,并且优先级为与[watchdog/x]相同的99,SCHED_FIFO的调度策略决定了只要它不放手,[watchdog/x]就无法运行,结果是会导致soft lockup。

接下来第二个问题是:

实时进程会不会导致其它进程得不到运行机会?

如果实时进程占着CPU不放,会不会导致其它进程得不到运行机会,包括管理员的shell也无法运行、连基本的管理任务也进行不了,最终造成整个系统失去控制?

通常不会。因为Linux kernel有一个RealTime Throttling机制,就是为了防止CPU消耗型的实时进程霸占所有的CPU资源而造成整个系统失去控制。它的原理很简单,就是保证无论如何普通进程都能得到一定比例(默认5%)的CPU时间,可以通过两个内核参数来控制:

  • /proc/sys/kernel/sched_rt_period_us
    缺省值是1,000,000 μs (1秒),表示实时进程的运行粒度为1秒。(注:修改这个参数请谨慎,太大或太小都可能带来问题)。
  • /proc/sys/kernel/sched_rt_runtime_us
    缺省值是 950,000 μs (0.95秒),表示在1秒的运行周期里所有的实时进程一起最多可以占用0.95秒的CPU时间。
    如果sched_rt_runtime_us=-1,表示取消限制,意味着实时进程可以占用100%的CPU时间(慎用,有可能使系统失去控制)。

所以,Linux kernel默认情况下保证了普通进程无论如何都可以得到5%的CPU时间,尽管系统可能会慢如蜗牛,但管理员仍然可以利用这5%的时间设法恢复系统,比如停掉失控的实时进程,或者给自己的shell进程赋予更高的实时优先级以便执行管理任务,等等。

Real-time Throttling支持cgroup,详见https://www.kernel.org/doc/Documentation/scheduler/sched-rt-group.txt

参考资料:
https://lwn.net/Articles/296419/

Linux Kernel Dump分析入门课程

我开发了一门Linux Kernel Dump分析入门课程,课时两天,使用丰富的实验案例,手把手地讲授Linux Kernel Dump分析的方法,以及针对各种不同panic类型的分析思路。

Linux kernel dump分析是一门实践性很强的技术,需要经过大量的案例训练才能运用自如。本课程的实验用例来自实际案例,涵盖了各种典型的dump类型,并在实践中结合理论,讲述与dump分析相关的内核知识点。

本课程是有偿的,适合从事Linux系统服务与运维的工程师,过去的授课反馈良好。联系方式:

微博@vmunix
微博@vmunix

微信
微信
课程提纲

Kernel dump 是什么

  • Kdump – 捕捉kernel dump的工具
  • Kdump的工作原理
  • Kdump的配置

Dump分析的工具crash(1)

准备环境

  • 根据vmcore文件获取内核版本及系统信息
  • kernel debuginfo 内核符号文件
  • Kernel source code
  • RHEL与SLES的不同
  • 时区设置
  • 运行crash utility:基于vmcore或基于live system

Dump分析的思路:从哪里开始

  • 判断panic类型
  • 系统信息 sys
  • Message buffer – log

Kernel panic的若干种类型

  • Hard lockup
    Kernel panic – not syncing: Watchdog detected hard LOCKUP on cpu 0
  • soft lockup
    kernel panic – not syncing: softlockup: hung tasks
  • hung task panic
    kernel panic – not syncing: hung_task: blocked tasks
  • oom
    Kernel panic – not syncing: Out of memory: system-wide panic_on_oom is enabled
  • 空指针/非法指针
    BUG: unable to handle kernel NULL pointer dereference at 0000000000000650
    BUG: unable to handle kernel paging request at ffff88081fc03cd0
  • MCE(Machine Check Exception)
    Kernel panic – not syncing: Fatal Machine Check
  • NMI
    Kernel panic – not syncing: NMI IOCK error: Not continuing
  • HP Watchdog timer module [hpwdt]
    Kernel panic – not syncing: An NMI occurred, please see the Integrated Management Log for details
  • SysRq
    PANIC: “SysRq: Trigger a crashdump”
  • BUG_ON() 断语
    kernel BUG at fs/inode.c:322!

理解函数调用栈(backtrace)

  • 代码的执行轨迹
  • CPU寄存器状态pt_regs
  • 栈帧里的数据
  • 内核栈溢出
  • 汇编指令
  • 调用约定(call convention)
    • call/ret/leave指令
    • 参数传递约定
    • 通用寄存器,caller-saved vs. callee-saved
  • 对照源代码
  • changelog
  • 内核模块
    • Taint flags
    • crash utility如何加载内核模块的调试信息

Hang分析

  • 思路
    • 是没有可运行的进程?
    • 还是有很多进程想运行但抢不到CPU?
  • 什么是uninterruptible sleep
  • 抢占式内核也有不能被抢占的情况
  • 自旋锁spinlock

crash工具的基本命令

  • 进程ps/task/runq/bt
  • 内存kmem/vm/swap/ipcs
  • IO : dev/mount/files/fuser
  • 网络 net

Crash utility扩展工具

  • PyKdump

 

/proc/meminfo之谜

/proc/meminfo是了解Linux系统内存使用状况的主要接口,我们最常用的”free”、”vmstat”等命令就是通过它获取数据的 ,/proc/meminfo所包含的信息比”free”等命令要丰富得多,然而真正理解它并不容易,比如我们知道”Cached”统计的是文件缓存页,manpage上说是“In-memory  cache  for  files read from the disk (the page cache)”,那为什么它不等于[Active(file)+Inactive(file)]?AnonHugePages与AnonPages、HugePages_Total有什么联系和区别?很多细节在手册中并没有讲清楚,本文对此做了一点探究。

负责输出/proc/meminfo的源代码是:
fs/proc/meminfo.c : meminfo_proc_show()

MemTotal

系统从加电开始到引导完成,firmware/BIOS要保留一些内存,kernel本身要占用一些内存,最后剩下可供kernel支配的内存就是MemTotal。这个值在系统运行期间一般是固定不变的。可参阅解读DMESG中的内存初始化信息

MemFree

表示系统尚未使用的内存。[MemTotal-MemFree]就是已被用掉的内存。

MemAvailable

有些应用程序会根据系统的可用内存大小自动调整内存申请的多少,所以需要一个记录当前可用内存数量的统计值,MemFree并不适用,因为MemFree不能代表全部可用的内存,系统中有些内存虽然已被使用但是可以回收的,比如cache/buffer、slab都有一部分可以回收,所以这部分可回收的内存加上MemFree才是系统可用的内存,即MemAvailable。/proc/meminfo中的MemAvailable是内核使用特定的算法估算出来的,要注意这是一个估计值,并不精确。

内存黑洞

追踪Linux系统的内存使用一直是个难题,很多人试着把能想到的各种内存消耗都加在一起,kernel text、kernel modules、buffer、cache、slab、page table、process RSS…等等,却总是与物理内存的大小对不上,这是为什么呢?因为Linux kernel并没有滴水不漏地统计所有的内存分配,kernel动态分配的内存中就有一部分没有计入/proc/meminfo中。

我们知道,Kernel的动态内存分配通过以下几种接口:

  • alloc_pages/__get_free_page: 以页为单位分配
  • vmalloc: 以字节为单位分配虚拟地址连续的内存块
  • slab allocator
    • kmalloc: 以字节为单位分配物理地址连续的内存块,它是以slab为基础的,使用slab层的general caches — 大小为2^n,名称是kmalloc-32、kmalloc-64等(在老kernel上的名称是size-32、size-64等)。

通过slab层分配的内存会被精确统计,可以参见/proc/meminfo中的slab/SReclaimable/SUnreclaim;

通过vmalloc分配的内存也有统计,参见/proc/meminfo中的VmallocUsed 和 /proc/vmallocinfo(下节中还有详述);

而通过alloc_pages分配的内存不会自动统计,除非调用alloc_pages的内核模块或驱动程序主动进行统计,否则我们只能看到free memory减少了,但从/proc/meminfo中看不出它们具体用到哪里去了。比如在VMware guest上有一个常见问题,就是VMWare ESX宿主机会通过guest上的Balloon driver(vmware_balloon module)占用guest的内存,有时占用得太多会导致guest无内存可用,这时去检查guest的/proc/meminfo只看见MemFree很少、但看不出内存的去向,原因就是Balloon driver通过alloc_pages分配内存,没有在/proc/meminfo中留下统计值,所以很难追踪。

内存都到哪里去了?

使用内存的,不是kernel就是用户进程,下面我们就分类讨论。

注:page cache比较特殊,很难区分是属于kernel还是属于进程,其中被进程mmap的页面自然是属于进程的了,而另一些页面没有被mapped到任何进程,那就只能算是属于kernel了。

1. 内核

内核所用内存的静态部分,比如内核代码、页描述符等数据在引导阶段就分配掉了,并不计入MemTotal里,而是算作Reserved(在dmesg中能看到)。而内核所用内存的动态部分,是通过上文提到的几个接口申请的,其中通过alloc_pages申请的内存有可能未纳入统计,就像黑洞一样。

下面讨论的都是/proc/meminfo中所统计的部分。

1.1 SLAB

通过slab分配的内存被统计在以下三个值中:

  • SReclaimable: slab中可回收的部分。调用kmem_getpages()时加上SLAB_RECLAIM_ACCOUNT标记,表明是可回收的,计入SReclaimable,否则计入SUnreclaim。
  • SUnreclaim: slab中不可回收的部分。
  • Slab: slab中所有的内存,等于以上两者之和。
1.2 VmallocUsed

通过vmalloc分配的内存都统计在/proc/meminfo的 VmallocUsed 值中,但是要注意这个值不止包括了分配的物理内存,还统计了VM_IOREMAP、VM_MAP等操作的值,譬如VM_IOREMAP是把IO地址映射到内核空间、并未消耗物理内存,所以我们要把它们排除在外。从物理内存分配的角度,我们只关心VM_ALLOC操作,这可以从/proc/vmallocinfo中的vmalloc记录看到:

注:/proc/vmallocinfo中能看到vmalloc来自哪个调用者(caller),那是vmalloc()记录下来的,相应的源代码可见:
mm/vmalloc.c: vmalloc > __vmalloc_node_flags > __vmalloc_node > __vmalloc_node_range > __get_vm_area_node > setup_vmalloc_vm

通过vmalloc分配了多少内存,可以统计/proc/vmallocinfo中的vmalloc记录,例如:

一些driver以及网络模块和文件系统模块可能会调用vmalloc,加载内核模块(kernel module)时也会用到,可参见 kernel/module.c。

1.3 kernel modules (内核模块)

系统已经加载的内核模块可以用 lsmod 命令查看,注意第二列就是内核模块所占内存的大小,通过它可以统计内核模块所占用的内存大小,但这并不准,因为”lsmod”列出的是[init_size+core_size],而实际给kernel module分配的内存是以page为单位的,不足 1 page的部分也会得到整个page,此外每个module还会分到一页额外的guard page。下文我们还会细说。

lsmod的信息来自/proc/modules,它显示的size包括init_size和core_size,相应的源代码参见:

注:我们可以在 /sys/module/<module-name>/ 目录下分别看到coresize和initsize的值。

kernel module的内存是通过vmalloc()分配的(参见下列源代码),所以在/proc/vmallocinfo中会有记录,也就是说我们可以不必通过”lsmod”命令来统计kernel module所占的内存大小,通过/proc/vmallocinfo就行了,而且还比lsmod更准确,为什么这么说呢?

因为给kernel module分配内存是以page为单位的,不足 1 page的部分也会得到整个page,此外,每个module还会分到一页额外的guard page。
详见:mm/vmalloc.c: __get_vm_area_node()

而”lsmod”列出的是[init_size+core_size],比实际分配给kernel module的内存小。我们做个实验来说明:

所以结论是kernel module所占用的内存包含在/proc/vmallocinfo的统计之中,不必再去计算”lsmod”的结果了,而且”lsmod”也不准。

1.4 HardwareCorrupted

当系统检测到内存的硬件故障时,会把有问题的页面删除掉,不再使用,/proc/meminfo中的HardwareCorrupted统计了删除掉的内存页的总大小。相应的代码参见 mm/memory-failure.c: memory_failure()。

 1.5 PageTables

Page Table用于将内存的虚拟地址翻译成物理地址,随着内存地址分配得越来越多,Page Table会增大,/proc/meminfo中的PageTables统计了Page Table所占用的内存大小。

注:请把Page Table与Page Frame(页帧)区分开,物理内存的最小单位是page frame,每个物理页对应一个描述符(struct page),在内核的引导阶段就会分配好、保存在mem_map[]数组中,mem_map[]所占用的内存被统计在dmesg显示的reserved中,/proc/meminfo的MemTotal是不包含它们的。(在NUMA系统上可能会有多个mem_map数组,在node_data中或mem_section中)。
而Page Table的用途是翻译虚拟地址和物理地址,它是会动态变化的,要从MemTotal中消耗内存。

1.6 KernelStack

每一个用户线程都会分配一个kernel stack(内核栈),内核栈虽然属于线程,但用户态的代码不能访问,只有通过系统调用(syscall)、自陷(trap)或异常(exception)进入内核态的时候才会用到,也就是说内核栈是给kernel code使用的。在x86系统上Linux的内核栈大小是固定的8K或16K(可参阅我以前的文章:内核栈溢出)。

Kernel stack(内核栈)是常驻内存的,既不包括在LRU lists里,也不包括在进程的RSS/PSS内存里,所以我们认为它是kernel消耗的内存。统计值是/proc/meminfo的KernelStack。

1.7 Bounce

有些老设备只能访问低端内存,比如16M以下的内存,当应用程序发出一个I/O 请求,DMA的目的地址却是高端内存时(比如在16M以上),内核将在低端内存中分配一个临时buffer作为跳转,把位于高端内存的缓存数据复制到此处。这种额外的数据拷贝被称为“bounce buffering”,会降低I/O 性能。大量分配的bounce buffers 也会占用额外的内存。

2. 用户进程

/proc/meminfo统计的是系统全局的内存使用状况,单个进程的情况要看/proc/<pid>/下的smaps等等。

2.1 Hugepages

Hugepages在/proc/meminfo中是被独立统计的,与其它统计项不重叠,既不计入进程的RSS/PSS中,又不计入LRU Active/Inactive,也不会计入cache/buffer。如果进程使用了Hugepages,它的RSS/PSS不会增加。

注:不要把 Transparent HugePages (THP)跟 Hugepages 搞混了,THP的统计值是/proc/meminfo中的”AnonHugePages”,在/proc/<pid>/smaps中也有单个进程的统计,这个统计值与进程的RSS/PSS是有重叠的,如果用户进程用到了THP,进程的RSS/PSS也会相应增加,这与Hugepages是不同的。

在/proc/meminfo中与Hugepages有关的统计值如下:

HugePages_Total 对应内核参数 vm.nr_hugepages,也可以在运行中的系统上直接修改 /proc/sys/vm/nr_hugepages,修改的结果会立即影响空闲内存 MemFree的大小,因为HugePages在内核中独立管理,只要一经定义,无论是否被使用,都不再属于free memory。在下例中我们设置256MB(128页)Hugepages,可以立即看到Memfree立即减少了262144kB(即256MB):

使用Hugepages有三种方式:
(详见 https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt)

  1. mount一个特殊的 hugetlbfs 文件系统,在上面创建文件,然后用mmap() 进行访问,如果要用 read() 访问也是可以的,但是 write() 不行。
  2. 通过shmget/shmat也可以使用Hugepages,调用shmget申请共享内存时要加上 SHM_HUGETLB 标志。
  3. 通过 mmap(),调用时指定MAP_HUGETLB 标志也可以使用Huagepages。

用户程序在申请Hugepages的时候,其实是reserve了一块内存,并未真正使用,此时/proc/meminfo中的 HugePages_Rsvd 会增加,而 HugePages_Free 不会减少。

等到用户程序真正读写Hugepages的时候,它才被消耗掉了,此时HugePages_Free会减少,HugePages_Rsvd也会减少。

我们说过,Hugepages是独立统计的,如果进程使用了Hugepages,它的RSS/PSS不会增加。下面举例说明,一个进程通过mmap()申请并使用了Hugepages,在/proc/<pid>/smaps中可以看到如下内存段,VmFlags包含的”ht”表示Hugepages,kernelPageSize是2048kB,注意RSS/PSS都是0:

 

2.2 AnonHugePages

AnonHugePages统计的是Transparent HugePages (THP),THP与Hugepages不是一回事,区别很大。

上一节说过,Hugepages在/proc/meminfo中是被独立统计的,与其它统计项不重叠,既不计入进程的RSS/PSS中,又不计入LRU Active/Inactive,也不会计入cache/buffer。如果进程使用了Hugepages,它的RSS/PSS不会增加。

而AnonHugePages完全不同,它与/proc/meminfo的其他统计项有重叠,首先它被包含在AnonPages之中,而且在/proc/<pid>/smaps中也有单个进程的统计,与进程的RSS/PSS是有重叠的,如果用户进程用到了THP,进程的RSS/PSS也会相应增加,这与Hugepages是不同的。下例截取自/proc/<pid>/smaps中的一段:

THP也可以用于shared memory和tmpfs,缺省是禁止的,打开的方法如下(详见 https://www.kernel.org/doc/Documentation/vm/transhuge.txt):

  • mount时加上”huge=always”等选项
  • 通过/sys/kernel/mm/transparent_hugepage/shmem_enabled来控制

因为缺省情况下shared memory和tmpfs不使用THP,所以进程之间不会共享AnonHugePages,于是就有以下等式:
【/proc/meminfo的AnonHugePages】==【所有进程的/proc/<pid>/smaps中AnonHugePages之和】
举例如下:

2.3 LRU

LRU是Kernel的页面回收算法(Page Frame Reclaiming)使用的数据结构,在解读vmstat中的Active/Inactive memory一文中有介绍。Page cache和所有用户进程的内存(kernel stack和huge pages除外)都在LRU lists上。

LRU lists包括如下几种,在/proc/meminfo中都有对应的统计值:

LRU_INACTIVE_ANON  –  对应 Inactive(anon)
LRU_ACTIVE_ANON  –  对应 Active(anon)
LRU_INACTIVE_FILE  –  对应 Inactive(file)
LRU_ACTIVE_FILE  –  对应 Active(file)
LRU_UNEVICTABLE  –  对应 Unevictable

注:

  • Inactive list里的是长时间未被访问过的内存页,Active list里的是最近被访问过的内存页,LRU算法利用Inactive list和Active list可以判断哪些内存页可以被优先回收。
  • 括号中的 anon 表示匿名页(anonymous pages)。
    用户进程的内存页分为两种:file-backed pages(与文件对应的内存页),和anonymous pages(匿名页),比如进程的代码、映射的文件都是file-backed,而进程的堆、栈都是不与文件相对应的、就属于匿名页。file-backed pages在内存不足的时候可以直接写回对应的硬盘文件里,称为page-out,不需要用到交换区(swap);而anonymous pages在内存不足时就只能写到硬盘上的交换区(swap)里,称为swap-out。
  • 括号中的 file 表示 file-backed pages(与文件对应的内存页)。
  • Unevictable LRU list上是不能pageout/swapout的内存页,包括VM_LOCKED的内存页、SHM_LOCK的共享内存页(又被统计在”Mlocked”中)、和ramfs。在unevictable list出现之前,这些内存页都在Active/Inactive lists上,vmscan每次都要扫过它们,但是又不能把它们pageout/swapout,这在大内存的系统上会严重影响性能,设计unevictable list的初衷就是避免这种情况,参见:
    https://www.kernel.org/doc/Documentation/vm/unevictable-lru.txt

LRU与/proc/meminfo中其他统计值的关系:

  • LRU中不包含HugePages_*。
  • LRU包含了 Cached 和 AnonPages。
2.4 Shmem

/proc/meminfo中的Shmem统计的内容包括:

  • shared memory
  • tmpfs。

此处所讲的shared memory又包括:

  • SysV shared memory [shmget etc.]
  • POSIX shared memory [shm_open etc.]
  • shared anonymous mmap [ mmap(…MAP_ANONYMOUS|MAP_SHARED…)]

因为shared memory在内核中都是基于tmpfs实现的,参见:
https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt
也就是说它们被视为基于tmpfs文件系统的内存页,既然基于文件系统,就不算匿名页,所以不被计入/proc/meminfo中的AnonPages,而是被统计进了:

  • Cached (i.e. page cache)
  • Mapped (当shmem被attached时候)

然而它们背后并不存在真正的硬盘文件,一旦内存不足的时候,它们是需要交换区才能swap-out的,所以在LRU lists里,它们被放在:

  • Inactive(anon) 或 Active(anon)
    注:虽然它们在LRU中被放进了anon list,但是不会被计入 AnonPages。这是shared memory & tmpfs比较拧巴的一个地方,需要特别注意。
  • 或 unevictable (如果被locked的话)

注意:
当shmget/shm_open/mmap创建共享内存时,物理内存尚未分配,要直到真正访问时才分配。/proc/meminfo中的 Shmem 统计的是已经分配的大小,而不是创建时申请的大小。

2.5 AnonPages

前面提到用户进程的内存页分为两种:file-backed pages(与文件对应的内存页),和anonymous pages(匿名页)。Anonymous pages(匿名页)的数量统计在/proc/meminfo的AnonPages中。

以下是几个事实,有助于了解Anonymous Pages:

  • 所有page cache里的页面(Cached)都是file-backed pages,不是Anonymous Pages。”Cached”与”AnoPages”之间没有重叠。
    注:shared memory 不属于 AnonPages,而是属于Cached,因为shared memory基于tmpfs,所以被视为file-backed、在page cache里,上一节解释过。
  • mmap private anonymous pages属于AnonPages(Anonymous Pages),而mmap shared anonymous pages属于Cached(file-backed pages),因为shared anonymous mmap也是基于tmpfs的,上一节解释过。
  • Anonymous Pages是与用户进程共存的,一旦进程退出,则Anonymous pages也释放,不像page cache即使文件与进程不关联了还可以缓存。
  • AnonPages统计值中包含了Transparent HugePages (THP)对应的 AnonHugePages 。参见:

 

2.6 Mapped

上面提到的用户进程的file-backed pages就对应着/proc/meminfo中的”Mapped”。Page cache中(“Cached”)包含了文件的缓存页,其中有些文件当前已不在使用,page cache仍然可能保留着它们的缓存页面;而另一些文件正被用户进程关联,比如shared libraries、可执行程序的文件、mmap的文件等,这些文件的缓存页就称为mapped。

/proc/meminfo中的”Mapped”就统计了page cache(“Cached”)中所有的mapped页面。”Mapped”是”Cached”的子集。

因为Linux系统上shared memory & tmpfs被计入page cache(“Cached”),所以被attached的shared memory、以及tmpfs上被map的文件都算做”Mapped”。

进程所占的内存页分为anonymous pages和file-backed pages,理论上应该有:
【所有进程的PSS之和】 == 【Mapped + AnonPages】。
然而我实际测试的结果,虽然两者很接近,却总是无法精确相等,我猜也许是因为进程始终在变化、采集的/proc/[1-9]*/smaps以及/proc/meminfo其实不是来自同一个时间点的缘故。

2.7 Cached

Page Cache里包括所有file-backed pages,统计在/proc/meminfo的”Cached”中。

  • Cached是”Mapped”的超集,就是说它不仅包括mapped,也包括unmapped的页面,当一个文件不再与进程关联之后,原来在page cache中的页面并不会立即回收,仍然被计入Cached,还留在LRU中,但是 Mapped 统计值会减小。【ummaped = (Cached – Mapped)】
  • Cached包含tmpfs中的文件,POSIX/SysV shared memory,以及shared anonymous mmap。
    注:POSIX/SysV shared memory和shared anonymous mmap在内核中都是基于tmpfs实现的,参见:
    https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt

“Cached”和”SwapCached”两个统计值是互不重叠的,源代码参见下一节。所以,Shared memory和tmpfs在不发生swap-out的时候属于”Cached”,而在swap-out/swap-in的过程中会被加进swap cache中、属于”SwapCached”,一旦进了”SwapCached”,就不再属于”Cached”了。

2.8 SwapCached

我们说过,匿名页(anonymous pages)要用到交换区,而shared memory和tmpfs虽然未统计在AnonPages里,但它们背后没有硬盘文件,所以也是需要交换区的。也就是说需要用到交换区的内存包括:”AnonPages”和”Shmem”,我们姑且把它们统称为匿名页好了。

交换区可以包括一个或多个交换区设备(裸盘、逻辑卷、文件都可以充当交换区设备),每一个交换区设备都对应自己的swap cache,可以把swap cache理解为交换区设备的”page cache”:page cache对应的是一个个文件,swap cache对应的是一个个交换区设备,kernel管理swap cache与管理page cache一样,用的都是radix-tree,唯一的区别是:page cache与文件的对应关系在打开文件时就确定了,而一个匿名页只有在即将被swap-out的时候才决定它会被放到哪一个交换区设备,即匿名页与swap cache的对应关系在即将被swap-out时才确立。

并不是每一个匿名页都在swap cache中,只有以下情形之一的匿名页才在:

  • 匿名页即将被swap-out时会先被放进swap cache,但通常只存在很短暂的时间,因为紧接着在pageout完成之后它就会从swap cache中删除,毕竟swap-out的目的就是为了腾出空闲内存;
    【注:参见mm/vmscan.c: shrink_page_list(),它调用的add_to_swap()会把swap cache页面标记成dirty,然后它调用try_to_unmap()将页面对应的page table mapping都删除,再调用pageout()回写dirty page,最后try_to_free_swap()会把该页从swap cache中删除。】
  • 曾经被swap-out现在又被swap-in的匿名页会在swap cache中,直到页面中的内容发生变化、或者原来用过的交换区空间被回收为止。
    【注:当匿名页的内容发生变化时会删除对应的swap cache,代码参见mm/swapfile.c: reuse_swap_page()。】

/proc/meminfo中的SwapCached背后的含义是:系统中有多少匿名页曾经被swap-out、现在又被swap-in并且swap-in之后页面中的内容一直没发生变化。也就是说,如果这些匿名页需要被swap-out的话,是无需进行I/O write操作的。

“SwapCached”不属于”Cached”,两者没有交叉。参见:

“SwapCached”内存同时也在LRU中,还在”AnonPages”或”Shmem”中,它本身并不占用额外的内存。

2.9 Mlocked

“Mlocked”统计的是被mlock()系统调用锁定的内存大小。被锁定的内存因为不能pageout/swapout,会从Active/Inactive LRU list移到Unevictable LRU list上。也就是说,当”Mlocked”增加时,”Unevictable”也同步增加,而”Active”或”Inactive”同时减小;当”Mlocked”减小的时候,”Unevictable”也同步减小,而”Active”或”Inactive”同时增加。

“Mlocked”并不是独立的内存空间,它与以下统计项重叠:LRU Unevictable,AnonPages,Shmem,Mapped等。

2.10 Buffers

“Buffers”表示块设备(block device)所占用的缓存页,包括:直接读写块设备、以及文件系统元数据(metadata)比如SuperBlock所使用的缓存页。它与“Cached”的区别在于,”Cached”表示普通文件所占用的缓存页。参见我的另一篇文章http://linuxperf.com/?p=32

“Buffers”所占的内存同时也在LRU list中,被统计在Active(file)或Inactive(file)。

注:通过阅读源代码可知,块设备的读写操作涉及的缓存被纳入了LRU,以读操作为例,do_generic_file_read()函数通过 mapping->a_ops->readpage() 调用块设备底层的函数,并调用 add_to_page_cache_lru() 把缓存页加入到LRU list中。参见:
filemap.c: do_generic_file_read > add_to_page_cache_lru

其它问题

DirectMap

/proc/meminfo中的DirectMap所统计的不是关于内存的使用,而是一个反映TLB效率的指标。TLB(Translation Lookaside Buffer)是位于CPU上的缓存,用于将内存的虚拟地址翻译成物理地址,由于TLB的大小有限,不能缓存的地址就需要访问内存里的page table来进行翻译,速度慢很多。为了尽可能地将地址放进TLB缓存,新的CPU硬件支持比4k更大的页面从而达到减少地址数量的目的, 比如2MB,4MB,甚至1GB的内存页,视不同的硬件而定。”DirectMap4k”表示映射为4kB的内存数量, “DirectMap2M”表示映射为2MB的内存数量,以此类推。所以DirectMap其实是一个反映TLB效率的指标。

Dirty pages到底有多少?

/proc/meminfo 中有一个Dirty统计值,但是它未能包括系统中全部的dirty pages,应该再加上另外两项:NFS_Unstable 和 Writeback,NFS_Unstable是发给NFS server但尚未写入硬盘的缓存页,Writeback是正准备回写硬盘的缓存页。即:

系统中全部dirty pages = ( Dirty + NFS_Unstable + Writeback )

注1:NFS_Unstable的内存被包含在Slab中,因为nfs request内存是调用kmem_cache_zalloc()申请的。

注2:anonymous pages不属于dirty pages。
参见mm/vmscan.c: page_check_dirty_writeback()
“Anonymous pages are not handled by flushers and must be written from reclaim context.”

为什么【Active(anon)+Inactive(anon)】不等于AnonPages?

因为Shmem(即Shared memory & tmpfs) 被计入LRU Active/Inactive(anon),但未计入 AnonPages。所以一个更合理的等式是:

【Active(anon)+Inactive(anon)】 = 【AnonPages + Shmem】

但是这个等式在某些情况下也不一定成立,因为:

  • 如果shmem或anonymous pages被mlock的话,就不在Active(non)或Inactive(anon)里了,而是到了Unevictable里,以上等式就不平衡了;
  • 当anonymous pages准备被swap-out时,分几个步骤:先被加进swap cache,再离开AnonPages,然后离开LRU Inactive(anon),最后从swap cache中删除,这几个步骤之间会有间隔,而且有可能离开AnonPages就因某些情况而结束了,所以在某些时刻以上等式会不平衡。
    【注:参见mm/vmscan.c: shrink_page_list():
    它调用的add_to_swap()会把swap cache页面标记成dirty,然后调用try_to_unmap()将页面对应的page table mapping都删除,再调用pageout()回写dirty page,最后try_to_free_swap()把该页从swap cache中删除。】
为什么【Active(file)+Inactive(file)】不等于Mapped?
  1. 因为LRU Active(file)和Inactive(file)中不仅包含mapped页面,还包含unmapped页面;
  2. Mapped中包含”Shmem”(即shared memory & tmpfs),这部分内存被计入了LRU Active(anon)或Inactive(anon)、而不在Active(file)和Inactive(file)中。
为什么【Active(file)+Inactive(file)】不等于 Cached?
  1. 因为”Shmem”(即shared memory & tmpfs)包含在Cached中,而不在Active(file)和Inactive(file)中;
  2. Active(file)和Inactive(file)还包含Buffers。
  • 如果不考虑mlock的话,一个更符合逻辑的等式是:
    【Active(file) + Inactive(file) + Shmem】== 【Cached + Buffers】
  • 如果有mlock的话,等式应该如下(mlock包括file和anon两部分,/proc/meminfo中并未分开统计,下面的mlock_file只是用来表意,实际并没有这个统计值):
    【Active(file) + Inactive(file) + Shmem + mlock_file】== 【Cached + Buffers】

注:
测试的结果以上等式通常都成立,但内存发生交换的时候以上等式有时不平衡,我猜可能是因为有些属于Shmem的内存swap-out的过程中离开Cached进入了Swapcached,但没有立即从swap cache删除、仍算在Shmem中的缘故。

 Linux的内存都用到哪里去了?

尽管不可能精确统计Linux系统的内存,但大体了解还是可以的。

kernel内存的统计方式应该比较明确,即

【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】

  • 注1:VmallocUsed其实不是我们感兴趣的,因为它还包括了VM_IOREMAP等并未消耗物理内存的IO地址映射空间,我们只关心VM_ALLOC操作,(参见1.2节),所以实际上应该统计/proc/vmallocinfo中的vmalloc记录,例如(此处单位是byte):

  • 注2:kernel module的内存被包含在VmallocUsed中,见1.3节。
  • 注3:X表示直接通过alloc_pages/__get_free_page分配的内存,没有在/proc/meminfo中统计,不知道有多少,就像个黑洞。
用户进程的内存主要有三种统计口径:
  1. 围绕LRU进行统计
    【(Active + Inactive + Unevictable) + (HugePages_Total * Hugepagesize)】
  2. 围绕Page Cache进行统计
    当SwapCached为0的时候,用户进程的内存总计如下:
    【(Cached + AnonPages + Buffers) + (HugePages_Total * Hugepagesize)】
    当SwapCached不为0的时候,以上公式不成立,因为SwapCached可能会含有Shmem,而Shmem本来被含在Cached中,一旦swap-out就从Cached转移到了SwapCached,可是我们又不能把SwapCached加进上述公式中,因为SwapCached虽然不与Cached重叠却与AnonPages有重叠,它既可能含有Shared memory又可能含有Anonymous Pages。
  3. 围绕RSS/PSS进行统计
    把/proc/[1-9]*/smaps 中的 Pss 累加起来就是所有用户进程占用的内存,但是还没有包括Page Cache中unmapped部分、以及HugePages,所以公式如下:
    ΣPss + (Cached – mapped) + Buffers + (HugePages_Total * Hugepagesize)
所以系统内存的使用情况可以用以下公式表示:
  • MemTotal = MemFree +【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】+【Active + Inactive + Unevictable + (HugePages_Total * Hugepagesize)】
  • MemTotal = MemFree +【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】+【Cached + AnonPages + Buffers + (HugePages_Total * Hugepagesize)】
  • MemTotal = MemFree +【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】+【ΣPss + (Cached – mapped) + Buffers + (HugePages_Total * Hugepagesize)】

 

free命令显示的buffers与cached的区别

free 命令是Linux系统上查看内存使用状况最常用的工具,然而很少有人能说清楚 “buffers” 与 “cached” 之间的区别:

我们先抛出结论,如果你对研究过程感兴趣可以继续阅读后面的段落:

“buffers” 表示块设备(block device)所占用的缓存页,包括:直接读写块设备、以及文件系统元数据(metadata)比如SuperBlock所使用的缓存页;
“cached” 表示普通文件数据所占用的缓存页。

下面是分析过程:

先用 strace 跟踪 free 命令,看看它是如何计算 “buffers” 和 “cached” 的:

显然 free 命令是从 /proc/meminfo 中读取信息的,跟我们直接读到的结果一样:

那么 /proc/meminfo 中的 “Buffers” 和 “Cached” 又是如何得来的呢?这回没法偷懒,只能去看源代码了。源代码文件是:fs/proc/meminfo.c ,我们感兴趣的函数是:meminfo_proc_show(),阅读得知:

“Cached” 来自于以下公式:
global_page_state(NR_FILE_PAGES) – total_swapcache_pages – i.bufferram

global_page_state(NR_FILE_PAGES) 表示所有的缓存页(page cache)的总和,它包括:

  • “Cached”
  • “Buffers” 也就是上面公式中的 i.bufferram,来自于 nr_blockdev_pages() 函数的返回值。
  • 交换区缓存(swap cache)

global_page_state(NR_FILE_PAGES) 来自 vmstat[NR_FILE_PAGES],vmstat[NR_FILE_PAGES] 可以通过 /proc/vmstat 来查看,表示所有缓存页的总数量:

注意以上nr_file_pages是以page为单位(一个page等于4KB),而free命令是以KB为单位的。

直接修改 nr_file_pages 的内核函数是:
__inc_zone_page_state(page, NR_FILE_PAGES) 和
__dec_zone_page_state(page, NR_FILE_PAGES),
一个用于增加,一个用于减少。

Swap Cache是什么?

用户进程的内存页分为两种:file-backed pages(与文件对应的内存页)和anonymous pages(匿名页)。匿名页(anonymous pages)是没有关联任何文件的,比如用户进程通过malloc()申请的内存页,如果发生swapping换页,它们没有关联的文件进行回写,所以只能写入到交换区里。

交换区可以包括一个或多个交换区设备(裸盘、逻辑卷、文件都可以充当交换区设备),每一个交换区设备在内存里都有对应的swap cache,可以把swap cache理解为交换区设备的”page cache”:page cache对应的是一个个文件,swap cache对应的是一个个交换区设备,kernel管理swap cache与管理page cache一样,用的都是radix-tree,唯一的区别是:page cache与文件的对应关系在打开文件时就确定了,而一个匿名页只有在即将被swap-out的时候才决定它会被放到哪一个交换区设备,即匿名页与swap cache的对应关系在即将被swap-out时才确立。

并不是每一个匿名页都在swap cache中,只有以下情形之一的匿名页才在:

  • 匿名页即将被swap-out时会先被放进swap cache,但通常只存在很短暂的时间,因为紧接着在pageout完成之后它就会从swap cache中删除,毕竟swap-out的目的就是为了腾出空闲内存;
    【注:参见mm/vmscan.c: shrink_page_list(),它调用的add_to_swap()会把swap cache页面标记成dirty,然后它调用try_to_unmap()将页面对应的page table mapping都删除,再调用pageout()回写dirty page,最后try_to_free_swap()会把该页从swap cache中删除。】
  • 曾经被swap-out现在又被swap-in的匿名页会在swap cache中,直到页面中的内容发生变化、或者原来用过的交换区空间被回收为止。
    【注:当匿名页的内容发生变化时会删除对应的swap cache,代码参见mm/swapfile.c: reuse_swap_page()。】
“cached”:

“Cached” 表示除去 “buffers” 和 “swap cache” 之外,剩下的也就是普通文件的缓存页的数量:
global_page_state(NR_FILE_PAGES) – total_swapcache_pages – i.bufferram
所以关键还是要理解 “buffers” 是什么含义。

“buffers” :

从源代码中看到,”buffers” 来自于 nr_blockdev_pages() 函数的返回值:

这段代码的意思是遍历所有的块设备(block device),累加每个块设备的inode的i_mapping的页数,统计得到的就是 buffers。显然 buffers 是与块设备直接相关的。

那么谁会更新块设备的缓存页数量(nrpages)呢?我们继续向下看。

搜索kernel源代码发现,最终更新mapping->nrpages字段的函数就是:
pagemap.h: add_to_page_cache
> filemap.c: add_to_page_cache_locked
>  __add_to_page_cache_locked
> page_cache_tree_insert
和:
filemap.c: delete_from_page_cache
> __delete_from_page_cache
> page_cache_tree_delete

这两个函数是通用的,block device 和 文件inode 都可以调用,至于更新的是块设备的(buffers)还是文件的(cached),取决于参数变量mapping:如果mapping对应的是块设备,那么相应的统计信息会反映在 “buffers” 中;如果mapping对应的是文件inode,影响的就是 “cached”。我们下面看看kernel中哪些地方会把块设备的mapping传递进来。

首先是块设备本身,打开时使用 bdev->bd_inode->i_mapping。

其次,文件系统的Superblock也是使用块设备:

sb表示SuperBlock,s_bdev就是块设备。Superblock是文件系统的metadata(元数据),不属于文件,没有对应的inode,所以,对metadata操作所涉及的缓存页都只能利用块设备mapping,算入 buffers 的统计值内。

如果文件含有间接块(indirect blocks),因为间接块也属于metadata,所以走的也是块设备的mapping。查看源代码,果然如此:

这样我们就知道了”buffers” 是块设备(block device)占用的缓存页,分为两种情况:

  • 直接对块设备进行读写操作;
  • 文件系统的metadata(元数据),比如 SuperBlock。
验证:

现在我们来做个测试,验证一下上述结论。既然文件系统的metadata会用到 “buffers”,我们用 find 命令扫描文件系统,观察 “buffers” 增加的情况:

再测试一下直接读取block device,观察”buffers”增加的现象:

 结论:

free 命令所显示的 “buffers” 表示块设备(block device)所占用的缓存页,包括直接读写块设备、以及文件系统元数据(metadata)如SuperBlock所使用的缓存页;
而 “cached” 表示普通文件所占用的缓存页。

用kmemleak检测内核内存泄漏

所谓内存泄漏(memory leak),是指分配出去的内存在用完之后忘了释放,造成内存浪费,可用的内存越来越少。内存泄漏是程序设计的错误导致的,既可能发生在用户程序里,也可能发生在内核中。

诊断内存泄漏问题的目标是定位为什么内存用完之后会忘了释放,最终都是需要阅读源代码,理解内在的逻辑,找出其中的错误。作为最基本的分析线索,我们至少需要观察内存的分配与释放操作,还有一些更高级的工具可以帮助我们找出无人引用的内存块以及最初分配它的backtrace,这就更有针对性了。诊断用户态程序内存泄漏最流行的工具是Valgrind,对于内核,类似的工具是kmemleak。

kmemleak的原理

kmemleak通过追踪kmalloc(), vmalloc(), kmem_cache_alloc()等函数,把分配内存的指针和大小、时间、stack trace等信息记录在一个rbtree中,等到调用free释放内存时就把相应的记录从rbtree中删除,也就是说rbtree中的记录就是已经分配出去但尚未释放的内存,其中有些内存尚未释放是因为还在被使用,这属于正常情况,而不正常的情况即内存尚未释放但又不会再被使用,就是“泄漏”的内存,那么如何找出泄漏的内存呢?kmemleak缺省每10分钟对内存做一次扫描,在内存中寻找rbtree中记录的地址,如果某个rbtree记录的地址在内存中找不到,就认为这个地址是无人引用的,以后也不可能再被用到,是“泄漏”的内存,然后,把这些泄漏的内存地址以及rbtree中记录的时间、大小、strack trace等相关信息通过 /sys/kernel/debug/kmemleak 这个接口展现给我们。

注:
kmemleak的扫描算法存在误报的可能,比如内存中碰巧有一个数据与rbtree中的某个地址相同,但它只是数据而非指针,kmemleak是无法分辨的,会把它当作访问内存的指针;再比如rbtree中的某个地址在内存中找不到,但程序可能还在用它,只是因为程序并没有直接保存访问地址,而是通过某种方式临时计算访问地址,这种情况kmemleak也无法分辨,会认为是泄漏。但是请注意,kmemleak 这个工具的目的是为了给进一步分析提供线索,并不需要绝对精确,小概率的误报并不影响这个工具的实用性。

怎样启用kmemleak

要启用kmemleak,前提是内核编译时在“Kernel hacking”中开启了 CONFIG_DEBUG_KMEMLEAK 选项。怎样知道一个运行系统的内核是否支持kmemleak呢?可以查看 /boot/config-$(uname -r) 配置文件中 CONFIG_DEBUG_KMEMLEAK 是否等于”y”。以RHEL6为例,它的debug kernel是支持kmemleak的,我们看它的config文件:

  • CONFIG_DEBUG_KMEMLEAK=y
    表示内核支持kmemleak;
  • CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=y
    表示kmemleak默认是关闭的,需要显式启用,启用的方式是在kernel command line中添加以下选项:
    kmemleak=on
怎样使用kmemleak

kmemleak的用户接口是:
/sys/kernel/debug/kmemleak
发送指令和输出信息都是通过以上文件进行的。要访问这个文件,必须先挂载以下文件系统:
# mount -t debugfs nodev /sys/kernel/debug/

kmemleak缺省每10分钟扫描内存一次,找到可疑的内存泄漏会在syslog中写一条记录并提示通过/sys/kernel/debug/kmemleak可以看到更详细的信息:

也可以手工发起kmemleak的内存扫描:
# echo scan > /sys/kernel/debug/kmemleak

还可以调整kmemleak内存扫描的频率等参数,更多的操作详见手册:
https://www.kernel.org/doc/html/latest/dev-tools/kmemleak.html

参考资料:
http://tekkamanninja.blog.chinaunix.net/uid-26859697-id-5758036.html

 

如何诊断SLUB问题

前文中,我们介绍了在RHEL6及较早的kernel上诊断slab泄漏问题的两种方法,可以说相当麻烦了,这是因为以前的slab没有提供原生的故障诊断机制。Linux kernel自2.6.23之后采用的Slub自带了故障诊断机制,就方便很多,然而习惯上仍然把slub称作slab。如何判断你的系统kernel是否在用slub呢?仅从kernel版本号是看不准的,比如RHEL6的kernel版本2.6.x仍然在使用slab,从RHEL7才开始采用Slub。有一个简单的判断方法,就是看是否存在/sys/kernel/slab目录,有就是slub,没有就是slab。

Slub的debug机制

了解slub的debug机制之前,先要弄清楚可能会出现哪些问题,无论是slab还是slub,问题无非是以下几类:

  • 内存泄露(leak),alloc之后忘了free,导致内存占用不断增长;
  • 越界(overrun),访问了alloc分配的区域之外的内存,覆盖了不属于自己的数据;
  • 使用已经释放的内存(use after free),正常情况下,已经被free释放的内存是不应该再被读写的,否则就意味着程序有bug;
  • 使用未经初始化的数据(use uninitialised bytes),缺省模式下alloc分配的内存是不被初始化的,内存值是随机的,直接使用的话后果可能是灾难性的。

Slub提供了red zone和poisoning等debug机制来检测以上问题。

  • Red zone来自于橄榄球术语,是指球场底线附近的区域,slub通过在每一个对象后面额外添加一块red zone区域来帮助检测越界(overrun)问题,在red zone里填充了特征字符,如果代码访问到了red zone就意味着越界了。
  • Poisoning是通过往slub对象中填充特征字符的方式来检测use-after-free、use-uninitialised等问题,比如在分配slub对象时填充0x5a,在释放时填充0x6b,然后debug代码检查时如果看到本该是0x6b的位置变成了别的内容,就可能是发生了use-after-free,而本该是0x5a的位置如果变成了其它内容就意味着可能是use-uninitialised问题。更多的poison字节定义参见以下文件:
    /lib/modules/$(uname -r)/build/include/linux/poison.h

(图)SLUB对象的格式
* 绿色的 Payload表示分配出去的 slub object;
* slub debug机制需要占用额外的内存,比如 Red zone,还有,为了追溯 slub object的分配和释放过程,需要额外的空间来存放 stack trace,即图中的 Tracking/Debugging;
* 图中的 FP是 Free Pointer的缩写,处于 free状态的 object是以链表的形式串在一起的,FP就是链表指针。

怎样开启slub debug

Slub本身包含了完整的debug功能,缺省是关闭的,需要的时候打开就行了。

开启slub debug有两种方式:

  1. 【启动时开启】
    在kernel command line中加入以下参数:
    slub_debug=<Debug-Options>,<slab name>
    它会在重启时生效。
    注:
    slub_debug后面不跟任何参数表示打开所有的debug功能;
    slub_debug=<Debug-Options> 对所有的slab打开指定的debug options;
    slub_debug=<Debug-Options>,<slab name> 对指定的slab打开指定的debug options;
    slub_debug=,<slab name> 对指定的slab打开所有的debug options。
  2. 【运行中开启】
    在运行系统上可以通过以下文件对指定的slab打开指定的debug option:
    /sys/kernel/slab/<slab name>/<debug_file>

Debug options如下:

debug option sysfs debug file 功能
F sanity_checks 激活完整性检查功能,在特定的环节比如free的时候增加各种条件判断,验证数据是否完好。
Z red_zone 用于检测overrun。通过在slub object后面插入一块额外的红色区域(橄榄球术语),一旦进入就表示有错。
P poison 用于检测use-after-free和use-uninitialised。给slub对象填充特征字符,比如在分配时填充0x5a,在释放时填充0x6b,根据特征字符是否被覆盖来检测是否出错。更多的poison字节定义参见: /lib/modules/$(uname -r)/build/include/linux/poison.h
U store_user 在slub object后面添加一块额外的空间,记录调用alloc/free的stack trace
T trace 在slub object alloc/free时,向系统日志中输出相关信息,包括stack trace

开启debug option会降低系统性能,所以尽量只开启必要的选项。

  • 如果slab出现data corruption问题,可以考虑read_zone,poison,store_user,sanity_checks;
  • 如果某个slab的大小持续疯涨,则可能是leak(内存泄露),可以开启trace,观察统计slab的alloc/free情况,寻找线索。
    对于内核内存泄漏问题,还有另一个工具可供选用,详见:用KMEMLEAK检测内核内存泄漏

在kernel commandline中开启slub debug与在运行系统上开启是有区别的。有些debug option不能在运行系统上开启,比如red_zone、store_user需要额外的存储空间来保存debug信息,如果不是从一开始就开启,那么以前分配出去的slub对象就没有debug数据区,会导致对齐问题,而且debug代码也很难分辨新老slub对象;再比如poison,如果不是从一开始就开启,那么以前分配出去的slub对象就没有填充特征字符,debug代码也辨别不了哪些slub对象是有填充的、哪些是没有填充的。所以,在运行系统上开启slub debug,如果指定的slab里面已经有了object,那么只能动态开启sanity_checks和trace,惟有当指定的slab还是空的,其它的debug option才可以动态开启。我们实际使用的时候,尽管去试好了,如果某个debug option不允许动态开启,命令就不会成功,比如:

 

要进行slub debug,有一个重要特性不可不知,那就是slab merging。

Slab merging

很多slab的大小和参数是相似的,slub会把这些不同的slab合并到一起,好处是可以减少内存碎片,提高内存使用效率,这个称为slab merging(合并)。

怎么知道一个slab有没有发生合并呢?通过查看以下文件:
/sys/kernel/slab/<slab name>/aliases
aliases表示参与合并的slab的数量(自己除外),如果大于0就意味着发生了合并。

slabinfo工具(见下一节的介绍)可以具体列出哪些slab合并到了一起:

Slab merging会干扰debug,因为不同的slab合并到了一起,出了问题以后很难分辨是哪一个slab导致的。关闭slab merging的方法有两个:

  • 在kernel command line中加入“slub_nomerge”;
  • 开启slub debug之后,slab merging就会自动关闭。如果slub_debug指定了某个slab,那么只有指定的slab会关闭merging。
    注:最好是在启动时开启slub_debug,如果是在运行系统上通过sysfs开启slub debug,那么之前已经合并的那些slab仍然会保持合并状态。
slabinfo工具

随内核源程序提供了一个slabinfo工具,但是需要自己手工编译。源程序的位置是在源代码树下的 tools/vm/slabinfo.c,编译方法是:
$ gcc -o slabinfo tools/vm/slabinfo.c
或者进入 tools/vm 目录下直接执行make:
$ make slabinfo

slabinfo工具能做的事情见它的帮助信息:

 

参考资料:

https://www.kernel.org/doc/Documentation/vm/slub.txt
https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-kernel-slab
https://lwn.net/Articles/340267/
https://gitlab.eurecom.fr/oai/odroid-linux-3.10.y-rt/commit/4c13dd3b48fcb6fbe44f241eb11a057ecd1cba75
一个实例分析

 

怎样诊断slab泄露问题

Slab allocator是Linux kernel的内存分配机制,各内核子系统、模块、驱动程序都可以使用,但用完应该记得释放,忘记释放就会造成“内存泄露”(memory leak)。如果导致泄露的代码使用率很低倒也罢了,若是使用率很高的话,系统的内存会被迅速耗尽。

在以下案例中,132 GB 内存,仅剩21 GB空闲,还有16 GB的交换区被用掉了,显然内存使用相当紧张,而内存的主要去向是slab,slab用了89 GB,其中不可回收的部分(SUnreclaim)就占了88 GB:

再细看/proc/slabinfo,发现“size-4096”占用了84 GB之多(21720567*4096):

通常Slab的名字就表明了其用途,比如”inode_cache”、”dentry”什么的,根据发生泄露的slab cache的名字,大致就知道是哪个子系统或模块的问题,然而本例比较复杂,因为从”size-4096″的名称完全看不出slab cache的用途。

即便从名字知道了是哪个子系统的问题,为了进一步定位故障点,我们还要看到具体是哪些函数、哪些代码在分配内存才行。要如何诊断此类问题呢?

首先取决于kernel用的是slab还是slub,slab其实是一个统称,Linux kernel自2.6.23之后就已经从Slab进化成Slub了,它自带原生的诊断功能,比Slab更方便。判断kernel是否在使用slub有一个简单的方法,就是看/sys/kernel/slab目录是否存在,如果存在的话就是slub,否则就是slab。SLUB的故障诊断请参考以下文章:如何诊断SLUB问题

SLAB

如果是slab的话,有两种常见方法:一是利用debug kernel的slab leak辅助功能,二是利用systemtap等工具。参见https://access.redhat.com/solutions/358933

使用kernel的DEBUG_SLAB_LEAK功能

这需要kernel编译的时候打开了”CONFIG_DEBUG_SLAB_LEAK”选项才行,默认是没打开的。

对RHEL或CentOS来说,debug kernel打开了此编译选项,可以安装名为kernel-debug-*的rpm软件包,然后重启系统并选择此debug kernel即可。

完成后/proc目录下会出现一个名为slab_allocators的文件,里面会记录类似如下的slab分配的信息,注意观察是什么代码在分配slab,有助于找到可疑的泄漏点。缺点是只记录了直接调用的函数,没有完整的backtrace:

使用systemtap

除了使用debug kernel之外,还有个方法就是用systemtap,对内核适当的位置植入探针,有助于找到可疑的slab分配,这需要对内核有一定的了解才行。

普通的slab cache是通过kmem_cache_alloc来分配的,可以用现成的systemtap probe vm.kmem_cache_alloc进行观测。但是在本例中不适用,因为本例中”size-4096″属于slab里的general purpose cache,是供kmalloc()使用的,所以systemtap应该针对kmalloc()进行探测,这里有一个现成的脚本 “kmalloc-top“,它的原理是对__kmalloc()下探针,记录backtraces,因为__kmalloc是实现kmalloc()的核心函数,有的代码会直接调用__kmalloc,所以探测它而不是kmalloc()才不会有遗漏。以上的脚本没有记录kmalloc的size,所以我修改了一下,加上了kmalloc size,修改过的内容如下:

以root身份执行:

间隔一段时间再ctrl-c退出,看到结果如下:

可以看到,大量的size-4096分配来自内核模块”sisips”,有理由对它产生怀疑。(因为这是Symantec的内核模块,系统上没有它的debuginfo,所以systemtap解析不了它的backtrace符号,只能显示出16进制的地址)。为了验证该模块是否真的导致了内存泄露,可以暂时禁用它,观察/proc/slabinfo看size-4096是否停止疯涨,如果停了,显然该模块就有问题了。

另一种方法:kmemleak

检测内核内存泄漏还有另一种方法,就是利用kmemleak工具,它并不是针对某一个slab,而是针对所有的内核内存。详见:
用KMEMLEAK检测内核内存泄漏