前面的文章提到,基于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 failed\n");
exit(1);
}if (ftruncate(fd, MAP_SIZE) < 0){
printf("ftruncate failed\n");
exit(1);
}
result = mmap(NULL, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if(result == MAP_FAILED){
printf("mapped failed\n");
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"新增的部分就来源于正在运行的测试程序。
那如何释放这段共享内存呢?情况可能会比你想象的复杂一些,详情请看下文分解。