`
javathinker
  • 浏览: 227524 次
  • 来自: ...
文章分类
社区版块
存档分类
最新评论

Linux内存占用分析 进程内存空间

阅读更多

经常使用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了解某个进程内存空间都分布,比如:

#cat /proc/1449/maps

0012e000-002a4000 r-xp 00000000 08:073539877 /lib/i386-linux-gnu/libc-2.13.so

002a4000-002a6000 r--p 00176000 08:073539877 /lib/i386-linux-gnu/libc-2.13.so

002a6000-002a7000 rw-p 00178000 08:073539877 /lib/i386-linux-gnu/libc-2.13.so

002a7000-002aa000 rw-p 00000000 00:000

08048000-0875b000 r-xp 00000000 08:074072287 /usr/local/mysql/libexec/mysqld

0875b000-0875d000 r--p 00712000 08:074072287 /usr/local/mysql/libexec/mysqld

0875d000-087aa000 rw-p 00714000 08:074072287 /usr/local/mysql/libexec/mysqld

PS:线性地址,访问权限, offset,设备号,inode,映射文件



VM分配与释放

内存总是被进程占用,这句话换过来可以这么理解:进程总是需要内存。当fork()或者exec()一个进程的时候,系统内核就会分配一定量的VM给进程,作为进程的内存空间,大小由BSS段,Data段的已定义的全局变量、静态变量、Text段中的字符直接量、程序本身的内存映像等,还有Stack段的局部变量决定。当然,还可以通过malloc()等函数动态分配内存,向上扩大heap

动态分配与静态分配,二者最大的区别在于:1.直到Run-Time的时候,执行动态分配,而在compile-time的时候,就已经决定好了分配多少Text+Data+BSS+Stack2.通过malloc()动态分配的内存,需要程序员手工调用free()释放内存,否则容易导致内存泄露,而静态分配的内存则在进程执行结束后系统释放(Text, Data),Stack段中的数据很短暂,函数退出立即被销毁。

我们使用几个示例小程序,加深理解

/* @filename: example-2.c */

#include <stdio.h>

intmain(intargc,char*argv[])

{

chararr[]="hello world";/* Stack段,rw--- */

char*p ="hello world";/* Text段,字符串直接量, r-x-- */

arr[1]='l';

*(++p)='l';/*出错了,Text段不能write */

return0;

}

PS:变量p,它在Stack段,但它所指的”hello world”是一个字符串直接量,放在Text段。

/* @filename:example_2_2.c */

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

char*get_str_1()

{

charstr[]="hello world";

returnstr;

}

char*get_str_2()

{

char*str ="hello world";

returnstr;

}

char*get_str_3()

{

chartmp[]="hello world";

char*str;

str =(char*)malloc(12 *sizeof(char));

memcpy(str, tmp, 12);

returnstr;

}

intmain(intargc,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;

}

return0;

}

PS:函数get_str_1()返回Stack段数据,编译时会报错。Heap中的数据,如果不用了,应该尽早释放free()

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

chardata_var ='1';

char*mem_killer()

{

char*p;

p =(char*)malloc(1024*1024*4);

memset(p,'\0', 1024*1024*4);

p = &data_var;//危险,内存泄露

returnp;

}

intmain(intargc,char*argv[])

{

char*p;

for(;;)

{

p = mem_killer();//函数中malloc()分配的内存没办法free()

printf("%c\n", *p);

sleep(20);

}

return0;

}

PS:使用malloc(),特别要留意heap段中的内存不用时,尽早手工free()。通过top输出的VIRTRES两值来观察进程占用VMRAM大小。

本节结束之前,介绍工具size。因为Text, BSS, Data段在编译时已经决定了进程将占用多少VM。可以通过size,知道这些信息。

# gcc example_2_3.c -o example_2_3
# size example_2_3
text data bss dec hex filename
1403 272 8 1683 693 example_2_3



malloc()

编码人员在编写程序之际,时常要处理变化数据,无法预料要处理的数据集变化是否大(phper可能难以理解),所以除了变量之外,还需要动态分配内存。GNU libc库提供了二个内存分配函数,分别是malloc()calloc()。调用malloc(size_t size)函数分配内存成功,总会分配size字节VM(再次强调不是RAM),并返回一个指向刚才所分配内存区域的开端地址。分配的内存会为进程一直保留着,直到你显示地调用free()释放它(当然,整个进程结束,静态和动态分配的内存都会被系统回收)。开发人员有责任尽早将动态分配的内存释放回系统。记住一句话:尽早free()

我们来看看,malloc()小示例。

/* @filename:example_2_4.c */

#include <stdio.h>

#include <stdlib.h>

intmain(intargc,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);

}

return0;

}

#gcc example_2_4.c –o example_2_4

#strace –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:53munmap(0xb772d000,311296) =0

PS:系统调用brk(0)取得当前堆的地址,也称为断点。

通过跟踪系统内核调用,可见glibc函数malloc()总是通过brk()mmap()系统调用来满足内存分配需求。函数malloc(),根据不同大小内存要求来选择brk(),还是mmap()128Kbytes是临界值。小块内存(<=128kbytes),会调用brk(),它将数据段的最高地址往更高处推(堆从底部向上增长)。大块内存,则使用mmap()进行匿名映射(设置标志MAP_ANONYMOUS)来分配内存,与堆无关,在堆之外。这样做是有道理的,试想:如果大块内存,也调用brk(),则容易被小块内存钉住,必竟用大块内存不是很频繁;反过来,小块内存分配更为频繁得多,如果也使用mmap(),频繁的创建内存映射会导致更多的开销,还有一点就是,内存映射的大小要求必须是(单位,内存页面大小,默认4Kbytes8Kbytes)的倍数,如果只是为了”hello world”这样小数据就映射一内存,那实在是太浪费了。

malloc()一样,释放内存函数free(),也会根据内存大小,选择使用brk()将断点往低处回推,或者选择调用munmap()解除映射。有一点需要注意:并不是每次调用free()小块内存,都会马上调用brk(),即堆并不会在每次内存被释放后就被缩减,而是会被glibc保留给下次malloc()使用(必竟小块内存分配较为频繁),直到glibc发现堆空闲大小显著大于内存分配所需数量时,则会调用brk()。但每次free()大块内存,都会调用munmap()解除映射。下面是二张malloc()小块内存和大块内存的示例图。


示意图:函数malloc(100000),小于128kbytes,往高处推(heap->)。留意紫圈标注


示意图:函数malloc(1024*1024),大于128kbytes,在heapstack之间。留意紫圈。PS:图中的Data Segment泛指BSS, Data, Heap。有些文档有说明:数据段有三个子区域,分别是BSS, Data, Heap

缺页异常(Fault Page)

每次调用malloc(),系统都只是给进程分配线性地址(VM),并没有随即分配页框(RAM)。系统尽量将分配页框的工作推迟到最后一刻用到时缺页异常处理。这种页框按需延迟分配策略最大好处之一:充分有效地善用系统稀缺资源RAM

当指针引用的内存页没有驻留在RAM中,即在RAM找不到与之对应的页框,则会发生缺页异常(对进程来说是透明的),内核便陷入缺页异常处理。发生缺页异常有几种情况:1.只分配了线性地址,并没有分配页框,常发生在第一次访问某内存页。2.已经分配了页框,但页框被回收,换出至磁盘(交换区)3.引用的内存页,在进程空间之外,不属于该进程,可能已被free()。我们使用一段伪代码来大致了解缺页异常。

/* @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

}

}

}

§L0,函数malloc()通过brk()给进程分配了100Kbytes的线性地址区域(VM).然而,系统并没有随即分配页框(RAM)。即此时,进程没有占用100Kbytes的物理内存。这也表明了,你时常在使用top的时候VIRT值增大,而RES值却不变的原因。

§L1,通过*p引用了100Kbytes的第一页(4Kbytes)。因为是第一次引用此页,在RAM中找不到与之相对应的页框。发生缺页异常(对于进程而言缺页异常是透明的),系统灵敏地捕获这一异常,进入缺页异常处理阶段:接下来,系统会分配一个页框(RAM)映射给它。我们把这种情况(被访问的页还没有被放在任何一个页框中,内核分配一新的页框并适当初始化来满足调用请求),也称为Demand Paging

§L2,过了很长一段时间,通过*p再次引用100Kbytes的第一页。若系统在RAM找不到它映射的页框(可能交换至磁盘了)。发生缺页异常,并被系统捕获进入缺页异常处理。接下来,系统则会分配一页页框(RAM),找到备份在磁盘的那,并将它换入内存(其实因为换入操作比较昂贵,所以不总是只换入一页,而是预换入多页。这也表明某些文档说:”vmstat某时出现不少si并不能意味着物理内存不足”)。凡是类似这种会迫使进程去睡眠(很可能是由于当前磁盘数据填充至页框(RAM)所花的时间),阻塞当前进程的缺页异常处理称为主缺页(major falut),也称为大缺页(参见下图)。相反,不会阻塞进程的缺页,称为次缺页(minor fault),也称为小缺面。

§L3,引用了100Kbytes的第二页。参见第一次访问100Kbytes第一页, Demand Paging

§L4,释放了内存:线性地址区域被删除,页框也被释放。

§L5,再次通过*p引用内存页,已被free()(用户进程本身并不知道)。发生缺页异常,缺面异常处理程序会检查出这个缺页不在进程内存空间之内。对待这种编程错误引起的缺页异常,系统会杀掉这个进程,并且报告著名的段错误(Segmentation fault)


主缺页异常处理过程示意图,参见Page Fault Handling

页框回收PFRA

随着网络并发用户数量增多,进程数量越来越多(比如一般守护进程会fork()子进程来处理用户请求),缺页异常也就更频繁,需要缓存更多的磁盘数据(参考下篇OS Page Cache)RAM也就越来越紧少。为了保证有够用的页框供给缺页异常处理,Linux有一套自己的做法,称为PFRAPFRA总会从用户态进内存程空间和页面缓存中,窃取页框满足供给。所谓窃取,指的是:将用户进程内存空间对应占用的页框中的数据swap out至磁盘(称为交换区),或者将OS页面缓存中的内存页(还有用户进程mmap()的内存页)flush(同步fsync())至磁盘设备。PS:如果你观察到因为RAM不足导致系统病态式般慢,通常都是因为缺页异常处理,以及PFRA盗页。我们从以下几个方面了解PFRA

候选页框:找出哪些页框是可以被回收?

§进程内存空间占用的页框,比如数据段中的页(Heap, Data),还有在HeapStack之间的匿名映射页(比如由malloc()分配的大内存)。但不包括Stack段中的页。

§进程空间mmap()的内存页,有映射文件,非匿名映射。

§缓存在页面缓存中Buffer/Cache占用的页框。也称OS Page Cache

页框回收策略:确定了要回收的页框,就要进一步确定先回收哪些候选页框

§尽量先回收页面缓存中的Buffer/Cache。其次再回收内存空间占用的页框。

§进程空间占用的页框,要是没有被锁定,都可以回收。所以,当某进程睡眠久了,占用的页框会逐渐地交换出去至交换区。

§使收LRU置换算法,将那些久而未用的页框优先被回收。这种被放在LRUunused链表的页,常被认为接下来也不太可能会被引用。

§相对回收Buffer/Cache而言,回收进程内存页,昂贵很多。所以,Linux默认只有swap_tendency(交换倾向值)值不小于100时,才会选择换出进程占用的RES。其实交换倾向值描述的是:系统越忙,且RES都被进程占用了,Buffer/Cache只占了一点点的时候,才开始回收进程占用页框。PS:这正表明了,某些DBA提议将MySQL InnoDB服务器vm.swappiness值设置为0,以此让InnoDB Buffer Pool数据在RES呆得更久。

§如果实在是没有页框可回收,PFRA使出最狠一招,杀掉一个用户态进程,并释放这些被占的页框。当然,这个被杀的进程不是胡乱选的,至少应该是占用较多页框,运行优选级低,且不是root用户的进程。

激活回收页框:什么时候会回收页框?

§紧急回收。系统内核发现没有够用的页框分配,供给读文件和内存缺页处理的时候,系统内核开始紧急回收页框。唤醒pdflush内核线程,先将1024页脏页从页面缓存写回磁盘。然后开始回收32页框,若反复回收13次,还收不齐32页框,则发狠杀一个进程。

§周期性回收。在紧急回收之前,PFRA还会唤醒内核线程kswapd。为了避免更多的紧急回收,当发现空闲页框数量低于设置的警告值时,内核线程kswapd就会被唤醒,回收页框。直到空闲的页框的数量达到设定的安全值。PS:RES资源紧张的时候,你可以通过ps命令看到更多的kswapd线程被唤醒。

§OOM。在高峰时期,RES高度紧张的时候,kswapd持续回收的页框供不应求,直到进入紧急回收,直到OOM

PagingSwapping

这二个关键字在很多地方出现,译过来应该是Paging(调页),Swapping(交换)PS:英语里面用得多的动词加上ing,就成了名词,比如building。咬文嚼字,实在是太难。看二图

Swapping的大部分时间花在数据传输上,交换的数据也越多,意味时间开销也随之增加。对于进程而言,这个过程是透明的。由于RAM资源不足,PFRA会将部分匿名页框的数据写入到交换区(swap area),备份之,这个动作称为so(swap out)。等到发生内存缺页异常的时候,缺页异常处理程序会将交换区(磁盘)的页面又读回物理内存,这个动作称为si(swap in)。每次Swapping,都有可能不只是一页数据,不管是si,还是soSwapping意味着磁盘操作,更新页表等操作,这些操作开销都不小,会阻塞用户态进程。所以,持续飚高的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.
更少的PagingSwapping
6.fork()
继承父进程的地址空间,不过是只读,使用cow技术,fork()函数特殊在于它返回二次。

分享到:
评论

相关推荐

    Linux内存IO

    Linux内存IO的培训PPT。 课程内容 Linux内存管理基础 I/O内存的访问 目标 理解Linux内存管理原理 掌握内核和用户空间内存分配函数的使用 掌握I/O内存的访问方法和步骤 第一章:内核地址空间 第二章:进程地址空间 ...

    linux 内存管理(虚拟地址到物理地址)

    Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能...

    linux进程的一切知识.zip

    linux进程的一切知识.zip 进程占用的内存空间布局,虚拟空间地址分布 进程启动的3种方式 监控子进程的状态 进程的终止 僵尸进程

    Linux系统下利用C程序输出某进程的内存占用信息

    大家应该都遇到过一种情况,在实际工作中有时需要程序打印出某个进程的内存占用情况以作参考, 下面介绍一种通过Linux下的伪文件系统 /proc 计算某进程内存占用的程序实现方法.下面话不多说了,来一起看看详细的介绍...

    疯狂内核之——Linux虚拟内存

    1.4 Linux内存布局 21 1.5 内核空间和用户空间 23 1.5.1 初始化临时内核页表 24 1.5.2 永久内核页表的初始化 32 1.5.3 第一次进入用户空间 41 1.5.4 内核映射机制实例 44 1.6 固定映射的线性地址 48 1.7 高端内存...

    Linux 系统内核空间与用户空间通信的实现与分析

    一般地,在使用虚拟内存技术的多任务系统上,内核和应用有不同的地址空间,因此,在内核和应用之间以及在应用与应用之间进行数据交换需要专门的机制来实现,众所周知,进程间通信(IPC)机制就是为实现应用与应用...

    Linux内核源代码情景分析 (上下册 高清非扫描 )

    2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2.9 页面的换入 2.10 内核...

    Linux /Unix 共享内存

    4) shmdt()函数删除本进程对这块内存的使用,shmdt()与shmat()相反,是用来禁止本进程访问一块共享内存的函数。 函数原型: int shmdt( char *shmaddr ); 参数char *shmaddr是那块共享内存的起始地址。 成功时返回0。...

    内存管理内存管理内存管理

    这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也...

    linux 内核源代码分析

    2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2. 9 页面的...

    信号量实现共享内存读写同步

    C语言编写程序,用信号量和共享内存实现读写同步,程序中父进程为读进程,子进程为写...开启共享内存空间和二值信号量,子进程占用信号量,父进程无法执行读,等待子进程写完释放信号量后父进程进行读操作。依次类推。

    linux内核源代码情景分析

    2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2.9 页面的换入 ...

    linux学习笔记(包含Linux系统和shell编程).rar

    内核会记录哪些内存页面正在使用中,自动把一段时间未访问的内存页面复制到交换空间区域(称之为换出,swapping out)——即使还有内存可用。当程序要访问一个已被换出的内存页面时,内核必须将物理内存中的另一个...

    深入分析Linux内核源码

    深入分析Linux内核源码 前言 ...2.4 Linux内存管理中Slab缓冲区 2.5 Linux中的VFS 2.6 Linux的连网 2.7 网络设备支持 2.8 模块支持 2.9 硬件接口 2.10 块设备 2.11 USB 设备 2 参考文献

    【嵌入式软件工程师面经】Linux系统编程(线程进程).pdf

    线程提供了一种方式使得进程可以在一个共享内存空间内并行执行多个任务。因为线程间共享相同的数据,它们之间的通信可以比进程间通信更方便和高效。但这也意味着必须小心同步操作,避免由于同时访问共享资源引起的...

    Linux性能及调优指南:内存架构

    1.2 Linux内存架构  为了执行一个进程,Linux内核为请求的进程分配一部分内存区域。该进程使用该内存区域作为其工作区并执行请求的工作。它与你的申请一个办公桌,然后使用办公桌来摆放纸张、文档和备忘录来执行你...

    使用C语言的fork()函数在Linux中创建进程的实例讲解

    使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程的上下文、代码段、进程堆栈、内存信息、打开的文件描述符、符号控制设定、进程优先级、进程组号、当前工作目录、...

    Linux运维监控工具(支持系统硬件信息,内存,cpu,温度,磁盘空间及IO,硬盘smart,系统负载,网络流量等监控).zip

    Linux运维监控工具(支持系统硬件信息,内存,cpu,温度,磁盘空间及IO,硬盘smart,系统负载,网络流量等监控,服务接口,大屏展示,拓扑图,进程监控,端口监控,docker监控,文件防篡改,日志监控,数据可视化,...

    详解Linux进程间通信——使用共享内存

    进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问...

    MySQL OOM 系列一 Linux内存分配

     一般写C语言程序,我们习惯使用malloc动态的申请内存空间(Java由JVM负责内存管理),malloc函数会向操作系统申请一段连续的内存单元,然后返回这段空间的起始地址。如果malloc函数返回为null则表示系统

Global site tag (gtag.js) - Google Analytics