进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
通信背景
1.由于进程是具有独立性的,进程想交互数据,成本会非常高。但是有些情况下需要多进程处理一件事情。
2.进程独立并不是彻底独立,有时候我们需要双方能够进行一定程度的信息交互。
我们要学的进程间通信,不是告诉我们如何通信,是他们两个如何先看到同一份资源。(文件,内存块…等方式)
共享内存实现进程间通信的原理
共享内存实际是操作系统在实际物理内存中开辟的一段内存。
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。 当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。
要实现进程间通信需要两个进程看到同一块空间,系统开辟的共享内存就是两个进程看到的同一资源。
注意:共享内存实现进程间通信是进程间通信最快的。
如何使⽤
要使⽤⼀块共享内存,进程必须先分配它。其他需要访问这个共享内存块的每⼀个进程都必须将这个共享绑定(attach)到⾃⼰的地址空间中(系统维护⼀个对该内存的引⽤计数器,通过ipcs -s 命令可查看有⼏个进程在使⽤该共享内存块)。当通信完毕后,所有进程从共享内存块脱离,由⼀个进程释放该共享内存块。要注意的是,所有⽤户申请的共享内存块最终⼤⼩都必须是向上取整为系统页⾯⼤⼩的整数倍。在Linux系统中,内存页⾯⼤⼩默认是4KB。
注意:当⼀个进程创建⼀块共享内存后,该进程在主动去释放该共享内存之前,被kill掉时,只会使该进程脱离(detach)该共享内存块,⽽不会释放该共享内存块,这时候可以使⽤命名ipcrm -m 去释放该资源。
相关函数
1、shmget
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
函数说明:得到⼀个共享内存标识符或创建⼀个共享内存对象并返回共享内存标识符。
参数:
key: ftok函数返回的I PC键值
size: ⼤于0的整数,新建的共享内存⼤⼩,以字节为单位,获取已存在的共享内存块标识符时,该参数为0,
shmflg: IPC_CREAT||IPC_EXCL 执⾏成功,保证返回⼀个新的共享内存标识符,附加参数指定IPC对象存储权限,如|0666
返回值:成功返回共享内存的标识符,出错返回-1,并设置error错误位。
key:共享内存的唯一值,这个参数需要由用户提供。
共享内存要被管理 -> struct shmid_ds -> struct ipc_perm -> key(shmget)(共享内存唯一值)
1. 为什么key值需要由用户提供?
进程间通信的前提是,先让不同的进程,看到同一份资源。如果由操作系统提供,创建共享内存的进程可以知道key值,但是使用共享内存的进程无法获取。所以key值必须由用户获取,然后在使用时标定key值,则能让使用共享内存的进程获取到。
共享内存,在内核中,让不同的进程看到同一份共享内存,做法是:让他们拥有同一个key即可。
匿名管道 --> 约定好使用同一个文件
共享内存 --> 约定好使用同一个唯一key
2.为什么key值要有唯一性?
操作系统中可能有很多个共享内存在被使用,所以我们就需要用一个唯一值来标识每一个共享内存。
3. 那么如何保证key值唯一性呢?
生成唯一key值函数:ftok函数。
key_t ftok(const char *pathname, int proj_id);
将文件路径和一个项目标识符,转化为唯一key值。
返回值:一个整数,创建成功,返回一个合法的共享内存标识符。失败,返回 -1。
2、shmat
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void shmaddr, int shmflg);
函数说明:连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调⽤进程的地址空间
参数:
shmid: 共享内存标识符
shmaddr: 指定共享内存出现在进程内存地址的什么位置,通常指定为NULL,让内核⾃⼰选择⼀个合适的地址位置
shmflg: SHM_RDONLY 为只读模式,其他参数为读写模式
返回值:成功返回附加好的共享内存地址,出错返回-1,并设置error错误位
3、shmdt
#include <sys/types.h>
#include <sys/shm.h>
void *shmdt(const void* shmaddr);
函数说明:与shmat函数相反,是⽤来断开与共享内存附加点的地址,禁⽌本进程访问此⽚共享内存,需要注意的是,该函数并不删除
所指定的共享内存区,⽽是将之前⽤shmat函数连接好的共享内存区脱离⽬前的进程
参数:shmddr 连接共享内存的起始地址
返回值:成功返回0,出错返回-1,并设置error。
4、shmctl
#include <sys/types.h>
#Include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
函数说明:控制共享内存块
参数:
shmid:共享内存标识符
cmd:
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构赋值到buf所指向的buf中
IPC_SET:改变共享内存的状态,把buf所指向的shmid_ds结构中的uid、gid、mode赋值到共享内存的shmid_ds结构内
IPC_RMID:删除这块共享内存
buf:共享内存管理结构体
返回值:成功返回0,出错返回-1,并设置error错误位。
代码演⽰
父子进程通信范例
shm.c源代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <error.h>
#define SIZE 1024
int main()
{int shmid ;char *shmaddr ;struct shmid_ds buf ;int flag = 0 ;int pid ;shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 ) ;if ( shmid < 0 ){perror("get shm ipc_id error") ;return -1 ;}pid = fork() ;if ( pid == 0 ){shmaddr = (char *)shmat( shmid, NULL, 0 ) ;if ( (int)shmaddr == -1 ){perror("shmat addr error") ;return -1 ;}strcpy( shmaddr, "Hi, I am child process!\n") ;shmdt( shmaddr ) ;return 0;} else if ( pid > 0) {sleep(3 ) ;flag = shmctl( shmid, IPC_STAT, &buf) ;if ( flag == -1 ){perror("shmctl shm error") ;return -1 ;}printf("shm_segsz =%d bytes\n", buf.shm_segsz ) ;printf("parent pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ;printf("chlid pid=%d, shm_lpid = %d \n",pid , buf.shm_lpid ) ;shmaddr = (char *) shmat(shmid, NULL, 0 ) ;if ( (int)shmaddr == -1 ){perror("shmat addr error") ;return -1 ;}printf("%s", shmaddr) ;shmdt( shmaddr ) ;shmctl(shmid, IPC_RMID, NULL) ;}else{perror("fork error") ;shmctl(shmid, IPC_RMID, NULL) ;}return 0 ;
}
编译 gcc shm.c –o shm。
执行 ./shm,执行结果如下:
shm_segsz =1024 bytes
shm_cpid = 9503
shm_lpid = 9504
Hi, I am child process!
⾸先,先来讲⼀下fork之后,发⽣了什么事情。
由fork创建的新进程被称为⼦进程(child process)。该函数被调⽤⼀次,但返回两次。两次返回的区别是⼦进程的返回值是0,⽽⽗进程的返回值则是新进程(⼦进程)的进程 id。将⼦进程id返回给⽗进程的理由是:因为⼀个进程的⼦进程可以多于⼀个,没有⼀个函数使⼀个进程可以获得其所有⼦进程的进程id。对⼦进程来说,之所以fork返回0给它,是因为它随时可以调⽤getpid()来获取⾃⼰的pid;也可以调⽤getppid()来获取⽗进程的id。(进程id 0总是由交换进程使⽤,所以⼀个⼦进程的进程id不可能为0 )。
fork之后,操作系统会复制⼀个与⽗进程完全相同的⼦进程,虽说是⽗⼦关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独⽴的,⼦进程数据空间中的内容是⽗进程的完整拷贝,指令指针也完全相同,⼦进程拥有⽗进程当前运⾏到的位置(两进程的程序计数器pc值相同,也就是说,⼦进程是从fork返回处开始执⾏的),但有⼀点不同,如果fork成功,⼦进程中fork的返回值是0,⽗进程中fork的返回值是⼦进程的进程号,如果fork不成功,⽗进程会返回错误。
可以这样想象,2个进程⼀直同时运⾏,⽽且步调⼀致,在fork之后,他们分别作不同的⼯作,也就是分岔了。这也是fork为什么叫fork的原因,⾄于哪⼀个最先运⾏,可能与操作系统(调度算法)有关,⽽且这个问题在实际应⽤中并不重要,如果需要⽗⼦进程协同,可以通过原语的办法解决。
多进程读写范例
多进程读写即一个进程写共享内存,一个或多个进程读共享内存。下面的例子实现的是一个进程写共享内存,一个进程读共享内存。
(1)下面程序实现了创建共享内存,并写入消息。
shmwrite.c源代码如下:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
typedef struct{char name[8];int age;
} people;
int main(int argc, char** argv)
{int shm_id,i;key_t key;char temp[8];people *p_map;char pathname[30] ;strcpy(pathname,"/tmp") ;key = ftok(pathname,0x03);if(key==-1){perror("ftok error");return -1;}printf("key=%d\n",key) ;shm_id=shmget(key,4096,IPC_CREAT|IPC_EXCL|0600); if(shm_id==-1){perror("shmget error");return -1;}printf("shm_id=%d\n", shm_id) ;p_map=(people*)shmat(shm_id,NULL,0);memset(temp, 0x00, sizeof(temp)) ;strcpy(temp,"test") ;temp[4]='0';for(i = 0;i<3;i++){temp[4]+=1;strncpy((p_map+i)->name,temp,5);(p_map+i)->age=0+i;}shmdt(p_map) ;return 0 ;
}
(2)下面程序实现从共享内存读消息。
shmread.c源代码如下:
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{char name[8];int age;
} people;
int main(int argc, char** argv)
{int shm_id,i;key_t key;people *p_map;char pathname[30] ;strcpy(pathname,"/tmp") ;key = ftok(pathname,0x03);if(key == -1){perror("ftok error");return -1;}printf("key=%d\n", key) ;shm_id = shmget(key,0, 0); if(shm_id == -1){perror("shmget error");return -1;}printf("shm_id=%d\n", shm_id) ;p_map = (people*)shmat(shm_id,NULL,0);for(i = 0;i<3;i++){printf( "name:%s\n",(*(p_map+i)).name );printf( "age %d\n",(*(p_map+i)).age );}if(shmdt(p_map) == -1){perror("detach error");return -1;}return 0 ;
}
(3)编译与执行
①编译gcc shmwrite.c -o shmwrite。
②执行./shmwrite,执行结果如下:
key=50453281
shm_id=688137
③编译gcc shmread.c -o shmread。
④执行./shmread,执行结果如下:
key=50453281
shm_id=688137
name:test1
age 0
name:test2
age 1
name:test3
age 2
⑤再执行./shmwrite,执行结果如下:
key=50453281
shmget error: File exists
⑥使用ipcrm -m 688137删除此共享内存。