前面的文章提到,基于mmap映射可以实现Linux中的共享内存(shared memory)。传统的共享内存是System V形式的,可通过"/proc/sys/kernel/shmall"参数限制其占用的最大物理内存空间,像这样:
echo 1024 > /proc/sys/kernel/shmall
这里"shmall"的数值是以page为单位的,因为page的大小通常为4KiB,所以设置1024意味着其总大小不能超过1024*4KiB=4096KiB。
重启之后,用"free -k"命令看下("k"表示以KiB为单位输出结果):
在"free"命令的输出中,"shared"就是表示共享内存的大小,这里是7564KiB,数值超过了4096,是之前的设置没有生效吗?
并不是,因为除了System V形式的共享内存,还有POSIX形式的共享内存,而"shmall"参数只对System V形式的共享内存有效,对POSIX形式的共享内存无效。要想限制POSIX形式的共享内存,得用其他的手段。
System V共享内存由内核管理,对用户不可见,而POSIX共享内存可通过"df -h"命令查看,在df命令的输出结果中,"Filesystem"为"tmpfs"或者"devtmpfs"的就是POSIX共享内存,其中"/run", "/sys/fs/cgroups"是系统目录,而"/dev/shm"是可由用户使用的。
这些POSIX共享内存是以内存文件系统的形式挂载的,如果不设置,那么挂载的默认大小为内存的一半(代码位于/mm/shmem.c)。
#ifdef CONFIG_TMPFS
static unsigned long shmem_default_max_blocks(void)
{
return totalram_pages / 2;
}
#endif
在我的机器上,内存大小是接近8个GiB,所以其默认大小是3.9GiB。如果要限制"/dev/shm"这个挂载点的共享文件系统的大小,则可以在"/etc/fstab"配置文件中添加如下的语句:
echo 'tmpfs /dev/shm tmpfs nodev,nosuid,size=128M 0 0' >> /etc/fstab
这里"size"的设置就比较简单直观了,不像"shmall"那样还需要换算一下大小。重启之后再用"df -h /dev/shm"命令看一下,"Size"表示的大小已经变成了我们之前设定的值。
创建
那这个限制真的能起作用吗?我们来做个实验。写如下一段代码:
#define MAP_SIZE (512*1024*1024)
int main(int argc, char *argv[])
{
int fd;
void* result;
fd = shm_open("shm1", O_RDWR|O_CREAT, 0644);
if(fd < 0){
printf("shm_open failedn");
exit(1);
}
if (ftruncate(fd, MAP_SIZE) < 0){
printf("ftruncate failedn");
exit(1);
}
result = mmap(NULL, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(result == MAP_FAILED){
printf("mapped failedn");
exit(1);
}
while(1);
}
shm_open()的作用主要就是在"/dev/shm"目录下打开(创建)一个文件,因为创建文件的时候没有设置大小,所以需要用ftruncate()来指定这个文件的大小。接下来是使用mmap()来将文件对应的共享内存区域映射到进程的地址空间,这里映射的范围和文件大小一致,都是512MiB。使用while()循环是为了方便查看该测试程序运行过程中,内存使用的情况。
在执行这段测试程序之前,先来看一下现在系统的一些信息,以便之后对比:
"/proc/meminfo"中的"Shmem"的值等同于"free"命令输出中的"shared"的值,其实这篇文章就介绍过,"free"命令的结果就是根据"/proc/meminfo"中的值计算出来的。"Mapped"表示映射后被进程使用的内存,"PageTables"表示所有进程的页表占用的内存之和。为了方便一起查看,我们这里就用"Shmem"来观察共享内存的的大小变化。
编译并执行上述的测试程序,虽然mmap映射的大小(512MiB)超过了"/dev/shm"的限制大小(128MiB),但进程运行过程中并没有出现什么异常。除了"/dev/shm"目录下因为open操作多了一个文件"shm1",其他几项数据的值都没有什么大的变化。
这是因为啊,mmap只是完成了一次内存的映射,并没有真正使用任何物理内存,所以不会出现超出限制大小的异常,"Mapped", "Shmem"和"PageTables"的值也都不会受到影响。
那么接下来就实际使用一下这段映射的内存,看看会发生什么,很简单,只需要在mmap()加一句memset()就可以了,像这样:
memset(result, 0, MAP_SIZE/2);
我们这里初始化了256MiB的内存,再次编译执行一下:
出现了"bus error",也就是SYSBUS错误,这里访问的内存区域(256MiB)并没有超过mmap映射文件的大小(512MiB),但是超过了我们前面对POSIX共享内存做出的限制(128MiB),说明之前使用的限制POXIS共享内存的方法是有效的。
那如何解除这种限制呢?永久性的办法当然还是和前面一样,填写"/etc/fstab"配置文件,但那毕竟需要重启才能生效,如果想立刻生效以便快速进行接下来进一步的实验,只需"remount"一下就可以了,像这样:
mount -o remount,size=2G /dev/shm
现在"/dev/shm"的允许大小就变成2GiB了。
还是刚才那个程序,再执行一下,没有出现异常了。看下现在内存使用的变化吧:
相比起之前的数据,"df"命令输出中的"Used"项的值增大了256MiB,"/proc/meminfo"中"Mapped"和"Shmem"也分别增大了256MiB,都等于测试程序中memset()使用的大小。
"PageTables"的值增加了500多KiB,单独查看一下测试程序使用的页表项(VmPTE和VmPMD),其占用内存就是500多KiB,这更加证实了"PageTables"新增的部分就来源于正在运行的测试程序。
释放
那如何释放这段共享内存呢?因为这段共享内存是作为"/dev/shm"文件系统中的一个文件存在的,试试直接用"rm"命令删掉这个文件会怎样。
rm /dev/shm/shm1
删除很顺利,并没有因为还有进程(就是我们的测试程序)在使用这段共享内存,就不可以删除这个文件。但是,再用之前的命令走一波:
"/dev/shm"下确实没有"shm1"这个文件了,"du -h"显示的文件系统大小也降下来了,可是其他几项反应内存占用情况的数值,都几乎纹丝不动啊,而且"df"和"du"的输出结果居然是不一致的。
这个时候再用"lsof /dev/shm"命令扫一下,可以看到"shm1"这个文件被标记为了"deleted"的状态,因为lsof(list open files)是列出所有打开的文件,而"shm1"这个文件并没有被正确的close,所以还是会显示在lsof的输出结果中。
看来直接粗暴地删掉代表共享内存的文件是不行的,那……更粗暴地直接杀掉进程呢?重新做一下这个实验,不删掉文件,只kill掉正在运行的测试程序,看下结果如何:
"Mapped"的值减小了256MiB,这个不难理解,因为上文说过,"Mapped"本身就是代表映射后被进程所使用的内存,现在进程都挂掉了,也就不会再使用内存了。进程消亡,进程页表自然也就被内核回收了,所以"PageTables"的值也回到了之前的状态。
可是"Used"和"Shmem"这两项的值还是巍然不动。而且这个时候用lsof检测已经看不出异常了。
单独删掉文件和单独杀掉进程都不行,那双管齐下呢?来看下结果吧:
嗯,一切终于都回到了最初的状态。那为什么测试结果会是这样呢?简单地解释就是:当创建一个文件的时候,会生成一个标识文件的inode,同时还会生成一个路径(即"link"),路径是为了方便查找inode的。当使用"rm /dev/shm/shm1"命令的时候,目标文件"shm1"并没有被真正地删除,删除的只是"/dev/shm/shm1"这个路径。
如果你对这个粗略的解释不够满意,那就请看下文详细的探究吧。
参考:
- 浅析Linux的共享内存与tmpfs文件系统
- Do tmpfs and devtmpfs share the same memory region?
- What is the meaning of the command lsof +L1
- http://man7.org/training/download/posix_shm_slides.pdf
原创文章,转载请注明出处。