并发控制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 }
如果进程间有亲缘关系(通常是父子进程或兄弟进程),通常不用人工创建交互文件(如上面代码的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 }
上面的代码用到了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 }
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 }
注:写进程只能执行一次,因为一旦创建了共享内存,没有调用shmctl删除之前,该共享内存仍然存在,下次再创建就会报错。本程序演示完一遍,可以人工删除共享内存。
PS: 终端查看命令
ipcs:查看所有XSI IPC ipcs -m:查看共享内存相关信息 ipcs -s ipcs -q ipcrm -m shmid:移除id为shmid的共享内存
XSI相对于mmap的好处就是没有了文件操作,直接在内存中交换数据。