进程间通信(IPC)介绍
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
一、一些基本概念
进程间通信(IPC):进程之间交换数据的过程叫进程间通信。
进程间通信的方式:
简单的进程间通信:
命令行:父进程通过exec函数创建子进程时可以附加一些数据。
环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表。
信号:父子进程之间可以根据进程号相互发送信号,进程简单通信。
文件:一个进程向文件中写入数据,另一个进程从文件中读取出来。
命令行、环境变量只能单身传递,信号太过于简单,文件通信不能实时。
XSI通信方式:X/open 计算机制造商组织。
共享内存、消息队列、信号量
网络进程间通信方式:网络通信就是不同机器的进程间通信方式。
传统的进程间通信方式:管道
二、管道
1、管道是一种古老的通信的方式(基本上不再使用)
2、早期的管道是一种半双工,现在大多数是全双工。
3、有名管道(这种管道是以文件方式存在的)。
int mkfifo(const char *pathname, mode_t mode);
例子:
-
-
-
-
-
-
-
-
-
int main()
-
{
-
// 创建管道文件
-
if(0 > mkfifo("test.txt",0644))
-
{
-
perror( "mkfifo");
-
return -1;
-
}
-
-
// 打开
-
int fd = open("test.txt",O_RDWR);
-
if(0 > fd)
-
{
-
perror( "open");
-
return -1;
-
}
-
-
// 准备缓冲区
-
char buf[255] = {};
-
// 写/读
-
while(1)
-
{
-
printf(">");
-
gets(buf);
-
int ret = write(fd,buf,strlen(buf));
-
printf("写入数据%d字节\n",ret);
-
if('q' == buf[0])break;
-
getchar();
-
bzero(buf, sizeof(buf));
-
ret = read(fd,buf, sizeof(buf));
-
printf("读取数据%d字节,内容:%s\n",ret,buf);
-
if('q' == buf[0])break;
-
}
-
// 关闭
-
close(fd);
-
}
管道通信的编程模式:
进程A 进程B
创建管道mkfifo
打开管道open 打开管道
写/读数据read/write 读/写数据
关闭管道close 关闭管道
4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,但这种管道只能用在fork创建的父子进程之间。
int pipe(int pipefd[2]);
pipefd[0] 用来读数据
pipefd[1] 用来写数据
以Linux中的C语言编程为例。
-
-
-
-
-
-
-
int main()
-
{
-
int fd[2];
-
int pid;
-
char buf[255]={};
-
-
if(pipe(fd)<0)
-
{
-
perror( "pipe");
-
return -1;
-
}
-
if((pid=fork())<0)
-
{
-
perror( "fork");
-
}
-
else if(pid>0)
-
{
-
-
printf("我是进程%d...",getpid());
-
close(fd[ 0]);
-
printf("请输入:\n");
-
gets(buf);
-
write(fd[ 1],buf,sizeof(buf));
-
pause();
-
-
}
-
else
-
{
-
getchar();
-
close(fd[ 1]);
-
bzero(buf, sizeof(buf));
-
printf("我是子进程%d,我的父进程是%d...\n",getpid(),getppid());
-
read(fd[ 0],buf,20);
-
printf("我读到了%s\n",buf);
-
kill(getppid(), 2);
-
-
}
-
}
此程序是一个简单的通过无名管道实现进程间的通信的程序!
三、共享内存
1、由内存维护一个共享的内存区域,其它进程把自己的虚拟地址映射到这块内存,然后多个进程之间就可以共享这块内存了。
2、这种进程间通信的好处是不需要信息复制,是进程间通信最快的一种方式。
3、但这种通信方式会面临同步的问题,需要与其它通信方式配合,最合适的就是信号。
共享内存的编程模式:
1、进程之间要约定一个键值
进程A 进程B
创建共享内存
加载共享内存 加载共享内存
卸载共享内存 卸载共享内存
销毁共享内存
int shmget(key_t key, size_t size, int shmflg);
功能:创建共享内存
size:共享的大小,尽量是4096的位数
shmflg:IPC_CREAT|IPC_EXCL
返回值:IPC对象标识符(类似文件描述符)
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:加载共享内存(进程的虚拟地址与共享的内存映射)
shmid:shmget的返回值
shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射。
shmflg:
SHM_RDONLY:限制内存的权限为只读
SHM_REMAP:映射已经存的共享内存。
SHM_RND:当shmaddr为空时自动分配
SHMLBA:shmaddr的值不能为空,否则出错
返回值:映射后的虚拟内存地址
int shmdt(const void *shmaddr);
功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:控制/销毁共享内存
cmd:
IPC_STAT:获取共享内存的属性
IPC_SET:设置共享内存的属性
IPC_RMID:删除共享内存
buf:
记录共享内存属性的对象
例子:
程序A
-
-
-
-
-
-
char* buf=NULL;
-
-
void sigint(int num)
-
{
-
printf("\r接收到数据:%s\n",buf);
-
printf(">");
-
fflush( stdout);
-
}
-
-
int main()
-
{
-
signal(SIGINT,sigint);
-
-
key_t key=39242236;
-
-
int pid=0;
-
printf("我是进程:%d\n",getpid());
-
printf("与我通信的进程是:");
-
scanf("%d",&pid);
-
getchar();
-
-
int shmid=shmget(key,4096,IPC_CREAT|0744);
-
if(0>shmid)
-
{
-
perror( "shmget");
-
return -1;
-
}
-
-
buf = shmat(shmid, NULL,SHM_RND);
-
while(1)
-
{
-
printf("请输入要发送给进程%d的内容:\n",pid);
-
gets(buf);
-
kill(pid,SIGINT);
-
}
-
shmdt(buf);
-
}
程序B:
-
-
-
-
-
-
char* buf=NULL;
-
-
void sigint(int num)
-
{
-
printf("\r接收到数据:%s\n",buf);
-
printf(">");
-
fflush( stdout);
-
}
-
-
int main()
-
{
-
signal(SIGINT,sigint);
-
-
key_t key=39242236;
-
-
int pid=0;
-
printf("我是进程:%d\n",getpid());
-
printf("与我通信的进程是:");
-
scanf("%d",&pid);
-
getchar();
-
-
int shmid=shmget(key,4096,0);
-
if(0>shmid)
-
{
-
perror( "shmget");
-
return -1;
-
}
-
-
buf = shmat(shmid, NULL,SHM_RND);
-
while(1)
-
{
-
printf("请输入要发送给进程%d的内容:\n",pid);
-
gets(buf);
-
kill(pid,SIGINT);
-
}
-
shmdt(buf);
-
}
上面两个程序分别运行得到进程A和进程B
通过获取进程id用kill函数来发送信号,从而实现A和B的通信,两个程序通过共享内存通信
四、消息队列
1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表。
int msgget(key_t key, int msgflg);
功能:创建或获取消息队列
msgflg:
创建:IPC_CREAT|IPC_EXEC
获取:0
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送消息
msqid:msgget的返回人值
msgp:消息(消息类型+消息内容)的首地址
msgsz:消息内存的长度(不包括消息类型)
msgflg:
MSG_NOERROR:当消息的实际长比msgsz还要长的话,
则按照msgsz长度截取再发送,否则产生错误。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
功能:从消息队列接收消息
msgp:存储消息的缓冲区
msgsz:要接收的消息长度
msgtyp:消息的的类型(它包含消息的前4个字节)
msgflg:
MSG_NOWAIT:如果要接收的消息不存在,直接返回。
否则阻塞等待。
MSG_EXCEPT:从消息队列中接收第一个不msgtyp类型的第一个消息。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:控制/销毁消息队列
cmd:
IPC_STAT:获取消息队的属性
IPC_SET:设置消息队列的属性
IPC_RMID:删除消息队列
例子:
A程序
-
-
-
-
-
//定义消息
-
typedef struct Msg
-
{
-
long type;
-
char buf[255];
-
}Msg;
-
-
int main()
-
{
-
key_t key=ftok(".",1);
-
int msgid=msgget(key,0777|IPC_CREAT);
-
if(0>msgid)
-
{
-
perror( "msgget");
-
return -1;
-
}
-
while(1)
-
{
-
Msg msg={};
-
msg.type= 1;
-
printf("请输入发送到消息队列中的内容:\n");
-
gets(msg.buf);
-
msgsnd(msgid,&msg, sizeof(msg.buf),0);
-
//当发送消息首字母为q退出
-
if('q'==msg.buf[0]) break;
-
msgrcv(msgid,&msg, sizeof(msg.buf),2,0);
-
printf("接收到:%s\n",msg.buf);
-
//当接收消息首字母为q退出
-
if('q'==msg.buf[0]) break;
-
-
-
}
-
}
B程序
-
-
-
-
-
//定义消息
-
typedef struct Msg
-
{
-
long type;
-
char buf[255];
-
}Msg;
-
-
int main()
-
{
-
key_t key=ftok(".",1);
-
int msgid=msgget(key,0);
-
if(0>msgid)
-
{
-
perror( "msgget");
-
return -1;
-
}
-
while(1)
-
{
-
Msg msg={};
-
msgrcv(msgid,&msg, sizeof(msg.buf),1,0);
-
printf("接收到:%s\n",msg.buf);
-
//当接收消息首字母为q退出
-
if('q'==msg.buf[0]) break;
-
printf("请输入发送到消息队列中的内容:\n");
-
gets(msg.buf);
-
msg.type= 2;
-
msgsnd(msgid,&msg, sizeof(msg.buf),0);
-
//当发送消息首字母为q退出
-
if('q'==msg.buf[0]) break;
-
-
-
}
-
}
进程A和B通过消息队列完成IPC
五、信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1、特点
-
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
-
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
-
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
-
支持信号量组。
2、原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
-
1 #include <sys/sem.h>
-
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
-
3 int semget(key_t key, int num_sems, int sem_flags);
-
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
-
5 int semop(int semid, struct sembuf semoparray[], size_t numops);
-
6 // 控制信号量的相关信息
-
7 int semctl(int semid, int sem_num, int cmd, ...);
当semget
创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems
),通常为1; 如果是引用一个现有的集合,则将num_sems
指定为 0 。
在semop
函数中,sembuf
结构的定义如下:
-
1 struct sembuf
-
2 {
-
3 short sem_num; // 信号量组中对应的序号,0~sem_nums-1
-
4 short sem_op; // 信号量值在一次操作中的改变量
-
5 short sem_flg; // IPC_NOWAIT, SEM_UNDO
-
6 }
其中 sem_op 是一次操作中的信号量的改变量:
-
若
sem_op > 0
,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。 -
若
sem_op < 0
,请求 sem_op 的绝对值的资源。- 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
- 当相应的资源数不能满足请求时,这个操作与
sem_flg
有关。- sem_flg 指定
IPC_NOWAIT
,则semop函数出错返回EAGAIN
。 - sem_flg 没有指定
IPC_NOWAIT
,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:- 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
- 此信号量被删除,函数smeop出错返回EIDRM;
- 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
- sem_flg 指定
-
若
sem_op == 0
,进程阻塞直到信号量的相应值为0:- 当信号量已经为0,函数立即返回。
- 如果信号量的值不为0,则依据
sem_flg
决定函数动作:- sem_flg指定
IPC_NOWAIT
,则出错返回EAGAIN
。 - sem_flg没有指定
IPC_NOWAIT
,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:- 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
- 此信号量被删除,函数smeop出错返回EIDRM;
- 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR
- sem_flg指定
在semctl
函数中的命令有多种,这里就说两个常用的:
SETVAL
:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。IPC_RMID
:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
3、例子
-
1 #include <stdio.h>
-
2 #include <stdlib.h>
-
3 #include <sys/sem.h>
-
4
-
5 // 联合体,用于semctl初始化
-
6 union semun
-
7 {
-
8 int val; /*for SETVAL*/
-
9 struct semid_ds *buf;
-
10 unsigned short *array;
-
11 };
-
12
-
13 // 初始化信号量
-
14 int init_sem(int sem_id, int value)
-
15 {
-
16 union semun tmp;
-
17 tmp.val = value;
-
18 if(semctl(sem_id, 0, SETVAL, tmp) == -1)
-
19 {
-
20 perror("Init Semaphore Error");
-
21 return -1;
-
22 }
-
23 return 0;
-
24 }
-
25
-
26 // P操作:
-
27 // 若信号量值为1,获取资源并将信号量值-1
-
28 // 若信号量值为0,进程挂起等待
-
29 int sem_p(int sem_id)
-
30 {
-
31 struct sembuf sbuf;
-
32 sbuf.sem_num = 0; /*序号*/
-
33 sbuf.sem_op = -1; /*P操作*/
-
34 sbuf.sem_flg = SEM_UNDO;
-
35
-
36 if(semop(sem_id, &sbuf, 1) == -1)
-
37 {
-
38 perror("P operation Error");
-
39 return -1;
-
40 }
-
41 return 0;
-
42 }
-
43
-
44 // V操作:
-
45 // 释放资源并将信号量值+1
-
46 // 如果有进程正在挂起等待,则唤醒它们
-
47 int sem_v(int sem_id)
-
48 {
-
49 struct sembuf sbuf;
-
50 sbuf.sem_num = 0; /*序号*/
-
51 sbuf.sem_op = 1; /*V操作*/
-
52 sbuf.sem_flg = SEM_UNDO;
-
53
-
54 if(semop(sem_id, &sbuf, 1) == -1)
-
55 {
-
56 perror("V operation Error");
-
57 return -1;
-
58 }
-
59 return 0;
-
60 }
-
61
-
62 // 删除信号量集
-
63 int del_sem(int sem_id)
-
64 {
-
65 union semun tmp;
-
66 if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
-
67 {
-
68 perror("Delete Semaphore Error");
-
69 return -1;
-
70 }
-
71 return 0;
-
72 }
-
73
-
74
-
75 int main()
-
76 {
-
77 int sem_id; // 信号量集ID
-
78 key_t key;
-
79 pid_t pid;
-
80
-
81 // 获取key值
-
82 if((key = ftok(".", 'z')) < 0)
-
83 {
-
84 perror("ftok error");
-
85 exit(1);
-
86 }
-
87
-
88 // 创建信号量集,其中只有一个信号量
-
89 if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
-
90 {
-
91 perror("semget error");
-
92 exit(1);
-
93 }
-
94
-
95 // 初始化:初值设为0资源被占用
-
96 init_sem(sem_id, 0);
-
97
-
98 if((pid = fork()) == -1)
-
99 perror("Fork Error");
-
100 else if(pid == 0) /*子进程*/
-
101 {
-
102 sleep(2);
-
103 printf("Process child: pid=%d\n", getpid());
-
104 sem_v(sem_id); /*释放资源*/
-
105 }
-
106 else /*父进程*/
-
107 {
-
108 sem_p(sem_id); /*等待资源*/
-
109 printf("Process father: pid=%d\n", getpid());
-
110 sem_v(sem_id); /*释放资源*/
-
111 del_sem(sem_id); /*删除信号量集*/
-
112 }
-
113 return 0;
-
114 }
上面的例子如果不加信号量,则父进程会先执行完毕。这里加了信号量让父进程等待子进程执行完以后再执行。
总结:进程间通信是实现两个程序传输数据的重要手段,非常值得学习和掌握