享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件 (特殊情况下还可以采用匿名映射)机制实现,也可以通过systemV共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

mmap内存文件映射吃

一、传统文件访问

unix访问文件的传统方法使用open打开他们,如果有多个进程访问一个文件,则每一个进程在再记得地址空间都包含有该文件的副本,这不必要地浪费了存储空间。下面说明了两个进程同时读一个文件的同一页的情形,系统要将该页从磁盘读到高速缓冲区中,每个进程再执行一个内存期内的复制操作将数据从高速缓冲区读到自己的地址空间。

linux共享内存介绍

二、共享内存映射

现在考虑林一种处理方法:进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成一个缺页终端,内核此时读入这一页到内存并更新页表使之指向它,以后,当进程B访问同一页面而出现缺页中断时,该页已经在内存,内核只需要将进程B的页表登记项指向次页即可。

linux共享内存介绍

三、mmap及其相关系统调用

mmap()系统调用使得进城之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write

等操作。

mmap()系统调用形式如下:

#include<sys/mman.h>

void mmap(void *addr, size_t len, int prot,int flags, int fildes, off_t off)
int msync(void *addr, size_t len, int flags);
int munmap(void *addr, size_t len);

mmap的作用是映射文件描述符和指定文件的(off_t off)区域至调用进程的(addr,addr *len)的内存区域,如下图所示:

linux共享内存介绍

参数:

  fd:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间进行通信)。

  len:是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。

  prot:指定空想内存的访问权限。可取如下几个值的或:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(不可访问)。

  flag:由以下几个常值指定:MAP_SHARED、MAP_PRIVATE、MAP_FIXED,其中,MAP_SHARED,MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。

  offset:一般设为0,表示从文件头开始映射。

  addr:指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

四、mmap基础用例

1 //测试文件 data.txt 后面的程序也要用到
2 aaaaaaaaa
3 bbbbbbbbb
4 cccccccccccc
5 ddddddddd

1、通过共享内存映射的方式修改文件

linux共享内存介绍
 1 #include <sys/mman.h>
 2 #include <sys/stat.h>
 3 #include<fcntl.h>
 4 #include<stdio.h>
 5 #include<stdlib.h>
 6 #include<unistd.h>
 7 #include<error.h>
 8 
 9 int main(int argc, char * argv[])
10 {
11      int fd, nread;
12      struct stat sb;
13      char *mapped;
14 
15 //打开文件
16       if((fd = open(argv[1], O_RDWR)) < 0){
17            perror("open") ;
18       }   
19 
20 //获取文件的属性
21       if((fstat(fd, &sb)) == -1 ){
22            perror("fstat") ;
23       }   
24       
25      
26 //将文件映射至进程的地址空间
27       if((mapped = mmap(NULL, sb.st_size, PROT_READ|\
28                PROT_WRITE, MAP_SHARED, fd, o)) ==(void*) -1){
29            perror("mmap") ;
30       }   
31      
32 //修改一个字符,同步到磁盘文件
33       mapped[20] = '9';
34       if((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1){
35            perror("msync") ;
36       
37 //释放存储映射区
38       if((munmap((void *)mapped,sb.st_size)) == -1){
39            perror("munmap");  
40       }   
41                  
42       return 0;
43 }
44   
linux共享内存介绍

2 私有映射无法修改文件

1 //将文件私有映射到进程的地址空间
2 if((mapped = (char *)mmap(NULL,sb.st_size,PROT_READ|
3                     PROT_WRITE, MAP_PRIVATE, fd, 0))==(void *)-1){
4         perror("mmap");

五、使用共享内存映射实现两个进程之间的通信

两个程序映射到同一个文件到自己的地址空间,进程A先运行,每个两秒读取映射区域,看是否发生变化,进程B后运行,它修改映射区域,然后退出,此时进程A能够观察到存储映射区的变化

进程A的代码:

linux共享内存介绍
 1 #include <sys/mman.h>  
 2 #include <sys/stat.h>  
 3 #include <fcntl.h>  
 4 #include <stdio.h>  
 5 #include <stdlib.h>  
 6 #include <unistd.h>  
 7 #include <error.h>  
 8             
 9 int main(int argc, char **argv)  
10 {  
11         int fd, nread;  
12         struct stat sb;  
13         char *mapped;  
14      
15       
16 /* 打开文件 */  
17         if ((fd = open(argv[1], O_RDWR)) < 0) {  
18             perror("open");  
19         }  
20       
21 /* 获取文件的属性 */  
22         if ((fstat(fd, &sb)) == -1) {  
23             perror("fstat");  
24         }  
25       
26 /* 将文件映射至进程的地址空间 */  
27         if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ            | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {  
28             perror("mmap");  
29         }  
30       
31 /* 文件已在内存, 关闭文件也可以操纵内存 */  
32         close(fd);  
33           
34 /* 每隔两秒查看存储映射区是否被修改 */  
35         while (1) {  
36             printf("%s\n", mapped);  
37             sleep(2);  
38         }  
39       
40         return 0;  
41 }   
linux共享内存介绍

进程B的代码

linux共享内存介绍
 1   #include <sys/mman.h>  
 2   #include <sys/stat.h>  
 3   #include <fcntl.h>  
 4   #include <stdio.h>  
 5   #include <stdlib.h>  
 6   #include <unistd.h>  
 7   #include <error.h>  
 8         
 9   int main(int argc, char **argv)  
10  {  
11          int fd;  
12          struct stat sb;  
13          char *mapped;  
14           
15  /* 打开文件 */  
16          if ((fd = open(argv[1], O_RDWR)) < 0) {  
17              perror("open");  
18          }  
19        
20  /* 获取文件的属性 */  
21          if ((fstat(fd, &sb)) == -1) {  
22              perror("fstat");  
23          }  
24  /* 私有文件映射将无法修改文件 */  
25          if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ 
26                     |PROT_WRITE,MAP_PRIVATE, fd, 0)) == (void*)-1) {  
27              perror("mmap");  
28          }  
29        
30  /* 映射完后, 关闭文件也可以操纵内存 */  
31          close(fd);  
32        
33  /* 修改一个字符 */  
34          mapped[20] = '9';  
35        
36          return 0;  
37  }  
linux共享内存介绍

六、通过匿名映射实现父子进程通信

linux共享内存介绍
 1 #include <sys/mman.h>  
 2 #include <stdio.h>  
 3 #include <stdlib.h>  
 4 #include <unistd.h>  
 5       
 6  #define BUF_SIZE 100  
 7       
 8 int main(int argc, char** argv)  
 9 {  
10         char    *p_map;  
11       
12 /* 匿名映射,创建一块内存供父子进程通信 */  
13         p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ |      PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);  
14       
15         if(fork() == 0) {  
16             sleep(1);  
17             printf("child got a message: %s\n", p_map);  
18             sprintf(p_map, "%s", "hi, dad, this is son");  
19             munmap(p_map, BUF_SIZE); //实际上,进程终止时,会自动解除映射。  
20             exit(0);  
21         }  
22       
23         sprintf(p_map, "%s", "hi, this is father");  
24         sleep(2);  
25         printf("parent got a message: %s\n", p_map);  
26       
27         return 0;  
28 }  
linux共享内存介绍

七、对mmap()返回地址的访问

linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:

linux共享内存介绍

总结一下就是,文件大小,mmap()的参数len都不能决定进程能访问的大小,而是容纳文件被映射部分的最小页面数决定进程能访问的大小,下面看一个实例:

linux共享内存介绍
#include <sys/mman.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <stdio.h>  
      
 int main(int argc, char** argv)  
 {  
        int fd,i;  
        int pagesize,offset;  
        char *p_map;  
        struct stat sb;  
      
 /* 取得page size */  
        pagesize = sysconf(_SC_PAGESIZE);  
        printf("pagesize is %d\n",pagesize);  
      
/* 打开文件 */  
        fd = open(argv[1], O_RDWR, 00777);  
        fstat(fd, &sb);  
        printf("file size is %zd\n", (size_t)sb.st_size);  
      
        offset = 0;   
        p_map = (char *)mmap(NULL, pagesize * 2,  PROT_READ|PROT_WRITE,   MAP_SHARED, fd, offset);  
        close(fd);  
          
        p_map[sb.st_size] = '9';  /* 导致总线错误 */  
        p_map[pagesize] = '9';    /* 导致段错误 */  
      
        munmap(p_map, pagesize * 2);  
      
       return 0;  
}  


共享内存之——system V共享内存

System V 的IPC对象有共享内存、消息队列、信号灯(量)。

注意:在IPC的通信模式下,不管是共享内存、消息队列还是信号灯,每个IPC的对象都有唯一的名字,称为"键(key)"。通过"键",进程能够识别所用的对象。"键"与IPC对象的关系就如同文件名称于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够公用一个文件。而在IPC的通信模式下,通过"键"的使用也能使得一个IPC对象能为多个进程所共用。


一、System V共享内存机制:

system V IPC 机制下的共享内存本质是一段特殊的内存区域,进程间需要共享的数据被放在该共享内存区域中,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。共享内存允许一个或多个进程通过同时出现在他们的虚拟地址空间的内存进行通信,而这块虚拟内存的页面被每个共享进程的页表条目所引用,同时并不需要再所有进程的虚拟内存都有相同的地址。

1.System V共享内存是一种最为搞笑的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。

2.为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不要进行数据的拷贝,从而大大提高效率。

3.由于多个进程共享一段内存,因此也需要依靠某种同步机制。

 二、共享内存的操作流程

1. 创建/打开共享内存

2.映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问

3.撤销共享内存的映射

4.删除共享内存对象

三、相关API

step.1----------------->获取一块共享内存

#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(key_t key, size_t size, int shmflg)

返回值:调用成功返回一个shmid(类似打开一个或创建一个文件获得的文件描述符一样);

       调用失败返回-1;

参数说明:

(1)key: 

这两种方式分配的共享内存,一般用来亲缘关系的进程间通信:

当key取值为IPC_PRIVATE,则函数shmget()将创建一块新的共享内存;

当key取值为0,而参数shmflg中设置了IPC_CREATE这个标志,则同样创建一块新的共享内存;

 

我们一般是通过ftok函数获取键值key

linux共享内存介绍
#include<sys/types.h>
#include<sys/ipc.h>

key_t ftok(const char * pathname, int proj_id)

/*pathname 就是指定的文件名(该文件必须是存在而且可以访问的)
id是标示符,和pathname一起完成创建键值的参数,虽然为int,但只有8个比特被使用,一般我们写一个字符代替*/
linux共享内存介绍

例如:

linux共享内存介绍
 1 #include<sys/types.h>
 2 #include<sys/ipc.h>
 3 #include<stdio.h>
 4 #include<stdlib.h>
 5 
 6 int main()
 7 {
 8       key_t  key1,key2;
 9       if((key1 = ftok(".",'a')) < 0)
10       {
11              perror("fail to ftok");
12              exit(EXIT_FAILURE); 
13        }
14        if((key2 = ftok(".",'b')) < 0)
15       {
16              perror("fail to ftok");
17              exit(EXIT_FAILURE); 
18        }
19        printf("key1 = %d\n",key1);
20        printf("key2 = %d\n",key2);
21 
22        return 0;    
23 }
linux共享内存介绍

(2)size:

是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。所以如果一个进程只申请一块只有体格字节的内存,内存也会分配蒸蒸一页(在i386机器中一页的缺省大小PACE_SIZE = 4096B)

(3)shmflg:

指定创建或打开的标志和读写的权限(ipc_perm中的mode成员)。

有效的标志包括IPC_CREAT和IPC_EXCL,他们的功能与opene的O_CREAT和O_EXCL相当。

IPC_CREAT   如果共享内存不存在,则创建一个共享内存,否则直接打开已存在的。

IPC_EXCL     只有在共享内存不存在的时候,新的内存才建立,否则就产生错误。

linux共享内存介绍
/*假设键值为key,创建一个共享内存大小为4k,访问权限为0666
   如果已经存在则返回其标示符*/
int shmid;
if((shmid = shmget(key, 4*1024, 0666 | IPC_CRREAT)) < 0)
{
    perror("Fail to shmget");
    exit(EXIT_FAILURE);  
}
linux共享内存介绍
linux共享内存介绍
/*假设键值为key,创建一个共享内存大小为1k,访问权限为0666
   如果已经存在则报错*/
int shmid;
if((shmid = shmget(key, 1024, 0666 | IPC_CRREAT|IPC_EXCL)) < 0)
{
    perror("Fail to shmget");
    exit(EXIT_FAILURE);  
}
linux共享内存介绍

step 2--------------------------->共享内存的映射

函数shmat将标示符为shmid共享内存映射到调用进程的地址空间中。

#include<sys/types.h>
#include<sys/shm.h>

void * shmat(int shmid, const void *shmaddr, int shmflg);

参数说明:

shmid:要映射的共享内存区标示符

shmaddr:将共享内存映射到指定地址(若为NULL,则表示由系统自动完成映射)

shmflg:SHM_RDONLY 共享内存只读

             默认0:共享内存可读写

返回值:调用成功返回映射后的地址,出错返回(void *)-1。

 step3------------------------------>撤销共享内存与用户进程之间的映射

int shmdt(const void * shmadr);

参数shmaddr 是 shmat映射成功返回的地址。

注意:当一个进程不再需要共享内存段时,它将调用shmdt()系统调用取消这个段,但是这并不是从内核真正地删除这个段,而是把相关shmid_ds结构的shm_nattch域的值减1,当这个值为0时,内核才从物理上删除这个共享段。

step4-------------------------------->控制共享内存(&删除)

#inlcude<sys/ipc.h>
#include<sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

shmid 共享内存标示符ID

cmd  IPC_STAT得到共享内存的状态

   IPC_SET改变共享内存的状态

     IPC_RMID删除共享内存

buf   是一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体 struct shmid_ds 指定:

linux共享内存介绍

注意:

1.IPC_RMID命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生最后一个进程离开这个共享段时。

2.当cmd为IPC_RMID时,第三个参数应为NULL。用这个函数“删除”共享内存

3.如果在代码中没有手动删除,共享内存并不会随着程序的终止而自动清理!



相关文章: