--摘自穷佐罗的Linux书node
使用文件或者管道进行进程间通讯会有不少局限性。管道只能在父进程和子进程间使用;经过文件共享,在处理效率上又差一些,并且访问文件描述符不如访问内存地址方便。linux
Linux系统在编程上提供的共享内存方案有三种:编程
mmap原本是存储映射功能。它能够将一个文件映射到内存中,在程序里就能够直接使用内存地址对文件内容进行访问。数组
#include <sys/mman.h> void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
Linux经过系统调用fork派生出的子进程和父进程共用内存地址空间,Linux的mmap实现了一种能够在父子进程之间共享内存地址的方式。缓存
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <sys/file.h> #include <sys/wait.h> #include <sys/mman.h> #define COUNT 100 int do_child(int *count) { int interval; // critical section interval = *count; interval++; usleep(1); *count = interval; // critical section exit(0); } int main() { pid_t pid; int count; int *shm_p; shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0); if(MAP_FAILED == shm_p) { perror("mmap()"); exit(1); } *shm_p = 0; for(count = 0; count < COUNT; count++) { pid = fork(); if(pid < 0) { perror("fork()"); exit(1); } if(pid == 0) { do_child(shm_p); } } for(count = 0; count < COUNT; count++) { wait(NULL); } printf("shm_p: %d\n", *shm_p); munmap(shm_p, sizeof(int)); exit(0); }
这段共享内存的使用是有竞争条件的。进程间通讯不只仅是通讯这么简单,还要处理相似的这样的临界区代码。在这里,能够采用文件锁进行处理。可是共享内存使用文件锁显得不太协调。除了不方便和效率低下之外,文件锁还不能进行更高级的进程控制。这里可使用信号量这种更高级的进程同步控制原语来实现相关功能。bash
下面这段程序用来帮助理解mmap的内存占用状况。ide
#include<unistd.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<fcntl.h> #include<string.h> #include<sys/file.h> #include<sys/wait.h> #include<sys/mman.h> #define COUNT 100 #define MEMSIZE 1024*1024*1023*2 int main() { pid_t pid; int count; void *shm_p; shm_p = mmap(NULL, MEMSIZE, PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0); if(MAP_FAILED == shm_p) { perror("mmap()"); exit(1); } bzero(shm_p, MEMSIZE); sleep(3000); munmap(shm_p, MEMSIZE); exit(0); }
申请了一段近2G的内存,并置0.观察内存变化函数
[zorro@zorrozou-pc0 sharemem]$ free -g total used free shared buff/cache available Mem: 15 2 2 0 10 11 Swap: 31 0 31 [zorro@zorrozou-pc0 sharemem]$ ./mmap_mem & [1] 32036 [zorro@zorrozou-pc0 sharemem]$ free -g total used free shared buff/cache available Mem: 15 2 0 2 12 9 Swap: 31 0 31
能够看出,这段内存被记录到shared和buff/cache中了。命令行
mmap有一个缺点,那就是共享的内存只能在父进程和fork产生的子进程间使用,除此以外的其它进程没法获得共享内存段的地址。code
XSI是X/Open组织对UNIX定义的一套接口标准(X/Open System Interface)。XSI共享内存在Linux底层的实现实际上跟mmap没有什么本质不一样,只是在使用方法上有所区别。
#include<sys/ipc.h> #include<sys/shm.h> int shmget(key_t key, size_t size, int shmflg); int shmctl(int shmid, int cmd, struct shmid_ds *buf); #include<sys/types.h> #include<sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); int shmdt(const void *shmaddr);
shmget的第三个参数,指定建立标志。支持的标志为:IPC_CREAT、IPC_EXCL。从Linux 2.6以后,还引入了支持大页的共享内存,标志为:SHM_HUGETLB、SHM_HUGE_2MB等。shemget除了能够建立一个新的共享内存外,还能够访问一个已经存在的内存,此时能够将shmflg置为0,不加任何标志打开。
shmget返回的int类型的shmid相似于文件描述符,注意只是相似,而并不是一样的实现,因此,不能用select、poll、epoll这样的方法去控制一个XSI共享内存。对于一个XSI共享内存,其key是系统全局惟一的,这就方便其它进程使用一样的key,打开一样一段共享内存,以便进行进程间通讯。而是用fork产生的子进程,能够直接经过shmid访问到相关共享内存段。这就是key的本质:系统中对XSI共享内存的全局惟一表示符。
#include<sys/types.h> #include<sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
key是经过ftok函数,使用一个约定好的文件名和proj_id生成的。ftok不会建立文件,因此必须指定一个存在而且进程能够访问的pathname路径。另外,ftok并非根据文件的路径和文件名生成key的,在具体实现上,它使用的是指定文件的inode编号和文件所在设备的设备编号。因此,不一样的文件名也可能获得同一个key(不一样的文件名指向同一个inode,硬连接)。一样的文件名也不必定就能获得相同的key,一个文件名有可能被删除重建,这种行为会致使inode变化。
#include<unistd.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<fcntl.h> #include<string.h> #include<sys/file.h> #include<sys/wait.h> #include<sys/mman.h> #include<sys/ipc.h> #include<sys/shm.h> #include<sys/types.h> #define COUNT 100 #define PATHNAME "/etc/passwd" int do_child(int proj_id) { int interval; int *shm_p, shm_id; key_t shm_key; if((shm_key = ftok(PATHNAME, proj_id)) == -1) { perror("ftok()"); exit(1); } shm_id = shmget(shm_key, sizeof(int), 0); if(shm_id < 0) { perror("shmget()"); exit(1); } //使用shmat将相关共享内存映射到本进程的内存地址 shm_p = (int *)shmat(shm_id, NULL, 0); if((void *)shm_p == (void *)-1) { perror("shmat()"); exit(1); } // critical section interval = *shm_p; interval++; usleep(1); *shm_p = interval; // critical section //使用shmdt解除本进程内存对共享内存的地址映射,本操做不会删除共享内存 if(shmdt(shm_p) < 0){ perror("shmdt()"); exit(1); } exit(0); } int main() { pid_t pid; int count; int *shm_p; int shm_id, proj_id; key_t shm_key; proj_id = 1234; if((shm_key = ftok(PATHNAME, proj_id)) == -1) { perror("ftok()"); exit(1); } //使用shm_key建立一个共享内存,若是系统中已经存在此共享内存,则报错退出。建立出来的共享内存权限为0600 shm_id = shmget(shm_key, sizeof(int), IPC_CREAT|IPC_EXCL|0600); if(shm_id < 0) { perror("shmget()"); exit(1); } shm_p = (int *)shmat(shm_id, NULL, 0); if((void *)shm_p == (void *) -1) { perror("shmat()"); exit(1); } *shm_p = 0; for(count = 0; count < COUNT; count++) { pid = fork(); if(pid < 0) { perror("fork()"); exit(1); } if(pid == 0) { do_child(proj_id); } } for(count = 0; count < COUNT; count ++) { wait(NULL); } printf("shm_p: %d\n", *shm_p); if(shmdt(shm_p) < 0) { perror("shmdt()"); exit(1); } if(shmctl(shm_id, IPC_RMID, NULL) < 0) { perror("shmctl"); exit(1); } exit(0); }
在某些状况下,也能够不经过一个key来建立共享内存。此时能够在key的参数所在位置填IPC_PRIVATE,这样内核会在保证不冲突的共享内存段id的状况下新建一段共享内存。由于只能是建立,因此flag位必定是IPC_CREAT。能够将shmid传给子进程。
当获取到shmid以后,就可使用shmat来进行地址映射。shmat以后,经过访问返回的当前进程的虚拟地址就能够访问到共享内存段了。注意使用以后要调用shmdt解除映射,不然对于长期运行的程序,可能会形成虚拟内存地址泄露。shmdt并不能删除共享内存段,只是解除共享内存段和进程虚拟地址的映射关系。只要shmid对应的共享内存段还存在,就可使用shmat继续映射使用。想要删除一个共享内存段,须要使用shmctl的IPC_RMID指令处理,或者在命令行中使用ipcrm删除指定的共享内存id或key。
shmctl还能够查看、修改共享内存的相关属性,能够在man 2 shmctl中查看。在系统中还可使用ipcs -m 命令查看系统中全部共享内存的信息。
ipcs - provide information on ipc facilities ipcs [-asmq] [-tclup] ipcs [-smq] -i id -m 共享内存 -q 消息队列 -s 信号量数组 -a all(缺省) 输出选项: -t time -p pid -c creator -l limits -u summary
在Linux系统中,使用XSI共享内存调用shmget时,能够经过设置shmflg参数来申请大页内存(huge pages)。
SHM_HUGETLB(since Linux 2.6) SHM_HUGE_2MB, SHM_HUGE_1GB(since Linux 3.8)
使用大页内存的好处是提升内核对内存管理的处理效率。由于在相同内存大小的状况下,使用大页内存(2M一页)将比使用通常内存页(4K一页)的内存页管理的数量大大减小,从而减小内存页表项的缓存压力和CPU cache缓存内存地址的映射压力。可是须要注意一些地方:
shm_id = shmget(IPC_PRIVATE, MEMSIZE, SHM_HUGETLB|0600)
若是要申请2G如下的大页内存,须要系统预留2G以上的大页内存。
echo 2048 > /proc/sys/vm/nr_hugepages cat /proc/meminfo | grep -i huge AnonHugePages: 841728 KB HugePages_Total: 2020 HugePages_Free: 2020 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB
2048是页数,每页2M。
还须要注意共享内存的限制:
echo 2147483648 > /proc/sys/kernel/shmmax echo 33554432 > /proc/sys/kernel/shmall
/proc/sys/kernel/shmall:限制系统用在共享内存上的内存页总数。一页通常是4k(能够经过getconf PAGE_SIZE查看)
/proc/sys/kernel/shmmax:限制一个共享内存段的最大长度,单位是字节
/proc/sys/kernel/shmmni:限制整个系统能够建立的最大的共享内存段的个数
POSIX共享内存实际上毫无新意,它本质上是mmap对文件的共享方式映射,只不过映射的是tmpfs文件系统上的文件。
tmpfs是将一部份内存空间用做文件系统,通常挂在/dev/shm目录。
Linux提供的POSIX共享内存,实际上就是在/dev/shm下建立一个文件,并将其mmap以后映射其内存地址便可。能够经过man shm_overview查看使用方法。
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <sys/file.h> #include <sys/wait.h> #include <sys/mman.h> #define COUNT 100 #define SHMPATH "shm" int do_child(char * shmpath) { int interval, shmfd, ret; int *shm_p; // 使用shm_open访问一个已经建立的POSIX共享内存 shmfd = shm_open(shmpath, O_RDWR, 0600); if (shmfd < 0) { perror("shm_open()"); exit(1); } // 用mmap将对应的tmpfs文件映射到本进程内存 */ shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, shmfd, 0); if (MAP_FAILED == shm_p) { perror("mmap()"); exit(1); } /* critical section */ interval = *shm_p; interval++; usleep(1); *shm_p = interval; /* critical section */ munmap(shm_p, sizeof(int)); close(shmfd); exit(0); } int main() { pid_t pid; int count, shmfd, ret; int *shm_p; /* 建立一个POSIX共享内存 */ shmfd = shm_open(SHMPATH, O_RDWR|O_CREAT|O_TRUNC, 0600); if (shmfd < 0) { perror("shm_open()"); exit(1); } /* 使用ftruncate设置共享内存段大小 */ ret = ftruncate(shmfd, sizeof(int)); if (ret < 0) { perror("ftruncate()"); exit(1); } /* 使用mmap将对应的tmpfs文件映射到本进程内存 */ shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, shmfd, 0); if (MAP_FAILED == shm_p) { perror("mmap()"); exit(1); } *shm_p = 0; for (count=0;count<COUNT;count++) { pid = fork(); if (pid < 0) { perror("fork()"); exit(1); } if (pid == 0) { do_child(SHMPATH); } } for (count=0;count<COUNT;count++) { wait(NULL); } printf("shm_p: %d\n", *shm_p); munmap(shm_p, sizeof(int)); close(shmfd); shm_unlink(SHMPATH); exit(0); }
编译该段代码的时候须要指定一个库,-lrt,这是linux的real time库。
一个进程能够在它的虚拟地址空间分配给一个共享内存端的最大大小(单位是字节)
echo 2147483648 > /proc/sys/kernel/shmmax 或 sysctl -w kernel.shmmax=2147483648 或 echo "kenerl.shmmax=2147483648" >> /etc/sysctl.conf
系统范围内共享内存段的数量
echo 4096 > /proc/sys/kernel/shmmni 或 sysctl -w kernel.shmmni=4096 或 echo "kernel.shmmni=4096" >> /etc/sysctl.conf
这个参数设置了系统范围内共享内存可使用的页数。单位是PAGE_SIZE(一般是4096,能够经过getconf PAGE_SIZE
得到)。
echo 2097152 > /proc/sys/kernel/shmall 或 sysctl -w kernel.shmall=2097152 或 echo "kernel.shmall=2097152" >> /etc/sysctl.conf
执行ipcs -m
查看系统全部的共享内存。若是status
字段是dest
,代表这段共享内存须要被删除。
ipcs -m -i $shmid