进程间通信
多个进程需要互相配合完成一项任务,并且需要做到有序访问临界资源称为进程同步问题。
多个进程需要访问共享资源(临界资源),而这个资源同时只能被一个进程访问不然会产生错乱,称为进程的互斥。
为什么需要进程间通信?
1. 数据传递
2. 资源共享,多个进程使用一个资源
3. 通知事件,告诉其他进程事件发生
通信方式分类:
- 管道
- System V IPC,消息队列,共享内存,信号量
- POSIX 比IPC多了互斥量,条件变量,读写锁
- 网络socket
管道
管道是unix系统IPC的最古老形式,提供半双工通信(只能在一个方向流动),通常只能用在有血缘关系的进程之间,比如父进程和子进程,或者同一个进程组里的进程。
最常见的使用场景:在管道线中输入一串由shell执行的命令序列,shell为每一条命令单独创建一个进程,然后将前一条命令进程的标准输出用管道与后一条命令的标准输入相连接。
本质:其原理是使用内核里的一块缓存区保存数据流。
使用:单进程管道没有用处,通常在调用pipe后调用fork,这样就创建了从父进程到子进程的IPC通道。调用fork后做什么取决于想要的数据流方向,由于子进程继承父进程文件描述符,所以管道的文件描述符fd0,fd1的引用计数为2。需要先调用close将不需要用的描述符计数-1,这样就搭建好了管道。
管道是有大小限制的,内核总缓存大小65536字节,一次写入大小不超过4096B.
//模拟shell管道
if(0 == fork()){
//子进程执行ls
//标准输出重定向到管道写端
dup2(fds[1], 1);
close(fds[0]);
execlp("ls", "ls", "-l", NULL);
perror("execlp");
exit(1);
}
else{
//父进程从管道接受输入流
dup2(fds[0], 0);
close(fds[1]);
execlp("wc", "wc", "-l", NULL);
perror("execlp");
exit(1);
}
视频解码器就类似于管道,拿走数据和接受数据以一定速度同时进行。
命名管道
匿名管道只能让子进程继承父进程文件描述符,从而得到管道描述符和内核的缓存。
命名管道是一种特殊文件,创建打开后,使两个无亲缘关系进程拿到同一块内核空间,与管道使用方法相同。
创建命名管道
int mkfifo (const char *filename, mode_t mode);
命名管道实现C/S通信:
/*server.c*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0);
int main()
{
umask(0);
if(mkfifo("mypipe", 0644) < 0){
ERR_EXIT("mkfifo");
}
int rfd = open("mypipe", O_RDONLY);
if(rfd < 0){
ERR_EXIT("open");
}
char buf[1024];
while(1){
buf[0] = 0;
printf("please wait\n");
ssize_t s = read(rfd, buf, sizeof(buf)-1);
if(s > 0){
buf[s-1] = 0;
printf("client say# %s\n", buf);
}
else if(s == 0){
printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);
}
else{
ERR_EXIT("read");
}
}
close(rfd);
return 0;
}
/*client.c*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0);
int main()
{
int wfd = open("mypipe", O_WRONLY);
if(wfd < 0){
ERR_EXIT("open");
}
char buf[1024];
while(1){
buf[0] = 0;
printf("please Enter #");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s > 0){
buf[s] = 0;
write(wfd, buf, strlen(buf));
}
else{
ERR_EXIT("read");
}
}
close(wfd);
return 0;
}
消息队列
消息队列提供一个进程向另外一个进程发送数据的方法,在内核中创建一块内存空间存放消息,由唯一的标识符qid标识。
msgget 用于创建,msgsnd将消息添加到队尾,msgrcv从队列取消息,但不一定按先进先出次序,也可以按消息类型取。
每个队列与一个结构体相关,存储了关于消息指针,消息个数,消息最大值,消息队列现在的大小等信息…
注:由于消息队列的效率和删除没有引用计数的问题,避免使用消息队列
创建消息队列
int msgget(key_t key, int msgflag);
key:消息队列的名字
msgflag:创建 IPC_CREAT | 权限
返回值:消息队列的标识符,类似于文件描述符。
发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflag);
参数:消息队列标识符,消息类型指针,消息类型的长度,状态控制。
msgflag:IPC_NOWAIT
自定义的消息类型
struct msg{
long type; //消息类型,区分队列中不同的消息通道
char text[100];
};
接受消息
ssize_t msgrcv(int qid, void *msgp, size_t msgsz, long msgtyp, int msgflag);
//send
void send(){
struct msg m;
int qid = msgget(123, IPC_CREAT);
long type;
scanf("%ld", &type);
m.type = type;
scanf("%s", m.text);
int ret = msgsnd(qid, &m, strlen(m.text), 0);
if(ret == -1)
perror("msgsnd");
}
//recv
void recv(){
struct msg m;
int qid = msgget(123, 0);
long type;
scanf("%ld", &type);
int ret = msgrcv(qid, &m, 100, type, 0);
printf("%s", m.text);
}
查看/删除消息队列
$ipcs -q
$ipcrm -Q key