在Linux系统中,我们经常使用free命令来查看系统内存的使用情况。在一个RHEL6的系统上,free命令的显示内容大致是这样一个状态:[root@tencent64~]#freetotalusedfreesharedbufferscachedMem:13225695272571772596851800176263253034704-/+buffers/cache:17774436114482516Swap:21011925082100684这里的默认显示单元是kb,所以我的服务是128G内存比较大。这个命令几乎是每一个用过Linux的人都不可避免的命令,但是越是这样的命令,似乎真正懂它的人就越少(我的意思是比例越小)。一般情况下,对这条命令输出的理解可以分为几个层次:不理解。这样的人第一反应是:我的天,我用了很多内存,70多G,却几乎不运行什么大程序?为什么会这样?linux太占内存了!我想我很了解它。这样的人一般会评价说:嗯,按照我专业的眼光,内存只用了17G左右,还有很多剩余内存可用。buffers/cache占用很多,说明系统中有一些进程读写了文件,不过没关系,这部分内存空闲的时候就用。真的明白了。这种人的反应,让人觉得他们最不懂linux了。他们的反应是:这就是免费节目,好吧,我知道。它是什么?你问我内存够不够用,我当然不知道!我他妈怎么知道你的程序是怎么写的?根据网上技术文档的内容,相信大多数对Linux有所了解的人应该在第二个层次。一般认为,当内存压力大时,buffer和cache占用的内存空间可以释放为freespace。但事实真的如此吗?在讨论这个话题之前,我们先简单介绍一下buffers和cached是什么意思:什么是buffer/cache?缓冲区和高速缓存是计算机技术中过度使用的两个术语,它在不同的上下文中具有不同的含义。在Linux内存管理中,这里的buffer是指Linux内存:Buffercache。这里的缓存指的是Linux内存:Pagecache。翻译成中文可以叫做buffercache和pagecache。历史上,其中一个(buffer)被用作写入io设备的缓存,另一个(cache)被用作io设备的读缓存。这里的io设备主要是指文件系统上的块设备文件和普通文件。但是现在,它们具有不同的含义。在现在的内核中,pagecache,顾名思义,就是对内存页的缓存。说白了,如果有页面分配和管理的内存,可以将pagecache作为它的缓存进行管理和使用。当然,并不是所有的内存都是由页来管理的,很多都是由块来管理的。如果这部分内存使用了缓存功能,则会集中到buffercache中使用。(从这个角度来说,是不是把buffercache的名字改成blockcache更好?)但是,并不是所有的block(块)都有固定的长度。系统上block的长度主要由使用的块设备决定,而page的长度在X86上无论是32位还是64位都是4k。一旦了解了这两个缓存系统之间的区别,就可以了解它们的用途。什么是页面缓存?页缓存主要用作文件系统上文件数据的缓存,尤其是当进程对文件有读/写操作时。仔细想想,作为一个可以将文件映射到内存的系统调用:mmap也应该使用pagecache吧?在目前的系统实现中,页面缓存也被用作其他文件类型的缓存设备,所以实际上页面缓存还负责缓存大部分的块设备文件。什么是缓冲区高速缓存?Buffercache主要是设计用来在系统读写块设备时将数据缓存在块上的系统使用的。这意味着对块的某些操作将使用缓冲区缓存进行缓存,例如当我们格式化文件系统时。通常,这两种缓存系统是一起使用的。例如,当我们写入一个文件时,pagecache的内容会发生变化,buffercache可以用来将page标记为不同的buffer,并记录修改了哪个buffer。这样,内核在后续执行脏数据回写时,不需要回写整个页,只需要回写修改的部分。如何回收缓存?Linux内核会在内存即将耗尽时触发内存回收工作,为急需内存的进程释放内存。一般来说,这个操作中主要的内存释放来自buffer/cache的释放。特别是当使用更多的缓存空间时。由于主要用于缓存,只是在内存充足的情况下加快进程对文件的读写速度,所以当内存压力大的时候,当然需要清空缓存分发给相关进程可用空间。所以一般来说,我们认为buffer/cache空间可以释放,这个理解是正确的。但是这个缓存清理工作并不是没有代价的。理解了缓存的作用,就可以理解为必须先清除缓存,保证缓存中的数据与对应文件中的数据一致后,才能释放缓存。因此,清除缓存的行为通常伴随着系统的高IO。因为内核需要比较缓存中的数据和对应的硬盘文件上的数据是否一致,如果不一致就需要写回,然后才能回收。除了在系统内存耗尽时清除缓存外,我们还可以使用如下文件手动触发清除缓存操作:[root@tencent64~]#cat/proc/sys/vm/drop_caches1方法是:echo1>/proc/sys/vm/drop_caches当然这个文件中可以设置的值分别是1、2、3。它们代表的意思是:echo1>/proc/sys/vm/drop_caches:表示清除pagecache。echo2>/proc/sys/vm/drop_caches:表示清除和回收slab分配器中的对象(包括目录项缓存和inode缓存)。slab分配器是内核中管理内存的一种机制,很多缓存数据的实现都使用了pagecache。echo3>/proc/sys/vm/drop_caches:表示清除pagecache和slab分配器中的缓存对象。所有缓存都可以回收吗?我们分析了缓存可以回收的情况。有没有不能回收的缓存?当然有。我们先来看第一种情况:tmpfs大家都知道,Linux提供了一个“临时”文件系统,叫做tmpfs,它可以使用一部分内存空间作为文件系统,使得内存空间可以作为目录文件使用。现在大多数Linux系统都有一个名为/dev/shm的tmpfs目录,就是这样一个存在。当然我们也可以手动创建自己的tmpfs,如下:[root@tencent64~]#mkdir/tmp/tmpfs[root@tencent64~]#mount-ttmpfs-osize=20Gnone/tmp/tmpfs/[root@tencent64~]#dfFilesystem1K-blocksUsedAvailableUse%Mountedon/dev/sda1103250003529604627091637%//dev/sda32064606495959401000136049%/usr/local/dev/mapper/vg-data103212320262442847172515627%/datatmpfs66128476147090045141947223%/dev/shmnone209715200209715200%/tmp/tmpfs于是我们新建了一个tmpfs,空间为20G,我们可以在/tmp/tmpfs中新建一个20G以内的文件。如果我们创建的文件实际占用的空间是内存,那么这些数据应该占用内存空间的哪一部分呢?根据pagecache的实现功能可以理解,既然是某种文件系统,自然要利用pagecache的空间进行管理。让我们试试?[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12636890119-/+buffers/cache:15111Swap:202[root@tencent64~]#ddif=/dev/zeroof=/tmp/tmpfs/testfilebs=1Gcount=1313+0recordsin13+0recordsout13128bytesbytes(14GB)copied,9.49858s,1.5GB/s[root@tencent64~]#[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12649760132-/+buffers:1/cache:wap5在tmpfs目录下创建了一个13G的文件,并且通过free命令前后对比,发现cached增加了13G,说明文件确实放在了内存中,内核使用了cache作为存储。看我们关心的指标:-/+buffers/cacheline。我们发现,在这种情况下,free命令仍然提示我们有110G内存可用,但是真的有这么多吗?我们可以手动触发内存回收,看看现在可以回收多少内存:[root@tencent64~]#echo3>/proc/sys/vm/drop_caches[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12643820029-/+buffers/cache:14111Swap:202可以看出cache占用的空间并没有像我们想象的那样完全释放,其中13G的空间还是被/tmp/tmpfs中的文件占用了。当然,我的系统中还有其他不可释放的缓存占用了剩余的16G内存空间。那么tmpfs占用的缓存空间什么时候释放呢?这是它的文件被删除的时候。如果不删除文件,那么无论内存耗尽多少,内核都不会自动删除tmpfs中的文件来释放它们。缓存空间。[root@tencent64~]#rm/tmp/tmpfs/testfile[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12630950016-/+buffers/cache:14111Swap:202这是我们分析的第一个缓存不能回收的情况.还有其他的情况,比如:共享内存共享内存是系统提供的一种常见的进程间通信(IPC)方式,但是这种通信方式无法在shell中申请和使用,所以我们需要一个简单的测试程序,代码如下:[root@tencent64~]#catshm.c#include#include#include#include#include#include#defineMEMSIZE2048*1024*1023intmain(){intshmid;char*ptr;pid_tpid;structshmid_dsbuf;intret;shmid=shmget(IPC_PRIVATE,MEMSIZE,0600);if(shmid<0){perror("shmget()");exit(1);}ret=shmctl(shmid,IPC_STAT,&buf);if(ret<0){perror("shmctl()");exit(1);}printf("shmid:%d\n",shmid);printf("shmsize:%d\n",buf.shm_segsz);buf.shm_segsz*=2;ret=shmctl(shmid,IPC_SET,&buf);if(ret<0){perror("shmctl()");exit(1);}ret=shmctl(shmid,IPC_SET,&buf);if(ret<0){perror("shmctl()");exit(1);}printf("shmid:%d\n",shmid);printf("shmsize:%d\n",buf.shm_segsz);pid=fork();if(pid<0){perror("fork()");exit(1);}if(pid==0){ptr=shmat(shmid,NULL,0);if(ptr==(void*)-1){perror("shmat()");exit(1);}bzero(ptr,MEMSIZE);strcpy(ptr,"你好!”);exit(0);}else{wait(NULL);ptr=shmat(shmid,NULL,0);if(ptr==(void*)-1){perror("shmat()");exit(1);}puts(ptr);exit(0);}}程序的功能很简单,就是申请一段小于2G的共享内存,然后开启一个子进程进行初始化共享内存,父进程在子进程初始化后,输出共享内存的内容,然后退出,但退出前共享内存并没有被删除。我们来看看这个程序执行前后的内存使用情况:[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12630950016-/+buffers/cache:14111Swap:202[root@tencent64~]#./shmshmid:294918shmsize:2145386496shmid:294918shmsize:-4194304Hello![root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12632930018-/+buffers/cache:14111Swap:202缓存空间从16G增加到18G。那么这个缓存是否可以回收呢?继续测试:[root@tencent64~]#echo3>/proc/sys/vm/drop_caches[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12632930018-/+buffers/cache:14111Swap:202结果还是不可回收。可以观察到即使没有人使用这块共享内存,它仍然会在缓存中保存很长时间,直到被删除。删除的方法有两种,一种是在程序中使用shmctl()去IPC_RMID,另一种是使用ipcrm命令。我们来删除试试:[root@tencent64~]#ipcs-m------SharedMemorySegments--------keyshmidownerpermsbytesnattchstatus0x00005feb0root6661200040x00005fe732769root66652428820x00005fe865538root666209715220x00038c0e131075root777207210x00038c14163844root777560339200x00038c09196613root77722124800x00000000294918root60021453864960[root@tencent64~]#ipcrm-m294918[root@tencent64~]#ipcs-m------SharedMemorySegments--------keyshmidownerpermsbytesnattchstatus0x00005feb0root6661200040x00005fe732769root66652428820x00005fe865538root666209715220x00038c0e131075root777207210x00038c14163844root777560339200x00038c09196613root7772212480[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12630950016-/+buffers/cache:14111Swap:202删除共享内存后,cache被正常释放了.此行为类似于tmpfs的逻辑。内核底层在实现共享内存(shm)、消息队列(msg)和信号量数组(sem)的POSIX:XSIIPC机制的内存存储时使用了tmpfs。这也是为什么共享内存的运行逻辑和tmpfs类似的原因。当然,一般情况下,shm占用的内存比较多,所以这里强调使用共享内存。说到共享内存,Linux还为我们提供了另一种共享内存的方法,那就是:mmapmmap()是一个非常重要的系统调用,从mmap本身的功能描述是看不出来的。从字面上看,mmap就是把一个文件映射到进程的虚拟内存地址,然后通过操作内存就可以操作文件的内容了。但实际上这个调用有着广泛的用途。malloc申请内存时,小段内存内核使用sbrk处理,大段内存使用mmap。当系统调用exec族函数执行时,由于本质上是加载一个可执行文件到内存中执行,内核自然可以使用mmap方式进行处理。我们这里只考虑一种情况,就是在使用mmap申请共享内存的时候,会不会也像shmget()一样使用缓存?同样,我们还需要一个简单的测试程序:[root@tencent64~]#catmmap.c#include#include#include#include#include#include#include#include#defineMEMSIZE1024*1024*1023*2#defineMPFILE"./mmapfile"intmain(){void*ptr;intfd;fd=open(MPFILE,O_RDWR);if(fd<0){perror("open()");exit(1);}ptr=mmap(NULL,MEMSIZE,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,fd,0);if(ptr==NULL){perror("malloc()");exit(1);}printf("%p\n",ptr);bzero(ptr,MEMSIZE);睡眠(100);munmap(ptr,MEMSIZE);关闭(fd);exit(1);}这次干脆不使用任何父子进程的方式,就一个进程,申请一块2G的mmap共享内存,然后在初始化这块空间后等待100秒,然后释放映射,这样我们需要睡在里面在100秒内检查我们的系统内存使用情况,看看它使用了多少空间?当然,在此之前必须先创建一个2G的文件./mmapfile。结果如下:[root@tencent64~]#ddif=/dev/zeroof=mmapfilebs=1Gcount=2[root@tencent64~]#echo3>/proc/sys/vm/drop_caches[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12630950016-/+buffers/cache:14111Swap:202然后执行测试程序:[root@tencent64~]#./mmap&[1]191570x7f1ae3635000[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:126329130:202[root@tencent64~]#echo3>/proc/sys/vm/drop_caches[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12632930018-/+buffers/cache:14111Swap:202我们可以看到在程序执行过程中,缓存一直是18G,比之前高了2G,此时这个缓存还是回收不了。然后我们在程序结束前等待100秒。[root@tencent64~]#[1]+Exit1./mmap[root@tencent64~]#[root@tencent64~]#free-gtotalusedfreesharedbufferscachedMem:12630950016-/+buffers/cache:14111Swap:202程序退出后,缓存被占用空间被释放。这样我们可以看到,使用mmap申请状态为MAP_SHARED的内存,内核也是使用cache进行存储的。在进程释放相关内存之前,这个缓存不能正常释放。其实mmap的MAP_SHARED方法申请的内存也是由内核中的tmpfs实现的。由此我们也可以推测,由于共享库的只读部分是通过mmap的MAP_SHARED方法在内存中进行管理的,实际上它们都占用了缓存,无法释放。最后,通过三个测试实例,我们发现Linux系统内存中的缓存在所有情况下都不能释放为空闲空间。而且也很清楚,即使可以释放缓存,对系统来说也不是没有代价的。总结要点,我们要记住以下几点:当缓存释放为文件缓存时,会造成高IO,这是缓存加速文件访问的代价。tmpfs中存放的文件会占用缓存空间,除非删除文件,否则缓存不会自动释放。使用shmget请求的共享内存会占用缓存空间,除非共享内存是ipcrm或者shmctl使用IPC_RMID,否则相关的缓存空间不会自动释放。mmap方法申请的带有MAP_SHARED标志的内存会占用缓存空间。除非进程munmaps这块内存,相关的缓存空间不会自动释放。其实shmget和mmap的共享内存是在内核层通过tmpfs实现的,tmpfs实现的存储都是cache。当你明白了这些,希望大家对free命令的理解能够达到我们说的第三个层次。我们应该明白,内存的使用并不是一个简单的概念,缓存并不能真正作为空闲空间来使用。如果我们真的想深入了解你系统上的内存使用是否合理,我们需要了解很多更详细的知识,对相关业务的执行做出更细致的判断。我们目前的实验场景是Centos6的环境,不同版本Linux的免费现实可能不一样。您可以自己找出不同的原因。当然,本文所描述的并不是所有无法释放缓存的情况。那么,在你的应用场景中,还有哪些缓存不能释放的场景呢?