共享内存都去哪儿了

一、现象及问题
在两台同样环境上同样部署的进程,通过top工具查看两个进程的内存使用情况,可以看到的是,两个进程占用的虚拟地址空间大小相同,但是占用物理页面进程差别极大,可以认为不是一个数量级上的。通过free工具看到系统中可用内存比较少,但是buffers和caches的数量都非常大。按照常规理解,这个也很正常,因为操作系统本身也是希望将一些信息尽可能多、尽可能长时间的存放在内存中(当然放在CPU的片上cache更好),从而减少代价更高的外部设备操作(典型的是磁盘读操作)。当然,这样的进程必然有一些特殊之处,具体来说,就是这个进程主要是作为缓存进程使用,它的内部主要将信息存放在了共享内存中,这是进程本身的特殊之处。单单这些,并不构成两者RSS使用相差悬殊的直接原因和充分理由,问题依然十分诡异。
二、proc中RSS的计算方式
和大部分linux下的进程监控程序一样,top工具也是通过读取proc文件系统中进程的信息来获得一个进程的参数,对于RSS也不例外,通常通过读取对应proc的smaps文件来获得RSS页面数量,这个原理其实也非常直观。再进一步,对于操作系统来说,它通过遍历进程所有用户态vma(irtual memory area),并判断这些vma中哪些页面使用的页表项的页面是否在内存中标志位_PAGE_PRESENT(对于大家最为熟悉的386系列来说,这个bit未就是32位整数的最低一个bit)来判断这个虚拟地址是否在内存中映射了物理页面。可以看到,这里实现的思路非常自然直观,既不花哨,更不高大上。
对于我们关注的这个问题来说,内核对于该信息的收集位于linux-2.6.21\fs\proc\task_mmu.c,其执行流程为:smaps_pgd_range===>>>smaps_pud_range===>>>smaps_pmd_range===>>>smaps_pte_range
pte = pte_offset_map_lock(vma->vm_mm, pmd, addr, &ptl);
do {
ptent = *pte;
if (!pte_present(ptent))
continue;
 
mss->resident += PAGE_SIZE;
                ……
} while (pte++, addr += PAGE_SIZE, addr != end);
这我们只关心其中对于RSS的累加,也就是mss->resident += PAGE_SIZE;,只要页面已经被映射了物理地址(!pte_present(ptent)),就向RSS累加一个页面的统计。
三、页面何时pte_present
对于使用了MMU的CPU来说,物理内存的分配并非在进程申请的时候一次性映射,通常是在第一次访问时通过缺页触发的异常处理函数中分配并完成映射。明显地,在大部分情况下,这种策略可以极大的提高系统中物理内存的使用效率。由于每个进程都有自觉不自觉的内存浪费倾向(大家点菜的时候一样),所以在真正需要的时候再分配物理页面可以避免物理内存的浪费,提高物理内存的利用率。
由于进程申请虚拟地址空间之后物理页面并没有映射,在真正访问的时候就会引发CPU异常。这个异常的流程在handle_pte_fault===>>>do_anonymous_page===>>>mk_pte(page, vma->vm_page_prot)来设置,这个vma->vm_page_prot有用户mmap的地址空间属性决定,具体值在不同的体系结构下又各不相同,但是有一个点是确定的,对于386系统来说,这里mk的pte是有PRESENT置位的,因为这个异常函数就是负责为缺页的逻辑地址空间分配物理页面并建立映射。
从这里看,proc/smaps通过遍历vma并逐个统计页面的使用情况也是准确有效的,那么为什么还是出现这种RSS使用两相差巨大的反常现象呢?
四、共享内存的引入
具体到这里的问题,进程使用的大部分资源都是在共享内存中的,在进程启动的时候会尝试打开约定的共享内存,如果打开成功则附加上去并跳过初始化,相反如果进程启动的时候创建了共享内存,那么就需要进行共享内存的初始化,由于代码的粗放型,这个初始化几乎踩遍了这片共享内存的每一个页面,而这个操作也迫使操作系统在此时此刻将这片共享内存的虚拟地址都分配了页面,从而看起来进程的RSS和虚拟地址空间是一个数量级上。
但是有一点冗余被忽略的细节要注意:这个映射是进程级别的。在进程第一次启动的时候,它迫使操作系统为每个地址分配物理页面并建立映射,这个映射是进程私有的,当进程退出之后,这个映射随之消失。在进程下次系统的时候,由于进程判断不需要初始化,所以在附加到共享内存之后并没有进行初始化,所以新进程中并没有这个完成地址映射关系,所以通过top(top再通过/proc/$pid/smaps)看到这个进程并没有使用太多的物理页面。
五、free中显示的buffers和caches
free这个工具大名鼎鼎,但是它的用户态代码非常简单,它的信息通常只是简单的从/proc/meminfo中获得,通常大家最为关注的total、free、buffers、caches也不例外。例如下面是我当前系统的一个简单统计信息
tsecer@harry: head /proc/meminfo 
MemTotal:        1012296 kB
MemFree:           78400 kB
Buffers:           19208 kB
Cached:            63376 kB
SwapCached:         4936 kB
Active:           378436 kB
Inactive:         385480 kB
Active(anon):     341196 kB
Inactive(anon):   345036 kB
Active(file):      37240 kB
tsecer@harry: free
             total       used       free     shared    buffers     cached
Mem:       1012296     934020      78276       4900      19208      63376
-/+ buffers/cache:     851436     160860
Swap:      2097148      71908    2025240
tsecer@harry: 
可以看到两者差别不大。
六、内核中的meminfo
linux-2.6.21\fs\proc\proc_misc.c:meminfo_read_proc
其中对于cached页面的计算通过下面接口获得
cached = global_page_state(NR_FILE_PAGES) -
而对于buffers的统计通过下面函数实现,两者也都并不复杂。
long nr_blockdev_pages(void)
{
struct list_head *p;
long ret = 0;
spin_lock(&bdev_lock);
list_for_each(p, &all_bdevs) {
struct block_device *bdev;
bdev = list_entry(p, struct block_device, bd_list);
ret += bdev->bd_inode->i_mapping->nrpages;
}
spin_unlock(&bdev_lock);
return ret;
}
七、共享内存的页面被统计在了哪里
在linux系统中,共享内存的底层实现依然是通过内存文件系统(用户态通常成为tmpfs),所以对于共享内存被算在了前面看到的cached中。在考虑共享内存实现的时候,使用tmpfs文件系统来实现当然更加的统一和简介,但是具体在这里把它算在cached中有不太何时,至少说很容易引起误解。通常的cached作为文件内容的缓存,是可以“落地到”文件中,而这个文件通常又都认为是持久性存储,所以落地之后可以将内存页面释放出来。这一个论断对于此处的共享内存来说显然不符。
八、如何查看共享内存使用的物理页面
在较早的版本中,内核并没有提供这样的一个接口来获得任意一个共享内存使用的物理页面大小,尽管这个实现非常简单,因为从新的内存版本中来看这个实现也就是寥寥几行代码,还是在早期版本已有的基础上修改而来。较早的版本(例如我正在看的2.6.21版本)尽管没有精确到任意一个制定共享内存的查看接口,但是有一个查看系统所有共享内存占用物理页面的接口。遗憾的是,这个接口并没有通过proc文件系统导出,用户态也没有通用的工具来使用这个接口,尽管实现起来也同样简单。
九、在早期内核中查看系统共享内存累计物理页面使用量
tsecer@harry: ipcs -m
 
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0x00000000 65536      root       600        4194304    2          dest         
0x00000000 327682     root       600        2097152    2          dest         
0x00000000 294915     root       600        16777216   2          dest         
 
tsecer@harry: cat shminfo.cpp 
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
 
int main()
{
struct shm_info shm_info;
if (-1 == shmctl(0, SHM_INFO, (shmid_ds*)&shm_info))
{
perror("shmctl failed");
return -1;
}
printf ("shmrss %ld\n", shm_info.shm_rss);
return 0;
}
tsecer@harry: g++ shminfo.cpp 
tsecer@harry: ./a.out 
shmrss 1104
tsecer@harry: cat /proc/sysvipc/shm 
       key      shmid perms                  size  cpid  lpid nattch   uid   gid  cuid  cgid      atime      dtime      ctime                   rss                  swap
         0      65536  1600               4194304  1811  2809      2     0     0     0     0 1482568594 1482568594 1481469196                765952                 20480
         0     327682  1600               2097152  1356   467      2     0     0     0     0 1482598606          0 1482598606                167936                     0
         0     294915  1600              16777216  1922   467      2     0     0     0     0 1481469290          0 1481469290               3588096                 24576
tsecer@harry: cat /proc/sysvipc/shm  | grep -v key | awk '{sum += $(NF-1)} END {print sum/4096}'
1104
也就是/proc/sysvipc/shm中统计各个shm的物理页面总和和通过shmctl(SHM_INFO)系统调用的到的共享内存累计使用页面数量相同。由于早期内核(例如2.6.21)没有在/proc/sysvipc/shm中导出每个共享内存的RSS(但是至少3.12内核中已经有该功能),所以只能通过简单的写个函数来查看系统累计物理内存使用量,这个小功能可能对于系统中共享内存总共使用物理页面较多的情况有一个大致了解。

猜你喜欢

转载自www.cnblogs.com/tsecer/p/10487743.html