位置:海鸟网 > IT > linux/Unix >

Linux内存点滴 用户进程内存空间


示意图:函数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。咬文嚼字,实在是太难。看二图


Linux内存点滴 用户进程内存空间

 

Swapping的大部分时间花在数据传输上,交换的数据也越多,意味时间开销也随之增加。对于进程而言,这个过程是透明的。由于RAM资源不足,PFRA会将部分匿名页框的数据写入到交换区(swap area),备份之,这个动作称为so(swap out)。等到发生内存缺页异常的时候,缺页异常处理程序会将交换区(磁盘)的页面又读回物理内存,这个动作称为si(swap in)。每次Swapping,都有可能不只是一页数据,不管是si,还是so。Swapping意味着磁盘操作,更新页表等操作,这些操作开销都不小,会阻塞用户态进程。所以,持续飚高的si/so意味着物理内存资源是性能瓶颈。


Linux内存点滴 用户进程内存空间



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()函数特殊在于它返回二次。