首先我们来了解一下什么是进程间通信?
进程间通信就如同人与人之间的交流一样,注重方式,各有利弊。通俗点讲进程间通信就是在不同进程之间传播或交换信息,那么,我们都知道进程之间是相互独立的,那么进程间通信的本质就是让两个毫不相干的进程看到同一份资源,那么我们又是如何解决这些矛盾的呢?
我们实现进程间通信的主要目的是什么呢?
1.数据传输:一个进程需要将它的数据发送给另一个进程
2.资源共享:多个进程之间共享同样的资源
3.通知事件:一个进程需要向另一个或者一组进程发送消息,通知它(它们)发生了某种事件(如进程终止要通知父进程)
4.进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望进程能够截断另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信的主要分类
进程间通信主要包括管道, 系统IPC(包括消息队列,信号,共享存储), 套接字(SOCKET)等等
今天,我们先来看一种-------->管道
什么是管道
想想我们生活中的各种管道(比如水管,电缆等),他们是不是都是用来传输某种资源的呢?类比而言,管道是Unix种最古老的进程间通信的方式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
管道的分类
匿名管道和命名管道
什么是匿名管道
匿名管道是一种最基本的IPC机制
匿名管道的特点
1.只能在具有亲缘关系的进程之间进行通信(通常一个管道由一个进程创建,然后该进程调用fork创建子进程,此后父子进程就可应用该管道了)
2.管道是基于字节流的
3.管道生命周期随进程(进程退出,管道不复存在)
4.内核会对管道操作进行同步与互斥
5.管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道(如下图)
如何创建匿名管道?
功能:创建一个无名管道
头文件:#include<unistd.h>
原型: int pipe(int fd[2]);
参数fd: 文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码
应用实例1:
从键盘上读取数据,写入管道,读取管道,写到屏幕
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int fds[2];
char buf[100];
int len;
if(pipe(fds)==-1)
perror("make pipe"),exit(1);
//read from stdin
while(fgets(buf,100,stdin)){
len=strlen(buf);
//write into pipe
if(write(fds[1],buf,len)!=len){
perror("write to pipe");
break;
}
memset(buf,0x00,sizeof(buf));
//read from pipe
if((len=read(fds[0],buf,100))==-1){
perror("read from pipe");
break;
}
//write to stdout
if(write(1,buf,len)!=len){
perror("write to stdout");
break;
}
}
结果显示
应用实例2
首先我们先来分析一下怎么用fork来共享管道原理(如图)
代码实现:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
if (pipe(fd) == -1){
perror("error pipe");
return 1;
}
//father write child read
pid_t pid = fork();//create child
if (pid<0){
perror("error fork");
return 2;
}
//chlid
else if (pid == 0){
close(fd[1]);//child close to write
char arr[128];
int count = 5;
while (count--){
ssize_t ss = read(fd[0], arr, sizeof(arr));
if (ss>0){
arr[ss] = '\0';
printf("father--->child: %s\n", arr);
}
else if (ss == 0){
printf("father close write");
break;
}
else{
perror("error read");
break;
}
}
close(fd[0]);
}
//father
else{
close(fd[0]);//father close to read
char *str = "hai,luying";
int count = 5;
while (count--){
write(fd[1], str, strlen(str));
sleep(1);
}
close(fd[1]);
}
return 0;
}
结果显示:
管道读写规则
1.当没有数据可读时(read 没有一点数据可读或 write 没有一点空间可以写入,如果disable O_NONBLOCK 则会阻塞,如果enable O_NONBLOCK 则会返回-1,errno = EAGAIN | EWOULDBLOCK 错误。)
(1)O_NONBLOCK bisable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<fcntl.h>
5 int main()
6 {
7 int fd[2];
8 if(pipe(fd)==-1){
9 perror("pipe error");
10 exit(EXIT_FAILURE);
11 }
12 pid_t pid;
13 pid=fork();
14 if(pid==-1){
15 perror("fork error");
16 exit(EXIT_FAILURE);
17 }
18 if(pid==0){
19 close(fd[0]);//close to read
20 sleep(5);
21 char* str="luying";
22 int len=strlen(str);
23 write(fd[1],str,len);
24 exit(EXIT_SUCCESS);
25 }
26 close(fd[1]);//close to write
27 char buf[20]={0};
28 read(fd[0],buf,20);
29 printf("wait to receive: %s\n",buf);
30 return 0;
31 }
运行结果
分析:子进程睡眠了5秒后,read调用阻塞,父进程暂时没有读出数据,5秒后子进程向管道中写入数据,父进程才读到数据
(2)O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd[2];
if (pipe(fd) == -1){
perror("pipe error");
}
pid_t pid;
if (pid == -1){
perror("fork error");
}
if (pid == 0){
close(fd[0]);//close to read
sleep(5);
char* str = "luying";
int len = strlen(str);
write(fd[1], str, len);
}
close(fd[1]);//close to write
char buf[20] = { 0 };
int ret = fcntl(fd[0], F_GETFL);//F_GETFL读取文件状态标志
//int fcntl(int fd, int cmd, ... /* arg */ );
fcntl(fd[0], F_SETFL, ret | O_NONBLOCK);//设置为阻塞模式
int net = read(fd[0], buf, 20);
if (net == -1){
perror("read error");
}
printf(" receive: %s\n", buf);
return 0;
}
运行结果:
分析:
2.当管道满的时候
(1)O_NONBLOCK disable:write调用阻塞,直到有进程读走数据
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main(void)
{
int fds[2];
if(pipe(fds) == -1){
perror("pipe error");
exit(EXIT_FAILURE);
}
int ret;
int count = 0;
while(1){
ret = write(fds[1],"luying",6);//fds[1]默认是阻塞模式
if(ret == -1){
perror("write error");
break;
}
count++;
}
return 0;
}
运行结果
fd打开时默认是阻塞模式,当pipe缓冲区满时,write操作确实阻塞了,等待其他进程将数据从管道中取走
(2)O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
接下来我们来验证几种有趣的情况~
(1)写端一直在写,读端不关闭自己的读端,但是他也不读,会出现什么问题呢?
我们用代码实现一下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0){
perror("error pipe");
return 1;
}
pid_t id = fork();
if (id == 0){//child ->w
close(fd[0]);
const char * msg = "hello father,I am child\n";
int count = 0;
while (1){
write(fd[1], msg, strlen(msg));
printf("%d\n", count++);
}
}
else{//father ->r
close(fd[1]);
while (1){}
}
}
结果显示:
分析,我们发现,写端写满之后,暂时不在写数据,那是否是在等待读端读走之后再继续写呢?为了验证我们的想法,我们修改下代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0){
perror("error pipe");
return 1;
}
pid_t id = fork();
if (id == 0){//child ->w
close(fd[0]);
const char * msg = "hello father,I am child\n";
int count = 0;
while (1){
write(fd[1], msg, strlen(msg));
printf("%d\n", count++);
}
}
else{//father ->r
close(fd[1]);
while (1){
sleep(5);
ssize_t s = read(fd[0], buf, sizeof(buf)-1);
if (s>0){
buf[s] = '\0';
printf("father say:%s\n", buf);
}
}
}
运行结果如下:分析:我们发现,验证了我们的猜测,子进程写满之后,隔了5秒父进程读走了数据之后,子进程开始继续写入文件
(2)如果今天写端间歇性的写,读端一直在读,那么会发生什么呢:
代码修改为:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0){
perror("error pipe");
return 1;
}
pid_t id = fork();
if (id == 0){//child ->w
close(fd[0]);
const char * msg = "hello father,I am child\n";
while (1){
write(fd[1], msg, strlen(msg));
sleep(10);
}
}
else{//father ->r
close(fd[1]);
int count = 0;
while (1){
ssize_t s = read(fd[0], buf, sizeof(buf)-1);
if (s>0){
buf[s] = '\0';
printf("father say:%s\n", buf);
}
printf("father read %d\n", count++);
}
}
运行结果:分析,我们发现写端每隔秒写一次,与此同时,读端也在等待写端写入,从而才读入数据
管道如果没数据,读端会一直等待,直到有数据写入,就立即读出
(3)那么如果今天写端写入固定数据之后不仅不写了,而且把写端关闭了,会出现什么情况呢?
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0){
perror("error pipe");
return 1;
}
pid_t id = fork();
int count = 0;
if (id == 0){//child ->w
close(fd[0]);
const char * msg = "hello father,I am child\n";
while (1){
write(fd[1], msg, strlen(msg));
printf("%d\n", count++);
if (count > 10){
close(fd[1]);
break;
}
sleep(1);
}
}
else{//father ->r
close(fd[1]);
int count = 0;
while (1){
ssize_t s = read(fd[0], buf, sizeof(buf)-1);
if (s>0){
buf[s] = '\0';
printf("father say:%s\n", buf);
}
else if (s == 0){
printf("pipe close\n");
break;
}
}
}
运行结果:
分析:我们可以看到,当写端不再写的时候,读端读到管道的末尾之后退出
(4)写端一致在写,读端不但不读,而且还关闭了读端,那么写端会发生什么情况呢?
修改代码如下:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0){
perror("error pipe");
return 1;
}
pid_t id = fork();
int count = 0;
if (id == 0){//child ->w
close(fd[0]);
const char * msg = "hello father,I am child\n";
while (1){
write(fd[1], msg, strlen(msg));
printf("%d\n", count++);
sleep(1);
}
}
else{//father ->r
close(fd[1]);
int count = 0;
while (1){
ssize_t s = read(fd[0], buf, sizeof(buf)-1);
if (s>0){
buf[s] = '\0';
printf("father say:%s\n", buf);
sleep(3);
break;
}
}
close(fd[0]);
int status = 0;
wait(&status);
printf("sig:%d\n", status & 0x7F);
}
}
运行结果如下:
分析:如果所有管道 读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
【注意】
1.如果所有管道写端对应的文件描述符被关闭,则read返回0;
2.如果所有管道 读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
3.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
4.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
那么PIPE_BUF是多大呢?
认识了匿名管道,我们来看看命名管道
命名管道
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作
命名管道与匿名管道的区别
(1)匿名管道由pipe函数创建并打开,而命名管道由mkfifo函数创建,打开用open
(2)匿名管道只能在具有血缘关系的进程间通信
(3)FIFO(命名管道)与pipe(匿名管道)之间只是创建于打开的方式不同,工作完成之后,他们具有相同的意义
创建命名管道
(1)命令行上创建
$ mkfifo filename

(2)程序中创建,相关函数
int mkfifo(const char *filename,mode_t mode);
创建命名管道:
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
命名管道的打开规则
1.如果当前打开操作是为读而打开FIFO时
(1)O_NONBLOCK disable:阻塞直到有相关进程为写而打开该FIFO
(2) O_NONBLOCK enable:立刻返回成功
2.如果当前打开操作是为写而打开FIFO时
(1)O_NONBLOCK disable:阻塞直到有相关进程为读而打开该FIFO
(2) (2) O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
应用实例----》用命名管道实现server&client通信
Makefile
.PHONY:all
all : server client
server : server.c
gcc - o $@ $ ^
client:client.c
gcc - o $@ $
.PHONY:clean
clean :
rm - f server client
server.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
int main()
{
if (mkfifo("./fifo", 0644)<0){
perror("error mkfifo\n");
return 1;
}
int fd = open("./fifo", O_RDONLY);
if (fd < 0){
perror("error open");
return 2;
}
char buf[100];
while (1){
ssize_t s = read(fd, buf, sizeof(buf)-1);
if (s > 0){
buf[s] = '\0';
printf("client---->server: %s\n", buf);
}
else if (s == 0){
printf("client quit\n");
break;
}
}
close(fd);
return 0;
}
运行结果
至此,今天,我们就把管道讨论完了,认识比较浅,代码能力也比较差,希望大家多多指教!