Home > programming > 为何多线程程序占用这么多内存(linux)

为何多线程程序占用这么多内存(linux)

我们的应用基本都是多线程的程序,程序启动以后如果在client发起命令之前内存占用量并不大,大概在几百兆左右,但是经过一段时间的运行呢,有的进程会占用到3~4G的内存。但是继续运行下去的话也不会继续增加了。由于增长到最高点以后内存使用量没有明显的大幅攀升,所以一直也没有特别的关注这个问题。最近呢,老大要看看这个问题究竟是咋回事,于是呢,有了本文。
一开始,我认为是程序逻辑分配了这么多内存。为了证明我的观点,我首先使用了ltrace。
ltrace -f -e “malloc,realloc,calloc,free” -p pid > trace 2>&1
上面是利用ltrace来检测程序对于malloc,realloc,calloc,free的调用情况。加上-f参数是因为是多线程的程序。当然,为了查看方便,我将上述命令的结果重定向到了一个文件。
当程序运行到了一个内存高点,例如3G的时候,取出trace文件,这个时候并没有让程序退出(否则可能会有很多free的调用)。trace文件大致是这个样子的:

[pid 21161] malloc(9) = 0x1b55a40
[pid 21161] malloc(9) = 0x1b58410
[pid 21161] free(0x1b55a40) =  <void>
于是,写了一个很简单的python脚本来分析程序的内存分配释放情况。最后得到的答案出乎我的意料。程序从启动稳定在300M左右到内存最高点期间的内存分配而没有释放的总量只有200k+,远小于1M。于是乎我怀疑自己统计错了。有重复做了几遍相同的操作,得到的答案都是一样的。

在多次使用ltrace无果以后,我开始怀疑程序内部是不是用到mmap之类的调用,于是使用strace对mmap,brk等系统函数的检测:
strace -f -e “brk,mmap,munmap” -p pid > trace 2>&1
测试方法同上,运行到内存高点以后查看strace文件。文件内容大致如下:
brk(0) = 0x6fd000
mmap(NULL, 4096, PROTREAD|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7f77f9853000
mmap(NULL, 82462, PROTREAD, MAPPRIVATE, 3, 0) = 0x7f77f9604000
我检查了一下trace文件也没有发现大量内存mmap动作,即便是brk动作引起的内存增长也不大。
于是乎我开始没有方向了 …
后来,我开始减少thread的数量开始测试,在测试的时候偶然发现一个很奇怪的现象。那就是如果进程创建了一个线程并且该线程运行以后内存使用量就会激增。我写了一个简单的程序,大致内容如下:

void* thread_run( void* ) {
    cout << "thread running,    VM SIZE:" << memusage() << " MB" << endl;
}

int main()
{
    cout << "process running,   VM SIZE:" << memusage() << " MB" << endl;

    pthread_t th;
    pthread_create(&th, 0, thread_run, 0);
    cout << "thread created,    VM SIZE:" << memusage() << " MB" << endl;

    pthread_join( th, 0 );

    cout << "after join thread, VM SIZE:" << memusage() << " MB" << endl;

    return 0;
}

以上调用的memusage()是用来获取当前进程的virtual memory usage的函数。
在我的机器上(CENTOS 6.4,X86-64)上运行结果如下:
process running, VM SIZE:13 MB
thread created, VM SIZE:23 MB
thread running, VM SIZE:87 MB
after join thread, VM SIZE:87 MB
也就是说,程序启动以后用了13M内存,创建了一个线程以后内存使用量增加了10M(这是因为64bit上缺省的线程stack size是10M,这个可以在创建线程的时候指定)。此时线程应该还没有运行,因为当前的主线程还没有被切换掉。当线程运行起来了以后,整个process的内存使用量立马又增加了64M。而且这个增加的64M内存在线程被join以后也没有被回收。

很神奇吧,反正我是没有反应过来是咋回事。接下来,让程序停在threadrun函数里面,然后使用pmap查看进程的内存情况。pmap -x的输出情况如下:


12346: ./test
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 8 8 8 r-x-- test
0000000000602000 4 4 4 rw--- test
000000000104e000 132 8 8 rw--- [ anon ]
0000003f71800000 128 104 0 r-x-- ld-2.12.so
0000003f71a1f000 4 4 4 r---- ld-2.12.so
0000003f71a20000 4 4 4 rw--- ld-2.12.so
0000003f71a21000 4 4 4 rw--- [ anon ]
0000003f72000000 1576 324 0 r-x-- libc-2.12.so
0000003f7218a000 2044 0 0 ----- libc-2.12.so
0000003f72389000 16 16 8 r---- libc-2.12.so
0000003f7238d000 4 4 4 rw--- libc-2.12.so
0000003f7238e000 20 16 16 rw--- [ anon ]
0000003f72400000 92 68 0 r-x-- libpthread-2.12.so
0000003f72417000 2048 0 0 ----- libpthread-2.12.so
0000003f72617000 4 4 4 r---- libpthread-2.12.so
0000003f72618000 4 4 4 rw--- libpthread-2.12.so
0000003f72619000 16 4 4 rw--- [ anon ]
0000003f72c00000 524 20 0 r-x-- libm-2.12.so
0000003f72c83000 2044 0 0 ----- libm-2.12.so
0000003f72e82000 4 4 4 r---- libm-2.12.so
0000003f72e83000 4 4 4 rw--- libm-2.12.so
0000003f7f000000 88 16 0 r-x-- libgccs-4.4.7-20120601.so.1
0000003f7f016000 2044 0 0 ----- libgccs-4.4.7-20120601.so.1
0000003f7f215000 4 4 4 rw--- libgccs-4.4.7-20120601.so.1
0000003f7fc00000 928 528 0 r-x-- libstdc++.so.6.0.13
0000003f7fce8000 2048 0 0 ----- libstdc++.so.6.0.13
0000003f7fee8000 28 28 28 r---- libstdc++.so.6.0.13
0000003f7feef000 8 8 8 rw--- libstdc++.so.6.0.13
0000003f7fef1000 84 12 12 rw--- [ anon ]
00007f7e4c000000 132 8 8 rw--- [ anon ]
00007f7e4c021000 65404 0 0 ----- [ anon ]
00007f7e50c2c000 4 0 0 ----- [ anon ]
00007f7e50c2d000 10260 28 28 rw--- [ anon ]
00007f7e51645000 12 8 8 rw--- [ anon ]
00007fffbb182000 84 12 12 rw--- [ stack ]
00007fffbb1ff000 4 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
total kB 89820 1260 188

请注意65404这一行,种种迹象表明,这个再加上它上面那一行(在这里是132)就是增加的那个64M)。后来增加thread的数量,就会有新增thread数量相应的65404的内存块。(后来发现可能不全是65404,我看到过65400,但是加上该行之上的哪一行数据都是64M)
而且我们看,65404这一行里面的该内存区域的权限,全部turn off,也就是该内存是不可读,不可写,不可执行。实在是不明白这个内存块有啥用处。

还有一点,就是估计到了一定thread的数量以后,增加量就不是64M了。我测试的时候发现创建了一个100个thread,所有的线程都不退出的话进程的内存总量是2999M。当然了如果线程一个跑完了再创建另外一个的话那么只有开始这个64M的增长量了。

不过到这里,我们已经知道了这个多出来64M内存并不是我们的程序本身的行为(当然我现在还并不知道到底是什么地方产生出来的)。也许这个ntpl的行为,也许这个是内核的行为,现在我还不清楚而已。这个留作以后的作业吧。

后记:
我今天对测试程序做了一个strace,得到了如下结果:
strace -f -e “brk,mmap,munmap,mprotect” test
brk(0) = 0x228f000
mmap(NULL, 4096, PROTREAD|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e30000
mmap(NULL, 82462, PROTREAD, MAPPRIVATE, 3, 0) = 0x7fab81e1b000
mmap(0x3f72000000, 3745960, PROTREAD|PROTEXEC, MAPPRIVATE|MAPDENYWRITE, 3, 0) = 0x3f72000000
mprotect(0x3f7218a000, 2093056, PROTNONE) = 0
mmap(0x3f72389000, 20480, PROT
READ|PROTWRITE, MAPPRIVATE|MAPFIXED|MAPDENYWRITE, 3, 0x189000) = 0x3f72389000
mmap(0x3f7238e000, 18600, PROTREAD|PROTWRITE, MAPPRIVATE|MAPFIXED|MAPANONYMOUS, -1, 0) = 0x3f7238e000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e1a000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e19000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e18000
mprotect(0x3f72389000, 16384, PROT
READ) = 0
mprotect(0x3f71a1f000, 4096, PROTREAD) = 0
munmap(0x7fab81e1b000, 82462) = 0
brk(0) = 0x228f000
brk(0x22b0000) = 0x22b0000
mmap(NULL, 99158576, PROT
READ, MAP_PRIVATE, 3, 0) = 0x7fab7bf87000

从上面没有看出应用程序有调用分配出64M左右的空间且权限是PROTNONE,或者更改某个约64M大小的区域的权限到PROTNONE的请求。难道这个6神秘的64M真的来自内核?看来这个问题只能说 “未完,待续 …”

Categories: programming Tags: , ,
  1. No comments yet.
  1. No trackbacks yet.