引子
最近遇到这样一个问题:生产环境的某个C++ GUI程序界面时常出现卡顿问题,经过排查与进程的大量IO有关,但是奇怪的是,即使IO已经结束,结束后操作界面时仍然会有卡顿问题。继续排查,发现进程常驻内存的代码段和数据段在大量IO之后变小了,排查过程在下面叙述。
问题排查
为了复现整个过程,使用以下demo代替GUI程序,能得到类似的效果:
#include <stdio.h>
const int ARRAY_SIZE = 200 * 1024 * 1024;
int array[ARRAY_SIZE] = {1};
int main()
{
int total = 0;
for (int i = 0; i < ARRAY_SIZE; i += 4096)
{
total += array[i];
}
printf("all data is loaded\n");
getchar();
return 0;
}
编译该demo,能够得到一个大小约为800M的可执行程序:
$ g++ main.cpp
$ ls -lh
total 801M
-rwxrwxr-x 1 imred imred 801M 7月 9 23:09 a.out
-rw-rw-r-- 1 imred imred 276 7月 9 23:09 main.cpp
运行该demo:
$ ./a.out
all data is loaded
待提示“all data is loaded”后使用top查看进程占用内存:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10171 imred 20 0 823708 819948 819872 S 0.0 10.2 0:01.08 a.out
其中RES字段,即常驻内存,占用800M左右。
为了方便,我们使用另一个进程进行大量IO操作,也能复现这个问题:使用cat命令读取一个大小约为8G(我的内存总大小)的文件:
$ ls -lh
total 7.7G
-rw-r--r-- 1 imred imred 7.7G 7月 9 23:45 bigfile.dat
$ cat bigfile.dat > /dev/null
待cat运行结束后,再次使用top查看进程内存占用情况:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10171 imred 20 0 823708 756 680 S 0.0 0.0 0:01.08 a.out
RES内存降到了几百KB。
重复上面的操作,但是这次我们不使用top比较IO前后内存占用情况,而是比较IO前后的/proc/[pid]/smaps文件,这个文件记录了进程内存布局,将IO前的该文件复制为smaps1.txt,IO后的该文件复制为smaps2.txt,比较这两个文件:
$ diff *.txt
47,48c47,48
< Rss: 819200 kB
< Pss: 819200 kB
---
> Rss: 8 kB
> Pss: 8 kB
51c51
< Private_Clean: 819192 kB
---
> Private_Clean: 0 kB
53c53
< Referenced: 819200 kB
---
> Referenced: 8 kB
62c62
< Locked: 819200 kB
---
> Locked: 8 kB
具体来看这部分有差异的内存段:
smaps1.txt:
560587c67000-5605b9c68000 rw-p 00001000 08:13 2621471 /home/imred/Documents/Workspace/bigexe/a.out
Size: 819204 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 819200 kB
Pss: 819200 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 819192 kB
Private_Dirty: 8 kB
Referenced: 819200 kB
Anonymous: 8 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 819200 kB
smaps2.txt:
560587c67000-5605b9c68000 rw-p 00001000 08:13 2621471 /home/imred/Documents/Workspace/bigexe/a.out
Size: 819204 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 8 kB
Pss: 8 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 8 kB
Referenced: 8 kB
Anonymous: 8 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 8 kB
这段内存区域驻留在内存中的大小从800M左右降到了只有几个KB,从其内存属性“rw-p”和大小,可以断定这个区域是demo的数据段。这将导致如果进程需要再次访问这部分数据,需要从磁盘中再次读取,这必然会影响性能。
在进行IO时,注意使用top观察系统可用内存大小,在我测试时,发现可用内存并没有明显下降,一直维持在50%以上。
因此问题现象总结下来就是:虽然可用内存非常充足,但是仍然出现了进程常驻内存被淘汰的情况,导致了程序卡顿。
原因分析
为什么我会认为可用内存是充足的呢?top命令的输出包含如下内容:
KiB Mem : 8041500 total, 3505324 free, 2518364 used, 2017812 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 5369872 avail Mem
如果是图形化的输出则是这样的:
KiB Mem : 33.6/8041500 [|||||||||||||||||||||||||||||||||| ]
KiB Swap: 0.0/2097148 [ ]
很明显,图形化输出显示当前有33.6%内存正在使用,对应到文字输出应该是used字段2518364 KiB,这些内存我认为应该是当前所有进程常驻内存与共享内存(只算一份)之和;剩下的66.4%的内存,对应到文字输出是avail Mem字段5369872 KiB,这些内存我认为是可以自由使用的,因此我认为内存是充足的。
然而事实是这样吗?下面来验证一下我的想法:
将所有脏磁盘缓存写回磁盘,清除磁盘缓存:
$ sync
$ echo 3 > /proc/sys/vm/drop_caches
记录当前内存使用情况:
KiB Mem : 8041500 total, 4133848 free, 2892348 used, 1015304 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 5048732 avail Mem
运行demo,直到输出“all data is loaded”,查看内存使用情况:
KiB Mem : 8041500 total, 3276864 free, 2898584 used, 1866052 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 5013160 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10359 imred 20 0 823708 820032 819956 S 0.0 10.2 0:01.09 a.out
发现了没有?used几乎没有增长,而buff/cache增长了800M左右,而demo的常驻内存(RES字段)恰好是800M左右,这说明什么问题呢?这说明了进程的常驻内存不见得都算在了used中,而是可能会算在buff/cache中。既然算在了buff/cache中,自然避免不了被淘汰的可能,当进行磁盘IO的时候,buff/cache会大量增长,增长的过程可能会导致demo进程算在buff/cache中的常驻内存被淘汰,也就出现了文章开头的问题:需要再次访问这些内存时,需要重新从磁盘中读取,导致卡顿。
那么问题来了,哪些内存会被算在buff/cache呢?我测试的结果是从文件中读取的、clean的内存段都会被算在buff/cache,如代码段、数据段和只读数据段,但是比较奇怪的是,其中只读数据段并没有出现被淘汰的情况,代码段和数据段都出现了被淘汰的情况,具体原因还不太清楚(这三种内存段的测试不是在同一个机器上做的,不保证在所有机器上结果都相同,也许和内核有关?),仍然需要进一步探究。