分类: C/C++
2015-10-19 09:21:51
经常使用top命令了解进程信息,其中包括内存方面的信息。命令top帮助文档是这么解释各个字段的。
VIRT , Virtual Image (kb)
RES, Resident size (kb)
SHR, Shared Mem size (kb)
%MEM, Memory usage(kb)
SWAP, Swapped size (kb)
CODE, Code size (kb)
DATA, Data+Stack size (kb)
nFLT, Page Fault count
nDRT, Dirty Pages count
尽管有注释,但依然感觉有些晦涩,不知所指何意?
正在运行的程序,叫进程。每个进程都有完全属于自己的,独立的,不被干扰的内存空间。此空间,被分成几个段(Segment),分别是Text, Data, BSS, Heap, Stack。用户进程内存空间,也是系统内核分配给该进程的VM(虚拟内存),但并不表示这个进程占用了这么多的RAM(物理内存)。这个空间有多大?命令top输出的VIRT值告诉了我们各个进程内存空间的大小(进程内存空间随着程序的执行会增大或者缩小)。你还可以通过/proc//maps,或者pmap –d 了解某个进程内存空间都分布,比如:
1
2
3
4
5
6
7
8
9
10
11
|
#cat /proc/ 1449 /maps
…
0012e000-002a4000 r-xp 00000000 08 : 07 3539877 /lib/i386-linux-gnu/libc- 2.13 .so
002a4000-002a6000 r--p 00176000 08 : 07 3539877 /lib/i386-linux-gnu/libc- 2.13 .so
002a6000-002a7000 rw-p 00178000 08 : 07 3539877 /lib/i386-linux-gnu/libc- 2.13 .so
002a7000-002aa000 rw-p 00000000 00 : 00 0
…
08048000 -0875b000 r-xp 00000000 08 : 07 4072287 /usr/local/mysql/libexec/mysqld
0875b000-0875d000 r--p 00712000 08 : 07 4072287 /usr/local/mysql/libexec/mysqld
0875d000-087aa000 rw-p 00714000 08 : 07 4072287 /usr/local/mysql/libexec/mysqld
…
|
PS:线性地址,访问权限, offset, 设备号,inode,映射文件
“内存总是被进程占用”,这句话换过来可以这么理解:进程总是需要内存。当fork()或者exec()一个进程的时候,系统内核就会分配一定量的VM给进程,作为进程的内存空间,大小由BSS段,Data段的已定义的全局变量、静态变量、Text段中的字符直接量、程序本身的内存映像等,还有Stack段的局部变量决定。当然,还可以通过malloc()等函数动态分配内存,向上扩大heap。
动态分配与静态分配,二者最大的区别在于:1. 直到Run-Time的时候,执行动态分配,而在compile-time的时候,就已经决定好了分配多少Text+Data+BSS+Stack。2.通过malloc()动态分配的内存,需要程序员手工调用free()释放内存,否则容易导致内存泄露,而静态分配的内存则在进程执行结束后系统释放(Text, Data), 但Stack段中的数据很短暂,函数退出立即被销毁。
我们使用几个示例小程序,加深理解
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
/* @filename: example-2.c */
# include <stdio.h>
int main( int argc, char *argv[])
{
char arr[] = "hello world" ; /* Stack段,rw--- */
char *p = "hello world" ; /* Text段,字符串直接量, r-x-- */
arr[ 1 ] = 'l' ;
*(++p) = 'l' ; /* 出错了,Text段不能write */
return 0 ;
}
PS:变量p,它在Stack段,但它所指的”hello world”是一个字符串直接量,放在Text段。
/* @filename:example_2_2.c */
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
char *get_str_1()
{
char str[] = "hello world" ;
return str;
}
char *get_str_2()
{
char *str = "hello world" ;
return str;
}
char *get_str_3()
{
char tmp[] = "hello world" ;
char *str;
str = (char *)malloc( 12 * sizeof(char));
memcpy(str, tmp, 12 );
return str;
}
int main( int argc, char *argv[])
{
char *str_1 = get_str_1(); //出错了,Stack段中的数据在函数退出时就销毁了
char *str_2 = get_str_2(); //正确,指向Text段中的字符直接量,退出程序后才会回收
char *str_3 = get_str_3(); //正确,指向Heap段中的数据,还没free()
printf( "%s\n" , str_1);
printf( "%s\n" , str_2);
printf( "%s\n" , str_3);
if (str_3 != NULL)
{
free(str_3);
str_3 = NULL;
}
return 0 ;
}
PS:函数get_str_1()返回Stack段数据,编译时会报错。Heap中的数据,如果不用了,应该尽早释放free()。
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>
char data_ var = '1' ;
char *mem_killer()
{
char *p;
p = (char *)malloc( 1024 * 1024 * 4 );
memset(p, '\0' , 1024 * 1024 * 4 );
p = &data_ var ; //危险,内存泄露
return p;
}
int main( int argc, char *argv[])
{
char *p;
for (;;)
{
p = mem_killer(); // 函数中malloc()分配的内存没办法free()
printf( "%c\n" , *p);
sleep( 20 );
}
return 0 ;
}
|
PS:使用malloc(),特别要留意heap段中的内存不用时,尽早手工free()。通过top输出的VIRT和RES两值来观察进程占用VM和RAM大小。
本节结束之前,介绍工具size。因为Text, BSS, Data段在编译时已经决定了进程将占用多少VM。可以通过size,知道这些信息。
编码人员在编写程序之际,时常要处理变化数据,无法预料要处理的数据集变化是否大(phper可能难以理解),所以除了变量之外,还需要动态分配内存。GNU libc库提供了二个内存分配函数,分别是malloc()和calloc()。调用malloc(size_t size)函数分配内存成功,总会分配size字节VM(再次强调不是RAM),并返回一个指向刚才所分配内存区域的开端地址。分配的内存会为进程一直保留着,直到你显示地调用free()释放它(当然,整个进程结束,静态和动态分配的内存都会被系统回收)。开发人员有责任尽早将动态分配的内存释放回系统。记住一句话:尽早free()!
我们来看看,malloc()小示例。
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
|
/* @filename:example_2_4.c */
# include <stdio.h>
# include <stdlib.h>
int main( int argc, char *argv[])
{
char *p_4kb, *p_128kb, *p_300kb;
if ((p_4kb = malloc( 4 * 1024 )) != NULL)
{
free(p_4kb);
}
if ((p_128kb = malloc( 128 * 1024 )) != NULL)
{
free(p_128kb);
}
if ((p_300kb = malloc( 300 * 1024 )) != NULL)
{
free(p_300kb);
}
return 0 ;
}
#gcc example_2_4.c –o example_2_4
#s trace –t ./example_2_4
…
00 : 02 : 53 brk( 0 ) = 0x8f58000
00 : 02 : 53 brk( 0x8f7a000 ) = 0x8f7a000
00 : 02 : 53 brk( 0x8f79000 ) = 0x8f79000
00 : 02 : 53 mmap2(NULL, 311296 , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, - 1 , 0 ) = 0xb772d000
00 : 02 : 53 munmap( 0xb772d000 , 311296 ) = 0
…
|
PS:系统调用brk(0)取得当前堆的地址,也称为断点。
通过跟踪系统内核调用,可见glibc函数malloc()总是通过brk()或mmap()系统调用来满足内存分配需求。函数malloc(),根据不同大小内存要求来选择brk(),还是mmap(), 128Kbytes是临界值。小块内存(<=128kbytes),会调用brk(),它将数据段的最高地址往更高处推(堆从底部向上增长)。大块内存,则使用mmap()进行匿名映射(设置标志MAP_ANONYMOUS)来分配内存,与堆无关,在堆之外。这样做是有道理的,试想:如果大块内存,也调用brk(),则容易被小块内存钉住,必竟用大块内存不是很频繁;反过来,小块内存分配更为频繁得多,如果也使用mmap(),频繁的创建内存映射会导致更多的开销,还有一点就是,内存映射的大小要求必须是“页”(单位,内存页面大小,默认4Kbytes或8Kbytes)的倍数,如果只是为了”hello world”这样小数据就映射一“页”内存,那实在是太浪费了。
跟malloc()一样,释放内存函数free(),也会根据内存大小,选择使用brk()将断点往低处回推,或者选择调用munmap()解除映射。有一点需要注意:并不是每次调用free()小块内存,都会马上调用brk(),即堆并不会在每次内存被释放后就被缩减,而是会被glibc保留给下次malloc()使用(必竟小块内存分配较为频繁),直到glibc发现堆空闲大小显著大于内存分配所需数量时,则会调用brk()。但每次free()大块内存,都会调用munmap()解除映射。下面是二张malloc()小块内存和大块内存的示例图。
示意图:函数malloc(100000),小于128kbytes,往高处推(heap->)。留意紫圈标注
示意图:函数malloc(1024*1024),大于128kbytes,在heap与stack之间。留意紫圈。PS:图中的Data Segment泛指BSS, Data, Heap。有些文档有说明:数据段有三个子区域,分别是BSS, Data, Heap。
每次调用malloc(),系统都只是给进程分配线性地址(VM),并没有随即分配页框(RAM)。系统尽量将分配页框的工作推迟到最后一刻—用到时缺页异常处理。这种页框按需延迟分配策略最大好处之一:充分有效地善用系统稀缺资源RAM。
当指针引用的内存页没有驻留在RAM中,即在RAM找不到与之对应的页框,则会发生缺页异常(对进程来说是透明的),内核便陷入缺页异常处理。发生缺页异常有几种情况:1.只分配了线性地址,并没有分配页框,常发生在第一次访问某内存页。2.已经分配了页框,但页框被回收,换出至磁盘(交换区)。3.引用的内存页,在进程空间之外,不属于该进程,可能已被free()。我们使用一段伪代码来大致了解缺页异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/* @filename: example_2_5.c */
…
demo()
{
char *p;
//分配了100Kbytes线性地址
if ((p = malloc( 1024 * 100 )) != NULL) // L0
{
*p = ‘t’; // L1
… //过去了很长一段时间,不管系统忙否,长久不用的页框都有可能被回收
*p = ‘m’; // L2
p[ 4096 ] = ‘p’; // L3
…
free(p); //L4
if (p == NULL)
{
*p = ‘l’; // L5
}
}
}
…
|
主缺页异常处理过程示意图,参见Page Fault Handling
随着网络并发用户数量增多,进程数量越来越多(比如一般守护进程会fork()子进程来处理用户请求),缺页异常也就更频繁,需要缓存更多的磁盘数据(参考下篇OS Page Cache),RAM也就越来越紧少。为了保证有够用的页框供给缺页异常处理,Linux有一套自己的做法,称为PFRA。PFRA总会从用户态进内存程空间和页面缓存中,“窃取”页框满足供给。所谓”窃取”,指的是:将用户进程内存空间对应占用的页框中的数据swap out至磁盘(称为交换区),或者将OS页面缓存中的内存页(还有用户进程mmap()的内存页)flush(同步fsync())至磁盘设备。PS:如果你观察到因为RAM不足导致系统病态式般慢,通常都是因为缺页异常处理,以及PFRA在”盗页”。我们从以下几个方面了解PFRA。
候选页框:找出哪些页框是可以被回收?
页框回收策略:确定了要回收的页框,就要进一步确定先回收哪些候选页框
激活回收页框:什么时候会回收页框?
这二个关键字在很多地方出现,译过来应该是Paging(调页),Swapping(交换)。PS:英语里面用得多的动词加上ing,就成了名词,比如building。咬文嚼字,实在是太难。看二图
Swapping的大部分时间花在数据传输上,交换的数据也越多,意味时间开销也随之增加。对于进程而言,这个过程是透明的。由于RAM资源不足,PFRA会将部分匿名页框的数据写入到交换区(swap area),备份之,这个动作称为so(swap out)。等到发生内存缺页异常的时候,缺页异常处理程序会将交换区(磁盘)的页面又读回物理内存,这个动作称为si(swap in)。每次Swapping,都有可能不只是一页数据,不管是si,还是so。Swapping意味着磁盘操作,更新页表等操作,这些操作开销都不小,会阻塞用户态进程。所以,持续飚高的si/so意味着物理内存资源是性能瓶颈。
Paging,前文我们有说过Demand Paging。通过线性地址找到物理地址,找到页框。这个过程,可以认为是Paging,对于进程来讲,也是透明的。Paging意味着产生缺页异常,也有可能是大缺页,也就意味着浪费更多的CPU时间片资源。
1.用户进程内存空间分为5段,Text, DATA, BSS, Heap, Stack。其中Text只读可执行,DATA全局变量和静态变量,Heap用完就尽早free(),Stack里面的数据是临时的,退出函数就没了。
2.glibc malloc()动态分配内存。使用brk()或者mmap(),128Kbytes是一个临界值。避免内存泄露,避免野指针。
3.内核会尽量延后Demand Paging。主缺页是昂贵的。
4.先回收Buffer/Cache占用的页框,然后程序占用的页框,使用LRU置换算法。调小vm.swappiness值可以减少Swapping,减少大缺页。
5.更少的Paging和Swapping
6.fork()继承父进程的地址空间,不过是只读,使用cow技术,fork()函数特殊在于它返回二次。