kdump简介

分析操作系统crash或hang的原因,需要用到kernel dump。Linux系统用来捕捉kernel dump的工具是kdump。

kdump的原理是启动一个特殊的dump-capture kernel把系统内存里的数据保存到文件里,为什么需要一个特殊的dump-capture kernel呢?因为原来的kernel已经出问题了,发生crash或hang了。

Dump-capture kernel 既可以是独立的,也可以与系统内核集成在一起–这需要硬件支持relocatable kernel才行。在X86_64系统上RHEL6/7和SLES11/12缺省都是与系统内核集成在一起的。

kdump工作的过程如下:

  1. 系统内核启动的时候,要给dump-capture kernel预留一块内存空间;
  2. 内核启动完成后,kdump service执行 kexec -p 命令把dump-capture kernel载入预留的内存里;
  3. 然后,如果系统发生crash,会自动reboot进入dump-capture kernel,dump-capture kernel只使用自己的预留内存,确保其余的内存数据不会被改动,它的任务是把系统内存里的数据写入到dump文件,比如/var/crash/vmcore,为了减小文件的大小,它会通过makedumpfile(8)命令对内存数据进行挑选和压缩;
  4. dump文件写完之后,dump-capture kernel自动reboot。
预留内存

配置kdump的一个关键环节是预留内存。预留的内存是有特殊要求的,它必须是连续的,在老的系统上比如SLES11还要求内存物理地址低于4GB(SLES12无此要求,因为新kernel允许使用高位内存)。可供预留的内存取决于硬件配置,BIOS/firmware占用的内存是不能预留的,EFI经常占用很多内存,I/O卡也要占用内存,在引导过程中都能看到:

为dump-capture kernel预留内存的方法是在kernel command line中加入如下参数:
crashkernel=size[@offset]

需要预留多少内存呢?
RHEL从6.2开始可以使用”crashkernel=auto”,让内核自行计算,如果有问题才手工指定。
SLES12则可以用”kdumptool calibrate”命令计算推荐值,具体方法参见SUSE DOC: Calculating crashkernel Allocation Size
更老的系统上需要根据系统总内存手工计算预留内存的大小,参考值请自行搜索。

不幸的是,确实有些系统存在无法预留足够内存的情况,如果dump-capture kernel需要的内存比较多,而硬件配置又比较复杂导致可用的连续内存不足,就有可能发生。这在老内核上更常见,而新内核允许使用高位内存就好多了。

通常RHEL 6.X最大建议768MB,实际上代码中最大的限制是896MB,但实际经验中800M以上就会有各种问题。

Redhat还提供了一个设置kdump的助手工具:
https://access.redhat.com/labs/kdumphelper/

kdump的性能

计算机的内存越来越大,kernel dump也越来越大,保存dump文件的时间也越来越长,为了提高速度,kdump的配置可以进行调整。

1,多CPU
缺省情况下dump-capture kernel只使用一个CPU,有时使用多个CPU会有帮助。这可以通过修改dump-capture kernel 的参数 nr_cpus=1 来实现,怎样修改dump-capture kernel的参数呢?不是在grub中,那里是普通kernel的参数,而是在 /etc/sysconfig/kdump 中,如下所示:
KDUMP_COMMANDLINE_APPEND=”irqpoll nr_cpus=4 …”

2,压缩算法
makedumpfile(8)默认的”-c”参数使用zlib,虽然压缩比很高但是速度很慢,通常低于30MB/s。可以选用速度更快的LZO,虽然压缩比稍微低一点但是速度可达800MB/s,把/etc/kdump.conf中的makedumpfile(8)的”-c”参数换成”-l”即可。

3,排除不需要的内存页
makedumpfile(8)的”-d”参数指定dump level,dump level是一个5-bit的编码,每个bit表示一种可以排除的内存页:
                    1 : Exclude the pages filled with zero.
                    2 : Exclude the non-private cache pages.
                    4 : Exclude all cache pages.
                    8 : Exclude the user process data pages.
                   16 : Exclude the free pages.
一般默认dump level是31,即排除以上所有类型的内存页。

参考资料:

http://lse.sourceforge.net/kdump/documentation/ols2oo5-kdump-paper.pdf
https://www.kernel.org/doc/Documentation/kdump/kdump.txt
http://people.redhat.com/nhorman/papers/ols-slides.pdf
http://events.linuxfoundation.org/sites/events/files/slides/slide_final_0.pdf

利用blktrace分析IO性能

在Linux系统上,如果I/O发生性能问题,有没有办法进一步定位故障位置呢?iostat等最常用的工具肯定是指望不上的,【容易被误读的iostat】一文中解释过await表示单个I/O所需的平均时间,但它同时包含了I/O Scheduler所消耗的时间和硬件所消耗的时间,所以不能作为硬件性能的指标,至于iostat的svctm更是一个废弃的指标,手册上已经明确说明了的。blktrace在这种场合就能派上用场,因为它能记录I/O所经历的各个步骤,从中可以分析是IO Scheduler慢还是硬件响应慢。

blktrace的原理

一个I/O请求进入block layer之后,可能会经历下面的过程:

  • Remap: 可能被DM(Device Mapper)或MD(Multiple Device, Software RAID) remap到其它设备
  • Split: 可能会因为I/O请求与扇区边界未对齐、或者size太大而被分拆(split)成多个物理I/O
  • Merge: 可能会因为与其它I/O请求的物理位置相邻而合并(merge)成一个I/O
  • 被IO Scheduler依照调度策略发送给driver
  • 被driver提交给硬件,经过HBA、电缆(光纤、网线等)、交换机(SAN或网络)、最后到达存储设备,设备完成IO请求之后再把结果发回。

blktrace能记录I/O所经历的各个步骤,来看一下它记录的数据,包含9个字段,下图标示了其中8个字段的含义,大致的意思是“哪个进程在访问哪个硬盘的哪个扇区,进行什么操作,进行到哪个步骤,时间戳是多少”:

blktrace-event-output

第7个字段在上图中没有标出来,它表示操作类型,具体含义是:”R” for Read, “W” for Write, “D” for block, “B” for Barrier operation。

第6个字段是Event,代表了一个I/O请求所经历的各个阶段,具体含义在blkparse的手册页中有解释,其中最重要的几个阶段如下:

Q – 即将生成IO请求
|
G – IO请求生成
|
I – IO请求进入IO Scheduler队列
|
D – IO请求进入driver
|
C – IO请求执行完毕

根据以上步骤对应的时间戳就可以计算出I/O请求在每个阶段所消耗的时间:

Q2G – 生成IO请求所消耗的时间,包括remap和split的时间;
G2I – IO请求进入IO Scheduler所消耗的时间,包括merge的时间;
I2D – IO请求在IO Scheduler中等待的时间;
D2C – IO请求在driver和硬件上所消耗的时间;
Q2C – 整个IO请求所消耗的时间(Q2I + I2D + D2C = Q2C),相当于iostat的await。

如果I/O性能慢的话,以上指标有助于进一步定位缓慢发生的地方:
D2C可以作为硬件性能的指标;
I2D可以作为IO Scheduler性能的指标。

附上event速查表:

blktrace的用法

使用blktrace需要挂载debugfs:
$ mount -t debugfs debugfs /sys/kernel/debug

利用blktrace查看实时数据的方法,比如要看的硬盘是sdb:
$ blktrace -d /dev/sdb -o – | blkparse -i –
需要停止的时候,按Ctrl-C。

以上命令也可以用下面的脚本代替:
$ btrace /dev/sdb

利用blktrace把数据记录在文件里,以供事后分析:
$ blktrace -d /dev/sdb
缺省的输出文件名是 sdb.blktrace.<cpu>,每个CPU对应一个文件。
你也可以用-o参数指定自己的输出文件名。

利用blkparse命令分析blktrace记录的数据:
$ blkparse -i sdb

注:
在以上数据中,有一些记录的event类型是”m”,那是IO Scheduler的调度信息,对研究IO Scheduler问题有意义:

  • cfq18166S – cfq是IO Scheduler的名称,18166是进程号,”S”表示Sync(同步IO),如果异步IO则用“A”表示(Async);
  • 它们的第三列sequence number都是0;
  • 它们表示IO Scheduler内部的关键函数,上例中是cfq,代码参见block/cfq-iosched.c,以下是各关键字所对应的内部函数:
    alloced <<< cfq_find_alloc_queue()
    insert_request <<< cfq_insert_request()
    add_to_rr <<< cfq_add_cfqq_rr()
    cfq workload slice:300 <<< choose_wl_class_and_type()
    set_active wl_class:0 wl_type:2 <<< __cfq_set_active_queue()
    fifo= (null) <<< cfq_check_fifo()
    dispatch_insert <<< cfq_dispatch_insert()
    dispatched a request <<< cfq_dispatch_requests()
    activate rq, drv=1 <<< cfq_activate_request()
    complete rqnoidle 0 <<< cfq_completed_request()
    set_slice=100 <<< cfq_set_prio_slice()
    arm_idle: 8 group_idle: 0 <<< cfq_arm_slice_timer()
    cfq schedule dispatch <<< cfq_schedule_dispatch()
利用btt分析blktrace数据

blkparse只是将blktrace数据转成可以人工阅读的格式,由于数据量通常很大,人工分析并不轻松。btt是对blktrace数据进行自动分析的工具。

btt不能分析实时数据,只能对blktrace保存的数据文件进行分析。使用方法:
把原本按CPU分别保存的文件合并成一个,合并后的文件名为sdb.blktrace.bin:
$ blkparse -i sdb -d sdb.blktrace.bin
执行btt对sdb.blktrace.bin进行分析:
$ btt -i sdb.blktrace.bin

下面是一个btt实例:

我们看到93.7461%的时间消耗在D2C,也就是硬件层,这是正常的,我们说过D2C是衡量硬件性能的指标,这里单个IO平均0.129201毫秒,已经是相当快了,单个IO最慢14.246176 毫秒,不算坏。Q2G和G2I都很小,完全正常。I2D稍微有点大,应该是cfq scheduler的调度策略造成的,你可以试试其它scheduler,比如deadline,比较两者的差异,然后选择最适合你应用特点的那个。

利用blktrace数据自制分析工具

blktrace在block layer采集了每一个I/O的数据,可以用于完成一些非常深入的分析任务,以下是一个利用它分析应用系统的I/O模式的例子:
剖析生产系统的I/O模式

剖析生产系统的I/O模式

了解I/O的特点对于优化系统性能非常重要,I/O是顺序的还是随机的,是读操作还是写操作,读写的比例是多少,I/O数据块的大小,这些都是影响性能的关键因素。很多存储设备都基于特定的I/O模式做过调校,通用的测试工具跑分都相当漂亮,然而一到实际环境区别就来了,同样的应用环境下,不同的设备表现可能天差地别。我就见过不同厂商的设备,档次差不多,测试跑分高的那个在生产环境下的IO响应速度却慢了十倍。所以跑分高的设备真的不一定适合你的应用。

如果能够模拟出应用的I/O模式,那么在问题复现、乃至设备选型等方面都会有很大帮助。在此之前,了解I/O模式是第一步,这并不容易,像iostat之类的工具只能看到平均值,然而应用系统的I/O请求有可能是波浪式的,一秒之内也可以时高时低,I/O延迟可能平均值不高但是波动很大,而且I/O块大小也可以是变化的,尤其现在大数据应用的块大小可能在很大的范围内变化,与过去常见的交易型数据库有所不同,它们的块大小基本是固定的。

要剖析生产系统的I/O模式,好像没有现成的工具。但是我们可以利用blktrace自己做一个,blktrace在内核的block layer记录每一个I/O,提供了分析的素材。它记录的格式如下:

下面是一个简化版的示例,主要利用了Event “Q”和”C”,分别表示IO开始和IO完成,两者之间的耗时就相当于iostat看到的await,但blktrace可以精确到单个IO:

输出示例:

这个例子统计了IO的读/写数量、最大延迟、延迟的分布情况、块大小及数量,这些信息比iostat要具体得多,有助于进一步了解系统的IO模式。blktrace数据还有更多的利用空间等待你去发掘,譬如还可以根据时间戳去统计每个毫秒内的IO数,有助于更微观地了解IO请求数量的波动。

参考:
利用BLKTRACE分析IO性能

从几个问题开始理解CFS调度器

CFS(完全公平调度器)是Linux内核2.6.23版本开始采用的进程调度器,它的基本原理是这样的:设定一个调度周期(sched_latency_ns),目标是让每个进程在这个周期内至少有机会运行一次,换一种说法就是每个进程等待CPU的时间最长不超过这个调度周期;然后根据进程的数量,大家平分这个调度周期内的CPU使用权,由于进程的优先级即nice值不同,分割调度周期的时候要加权;每个进程的累计运行时间保存在自己的vruntime字段里,哪个进程的vruntime最小就获得本轮运行的权利。

那么问题就来了:

新进程的vruntime的初值是不是0啊?

假如新进程的vruntime初值为0的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占CPU的优势,老进程就要饿死了,这显然是不公平的。所以CFS是这样做的:每个CPU的运行队列cfs_rq都维护一个min_vruntime字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值就以它所在运行队列的min_vruntime为基础来设置,与老进程保持在合理的差距范围内。参见后面的源代码。

新进程的vruntime初值的设置与两个参数有关:
sched_child_runs_first:规定fork之后让子进程先于父进程运行;
sched_features的START_DEBIT位:规定新进程的第一次运行要有延迟。

注:
sched_features是控制调度器特性的开关,每个bit表示调度器的一个特性。在sched_features.h文件中记录了全部的特性。START_DEBIT是其中之一,如果打开这个特性,表示给新进程的vruntime初始值要设置得比默认值更大一些,这样会推迟它的运行时间,以防进程通过不停的fork来获得cpu时间片。

如果参数 sched_child_runs_first打开,意味着创建子进程后,保证子进程会在父进程之前运行。

子进程在创建时,vruntime初值首先被设置为min_vruntime;然后,如果sched_features中设置了START_DEBIT位,vruntime会在min_vruntime的基础上再增大一些。设置完子进程的vruntime之后,检查sched_child_runs_first参数,如果为1的话,就比较父进程和子进程的vruntime,若是父进程的vruntime更小,就对换父、子进程的vruntime,这样就保证了子进程会在父进程之前运行。

休眠进程的vruntime一直保持不变吗?

如果休眠进程的 vruntime 保持不变,而其他运行进程的 vruntime 一直在推进,那么等到休眠进程终于唤醒的时候,它的vruntime比别人小很多,会使它获得长时间抢占CPU的优势,其他进程就要饿死了。这显然是另一种形式的不公平。CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多。

 休眠进程在唤醒时会立刻抢占CPU吗?

这是由CFS的唤醒抢占 特性决定的,即sched_features的WAKEUP_PREEMPT位。

由于休眠进程在唤醒时会获得vruntime的补偿,所以它在醒来的时候有能力抢占CPU是大概率事件,这也是CFS调度算法的本意,即保证交互式进程的响应速度,因为交互式进程等待用户输入会频繁休眠。除了交互式进程以外,主动休眠的进程同样也会在唤醒时获得补偿,例如通过调用sleep()、nanosleep()的方式,定时醒来完成特定任务,这类进程往往并不要求快速响应,但是CFS不会把它们与交互式进程区分开来,它们同样也会在每次唤醒时获得vruntime补偿,这有可能会导致其它更重要的应用进程被抢占,有损整体性能。

我曾经处理过一个案例,服务器上有两类应用进程:
A进程定时循环检查有没有新任务,如果有的话就简单预处理后通知B进程,然后调用nanosleep()主动休眠,醒来后再重复下一个循环;
B进程负责数据运算,是CPU消耗型的;
B进程的运行时间很长,而A进程每次运行时间都很短,但睡眠/唤醒却十分频繁,每次唤醒就会抢占B,导致B的运行频繁被打断,大量的进程切换带来很大的开销,整体性能下降很厉害。
那有什么办法吗?有,最后我们通过禁止CFS唤醒抢占 特性解决了问题:

禁用唤醒抢占 特性之后,刚唤醒的进程不会立即抢占运行中的进程,而是要等到运行进程用完时间片之后。在以上案例中,经过这样的调整之后B进程被抢占的频率大大降低了,整体性能得到了改善。

如果禁止唤醒抢占特性对你的系统来说太过激进的话,你还可以选择调大以下参数:

sched_wakeup_granularity_ns
这个参数限定了一个唤醒进程要抢占当前进程之前必须满足的条件:只有当该唤醒进程的vruntime比当前进程的vruntime小、并且两者差距(vdiff)大于sched_wakeup_granularity_ns的情况下,才可以抢占,否则不可以。这个参数越大,发生唤醒抢占就越不容易。

进程占用的CPU时间片可以无穷小吗?

假设有两个进程,它们的vruntime初值都是一样的,第一个进程只要一运行,它的vruntime马上就比第二个进程更大了,那么它的CPU会立即被第二个进程抢占吗?答案是这样的:为了避免过于短暂的进程切换造成太大的消耗,CFS设定了进程占用CPU的最小时间值,sched_min_granularity_ns,正在CPU上运行的进程如果不足这个时间是不可以被调离CPU的。

sched_min_granularity_ns发挥作用的另一个场景是,本文开门见山就讲过,CFS把调度周期sched_latency按照进程的数量平分,给每个进程平均分配CPU时间片(当然要按照nice值加权,为简化起见不再强调),但是如果进程数量太多的话,就会造成CPU时间片太小,如果小于sched_min_granularity_ns的话就以sched_min_granularity_ns为准;而调度周期也随之不再遵守sched_latency_ns,而是以 (sched_min_granularity_ns * 进程数量) 的乘积为准。

进程从一个CPU迁移到另一个CPU上的时候vruntime会不会变?

在多CPU的系统上,不同的CPU的负载不一样,有的CPU更忙一些,而每个CPU都有自己的运行队列,每个队列中的进程的vruntime也走得有快有慢,比如我们对比每个运行队列的min_vruntime值,都会有不同:

如果一个进程从min_vruntime更小的CPU (A) 上迁移到min_vruntime更大的CPU (B) 上,可能就会占便宜了,因为CPU (B) 的运行队列中进程的vruntime普遍比较大,迁移过来的进程就会获得更多的CPU时间片。这显然不太公平。

CFS是这样做的:
当进程从一个CPU的运行队列中出来 (dequeue_entity) 的时候,它的vruntime要减去队列的min_vruntime值;
而当进程加入另一个CPU的运行队列 ( enqueue_entiry) 时,它的vruntime要加上该队列的min_vruntime值。
这样,进程从一个CPU迁移到另一个CPU之后,vruntime保持相对公平。

 

/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和devtmpfs。

注:所有tmpfs类型的文件系统占用的空间都计入共享内存,devtmpfs是/dev文件系统的类型,/dev/下所有的文件占用的空间也属于共享内存。可以用ls和du命令查看。如果文件在没有关闭的情况下被删除,空间仍然不会释放,shmem不会减小,可以用 “lsof -a +L1 /<mount_point>” 命令列出这样的文件。

此处所讲的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)】

 

为什么手工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”查看;
  • tmpfs文件系统和devtmpfs文件系统
    所有tmpfs文件系统占用的空间都计入共享内存,devtmpfs是/dev文件系统的类型,/dev/下所有的文件占用的空间也属于共享内存。可以用ls和du命令查看。如果文件没有关闭的情况下被删除,空间仍然不会释放,可以用 “lsof -a +L1 /<mount_point>” 命令查看。
  • 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或POSIX 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部分。

fsck与日志文件系统

日志文件系统(Journal File System)解决了掉电或系统崩溃造成元数据不一致的问题,细节参见《日志文件系统是怎样工作的》,它的原理是在进行写操作之前,把即将进行的各个步骤(称为transaction)事先记录下来,包括:从data block bitmap中分配一个数据块、在inode中添加指向数据块的指针、把用户数据写入数据块等,这些transaction保存在文件系统单独开辟的一块空间上,称为日志(journal),日志保存成功之后才进行真正的写操作–把文件系统的元数据和用户数据写进硬盘(称为checkpoint),万一写操作的过程中掉电,下次挂载文件系统之前把保存好的日志重新执行一遍就行了(术语叫做replay),保证文件系统的一致性。

Journal replay所需的时间很短,可以通过fsck或者mount命令完成。既然mount命令就可以做journal replay,那还要fsck干什么呢?fsck(file system check)所做的事情可不仅仅是journal replay这么简单,它可以对文件系统进行彻底的检查,扫描所有的inode、目录、superblock、allocation bitmap等等,称为full check。fsck进行full check所需的时间很长,而且文件系统越大,所需的时间也越长。

可能导致文件系统损坏的因素很多,不只是掉电或系统崩溃,比如软件bug和硬件错误都有可能损坏文件系统,而这类问题不是journal replay能解决的,必须用fsck才行。

fsck可以手工执行,也可以在boot时自动执行。无论手工执行还是自动执行,文件系统都必须处于未挂载(unmounted)状态。

fsck会否在boot过程中自动执行

最早的时候fsck缺省在boot的过程中自动执行,有了日志文件系统之后,掉电和系统崩溃对文件系统的潜在伤害可以通过journal replay得到解决,执行fsck就不是那么紧迫了,而且随着文件系统越来越大,full check所需的时间越来越长,在boot过程中自动进行fsck带来了诸多不便,也因此引发了很多争议,但fsck又不能不做,因为有些软硬件bug导致的文件系统损坏只有fsck才能处理。现在的倾向是把执行fsck的时机交给系统管理员去决定,是手工执行还是自动执行根据实际需要而定。其中一个讨论如下:
https://bugzilla.redhat.com/show_bug.cgi?id=879315

fsck是否在boot过程中自动执行是通过/etc/fstab的第6个字段控制的,如果为0就表示禁止fsck自动执行:

在已经禁止fsck自动执行的情况下,仍然可以强制下次boot时自动执行fsck,方法如下:

如果要reboot时不做fsck,可以用以下命令重启,它会忽略/etc/fstab的设置,启动时直接跳过fsck:

不同的文件系统有不同的fsck工具

不同类型的文件系统有各自的fsck工具,比如ext3文件系统对应的是fsck.ext3,xfs文件系统对应的是fsck.xfs。fsck命令只是个外壳,它本身没有能力去检查所有类型的文件系统,会根据文件系统的类型去调用相应的fsck工具。

  • ext2/ext3/ext4

ext2, ext3和ext4的fsck工具都是e2fsck,fsck.ext2、fsck.ext3和fsck.ext4都是指向e2fsck的链接。

ext2不是日志文件系统,没有日志(journal),e2fsck执行时会进行full check,耗时较长,尤其对大文件系统耗时更长。

ext3和ext4都是日志文件系统,e2fsck执行的时候,缺省做完journal replay就会返回,速度很快,除非superblock中的标记要求进行full check,(如果文件系统在运行过程中发现metadata不一致的话会在superblock中做标记,e2fsck执行时发现这个标记就会进行full check)。参见e2fsck的manpage:

ext3/ext4是否进行full check是由下列几个因素决定的:

  • superblock中的标记要求进行full check(即以上manpage所提到的);
  • e2fsck命令加了”-f”参数,强制进行full check;
  • ext3/ext4文件系统有两个参数决定是否进行full check:
    “Maximum mount count” 和 “Check interval”。

用”tune2fs -l”命令可以查看ext文件系统的参数,在下例中的文件系统每当mount次数达到25次的时候(Maximum mount count 值是25),一旦执行e2fsck就会进行full check;每过6个月(Check interval 值是6 months),一旦执行e2fsck就会进行full check:

修改这两个参数分别用 tune2fs 命令的 “-c” 和 “-i” 参数,比如”tune2fs -c 0 -i 0 /dev/sda”可以禁止”Maximum mount count”和”Check interval”达到指定值导致的full check。参见tune2fs的manpage:

 

  • XFS

XFS是日志文件系统,mount过程会进行journal replay等操作。如果允许boot过程中自动执行fsck,fsck会直接成功返回,因为对应的fsck.xfs什么也不做。

要想检查XFS文件系统,可以手工执行”xfs_repair -n”命令。
注:
虽然有xfs_check命令也可以对XFS文件系统做检查,但不建议使用(参见xfs_check的manpage),它很慢,尤其是大文件系统上更慢。

xfs_repair命令的前提条件是journal log必须干净,如果XFS文件系统没有正常umount,那在执行xfs_repair之前应该先mount一下,让它完成journal replay,然后再umount,因为xfs_repair应该在文件系统未挂载的状态下执行。
注:
如果XFS的journal replay失败,意味着journal log有可能损坏了,可以用”xfs_repair -L”清除journal log,但务必谨慎,因为丢弃journal有可能会导致文件系统一致性受损。

为什么有时候reboot过程中日志文件系统会进行很长时间的fsck?

通常是因为:boot时自动执行了fsck并且fsck在进行full check。需要检查以下事项:

  1. /etc/fstab的第6字段是否非0,因为它会自动执行fsck;
  2. 是否存在/forcefsck文件,因为它会强制下次boot时自动执行fsck;
  3. 是否通过 shutdown -Fr 重启的系统,因为它会强制boot时自动执行fsck;
  4. 针对具体的文件系统类型,看在什么情况下会进行full check,譬如ext2文件系统,任何时候执行e2fsck都会进行full check;而ext3/ext4则视情况而定–如果文件系统参数设定了”Maximum mount count”或”Check interval”,又或者superblock因为发现不一致而作了full check标记(详见前面段落),则e2fsck执行时会进行full check。

参考资料:

抢占(preemption)是如何发生的

进程切换有自愿(Voluntary)和强制(Involuntary)之分,在前文中详细解释了两者的不同,简单来说,自愿切换意味着进程需要等待某种资源,强制切换则与抢占(Preemption)有关。

抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程中并不需要得到进程的配合,在随后的某个时刻被抢占的进程还可以恢复运行。发生抢占的原因主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。

抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不一定是连续的,有些特殊情况下甚至会间隔相当长的时间:

  1. 触发抢占:给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换。
  2. 执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。

抢占只在某些特定的时机发生,这是内核的代码决定的。

触发抢占的时机

每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。

直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched();
触发抢占的函数是resched_task()。

TIF_NEED_RESCHED标志什么时候被设置呢?在以下时刻:

  • 周期性的时钟中断

时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占:

Linux的进程调度是模块化的,不同的调度策略比如CFS、Real-Time被封装成不同的调度类,每个调度类都可以实现自己的task_tick方法,调度器核心层根据进程所属的调度类调用对应的方法,比如CFS对应的是task_tick_fair,Real-Time对应的是task_tick_rt,每个调度类对进程的时间片都有不同的定义。

  • 唤醒进程的时候

当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。

  • 新进程创建的时候

如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的 task_fork方法触发抢占:

  • 进程修改nice值的时候

如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。

  • 进行负载均衡的时候

在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。

不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占:

RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。

执行抢占的时机

触发抢占通过设置进程的TIF_NEED_RESCHED标志告诉调度器需要进行抢占操作了,但是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。

抢占如果发生在进程处于用户态的时候,称为User Preemption(用户态抢占);如果发生在进程处于内核态的时候,则称为Kernel Preemption(内核态抢占)。

执行User Preemption(用户态抢占)的时机

  1. 从系统调用(syscall)返回用户态时;
  2. 从中断返回用户态时。

执行Kernel Preemption(内核态抢占)的时机

Linux在2.6版本之后就支持内核抢占了,但是请注意,具体取决于内核编译时的选项:

  • CONFIG_PREEMPT_NONE=y
    不允许内核抢占。这是SLES的默认选项。
  • CONFIG_PREEMPT_VOLUNTARY=y
    在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。
  • CONFIG_PREEMPT=y
    允许完全内核抢占。

在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:

  1. 中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
  2. 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
    在preempt_enable()中,会最终调用 preempt_schedule 来执行抢占。preempt_schedule()是对schedule()的包装。

 

进程切换:自愿(voluntary)与强制(involuntary)

从进程的角度看,CPU是共享资源,由所有的进程按特定的策略轮番使用。一个进程离开CPU、另一个进程占据CPU的过程,称为进程切换(process switch)。进程切换是在内核中通过调用schedule()完成的。

发生进程切换的场景有以下三种:

  1. 进程运行不下去了:
    比如因为要等待IO完成,或者等待某个资源、某个事件,典型的内核代码如下:
  2. 进程还在运行,但内核不让它继续使用CPU了:
    比如进程的时间片用完了,或者优先级更高的进程来了,所以该进程必须把CPU的使用权交出来;
  3. 进程还可以运行,但它自己的算法决定主动交出CPU给别的进程:
    用户程序可以通过系统调用sched_yield()来交出CPU,内核则可以通过函数cond_resched()或者yield()来做到。

进程切换分为自愿切换(Voluntary)和强制切换(Involuntary),以上场景1属于自愿切换,场景2和3属于强制切换。如何分辨自愿切换和强制切换呢?

  • 自愿切换发生的时候,进程不再处于运行状态,比如由于等待IO而阻塞(TASK_UNINTERRUPTIBLE),或者因等待资源和特定事件而休眠(TASK_INTERRUPTIBLE),又或者被debug/trace设置为TASK_STOPPED/TASK_TRACED状态;
  • 强制切换发生的时候,进程仍然处于运行状态(TASK_RUNNING),通常是由于被优先级更高的进程抢占(preempt),或者进程的时间片用完了。

注:实际情况更复杂一些,由于Linux内核支持抢占,kernel preemption有可能发生在自愿切换的过程之中,比如进程正进入休眠,本来如果顺利完成的话就属于自愿切换,但休眠的过程并不是原子操作,进程状态先被置成TASK_INTERRUPTIBLE,然后进程切换,如果Kernel Preemption恰好发生在两者之间,那就打断了休眠过程,自愿切换尚未完成,转而进入了强制切换的过程(虽然是强制切换,但此时的进程状态已经不是运行状态了),下一次进程恢复运行之后会继续完成休眠的过程。所以判断进程切换属于自愿还是强制的算法要考虑进程在切换时是否正处于被抢占(preempt)的过程中,参见以下内核代码:

最后,澄清几个容易产生误解的场景:

  • 进程可以通过调用sched_yield()主动交出CPU,这不是自愿切换,而是属于强制切换,因为进程仍然处于运行状态。
  • 有时候内核代码会在耗时较长的循环体内通过调用 cond_resched()或yield() ,主动让出CPU,以免CPU被内核代码占据太久,给其它进程运行机会。这也属于强制切换,因为进程仍然处于运行状态。

进程自愿切换(Voluntary)和强制切换(Involuntary)的次数被统计在 /proc/<pid>/status 中,其中voluntary_ctxt_switches表示自愿切换的次数,nonvoluntary_ctxt_switches表示强制切换的次数,两者都是自进程启动以来的累计值。

也可以用 pidstat -w 命令查看进程切换的每秒统计值:

自愿切换和强制切换的统计值在实践中有什么意义呢?
大致而言,如果一个进程的自愿切换占多数,意味着它对CPU资源的需求不高。如果一个进程的强制切换占多数,意味着对它来说CPU资源可能是个瓶颈,这里需要排除进程频繁调用sched_yield()导致强制切换的情况。

临时端口号(ephemeral port)的动态分配

网络端口号是如何分配的?除了给常用服务保留的Well-known Port numbers之外,给客户端的端口号通常是动态分配的,称为ephemeral port(临时端口),在Linux系统上临时端口号的取值范围是通过这个内核参数定义的:net.ipv4.ip_local_port_range (/proc/sys/net/ipv4/ip_local_port_range),端口号动态分配时并不是从小到大依次选取的,而是按照特定的算法随机分配的。

临时端口号的分配发生在以下两处:
– bind();
– connect()。

bind()通过inet_csk_get_port()获取端口号,利用了net_random()产生的随机数 :

connect()通过inet_hash_connect()分配端口号。核心的代码是:
port = low + (i + offset) % remaining;
其中 offset 是随机数。

为以上代码生成随机数port_offset的函数是:

 

综上,临时端口号是这样产生的:

生成一个随机数,利用随机数在ip_local_port_range范围内取值,如果取到的值在ip_local_reserved_ports范围内 ,那就再依次取下一个值,直到不在ip_local_reserved_ports范围内为止。

注:
/proc/sys/net/ipv4/ip_local_reserved_ports (net.ipv4.ip_local_reserved_ports) 是应用程序保留的端口号,不会参与内核动态分配。有些软件比如SAP通常会保留大量的端口号,如果导致剩下的临时端口数量太少的话,动态分配的随机算法往往会产生重复的端口号,造成新分配的端口号总是相同的现象。