Bootstrap

Linux内存的缺页与置换

一)缺页

当CPU请求一个不在RAM中的内存页时,会发生缺页,比如我们从内存读取/写入数据,而数据未在内存,此时都会发生缺页.

我们通过下面的程序对内存缺页情况进行测试,程序通过分配大块内存以供程序使用,该程序只访问一次内存就不再使用它,
它的做法是通过malloc分配内存,并在每页修改1个字节,然后进入睡眠状态.
注:Linux非常灵敏,它不提供任何物理存储给未被修改过的页,所以我们必须在一个已分配区域的每页中读出或写入至少1个字节,来消耗内存中的页.

测试程序hog.c如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int
main (int argc, char *argv[])
{
        if (argc != 2)
                exit (0);

        size_t mb = strtoul(argv[1],NULL,0);

        size_t nbytes = mb * 0x100000;
        char *ptr = (char *) malloc(nbytes);
        if (ptr == NULL){
                perror("malloc");
                exit (EXIT_FAILURE);
        }

        size_t i;
        const size_t stride = sysconf(_SC_PAGE_SIZE);
        for (i = 0;i < nbytes; i+= stride) {
                ptr[i] = 0;
        }

        printf("allocated %d mb\n", mb);
        pause();
        return 0;
}

编译
gcc  hog.c -o hog

查看当前的内存
free -m
             total       used       free     shared    buffers     cached
Mem:           503        206        296          0         30        140
-/+ buffers/cache:         36        467
Swap:         1027          0       1027

我们通过使用GNU time命令来查看缺页的次数
\time ./hog 100
allocated 100 mb
Command terminated by signal 2
0.00user 3.12system 0:04.52elapsed 69%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+25719minor)pagefaults 0swaps

注:
25719minor表示缺页25719次,每次4KB,正好是100MB的内存分配.

major表示主缺页,主缺页是要求输入/输出到磁盘的缺页.
minor表示次缺页,次缺页是任何其它的缺页.



二)置换

置换是指程序请求内存,而物理内存不足时,内核将被迫把缺页存储到置换分区,也就是说,它将最近最少使用的页面(LRU)置换到SWAP.
如果SWAP不足,或没有SWAP,就会发生内存分配失败.如下:
swapoff -a
./hog 500 &
[1] 2901
malloc: Cannot allocate memory

[1]+  Exit 1                  ./hog 500

下面看看置换的发生,先查看当前的内存.
swapon -a
free -m  
             total       used       free     shared    buffers     cached
Mem:           503         43        459          0          0          9
-/+ buffers/cache:         33        469
Swap:         1027          0       1027

现在有469MB的空闲空间,此时我们通过hog程序,请求500MB的内存空间,将会发生置换,如下
\time ./hog 500 
allocated 500 mb(程序pause在这里,要用CTRL+C中断才能看到下面的信息,在这之间我们可以在另一个终端查看内存空间)
Command terminated by signal 2
0.02user 3.53system 0:07.19elapsed 49%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+128142minor)pagefaults 0swaps

在另一个终端用free查看内存如下:
free -m
             total       used       free     shared    buffers     cached
Mem:           503        497          6          0          0          3
-/+ buffers/cache:        493          9
Swap:         1027         53        974
这里我们看到产生了53MB的内存置换.

回到第一个终端,我们看到缺页的信息是:0major+128142minor
这0个主缺页是指当进程请求一个驻留在磁盘上的页时,才发生主缺页,在这种情况下,页面不存在,因此它们没有驻留在磁盘上,所以不被计入主缺页数.
虽然hog进程引起系统向磁盘写入页,但实际上它没有把那些页写入磁盘,实际写入是由kswapd完成的.kswapd内核线程负责把数据从内存移到磁盘的烦琐工作,
只有当拥有那些页的进程再次调用它们时才会产生一个主缺页.


三)top命令

top命令是周期性的更新.
下面介绍几个top命令的使用技巧.

1)切换显示命令名称和完整命令

输入c

2)根据驻留内存大小进行排序

输入M

3)根据CPU使用百分比大小进行排序

输入P


4)根据时间/累计时间进行排序

输入T


5)显示线程

输入H


6)将top的信息划分为4个屏幕,分别DEF,Job,Mem,Usr

输入Shift+A

1:Def - 12:50:16 up  1:14,  2 users,  load average: 0.00, 0.01, 0.00
Tasks:  72 total,   2 running,  70 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.0%us,  5.6%sy,  0.0%ni, 94.4%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:    515600k total,    72684k used,   442916k free,     3372k buffers
Swap:  1052248k total,    23140k used,  1029108k free,    48128k cached

1  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                               
   138 root      13  -5     0    0    0 S  0.0  0.0   0:04.23 [kswapd0]                                                             
  2569 root      15   0  8216  664  436 R  5.6  0.1   0:02.69 sshd: root@pts/0,pts/1                                                
  2463 root      18   0  1924  160  140 S  0.0  0.0   0:02.32 hald-addon-storage: polling /dev/hdc                                  
  2405 root      15   0 25188 6180 1792 S  0.0  1.2   0:02.23 /usr/bin/python /usr/sbin/yum-updatesd                                
  2439 haldaemo  18   0  5348  640  392 S  0.0  0.1   0:01.81 hald                                                                  
     1 root      15   0  2032  160  140 S  0.0  0.0   0:01.18 init [3]                                                              
  3234 root      15   0  2164 1000  796 R  0.0  0.2   0:00.95 top                                                                   
  2422 avahi     15   0  3696  352  260 S  0.0  0.1   0:00.94 avahi-daemon: running [test1.local]                                   
   323 root      10  -5     0    0    0 S  0.0  0.0   0:00.75 [kjournald]                                                           
2  PID  PPID    TIME+  %CPU %MEM  PR  NI S  VIRT SWAP  RES  UID COMMAND                                                             
  3234  2721   0:00.95  0.0  0.2  15   0 R  2164 1164 1000    0 top                                                                 
  2721  2569   0:00.29  0.0  0.2  15   0 S  4620 3812  808    0 bash                                                                
  2571  2569   0:00.45  0.0  0.1  16   0 S  4616 4020  596    0 bash                                                                
  2569  2271   0:02.69  5.6  0.1  15   0 R  8216 7552  664    0 sshd                                                                
  2539     1   0:00.01  0.0  0.0  24   0 S  1624 1524  100    0 mingetty                                                            
  2516     1   0:00.01  0.0  0.0  24   0 S  1624 1524  100    0 mingetty                                                            
  2515     1   0:00.01  0.0  0.0  24   0 S  1628 1528  100    0 mingetty                                                            
  2514     1   0:00.00  0.0  0.0  18   0 S  1624 1524  100    0 mingetty                                                            
  2511     1   0:00.00  0.0  0.0  18   0 S  1624 1524  100    0 mingetty                                                            
3  PID %MEM  VIRT SWAP  RES CODE DATA  SHR nFLT nDRT S  PR  NI %CPU COMMAND                                                         
  2405  1.2 25188  18m 6180    4 8688 1792  264    0 S  15   0  0.0 yum-updatesd                                                    
  3234  0.2  2164 1164 1000   52  344  796    0    0 R  15   0  0.0 top                                                             
  1866  0.2  9612 8672  940    4 1664  512   72    0 S  12  -3  0.0 python                                                          
  2721  0.2  4620 3812  808  684  400  620   77    0 S  15   0  0.0 bash                                                            
  2235  0.1 12716  11m  672    4 3656  376   75    0 S  15   0  0.0 python                                                          
  2569  0.1  8216 7552  664  368  736  436   90    0 R  15   0  5.6 sshd                                                            
  2439  0.1  5348 4708  640  260 1908  392   66    0 S  18   0  0.0 hald                                                            
  2571  0.1  4616 4020  596  684  396  452   84    0 S  16   0  0.0 bash                                                            
  1549  0.1  2280 1768  512  432  420  392   31    0 S  15   0  0.0 dhclient                                                        
4  PID  PPID  UID USER     RUSER    TTY         TIME+  %CPU %MEM S COMMAND                                                          
  2371     1   43 xfs      xfs      ?          0:00.07  0.0  0.0 S xfs                                                              
  1941     1   32 rpc      rpc      ?          0:00.01  0.0  0.0 S portmap                                                          
  2405     1    0 root     root     ?          0:02.23  0.0  1.2 S yum-updatesd                                                     
  3234  2721    0 root     root     pts/1      0:00.95  0.0  0.2 R top                                                              
  1866  1864    0 root     root     ?          0:00.30  0.0  0.2 S python                                                           
  2721  2569    0 root     root     pts/1      0:00.29  0.0  0.2 S bash                                                             
  2235     1    0 root     root     ?          0:00.24  0.0  0.1 S python                                                           
  2569  2271    0 root     root     ?          0:02.69  5.6  0.1 R sshd                                                             
  2571  2569    0 root     root     pts/0      0:00.45  0.0  0.1 S bash  

可以自定义选择要定义的屏幕的显示字段,比如我们要Mem屏幕增加Time字段的显示,要进行如下操作.

首先输入w切换窗口,使当前的屏幕为Mem,再输入f,将看到如下的信息.

Current Fields:  ANOPQRSTUVbcdefgjlmyzWHIKX  for window 3:Mem
Toggle fields via field letter, type any other key to return 

* A: PID        = Process Id
* N: %MEM       = Memory usage (RES)
* O: VIRT       = Virtual Image (kb)
* P: SWAP       = Swapped size (kb)
* Q: RES        = Resident size (kb)
* R: CODE       = Code size (kb)
* S: DATA       = Data+Stack size (kb)
* T: SHR        = Shared Mem size (kb)
* U: nFLT       = Page Fault count
* V: nDRT       = Dirty Pages count
  b: PPID       = Parent Process Pid
  c: RUSER      = Real user name
  d: UID        = User Id
  e: USER       = User Name
  f: GROUP      = Group Name
  g: TTY        = Controlling Tty
  j: P          = Last used cpu (SMP)
  l: TIME       = CPU Time
  m: TIME+      = CPU Time, hundredths
  y: WCHAN      = Sleeping in Function
  z: Flags      = Task Flags <sched.h>
* W: S          = Process Status
* H: PR         = Priority
* I: NI         = Nice value
* K: %CPU       = CPU usage
* X: COMMAND    = Command name/line


输入L,表示选择  l: TIME       = CPU Time,选择后将在被选项前出现*号,如:* L: TIME       = CPU Time,再输入Return,就可以在Mem屏看到time的信息了,如下:
3  PID %MEM  VIRT SWAP  RES CODE DATA  SHR nFLT nDRT   TIME S  PR  NI %CPU COMMAND                                                  
  2405  1.2 25188  18m 6180    4 8688 1792  264    0   0:02 S  15   0  0.0 yum-updatesd                                             
  3234  0.2  2164 1164 1000   52  344  796    0    0   0:01 R  15   0  0.7 top                                                      
  1866  0.2  9612 8672  940    4 1664  512   72    0   0:00 S  12  -3  0.0 python                                                   
  2721  0.2  4620 3812  808  684  400  620   77    0   0:00 S  15   0  0.0 bash                                                     
  2235  0.1 12716  11m  672    4 3656  376   75    0   0:00 S  15   0  0.0 python                                                   
  2569  0.1  8216 7552  664  368  736  436   90    0   0:02 S  15   0  0.0 sshd                                                     
  2439  0.1  5348 4708  640  260 1908  392   66    0   0:01 S  18   0  0.0 hald                                                     
  2571  0.1  4616 4020  596  684  396  452   84    0   0:00 S  16   0  0.0 bash                                                     
  1549  0.1  2280 1768  512  432  420  392   31    0   0:00 S  19   0  0.0 dhclient 
  
同样,我们也可以以指定的列进行排序,例如想将Mem屏以VIRT(虚拟内存)进行排序,进行下面的操作:
输入O(大写),表示排序.
看到只有N: %MEM       = Memory usage (RES)行有*号,说明现在选中的是内存的百分比.
我们输入O(大写),表示选中  o: VIRT       = Virtual Image (kb)如下:

Current Sort Field:  N  for window 3:Mem
Select sort field via field letter, type any other key to return 

  a: PID        = Process Id
  b: PPID       = Parent Process Pid
  c: RUSER      = Real user name
  d: UID        = User Id
  e: USER       = User Name
  f: GROUP      = Group Name
  g: TTY        = Controlling Tty
  h: PR         = Priority
  i: NI         = Nice value
  j: P          = Last used cpu (SMP)
  k: %CPU       = CPU usage
  l: TIME       = CPU Time
  m: TIME+      = CPU Time, hundredths
  N: %MEM       = Memory usage (RES)
* O: VIRT       = Virtual Image (kb)
  p: SWAP       = Swapped size (kb)
  q: RES        = Resident size (kb)
  r: CODE       = Code size (kb)
  s: DATA       = Data+Stack size (kb)
  t: SHR        = Shared Mem size (kb)
  u: nFLT       = Page Fault count
  v: nDRT       = Dirty Pages count
  w: S          = Process Status
  x: COMMAND    = Command name/line
  y: WCHAN      = Sleeping in Function
  z: Flags      = Task Flags <sched.h>
  
输入回车返回主屏后,就是以VIRT进行排序了,如下:
3  PID %MEM  VIRT SWAP  RES CODE DATA  SHR nFLT nDRT   TIME S  PR  NI %CPU COMMAND                                                  
  2405  1.2 25188  18m 6180    4 8688 1792  264    0   0:02 S  15   0  0.0 yum-updatesd                                             
  2235  0.1 12716  11m  672    4 3656  376   75    0   0:00 S  15   0  0.0 python                                                   
  2141  0.0 12692  12m  244   84  10m  188    1    0   0:00 S  25   0  0.0 pcscd                                                    
  1864  0.1 12048  11m  300   92  10m  220   14    0   0:00 S  16  -3  0.0 auditd                                                   
  2252  0.0  9644 9484  160  364  644  156    5    0   0:00 S  18   0  0.0 cupsd                                                    
  1866  0.2  9612 8672  940    4 1664  512   72    0   0:00 S  12  -3  0.0 python                                                   
  2190  0.1  9332 8868  464  196 7168  360   37    0   0:00 S  25   0  0.0 automount                                                
  2569  0.1  8216 7552  664  368  736  436   90    0   0:02 S  15   0  0.0 sshd                                                     
  2439  0.1  5348 4708  640  260 1908  392   66    0   0:01 S  18   0  0.0 hald   
  

四)内存缺页与置换的综合实例

为完成最后一个测试,我们把上个程序做了更改,加入了信号处理及时间输出,如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>

void handler(int sig)
{
}

#define TIMESPEC2FLOAT(tv) ((double) (tv).tv_sec+(double) (tv).tv_nsec*1e-9)

int
main (int argc,char *argv[])
{
        if (argc != 2)
                exit(0);

        signal(SIGUSR1, handler);

        size_t mb = strtoul(argv[1], NULL, 0);
        size_t nbytes = mb * 0x100000;
        char *ptr = (char *) malloc (nbytes);

        if (ptr == NULL){
                perror("malloc");
                exit(EXIT_FAILURE);
        }

        int val = 0;
        const size_t stride = sysconf(_SC_PAGE_SIZE);

        while(1){
                int i;
                struct timespec t1, t2;

                clock_gettime(CLOCK_REALTIME, &t1);

                for (i = 0;i<nbytes; i += stride){
                        ptr[i] = val;
                }
                val++;

                clock_gettime(CLOCK_REALTIME, &t2);
                printf("touched %d mb; in %.6f sec\n", mb,
                        TIMESPEC2FLOAT(t2) - TIMESPEC2FLOAT(t1));

                pause();
        }
return 0;
}

编译源程序,并做这个程序的两个软链接,如下:
gcc -O2 -o son-of-hog son-of-hog.c -lrt
ln -s son-of-hog hog-a
ln -s son-of-hog hog-b

查看当前的内存:
free -m
             total       used       free     shared    buffers     cached
Mem:           503        185        317          0         10        140
-/+ buffers/cache:         35        468
Swap:         1027          0       1027

注:此时我们看到有468MB的空闲内存,这里面有140MB的cached和10MB的buffers

清空置换空间(swap):
swapoff -a
swapon -a
free -m
             total       used       free     shared    buffers     cached
Mem:           503         50        452          0          1         16
-/+ buffers/cache:         33        470
Swap:         1027          0       1027

我们执行hog-a,以此占用300MB的内存空间,查看它分配内存的时间:
./hog-a 300 &
[1] 2592
touched 300 mb; in 2.784745 sec

注:
这里分配300MB的内存共用了2.7秒多.这里面有换页和回收cache的时间.


对这个进程使用一个SIGUSR1将再次唤醒并访问内存:
kill -USR1 %1
touched 300 mb; in 0.010827 sec

注:
这里只用了10ms.

我们这时执行hog-b,看看它会占用多少时间:
./hog-b 300 & 
[2] 2601
touched 300 mb; in 4.432349 sec

注:
因为物理内存都被分配了,为给hog-b分配空间,系统做了置换处理,即将物理内存分配给hog-b,而将hog-a占用的内存置换到swap.

对hog-b发送SIGUSR1信号,再次唤醒程序,这时只用143ms
pkill -USR1 hog-b
touched 300 mb; in 0.143772 sec

我们用top命令来查看:

top -p $(pgrep hog-a) -p $(pgrep hog-b)

1:Def - 06:12:26 up 10 min,  1 user,  load average: 0.01, 0.10, 0.10
Tasks:   2 total,   0 running,   2 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.0%us,  0.0%sy,  0.0%ni, 99.7%id,  0.3%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:    515600k total,   509468k used,     6132k free,      956k buffers
Swap:  1052248k total,   162924k used,   889324k free,    11312k cached

1  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                               
  2601 root      18   0  301m 300m  428 S  0.0 59.7   0:03.32 ./hog-b 300                                                           
  2592 root      18   0  301m 154m  428 S  0.0 30.7   0:02.76 ./hog-a 300                                                           
2  PID  PPID    TIME+  %CPU %MEM  PR  NI S  VIRT SWAP  RES  UID COMMAND                                                             
  2601  2553   0:03.32  0.0 59.7  18   0 S  301m 1112 300m    0 hog-b                                                               
  2592  2553   0:02.76  0.0 30.7  18   0 S  301m 147m 154m    0 hog-a                                                               
3  PID %MEM  VIRT SWAP  RES CODE DATA  SHR nFLT nDRT S  PR  NI %CPU COMMAND                                                         
  2601 59.7  301m 1112 300m    4 300m  428  235    0 S  18   0  0.0 hog-b                                                           
  2592 30.7  301m 147m 154m    4 300m  428    0    0 S  18   0  0.0 hog-a                                                           
4  PID  PPID  UID USER     RUSER    TTY         TIME+  %CPU %MEM S COMMAND                                                          
  2601  2553    0 root     root     pts/0      0:03.32  0.0 59.7 S hog-b                                                            
  2592  2553    0 root     root     pts/0      0:02.76  0.0 30.7 S hog-a 
  
这里做几个说明:
1)两个程序都用了300MB的虚拟内存.(VIRT 301m)
2)由于hog-a先运行,而后占用的内存数据被置换到swap,所以只占用了154MB的物理内存(RES 154MB),占用了147MB的SWAP空间(SWAP 147).
3)由于hog-b后运行,所以用抢占了hog-a的物理内存,此时它占用了300M的物理内存(RES 300MB),由于置换它产生了235次页错误(Page Fault count).
;