技术分享
Linux内存点滴 用户进程内存空间
2017-06-06
Linux操作系统的内存使用方法详细解析
我是一名程序员,那么我在这里以一个程序员的角度来讲解Linux内存的使用。
一提到内存管理,我们头脑中闪出的两个概念,就是虚拟内存,与物理内存。这两个概念主要来自于linux内核的支持。
Linux在内存管理上份为两级,一级是线性区,类似于00c73000-00c88000,对应于虚拟内存,它实际上不占用实际物理内存;一级是具体的物理页面,它对应我们机器上的物理内存。
这里要提到一个很重要的概念,内存的延迟分配。Linux内核在用户申请内存的时 候,只是给它分配了一个线性区(也就是虚存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用 宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。
char *p=malloc(2048) //这里只是分配了虚拟内存2048,并不占用实际内存。
strcpy(p,”123”) //分配了物理页面,虽然只是使用了3个字节,但内存还是为它分配了2048字节的物理内存。
free(p) //通过虚拟地址,找到其所对应的物理页面,释放物理页面,释放线性区。
我们知道用户的进程和内核是运行在不同的级别,进程与内核之间的通讯是通过系统调用来完成的。进程在申请和释放内存,主要通过brk,sbrk,mmap,unmmap这几个系统调用,传递的参数主要是对应的虚拟内存。
注意一点,在进程只能访问虚拟内存,它实际上是看不到内核物理内存的使用,这对于进程是完全透明的。
glibc内存管理器
那么我们每次调用malloc来分配一块内存,都进行相应的系统调用呢?
答案是否定的,这里我要引入一个新的概念,glibc的内存管理器。
我们知道malloc和free等函数都是包含在glibc库里面的库函数,我们试想一下,每做一次内存操作,都要调用系统调用的话,那么程序将多么的低效。
实际上glibc采用了一种批发和零售的方式来管理内存。glibc每次通过系统调用的方式申请一大块内存(虚拟内存),当进程申请内存时,glibc就从自己获得的内存中取出一块给进程。
内存管理器面临的困难
我们在写程序的时候,每次申请的内存块大小不规律,而且存在频繁的申请和释放,这样 不可避免的就会产生内存碎块。而内存碎块,直接会导致大块内存申请无法满足,从而更多的占用系统资源;如果进行碎块整理的话,又会增加cpu的负荷,很多 都是互相矛盾的指标,这里我就不细说了。
我们在写程序时,涉及内存时,有两个概念heap和stack。传统的说法stack的内存地址是向下增长的,heap的内存地址是向上增长的。
函数malloc和free,主要是针对heap进行操作,由程序员自主控制内存的访问。
在这里heap的内存地址向上增长,这句话不完全正确。
glibc对于heap内存申请大于128k的内存申请,glibc采用mmap的方式向内核申请内存,这不能保证内存地址向上增长;小于128k的则采用brk,对于它来讲是正确的。128k的阀值,可以通过glibc的库函数进行设置。
这里我先讲大块内存的申请,也即对应于mmap系统调用。
对于大块内存申请,glibc直接使用mmap系统调用为其划分出另一块虚拟地址,供进程单独使用;在该块内存释放时,使用unmmap系统调用将这块内存释放,这个过程中间不会产生内存碎块等问题。
针对小块内存的申请,在程序启动之后,进程会获得一个heap底端的地址,进程每次 进行内存申请时,glibc会将堆顶向上增长来扩展内存空间,也就是我们所说的堆地址向上增长。在对这些小块内存进行操作时,便会产生内存碎块的问题。实 际上brk和sbrk系统调用,就是调整heap顶地址指针。
那么heap堆的内存是什么时候释放呢?
当glibc发现堆顶有连续的128k的空间是空闲的时候,它就会通过brk或sbrk系统调用,来调整heap顶的位置,将占用的内存返回给系统。这时,内核会通过删除相应的线性区,来释放占用的物理内存。
下面我要讲一个内存空洞的问题:
一个场景,堆顶有一块正在使用的内存,而下面有很大的连续内存已经被释放掉了,那么这块内存是否能够被释放?其对应的物理内存是否能够被释放?
很遗憾,不能。
这也就是说,只要堆顶的部分申请内存还在占用,我在下面释放的内存再多,都不会被返回到系统中,仍然占用着物理内存。为什么会这样呢?
这主要是与内核在处理堆的时候,过于简单,它只能通过调整堆顶指针的方式来调整调整程序占用的线性区;而又只能通过调整线性区的方式,来释放内存。所以只要堆顶不减小,占用的内存就不会释放。
提一个问题:
char *p=malloc(2);
free(p)
为什么申请内存的时候,需要两个参数,一个是内存大小,一个是返回的指针;而释放内存的时候,却只要内存的指针呢?
这主要是和glibc的内存管理机制有关。glibc中,为每一块内存维护了一个chunk的结构。glibc在分配内存时,glibc先填写chunk结构中内存块的大小,然后是分配给进程的内存。
chunk ------size
p------------ content
在进程释放内存时,只要 指针-4 便可以找到该块内存的大小,从而释放掉。
注:glibc在做内存申请时,最少分配16个字节,以便能够维护chunk结构。
glibc提供的调试工具:
为了方便调试,glibc 为用户提供了 malloc 等等函数的钩子(hook),如 __malloc_hook
对应的是一个函数指针,
void *function (size_t size, const void *caller)
其中 caller 是调用 malloc 返回值的接受者(一个指针的地址)。另外有 __malloc_initialize_hook函数指针,仅仅会调用一次(第一次分配动态内存时)。(malloc.h)
一些使用 malloc 的统计量(SVID 扩展)可以用 struct mallinfo 储存,
可调用获得。
struct mallinfo mallinfo (void)
如何检测 memory leakage?glibc 提供了一个函数
void mtrace (void)及其反作用void muntrace (void)
这时会依赖于一个环境变量 MALLOC_TRACE 所指的文件,把一些信息记录在该文件中
用于侦测 memory leakage,其本质是安装了前面提到的 hook。一般将这些函数用
#ifdef DEBUGGING 包裹以便在非调试态下减少开销。产生的文件据说不建议自己去读,
而使用 mtrace 程序(perl 脚本来进行分析)。下面用一个简单的例子说明这个过程,这是
源程序:
#include
#include
#include
intmain( int argc, char *argv[] )
{
int *p, *q ;
#ifdef DEBUGGING
mtrace( ) ;
#endif
p = malloc( sizeof( int ) ) ;
q = malloc( sizeof( int ) ) ;
printf( "p = %p\nq = %p\n", p, q ) ;
*p = 1 ;
*q = 2 ;
free( p ) ;
return 0 ;
}
很简单的程序,其中 q 没有被释放。我们设置了环境变量后并且 touch 出该文件
执行结果如下:
p = 0x98c0378q = 0x98c0388
该文件内容如下
= Star
t@./test30:[0x8048446] + 0x98c0378 0x4
@ ./test30:[0x8048455] + 0x98c0388 0x4
@ ./test30:[0x804848f] - 0x98c0378
到这里我基本上讲完了,我们写程序时,数据部分内存使用的问题。
代码占用的内存
数据部分占用内存,那么我们写的程序是不是也占用内存呢?
在linux中,程序的加载,涉及到两个工具,linker 和loader。Linker主要涉及动态链接库的使用,loader主要涉及软件的加载。
1、 exec执行一个程序
2、 elf为现在非常流行的可执行文件的格式,它为程序运行划分了两个段,一个段是可以执行的代码段,它是只读,可执行;另一个段是数据段,它是可读写,不能执行。
3、 loader会启动,通过mmap系统调用,将代码端和数据段映射到内存中,其实也就是为其分配了虚拟内存,注意这时候,还不占用物理内存;只有程序执行到了相应的地方,内核才会为其分配物理内存。
4、 loader会去查找该程序依赖的链接库,首先看该链接库是否被映射进内存中,如果没有使用mmap,将代码段与数据段映射到内存中,否则只是将其加入进程的地址空间。这样比如glibc等库的内存地址空间是完全一样。
因此一个2M的程序,执行时,并不意味着为其分配了2M的物理内存,这与其运行了的代码量,与其所依赖的动态链接库有关。
运行过程中链接动态链接库与编译过程中链接动态库的区别。
我们调用动态链接库有两种方法:一种是编译的时候,指明所依赖的动态链接库,这样loader可以在程序启动的时候,来所有的动态链接映射到内存中;一种是在运行过程中,通过dlopen和dlfree的方式加载动态链接库,动态将动态链接库加载到内存中。
这两种方式,从编程角度来讲,第一种是最方便的,效率上影响也不大,在内存使用上有些差别。
第一种方式,一个库的代码,只要运行过一次,便会占用物理内存,之后即使再也不使用,也会占用物理内存,直到进程的终止。
第二中方式,库代码占用的内存,可以通过dlfree的方式,释放掉,返回给物理内存。
这个差别主要对于那些寿命很长,但又会偶尔调用各种库的进程有关。如果是这类进程,建议采用第二种方式调用动态链接库。
占用内存的测量
测量一个进程占用了多少内存,linux为我们提供了一个很方便的方法,/proc目录为我们提供了所有的信息,实际上top等工具也通过这里来获取相应的信息。
/proc/meminfo 机器的内存使用信息
/proc/pid/maps pid为进程号,显示当前进程所占用的虚拟地址。
/proc/pid/statm 进程所占用的内存
[root@localhost ~]# cat /proc/self/statm
654 57 44 0 0 334 0
输出解释
CPU 以及CPU0。。。的每行的每个参数意思(以第一行为例)为:
参数 解释 /proc//status
Size (pages) 任务虚拟地址空间的大小 VmSize/4
Resident(pages) 应用程序正在使用的物理内存的大小 VmRSS/4
Shared(pages) 共享页数 0
Trs(pages) 程序所拥有的可执行虚拟内存的大小 VmExe/4
Lrs(pages) 被映像到任务的虚拟内存空间的库的大小 VmLib/4
Drs(pages) 程序数据段和用户态的栈的大小 (VmData+ VmStk )4
dt(pages) 04
查看机器可用内存
/proc/28248/>free
total used free shared buffers cached
Mem: 1023788 926400 97388 0 134668 503688
-/+ buffers/cache: 288044 735744
Swap: 1959920 89608 1870312
我们通过free命令查看机器空闲内存时,会发现free的值很小。这主要是因为,在linux中有这么一种思想,内存不用白不用,因此它尽可能的cache和buffer一些数据,以方便下次使用。但实际上这些内存也是可以立刻拿来使用的。
所以 空闲内存=free+buffers+cached=total-used
查看进程使用的内存
查看一个进程使用的内存,是一个很令人困惑的事情。因为我们写的程序,必然要用到动态链接库,将其加入到自己的地址空间中,但是/proc/pid/statm统计出来的数据,会将这些动态链接库所占用的内存也简单的算进来。
这样带来的问题,动态链接库占用的内存有些是其他程序使用时占用的,却算在了你这里。你的程序中包含了子进程,那么有些动态链接库重用的内存会被重复计算。
因此要想准确的评估一个程序所占用的内存是十分困难的,通过写一个module的方式,来准确计算某一段虚拟地址所占用的内存,可能对我们有用。(T002)
linux下的内存查看(virt,res,shr,data的意义)
其实在认真阅读了这篇名为“计算内存使用”的文章之后,还是处于半迷糊状态。这位作者就说Linux下面没有特别好的显示内存占用的工具,虽然有top和free,但都说得不清楚,就跟巫毒教的魔术似的。
比如top这个工具,它会显示3种数据,作者分别解释如下:
以下是引用片段: VIRT:virtual memory usage。Virtual这个词很神,一般解释是:virtual adj.虚的, 实质的, [物]有效的, 事实上的。到底是虚的还是实的?让Google给Define之后,将就明白一点,就是这东西还是非物质的,但是有效果的,不发生在真实世界的,发生在软件世界的等等。这个内存使用就是一个应用占有的地址空间,只是要应用程序要求的,就全算在这里,而不管它真的用了没有。写程序怕出错,又不在乎占用的时候,多开点内存也是很正常的。 RES:resident memory usage。常驻内存。这个值就是该应用程序真的使用的内存,但还有两个小问题,一是有些东西可能放在交换盘上了(SWAP),二是有些内存可能是共享的。 SHR:shared memory。共享内存。就是说这一块内存空间有可能也被其他应用程序使用着;而Virt - Shr似乎就是这个程序所要求的并且没有共享的内存空间。 DATA:数据占用的内存。如果top没有显示,按f键可以显示出来。这一块是真正的该程序要求的数据空间,是真正在运行中要使用的。 |
所以DATA的含义比较确定,甚至可以用程序读取的数据量计算出来;SHR是一个潜在的可能会被共享的数字,如果只开一个程序,也没有别人共同使用它;VIRT里面的可能性更多,比如它可能计算了被许多X的库所共享的内存;RES应该是比较准确的,但不含有交换出去的空间;但基本可以说RES是程序当前使用的内存量。
我们猜你喜欢:
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: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,映射文件
VM分配与释放
“内存总是被进程占用”,这句话换过来可以这么理解:进程总是需要内存。当fork()或者exec()一个进程的时候,系统内核就会分配一定量的VM给进程,作为进程的内存空间,大小由BSS段,Data段的已定义的全局变量、静态变量、Text段中的字符直接量、程序本身的内存映像等,还有Stack段的局部变量决定。当然,还可以通过malloc()等函数动态分配内存,向上扩大heap。
动态分配与静态分配,二者最大的区别在于:1. 直到Run-Time的时候,执行动态分配,而在compile-time的时候,就已经决定好了分配多少Text+Data+BSS+Stack。2.通过malloc()动态分配的内存,需要程序员手工调用free()释放内存,否则容易导致内存泄露,而静态分配的内存则在进程执行结束后系统释放(Text, Data), 但Stack段中的数据很短暂,函数退出立即被销毁。
我们使用几个示例小程序,加深理解
/* @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,知道这些信息。
# size example_2_3
textdatabssdec hexfilename
140327281683693example_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> 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 #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:53 munmap(0xb772d000, 311296) = 0 … PS:系统调用brk(0)取得当前堆的地址,也称为断点。
通过跟踪系统内核调用,可见glibc函数malloc()总是通过brk()或mmap()系统调用来满足内存分配需求。函数malloc(),根据不同大小内存要求来选择brk(),还是mmap(), 128Kbytes是临界值。小块内存()。留意紫圈标注
示意图:函数malloc(1024*1024),大于128kbytes,在heap与stack之间。留意紫圈。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)。
页框回收PFRA
随着网络并发用户数量增多,进程数量越来越多(比如一般守护进程会fork()子进程来处理用户请求),缺页异常也就更频繁,需要缓存更多的磁盘数据(参考下篇OS Page Cache),RAM也就越来越紧少。为了保证有够用的页框供给缺页异常处理,Linux有一套自己的做法,称为PFRA。PFRA总会从用户态进内存程空间和页面缓存中,“窃取”页框满足供给。所谓”窃取”,指的是:将用户进程内存空间对应占用的页框中的数据swap out至磁盘(称为交换区),或者将OS页面缓存中的内存页(还有用户进程mmap()的内存页)flush(同步fsync())至磁盘设备。PS:如果你观察到因为RAM不足导致系统病态式般慢,通常都是因为缺页异常处理,以及PFRA在”盗页”。我们从以下几个方面了解PFRA。
候选页框:找出哪些页框是可以被回收?
进程内存空间占用的页框,比如数据段中的页(Heap, Data),还有在Heap与Stack之间的匿名映射页(比如由malloc()分配的大内存)。但不包括Stack段中的页。
进程空间mmap()的内存页,有映射文件,非匿名映射。
缓存在页面缓存中Buffer/Cache占用的页框。也称OS Page Cache。
页框回收策略:确定了要回收的页框,就要进一步确定先回收哪些候选页框
尽量先回收页面缓存中的Buffer/Cache。其次再回收内存空间占用的页框。
进程空间占用的页框,要是没有被锁定,都可以回收。所以,当某进程睡眠久了,占用的页框会逐渐地交换出去至交换区。
使收LRU置换算法,将那些久而未用的页框优先被回收。这种被放在LRU的unused链表的页,常被认为接下来也不太可能会被引用。
相对回收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。
Paging 和Swapping
这二个关键字在很多地方出现,译过来应该是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()函数特殊在于它返回二次。

- 标签:
-
技术分享