分享

Linux Performance | @vmunix

 策马流浪 2019-10-14

利用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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$ blktrace -d /dev/sdb
$ blkparse -i sdb
  # 第一个IO开始:
  8,16   1        1     0.000000000 18166  Q   R 0 + 1 [dd]
  8,16   1        0     0.000009827     0  m   N cfq18166S  /user.slice alloced
  8,16   1        2     0.000010451 18166  G   R 0 + 1 [dd]
  8,16   1        3     0.000011056 18166  P   N [dd]
  8,16   1        4     0.000012255 18166  I   R 0 + 1 [dd]
  8,16   1        0     0.000013477     0  m   N cfq18166S  /user.slice insert_request
  8,16   1        0     0.000014526     0  m   N cfq18166S  /user.slice add_to_rr
  8,16   1        5     0.000016643 18166  U   N [dd] 1
  8,16   1        0     0.000017522     0  m   N cfq workload slice:300
  8,16   1        0     0.000018880     0  m   N cfq18166S  /user.slice set_active wl_class:0 wl_type:2
  8,16   1        0     0.000020594     0  m   N cfq18166S  /user.slice fifo=          (null)
  8,16   1        0     0.000021462     0  m   N cfq18166S  /user.slice dispatch_insert
  8,16   1        0     0.000022898     0  m   N cfq18166S  /user.slice dispatched a request
  8,16   1        0     0.000023786     0  m   N cfq18166S  /user.slice activate rq, drv=1
  8,16   1        6     0.000023977 18166  D   R 0 + 1 [dd]
  8,16   0        1     0.014270153     0  C   R 0 + 1 [0]
  # 第一个IO结束。
  8,16   0        0     0.014278115     0  m   N cfq18166S  /user.slice complete rqnoidle 0
  8,16   0        0     0.014280044     0  m   N cfq18166S  /user.slice set_slice=100
  8,16   0        0     0.014282217     0  m   N cfq18166S  /user.slice arm_idle: 8 group_idle: 0
  8,16   0        0     0.014282728     0  m   N cfq schedule dispatch
  # 第二个IO开始:
  8,16   1        7     0.014298247 18166  Q   R 1 + 1 [dd]
  8,16   1        8     0.014300522 18166  G   R 1 + 1 [dd]
  8,16   1        9     0.014300984 18166  P   N [dd]
  8,16   1       10     0.014301996 18166  I   R 1 + 1 [dd]
  8,16   1        0     0.014303864     0  m   N cfq18166S  /user.slice insert_request
  8,16   1       11     0.014304981 18166  U   N [dd] 1
  8,16   1        0     0.014306368     0  m   N cfq18166S  /user.slice dispatch_insert
  8,16   1        0     0.014307793     0  m   N cfq18166S  /user.slice dispatched a request
  8,16   1        0     0.014308763     0  m   N cfq18166S  /user.slice activate rq, drv=1
  8,16   1       12     0.014308962 18166  D   R 1 + 1 [dd]
  8,16   0        2     0.014518615     0  C   R 1 + 1 [0]
  # 第二个IO结束。
  8,16   0        0     0.014523548     0  m   N cfq18166S  /user.slice complete rqnoidle 0
  8,16   0        0     0.014525334     0  m   N cfq18166S  /user.slice arm_idle: 8 group_idle: 0
  8,16   0        0     0.014525676     0  m   N cfq schedule dispatch
  # 第三个IO开始:
  8,16   1       13     0.014531022 18166  Q   R 2 + 1 [dd]
  ...

注:
在以上数据中,有一些记录的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实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
            ALL           MIN           AVG           MAX           N
--------------- ------------- ------------- ------------- -----------
Q2Q               0.000138923   0.000154010   0.014298247       94558
Q2G               0.000001154   0.000001661   0.000017313       94559
G2I               0.000000883   0.000001197   0.000012011       94559
I2D               0.000004722   0.000005761   0.000027286       94559
D2C               0.000118840   0.000129201   0.014246176       94558
Q2C               0.000125953   0.000137820   0.014270153       94558
==================== Device Overhead ====================
       DEV |       Q2G       G2I       Q2M       I2D       D2C
---------- | --------- --------- --------- --------- ---------
(  8, 16) |   1.2050%   0.8688%   0.0000%   4.1801%  93.7461%
---------- | --------- --------- --------- --------- ---------
   Overall |   1.2050%   0.8688%   0.0000%   4.1801%  93.7461%
...

我们看到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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/bin/bash
if [ $# -ne 1 ]; then
        echo "Usage: $0 <block_device_name>"
        exit
fi
if [ ! -b $1 ]; then
        echo "could not find block device $1"
        exit
fi
duration=10
echo "running blktrace for $duration seconds to collect data..."
timeout $duration blktrace -d $1 >/dev/null 2>&1
DEVNAME=`basename $1`
echo "parsing blktrace data..."
blkparse -i $DEVNAME |sort -g -k8 -k10 -k4 |awk '
BEGIN   {
        total_read=0;
        total_write=0;
        maxwait_read=0;
        maxwait_write=0;
}
{
        if ($6=="Q") {
                queue_ts=$4;
                block=$8;
                nblock=$10;
                rw=$7;
        };
        if ($6=="C" && $8==block && $10==nblock && $7==rw) {
                await=$4-queue_ts;
                if (rw=="R") {
                        if (await>maxwait_read) maxwait_read=await;
                        total_read++;
                        read_count_block[nblock]++;
                        if (await>0.001) read_count1++;
                        if (await>0.01) read_count10++;
                        if (await>0.02) read_count20++;
                        if (await>0.03) read_count30++;
                }
                if (rw=="W") {
                        if (await>maxwait_write) maxwait_write=await;
                        total_write++;
                        write_count_block[nblock]++;
                        if (await>0.001) write_count1++;
                        if (await>0.01) write_count10++;
                        if (await>0.02) write_count20++;
                        if (await>0.03) write_count30++;
                }
        }
} END   {
        printf("========\nsummary:\n========\n");
        printf("total number of reads: %d\n", total_read);
        printf("total number of writes: %d\n", total_write);
        printf("slowest read : %.6f second\n", maxwait_read);
        printf("slowest write: %.6f second\n", maxwait_write);
        printf("reads\n> 1ms: %d\n>10ms: %d\n>20ms: %d\n>30ms: %d\n", read_count1, read_count10, read_count20, read_count30);
        printf("writes\n> 1ms: %d\n>10ms: %d\n>20ms: %d\n>30ms: %d\n", write_count1, write_count10, write_count20, write_count30);
        printf("\nblock size:%16s\n","Read Count");
        for (i in read_count_block)
                printf("%10d:%16d\n", i, read_count_block[i]);
        printf("\nblock size:%16s\n","Write Count");
        for (i in write_count_block)
                printf("%10d:%16d\n", i, write_count_block[i]);
}'

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
========
summary:
========
total number of reads: 1081513
total number of writes: 0
slowest read : 0.032560 second
slowest write: 0.000000 second
reads
> 1ms: 18253
>10ms: 17058
>20ms: 17045
>30ms: 780
writes
> 1ms: 0
>10ms: 0
>20ms: 0
>30ms: 0
block size:      Read Count
       256:           93756
       248:            1538
        64:           98084
        56:            7475
         8:          101218
        48:           15889
       240:            1637
       232:            1651
       224:            1942
        40:           21693
       216:            1811
        32:          197893
       208:            1907
        24:           37787
       128:           97382
        16:          399850

这个例子统计了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值为基础,给予一定的补偿,但不能补偿太多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
        u64 vruntime = cfs_rq->min_vruntime;
        /*
         * The 'current' period is already promised to the current tasks,
         * however the extra weight of the new task will slow them down a
         * little, place the new task so that it fits in the slot that
         * stays open at the end.
         */
        if (initial && sched_feat(START_DEBIT)) /* initial表示新进程 */
                vruntime += sched_vslice(cfs_rq, se);
        /* sleeps up to a single latency don't count. */
        if (!initial) { /* 休眠进程 */
                unsigned long thresh = sysctl_sched_latency; /* 一个调度周期 */
                /*
                 * Halve their sleep time's effect, to allow
                 * for a gentler effect of sleepers:
                 */
                if (sched_feat(GENTLE_FAIR_SLEEPERS)) /* 若设了GENTLE_FAIR_SLEEPERS */
                        thresh >>= 1; /* 补偿减为调度周期的一半 */
                vruntime -= thresh;
        }
        /* ensure we never gain time by being placed backwards. */
        vruntime = max_vruntime(se->vruntime, vruntime);
        se->vruntime = vruntime;
}
 休眠进程在唤醒时会立刻抢占CPU吗?

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

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

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

1
# echo NO_WAKEUP_PREEMPT > /sys/kernel/debug/sched_features

禁用唤醒抢占 特性之后,刚唤醒的进程不会立即抢占运行中的进程,而是要等到运行进程用完时间片之后。在以上案例中,经过这样的调整之后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值,都会有不同:

1
2
3
# grep min_vruntime /proc/sched_debug
.min_vruntime : 12403175.972743
.min_vruntime : 14422108.528121

如果一个进程从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保持相对公平。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
...
        /*
         * Normalize the entity after updating the min_vruntime because the
         * update can refer to the ->curr item and we need to reflect this
         * movement in our normalized position.
         */
        if (!(flags & DEQUEUE_SLEEP))
                se->vruntime -= cfs_rq->min_vruntime;
...
}
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
        /*
         * Update the normalized vruntime before updating min_vruntime
         * through callig update_curr().
         */
        if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
                se->vruntime += cfs_rq->min_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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
MemTotal:        3809036 kB
MemFree:          282012 kB
MemAvailable:     865620 kB
Buffers:               0 kB
Cached:           854972 kB
SwapCached:       130900 kB
Active:          1308168 kB
Inactive:        1758160 kB
Active(anon):    1010416 kB
Inactive(anon):  1370480 kB
Active(file):     297752 kB
Inactive(file):   387680 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       4063228 kB
SwapFree:        3357108 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:       2104412 kB
Mapped:            40988 kB
Shmem:            169540 kB
Slab:             225420 kB
SReclaimable:     134220 kB
SUnreclaim:        91200 kB
KernelStack:        5936 kB
PageTables:        35628 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     5967744 kB
Committed_AS:    5626436 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      351900 kB
VmallocChunk:   34359363652 kB
HardwareCorrupted:     0 kB
AnonHugePages:    139264 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:      204484 kB
DirectMap2M:     3915776 kB
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记录看到:

1
2
3
4
5
6
7
8
9
# grep vmalloc /proc/vmallocinfo
...
0xffffc90004702000-0xffffc9000470b000   36864 alloc_large_system_hash+0x171/0x239 pages=8 vmalloc N0=8
0xffffc9000470b000-0xffffc90004710000   20480 agp_add_bridge+0x2aa/0x440 pages=4 vmalloc N0=4
0xffffc90004710000-0xffffc90004731000  135168 raw_init+0x41/0x141 pages=32 vmalloc N0=32
0xffffc90004736000-0xffffc9000473f000   36864 drm_ht_create+0x55/0x80 [drm] pages=8 vmalloc N0=8
0xffffc90004744000-0xffffc90004746000    8192 dm_table_create+0x9e/0x130 [dm_mod] pages=1 vmalloc N0=1
0xffffc90004746000-0xffffc90004748000    8192 dm_table_create+0x9e/0x130 [dm_mod] pages=1 vmalloc N0=1
...

注:/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记录,例如:

1
2
# grep vmalloc /proc/vmallocinfo | awk '{total+=$2}; END {print total}'
23375872

一些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。下文我们还会细说。

1
2
3
4
5
6
7
8
9
10
11
# lsmod | less
Module                  Size  Used by
rpcsec_gss_krb5        31477  0
auth_rpcgss            59343  1 rpcsec_gss_krb5
nfsv4                 474429  0
dns_resolver           13140  1 nfsv4
nfs                   246411  1 nfsv4
lockd                  93977  1 nfs
sunrpc                295293  5 nfs,rpcsec_gss_krb5,auth_rpcgss,lockd,nfsv4
fscache                57813  2 nfs,nfsv4
...

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

1
2
3
4
5
6
7
8
// kernel/module.c
static int m_show(struct seq_file *m, void *p)
{
...
        seq_printf(m, "%s %u",
                   mod->name, mod->init_size + mod->core_size);
...
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// kernel/module.c
static int move_module(struct module *mod, struct load_info *info)
{
...
        ptr = module_alloc_update_bounds(mod->core_size);
...
        if (mod->init_size) {
                ptr = module_alloc_update_bounds(mod->init_size);
...
}
// 注:module_alloc_update_bounds()最终会调用vmalloc_exec()

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 先卸载floppy模块
$ modprobe -r floppy
# 确认floppy模块已经不在了
$ lsmod | grep floppy
# 记录vmallocinfo以供随后比较
$ cat /proc/vmallocinfo > vmallocinfo.1
# 加载floppy模块
$ modprobe -a floppy
# 注意floppy模块的大小是69417字节:
$ lsmod | grep floppy
floppy                 69417  0
$ cat /proc/vmallocinfo > vmallocinfo.2
# 然而,我们看到vmallocinfo中记录的是分配了73728字节:
$ diff vmallocinfo.1 vmallocinfo.2
68a69
> 0xffffffffa03d7000-0xffffffffa03e9000   73728 module_alloc_update_bounds+0x14/0x70 pages=17 vmalloc N0=17
# 为什么lsmod看到的内存大小与vmallocinfo不同呢?
# 因为给kernel module分配内存是以page为单位的,而且外加一个guard page
# 我们来验证一下:
$ bc -q
69417%4096
3881    <--- 不能被4096整除
69417/4096
16      <--- 相当于16 pages,加上面的3881字节,会分配17 pages
18*4096 <--- 17 pages 加上 1个guard page
73728   <--- 正好是vmallocinfo记录的大小

所以结论是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有关的统计值如下:

1
2
3
4
5
6
7
MemFree: 570736 kB
...
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB

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

1
2
3
4
5
6
7
8
9
10
# echo 128 > /proc/sys/vm/nr_hugepages
# cat /proc/meminfo
...
MemFree: 308592 kB
...
HugePages_Total: 128
HugePages_Free: 128
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB

使用Hugepages有三种方式:
(详见 https://www./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 不会减少。

1
2
3
4
5
HugePages_Total: 128
HugePages_Free: 128
HugePages_Rsvd: 128
HugePages_Surp: 0
Hugepagesize: 2048 kB

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

1
2
3
4
5
HugePages_Total: 128
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
2aaaaac00000-2aaabac00000 rw-p 00000000 00:0c 311151 /anon_hugepage (deleted)
Size: 262144 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 2048 kB
MMUPageSize: 2048 kB
Locked: 0 kB
VmFlags: rd wr mr mw me de ht
...
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中的一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
7efcf0000000-7efd30000000 rw-p 00000000 00:00 0
Size:            1048576 kB
Rss:              313344 kB
Pss:              313344 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:    313344 kB
Referenced:       239616 kB
Anonymous:        313344 kB
AnonHugePages:    313344 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd wr mr mw me dc ac hg mg

THP也可以用于shared memory和tmpfs,缺省是禁止的,打开的方法如下(详见 https://www./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之和】
举例如下:

1
2
3
4
# grep AnonHugePages /proc/[1-9]*/smaps | awk '{total+=$2}; END {print total}'
782336
# grep AnonHugePages /proc/meminfo
AnonHugePages:    782336 kB
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./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./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 。参见:

1
2
3
4
5
6
7
8
9
10
fs/proc/meminfo.c:
static int meminfo_proc_show(struct seq_file *m, void *v)
{
...
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
                K(global_page_state(NR_ANON_PAGES)
                  + global_page_state(NR_ANON_TRANSPARENT_HUGEPAGES) *
                  HPAGE_PMD_NR),
...
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./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”,两者没有交叉。参见:

1
2
3
4
5
6
7
8
fs/proc/meminfo.c:
static int meminfo_proc_show(struct seq_file *m, void *v)
{
...
        cached = global_page_state(NR_FILE_PAGES) -
                        total_swapcache_pages() - i.bufferram;
...
}

“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:///?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):

1
2
# grep vmalloc /proc/vmallocinfo | awk '{total+=$2}; END {print total}'
23375872
  • 注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通常被认为是可以回收的:

1
2
3
4
5
$ free
             total       used       free     shared    buffers     cached
Mem:      32764716    1067548   31697168     158332         12     593096
-/+ buffers/cache:     474440   32290276
Swap:      2103292          0    2103292

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

1
$ echo 1 > /proc/sys/vm/drop_caches

注: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./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命令的影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
挂载一个3G大小的tmpfs:
$ mount -t tmpfs -o size=3G none /mytmpfs/
此时free命令看到的内存使用状况:
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    2343336   63599400       9492       8952      92848
-/+ buffers/cache:    2241536   63701200 
Swap:     33038332          0   33038332 
在tmpfs上新建一个2G大小的文件,free命令看到"cached"增加了2GB,注意"shared"也增加了2GB:
$ dd if=/dev/zero of=/mytmpfs/testfile bs=1G count=2
2+0 records in
2+0 records out
2147483648 bytes (2.1 GB) copied, 2.42736 s, 885 MB/s
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    4423404   61519332    2106644       9088    2190064
-/+ buffers/cache:    2224252   63718484 
Swap:     33038332          0   33038332 
执行drop_caches,再观察free命令的"cached"值,发现刚才增加的2GB并未被回收:
$ sync
$ sudo sh -c 'echo 1 > /proc/sys/vm/drop_caches'
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    4430388   61512348    2106644       5284    2183096
-/+ buffers/cache:    2242008   63700728 
Swap:     33038332          0   33038332 
最后删除tmpfs上的文件,free命令看到"Cached"和"Shared"同时减少2GB:
$ rm /mytmpfs/testfile
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    2324096   63618640       9784       6228      87412
-/+ buffers/cache:    2230456   63712280 
Swap:     33038332          0   33038332

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

Linux kernel利用tmpfs实现共享内存(shared memory),参见:
https://www./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大小的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAP_SIZE 268435456
int main()
{
    char *addr;
    ssize_t s;
    addr = mmap(NULL, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0);
    if (addr == MAP_FAILED) {
        fprintf(stderr, "mmap failed\n");
        exit(EXIT_FAILURE);
    }
    memset(addr, 9, MAP_SIZE);
    printf("mmap done, memset done, check free output. Press any key to exit...\n");
    getchar();
    exit(EXIT_SUCCESS);
}

实验过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
先回收cache,设置初始状态:
$ sync; sudo sh -c 'echo 1 > /proc/sys/vm/drop_caches'; free
             total       used       free     shared    buffers     cached
Mem:      65942736    2401684   63541052       9576       3648      54820
-/+ buffers/cache:    2343216   63599520 
Swap:     33038332          0   33038332 
然后执行我们的测试程序,申请256MB共享内存:
$ ./mmap_test
mmap done, memset done, check free output. Press any key to exit...
在另一个窗口里观察,看到cache值增加了256MB,注意"shared"也同时增加了256MB:
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    2652796   63289940     271720       6380     321760
-/+ buffers/cache:    2324656   63618080 
Swap:     33038332          0   33038332 
执行drop_caches,发现刚才新增的256MBcache值并未被回收:
$ sync; sudo sh -c 'echo 1 > /proc/sys/vm/drop_caches'; free
             total       used       free     shared    buffers     cached
Mem:      65942736    2666452   63276284     271720       3628     316532
-/+ buffers/cache:    2346292   63596444 
Swap:     33038332          0   33038332 
退出mmap_test程序,再看cache值就减少了256MB,注意"shared"也同时减少256MB:
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    2400268   63542468       9576       4796      59148
-/+ buffers/cache:    2336324   63606412 
Swap:     33038332          0   33038332

结论: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./show_bug.cgi?id=879315

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# man fstab
FSTAB(5)                   Linux Programmer's Manual                  FSTAB(5)
...
       The sixth field (fs_passno).
              This field is used by the fsck(8) program to determine the order
              in  which  filesystem  checks are done at reboot time.  The root
              filesystem should be specified with a fs_passno of 1, and  other
              filesystems  should have a fs_passno of 2.  Filesystems within a
              drive will be checked sequentially, but filesystems on different
              drives  will  be checked at the same time to utilize parallelism
              available in the hardware.  If the sixth field is not present or
              zero,  a value of zero is returned and fsck will assume that the
              filesystem does not need to be checked.
...

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

1
2
3
# touch /forcefsck
或者用以下命令重启:
# shutdown -Fr

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

1
# shutdown -fr
不同的文件系统有不同的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:

1
2
3
4
5
6
7
8
9
E2FSCK(8)                   System Manager's Manual                  E2FSCK(8)
...
e2fsck is used to check the ext2/ext3/ext4 family of file systems. For
ext3 and ext4 filesystems that use a journal, if the system has been
shut down uncleanly without any errors, normally, after replaying the
committed transactions in the journal, the file system should be
marked as clean. Hence, for filesystems that use journalling, e2fsck
will normally replay the journal and exit, unless its superblock indi‐
cates that further checking is required.

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:

1
2
3
4
5
6
7
8
$ sudo tune2fs -l /dev/sdd1
...
Mount count:              3
Maximum mount count:      25
Last checked:             Mon Oct 30 10:49:51 2017
Check interval:           15552000 (6 months)
Next check after:         Sat Apr 28 10:49:51 2018
...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
       -c max-mount-counts
              Adjust  the  number of mounts after which the filesystem will be
              checked by e2fsck(8).  If max-mount-counts is 0 or -1, the  num-
              ber  of  times  the filesystem is mounted will be disregarded by
              e2fsck(8) and the kernel.
              Staggering the mount-counts at which  filesystems  are  forcibly
              checked  will  avoid  all  filesystems being checked at one time
              when using journaled filesystems.
              ...
       -i  interval-between-checks[d|m|w]
              Adjust  the maximal time between two filesystem checks.  No suf-
              fix or d will interpret the  number  interval-between-checks  as
              days, m as months, and w as weeks.  A value of zero will disable
              the time-dependent checking.
  • XFS

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fsck.xfs(8)
NAME
       fsck.xfs - do nothing, successfully
SYNOPSIS
       fsck.xfs [ filesys ... ]
DESCRIPTION
       fsck.xfs  is  called by the generic Linux fsck(8) program at startup to
       check and repair an XFS filesystem.  XFS is a journaling filesystem and
       performs  recovery  at  mount(8)  time if necessary, so fsck.xfs simply
       exits with a zero exit status.
       If you wish to check the consistency of an XFS filesystem, or repair  a
       damaged  or corrupt XFS filesystem, see xfs_check(8) and xfs_repair(8).

要想检查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方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占:

1
2
3
4
5
6
void scheduler_tick(void)
{
        ...
        curr->sched_class->task_tick(rq, curr, 0);
        ...
}

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方法触发抢占:

1
2
3
4
5
6
7
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
        ...
        if (p->sched_class->task_fork)
                p->sched_class->task_fork(p);
        ...
}
  • 进程修改nice值的时候

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

  • 进行负载均衡的时候

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

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

1
2
3
4
5
6
7
8
load_balance()
{
        ...
        move_tasks();
        ...
        resched_cpu();
        ...
}

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

执行抢占的时机

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    源文件:arch/x86/kernel/entry_64.S
    sysret_careful:
            bt $TIF_NEED_RESCHED,%edx
            jnc sysret_signal
            TRACE_IRQS_ON
            ENABLE_INTERRUPTS(CLBR_NONE)
            pushq_cfi %rdi
            call schedule
            popq_cfi %rdi
            jmp sysret_check
  2. 从中断返回用户态时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    retint_careful:
            CFI_RESTORE_STATE
            bt    $TIF_NEED_RESCHED,%edx
            jnc   retint_signal
            TRACE_IRQS_ON
            ENABLE_INTERRUPTS(CLBR_NONE)
            pushq_cfi %rdi
            call  schedule
            popq_cfi %rdi
            GET_THREAD_INFO(%rcx)
            DISABLE_INTERRUPTS(CLBR_NONE)
            TRACE_IRQS_OFF
            jmp retint_check

执行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()的包装。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #ifdef CONFIG_PREEMPT
            /* Returning to kernel space. Check if we need preemption */
            /* rcx:  threadinfo. interrupts off. */
    ENTRY(retint_kernel)
            cmpl $0,TI_preempt_count(%rcx)
            jnz  retint_restore_args
            bt  $TIF_NEED_RESCHED,TI_flags(%rcx)
            jnc  retint_restore_args
            bt   $9,EFLAGS-ARGOFFSET(%rsp)  /* interrupts off? */
            jnc  retint_restore_args
            call preempt_schedule_irq
            jmp exit_intr
    #endif
  2. 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
    在preempt_enable()中,会最终调用 preempt_schedule 来执行抢占。preempt_schedule()是对schedule()的包装。

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

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

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

  1. 进程运行不下去了:
    比如因为要等待IO完成,或者等待某个资源、某个事件,典型的内核代码如下:

    1
    2
    3
    4
    //把进程放进等待队列,把进程状态置为TASK_UNINTERRUPTIBLE
    prepare_to_wait(waitq, wait, TASK_UNINTERRUPTIBLE);
    //切换进程
    schedule();
  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)的过程中,参见以下内核代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void __sched __schedule(void)
{
        ...
        switch_count = &prev->nivcsw;//强制切换的次数
        if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {//进程处于非运行状态并且允许抢占
...
                switch_count = &prev->nvcsw;//自愿切换的次数
        }
...
        if (likely(prev != next)) {
                rq->nr_switches++;
                rq->curr = next;
                ++*switch_count;//进程切换次数累加
                context_switch(rq, prev, next); /* unlocks the rq */
                /*
                 * The context switch have flipped the stack from under us
                 * and restored the local variables which were saved when
                 * this task called schedule() in the past. prev == current
                 * is still correct, but it can be moved to another cpu/rq.
                 */
                cpu = smp_processor_id();
                rq = cpu_rq(cpu);
        } else
                raw_spin_unlock_irq(&rq->lock);
        ...
}
not_running && preemptive : voluntary

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

  • 进程可以通过调用sched_yield()主动交出CPU,这不是自愿切换,而是属于强制切换,因为进程仍然处于运行状态。

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

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

1
2
3
# grep ctxt /proc/26995/status
voluntary_ctxt_switches:        79
nonvoluntary_ctxt_switches:     4

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

1
2
3
4
5
6
# pidstat -w 1
Linux 3.10.0-229.14.1.el7.x86_64 (bj71s060)     02/01/2018      _x86_64_       (2 CPU)
12:05:20 PM   UID       PID   cswch/s nvcswch/s  Command
12:05:21 PM     0      1299      0.94      0.00  httpd
12:05:21 PM     0     27687      0.94      0.00  pidstat

自愿切换和强制切换的统计值在实践中有什么意义呢?
大致而言,如果一个进程的自愿切换占多数,意味着它对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()产生的随机数 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SLES12 kernel 3.12.69-60.64.32:
0104 int inet_csk_get_port(struct sock *sk, unsigned short snum)
0105 {
0106         struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
0107         struct inet_bind_hashbucket *head;
0108         struct inet_bind_bucket *tb;
0109         int ret, attempts = 5;
0110         struct net *net = sock_net(sk);
0111         int smallest_size = -1, smallest_rover;
0112         kuid_t uid = sock_i_uid(sk);
0113
0114         local_bh_disable();
0115         if (!snum) {
0116                 int remaining, rover, low, high;
0117
0118 again:
0119                 inet_get_local_port_range(&low, &high);
0120                 remaining = (high - low) + 1;
0121                 smallest_rover = rover = net_random() % remaining + low;
0122
0123                 smallest_size = -1;
0124                 do {
0125                         if (inet_is_reserved_local_port(rover))
0126                                 goto next_nolock;
...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SLES12 kernel 3.12.69-60.64.32:
0477 int __inet_hash_connect(struct inet_timewait_death_row *death_row,
0478                 struct sock *sk, u32 port_offset,
0479                 int (*check_established)(struct inet_timewait_death_row *,
0480                         struct sock *, __u16, struct inet_timewait_sock **),
0481                 int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))
0482 {
0483         struct inet_hashinfo *hinfo = death_row->hashinfo;
0484         const unsigned short snum = inet_sk(sk)->inet_num;
0485         struct inet_bind_hashbucket *head;
0486         struct inet_bind_bucket *tb;
0487         int ret;
0488         struct net *net = sock_net(sk);
0489         int twrefcnt = 1;
0490
0491         if (!snum) {
0492                 int i, remaining, low, high, port;
0493                 static u32 hint;
0494                 u32 offset = hint + port_offset;
0495                 struct inet_timewait_sock *tw = NULL;
0496
0497                 inet_get_local_port_range(&low, &high);
0498                 remaining = (high - low) + 1;
0499
0500                 local_bh_disable();
0501                 for (i = 1; i <= remaining; i++) {
0502                         port = low + (i + offset) % remaining;
0503                         if (inet_is_reserved_local_port(port))
0504                                 continue;
...

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

1
2
3
4
5
6
7
0391 static inline u32 inet_sk_port_offset(const struct sock *sk)
0392 {
0393         const struct inet_sock *inet = inet_sk(sk);
0394         return secure_ipv4_port_ephemeral(inet->inet_rcv_saddr,
0395                                           inet->inet_daddr,
0396                                           inet->inet_dport);
0397 }

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

生成一个随机数,利用随机数在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通常会保留大量的端口号,如果导致剩下的临时端口数量太少的话,动态分配的随机算法往往会产生重复的端口号,造成新分配的端口号总是相同的现象。

内核栈溢出

在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的第一个字段就是进程的指针。

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

1
2
3
4
5
6
7
8
9
...
Call Trace:
[<ffffffff8106e3e7>] ? warn_slowpath_common+0x87/0xc0
BUG: unable to handle kernel NULL pointer dereference at 00000000000009e8
IP: [<ffffffff8100f4dd>] print_context_stack+0xad/0x140
PGD 5fdb8ae067 PUD 5fdbee9067 PMD 0
Thread overran stack, or stack corrupted
Oops: 0000 [#1] SMP
...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
crash64> struct task_struct ffff8800374cb540
struct task_struct {
  state = 2,
  stack = 0xffff8800bae2a000,
...
crash64> thread_info 0xffff8800bae2a000
struct thread_info {
  task = 0xffff8800458efba0,
  exec_domain = 0xffffffff,
  flags = 0,
  status = 0,
  cpu = 91904,
  preempt_count = 0,
...

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

1
2
# mount -t debugfs nodev /sys/kernel/debug
# echo 1 > /proc/sys/kernel/stack_tracer_enabled

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

1
2
# cat /sys/kernel/debug/tracing/stack_max_size
# cat /sys/kernel/debug/tracing/stack_trace

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# cat /sys/kernel/debug/tracing/stack_max_size
7272
# cat /sys/kernel/debug/tracing/stack_trace
        Depth    Size   Location    (61 entries)
        -----    ----   --------
  0)     7080     224   select_task_rq_fair+0x3be/0x980
  1)     6856     112   try_to_wake_up+0x14a/0x400
  2)     6744      16   wake_up_process+0x15/0x20
  3)     6728      16   wakeup_softirqd+0x35/0x40
  4)     6712      48   raise_softirq_irqoff+0x4f/0x90
  5)     6664      48   __blk_complete_request+0x132/0x140
  6)     6616      16   blk_complete_request+0x25/0x30
  7)     6600      32   scsi_done+0x2f/0x60
  8)     6568      48   megasas_queue_command+0xd1/0x140 [megaraid_sas]
  9)     6520      48   scsi_dispatch_cmd+0x1ac/0x340
10)     6472      96   scsi_request_fn+0x415/0x590
11)     6376      32   __generic_unplug_device+0x32/0x40
12)     6344     112   __make_request+0x170/0x500
13)     6232     224   generic_make_request+0x21e/0x5b0
14)     6008      80   submit_bio+0x8f/0x120
15)     5928     112   _xfs_buf_ioapply+0x194/0x2f0 [xfs]
16)     5816      48   xfs_buf_iorequest+0x4f/0xe0 [xfs]
17)     5768      32   xlog_bdstrat+0x2a/0x60 [xfs]
18)     5736      80   xlog_sync+0x1e0/0x3f0 [xfs]
19)     5656      48   xlog_state_release_iclog+0xb3/0xf0 [xfs]
20)     5608     144   _xfs_log_force_lsn+0x1cc/0x270 [xfs]
21)     5464      32   xfs_log_force_lsn+0x18/0x40 [xfs]
22)     5432      80   xfs_alloc_search_busy+0x10c/0x160 [xfs]
23)     5352     112   xfs_alloc_get_freelist+0x113/0x170 [xfs]
24)     5240      48   xfs_allocbt_alloc_block+0x33/0x70 [xfs]
25)     5192     240   xfs_btree_split+0xbd/0x710 [xfs]
26)     4952      96   xfs_btree_make_block_unfull+0x12d/0x190 [xfs]
27)     4856     224   xfs_btree_insrec+0x3ef/0x5a0 [xfs]
28)     4632     144   xfs_btree_insert+0x93/0x180 [xfs]
29)     4488     176   xfs_free_ag_extent+0x414/0x7e0 [xfs]
30)     4312     224   xfs_alloc_fix_freelist+0xf4/0x480 [xfs]
31)     4088      96   xfs_alloc_vextent+0x173/0x600 [xfs]
32)     3992     240   xfs_bmap_btalloc+0x167/0x9d0 [xfs]
33)     3752      16   xfs_bmap_alloc+0xe/0x10 [xfs]
34)     3736     432   xfs_bmapi+0x9f6/0x11a0 [xfs]
35)     3304     272   xfs_iomap_write_allocate+0x1c5/0x3b0 [xfs]
36)     3032     208   xfs_iomap+0x389/0x440 [xfs]
37)     2824      32   xfs_map_blocks+0x2d/0x40 [xfs]
38)     2792     272   xfs_page_state_convert+0x2f8/0x750 [xfs]
39)     2520      80   xfs_vm_writepage+0x86/0x170 [xfs]
40)     2440      32   __writepage+0x17/0x40
41)     2408     304   write_cache_pages+0x1c9/0x4a0
42)     2104      16   generic_writepages+0x24/0x30
43)     2088      48   xfs_vm_writepages+0x5e/0x80 [xfs]
44)     2040      16   do_writepages+0x21/0x40
45)     2024     128   __filemap_fdatawrite_range+0x5b/0x60
46)     1896      48   filemap_write_and_wait_range+0x5a/0x90
47)     1848     320   xfs_write+0xa2f/0xb70 [xfs]
48)     1528      16   xfs_file_aio_write+0x61/0x70 [xfs]
49)     1512     304   do_sync_readv_writev+0xfb/0x140
50)     1208     224   do_readv_writev+0xcf/0x1f0
51)      984      16   vfs_writev+0x46/0x60
52)      968     208   nfsd_vfs_write+0x107/0x430 [nfsd]
53)      760      96   nfsd_write+0xe7/0x100 [nfsd]
54)      664     112   nfsd3_proc_write+0xaf/0x140 [nfsd]
55)      552      64   nfsd_dispatch+0xfe/0x240 [nfsd]
56)      488     128   svc_process_common+0x344/0x640 [sunrpc]
57)      360      32   svc_process+0x110/0x160 [sunrpc]
58)      328      48   nfsd+0xc2/0x160 [nfsd]
59)      280      96   kthread+0x96/0xa0
60)      184     184   child_rip+0xa/0x20

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多