并发控制2:进程通信之共享内存

1. 共享内存简介

  在Linux系统中,进程可以通过共享内存实现进程间通信。共享内存主要有两种实现方法:XSI共享存储(通过shmget,shmat,shmdt等函数实现)和内存映射(mmap实现)两者的主要区别是前者没有相关的文件,共享的是内存匿名段;而后者通常需要指定一个文件路径,调用open函数打开之后实现mmap内存映射。涉及到多个进程共享存储区,就必须有访问控制,这通常通过信号量,记录锁和互斥量实现。下面分别介绍每一种方法的使用细节。

 

2. mmap内存映射

  在虚拟内存与mmap,brk中,我们简要介绍了mmap的使用方法,mmap最常见的应用之一就是用于进程通信的共享内存。如果是没有亲缘关系的进程间通信,只需要两个进程同时映射到同一个物理文件即可,下面给出一个简单的示例:

 

 1 #include <sys/mman.h>
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 #include <string.h>
 5 #include <fcntl.h>
 6 #include <unistd.h>
 7 
 8 #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH)
 9 
10 //注:为了简便,此处没有同步控制策略,先运行写进程,再运行读进程即可
11 
12 //写进程,写入mmap.txt
13 int main()
14 {
15     int fd;
16     if((fd=open("map.txt",O_RDWR|O_CREAT|O_TRUNC,FILE_MODE))<0)
17     {
18         printf("open file failed\n");
19         exit(1);
20     }
21 
22 
23     if(ftruncate(fd,50)<0)  //文件大小50字节
24     {
25         printf("ftruncate error\n");
26         exit(1);
27     }
28     
29     char *buf;//起始地址
30     char* msg="hello world\nnice to see you\n";
31     
32     buf=(char*)mmap(NULL,50,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
33     close(fd);
34     
35     memcpy(buf,msg,strlen(msg));
36     exit(0);
37 }
38 
39 //读进程,读取mmap.txt
40 int main()
41 {
42     int fd;
43     if((fd=open("map.txt",O_RDWR))<0)
44     {
45         printf("open file failed\n");
46         exit(1);
47     }
48     char *buf;//起始地址
49     buf=(char*)mmap(NULL,50,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
50     close(fd);
51     printf("%s",buf);
52     exit(0);
53     
54 }
View Code

 

  如果进程间有亲缘关系(通常是父子进程或兄弟进程),通常不用人工创建交互文件(如上面代码的mmap.txt)。Linux提供了"/dev/zero"设备来进行存储映射,该设备是0字节无限资源,当用mmap对其进行存储映射时,有特以下特点:

(1) 创建一个未命名存储区,长度为mmap第二个参数指定的长度向上补充到最近页长;

(2) 存储区全初始化为0;

(3) 如果多个进程的共同祖先对mmap指定了MAP_SHARED,那么这些进程可以共享该存储区。

基于以上特性,有亲缘关系的进程就可以通过/dev/zero实现存储映射与共享。下面给出简单示例:

 1 #include <sys/mman.h>
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 #include <string.h>
 5 #include <fcntl.h>
 6 #include <unistd.h>
 7 #include <semaphore.h>
 8 #include <sys/wait.h>
 9 
10 #define SIZE sizeof(long)
11 #define loop 10000
12 #define NAME1 "mysem1"
13 #define NAME2 "mysem2"
14 
15 static int update(long* ptr)
16 {
17     return ((*ptr)++);
18 }
19 
20 
21 int main()
22 {
23     int fd,i;
24     
25     if((fd=open("/dev/zero",O_RDWR|O_TRUNC))<0)
26     {
27         printf("open file failed\n");
28         exit(1);
29     }
30 
31     long* data;
32     if((data=(long*)mmap(0,SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED)
33     {
34         printf("mmap error\n");
35         exit(1);
36     }    
37     close(fd);
38     
39     
40     pid_t pid;
41     sem_t *sem1,*sem2;
42     sem1=sem_open(NAME1,O_CREAT|O_EXCL,00777,0);
43     sem2=sem_open(NAME2,O_CREAT|O_EXCL,00777,1);
44     
45     if(sem1==SEM_FAILED || sem2==SEM_FAILED)
46     {
47         printf("sem open failed\n");
48         sem_unlink(NAME1);
49         sem_unlink(NAME2);
50     }
51 
52     if((pid=fork())<0)
53     {
54         printf("fork error\n");
55         exit(1);
56     }
57     if(pid>0)
58     {
59         for(i=0;i<loop;i+=2)
60         {
61             sem_wait(sem2);
62             if(update(data)!=i)
63                 printf(" parent update error\n");
64             printf("parent:%d\n",i);
65             sem_post(sem1);
66         }
67         
68         //printf("result:%l\n",(*data));
69         sem_close(sem1);
70         sem_close(sem2);
71         
72         sem_unlink(NAME1);
73         sem_unlink(NAME2);
74         int status;
75         wait(&status);
76     }
77     else
78     {
79         for(i=1;i<=loop;i+=2)
80         {
81             sem_wait(sem1);
82             if(update(data)!=i)
83                 printf("child update error\n");
84             printf("child:%d\n",i);
85             sem_post(sem2);
86         }
87         exit(0);
88     }
89     
90     exit(0);
91 }
View Code

上面的代码用到了POSIX信号量进行同步。关于POSIX信号量相关知识,参考进程通信之信号量。除了/dev/zero设备,还可以匿名映射建立与后代进程共享的内存区。只需要把mmap改为:

//匿名映射,添加MAP_ANON同时文件描述符改为-1
data=(long*)mmap(0,SIZE,PROT_READ|PROT_WRITE,MAP_ANON|MAP_SHARED,-1,0);

  可以看到此中方法最大的缺点是:两个进程通过IO文件来进行交互,极大的减慢的进程通信速度

 

 

 3. XSI共享内存

  XSI共享内存实现主要通过shmget(), shmat(), shmdt(), shmctl()四个函数实现。

3. 1 标识符和键

  每个内核IPC结构(消息队列,信号量,共享内存)都有一个非负整数的标识符加以引用(此标识符不同于文件描述符)。标识符是IPC结构的内部名(OS用来调用识别的名字),为了使多个进程能在同一个IPC对象上汇聚,需要给IPC对象指定一个外部名称,即为键(外部识别IPC的标志)。那么如何让多个进程在一个IPC上汇聚呢(要么知道该IPC的标识符,要么知道键)?一般有以下三种方法:

(1) 服务器进程在创建内核IPC的时候,指定键为IPC_PRIVATE,将返回的标识符存在某个文件中,客户端进程从文件获取标识符(如果是父子进程,则可以直接获取标识符,不需要文件);

(2) 在一个公共头文件中指定键,然后服务器进程通过该键创建IPC。此时可能的问题是键已经与一个IPC结构结合,此时调用get函数(shmget,msgget,semget)将出错返回。服务器处理这一错误,删除已存在的IPC重新创建;

(3) 客户进程和服务器进程共用一个路径名和ID(ID号为0~255),调用ftok函数转化为键。

#include <sys/ipc.h>

key_t ftok(const char* path,int id); //将path和ID转化为key

注:即使路径不同,如果ID相同,仍然可能产生相同的key。

 如果希望创建新的IPC,并确保没有具有同一标识符的其他IPC存在,可以在创建flags的时候指定IPC_CREAT和IPC_EXCL,如果该IPC已经存在,则返回EEXIST。

每个IPC结构关联了一个ipc_perm结构体,该结构体规定了权限和所有者,至少需要包含以下成员:

struct ipc_perm
{
uid_t uid; //ower's effective user id
gid_t gid; //ower's effective group id
uid_t cuid; //creator's effective user id
gid_t cgid; //creator's effective group id
mode_t mode;
}

 

3.2 使用共享内存的步骤

 

每个XSI共享内存IPC都有一个专有结构体:

struct shmid_ds
{
struct ipc_perm shm_perm;
size_t shm_segsz;  //共享内存的大小
pid_t shm_lpid; //pid of last shmop()
pid_t shm_cpid; //pid of creator
shmatt_t shm_nattach; //连接到ipc的进程数
time_t shm_atime; //last attach time
time_t shm_dtime; //last detach time
time_t shm_ctime; //last change time
}

其中,实际内存大小是shm_segsz向上到页大小的整倍数。

使用共享内存时,主要步骤如下:

(1) 调用ftok()函数为共享内存获取一个键值,这相当于给一段内存地址绑定一个引用名;

(2) 调用shmget()获取该内存的ID(创建共享内存);

(3) 调用shmat将进程内变量地址与共享内存地址之间建立映射关系(用于进程内部绑定共享内存和进程内的地址);

(4) 调用shmdt()解除绑定。

shmctl用于设置共享内存相关属性,也经常用到,下面看看每个函数的具体内容:

#include <sys/shm.h>

int  shmget(key_t key, size_t size, int shmflg);  //根据key创建共享内存,返回标识符
void*  shmat(int shmid, const void* shmaddr, int shmflg); //将共享内存与进程本地地址连接起来,返回本地地址,shmaddr一般为0,让进程自己选择映射地址
int  shmdt(const void*  shmaddr);  //解除地址绑定
int  shmctl(int shmid, int cmd, struct shmid_ds* buf); //共享内存属性设置
/*
shmctl中cmd主要有:
IPC_STAT:获取shmid_ds结构体内容,存到buf中;
IPC_SET:按buf结构体的值设置共享存储shmid_ds中的shm_perm.uid,shm_perm.gid,shm_perm.mode,只有有效用户ID=shm_perm.cuid或shm_perm.uid或有超级用户特权的进程才能执行该函数;
IPC_RMID:删除共享存储,除非最后一个与该共享存储连接的进程detach,否则不会真的删除。但是进程内标识符已被删除,无法调用shmat;
SHM_LOCK:对共享存储加锁,只有超级用户才能执行;
SHM_UNLOCK
*/

下面演示具体使用方法:两个进程,一个写入,一个读出

 1 //写入进程
 2 #include <stdlib.h>
 3 #include <stdio.h>
 4 #include <string.h>
 5 #include <sys/shm.h>
 6 #include <sys/ipc.h>
 7 #include <errno.h>
 8 
 9 struct Msg
10 {
11     char news[20];
12     int id;
13 };
14 
15 int main()
16 {
17     key_t key=1234; //因为示例只有一个shm,所以人工指定键值
18     struct Msg* addr=NULL; //要映射的进程内地址
19     struct shmid_ds buf;
20     int shmid=shmget(key,1024,IPC_CREAT|IPC_EXCL|0600);
21     if(shmid<0)
22     {
23         printf("shmget error\n");
24         exit(1);
25     }
26     
27     addr=(Msg*)shmat(shmid,0,0);
28     if(addr==NULL)
29     {
30         printf("shmat error\n");
31         exit(1);
32     }
33     char* news1="hello world";
34     strcpy(addr->news,news1);
35     addr->id=1;
36     
37     char* news2="new message";
38     strcpy((addr+1)->news,news2);
39     (addr+1)->id=2;
40     
41     shmdt(addr);
42     exit(0);
43 }
View Code
 1 //读进程
 2 #include <stdlib.h>
 3 #include <stdio.h>
 4 #include <string.h>
 5 #include <sys/shm.h>
 6 #include <sys/ipc.h>
 7 
 8 struct Msg
 9 {
10     char news[20];
11     int id;
12 };
13 
14 int main()
15 {
16     int  shmid;
17     struct shmid_ds buf;
18     struct Msg *addr=NULL;
19     
20     shmid=shmget(1234,0,0);
21     if(shmid==-1)
22     {
23         printf("shmget error\n");
24         exit(1);
25     }
26     shmctl(shmid,IPC_STAT,&buf);
27     printf("memory size:%d\n",(int)(buf.shm_segsz));
28 
29     
30     addr=(Msg*)shmat(shmid,0,0);
31     if(addr==NULL)
32     {
33         printf("shmat error\n");
34         exit(1);
35     }
36     
37     for(int i=0;i<2;i++)
38     {
39         printf("ID %d:%s\n",(addr+i)->id,(addr+i)->news);
40     }
41     shmdt(addr);
42     exit(0);
43 }
View Code

注:写进程只能执行一次,因为一旦创建了共享内存,没有调用shmctl删除之前,该共享内存仍然存在,下次再创建就会报错。本程序演示完一遍,可以人工删除共享内存。

PS: 终端查看命令

ipcs:查看所有XSI IPC
ipcs -m:查看共享内存相关信息
ipcs -s
ipcs -q
ipcrm -m shmid:移除id为shmid的共享内存

XSI相对于mmap的好处就是没有了文件操作,直接在内存中交换数据。

 

posted @ 2020-05-27 10:54  晨枫1  阅读(991)  评论(0编辑  收藏  举报