相信大家平时没少用微信、QQ这样的聊天工具,通过这样的聊天工具通讯属于互联网通信,在往下一级便是内网通信,也就是在同一局域网内互相通信,再接着便是我们今天要讲的进程间通信了
进程间通信有以下几种方式:信号(Signal)、消息队列、管道(pipe)等
今天我们来分别讲一讲两种管道:匿名管道和命名管道
既然说到了管道,大家不妨想想,管道都有什么特性呢?
相信大家都用过吸管喝过水,那么就来拿这来举个例子
- 吸管内的水应该具有方向性,同一时间内只能是由一端流向另一端,匿名管道内的数据也是如此,同一时间内,数据只能是从写端流向读端,也就是绝大多数时间内,访问管道单工使用。
- 当我们掐住吸管的两头时,吸管内的水应该可以在里面存放一段时间,匿名管道也是如此。数据就好比水,掐住吸管两头就好比进程不进行数据的读写 , 匿名管道也应该有暂存数据的能力,也就是管道缓冲区,但是这个管道缓冲区很小,在Ubuntu16.0中只有4K(不同的ubuntu版本,管道缓冲区大小不同)
- 匿名管道只能完成亲缘进程之间的通信。为什么匿名管道会有这样的特性呢?这就需要我们了解一下匿名管道是如何创建的了
假设现在有两个进程,分别是进程1和进程2,进程1中有一条信息想要让进程2读取到,接下来我想问大家一个问题:管道是建立在进程的用户层还是内核层呢?
首先大家要知道一些概念:
- 一个进程的用户层是不允许其他进程访问的,一个进程的用户层是自己完全私有,只允许本进程创建数据或对用户层中的数据进行读写
- 同一终端下进程的内核层一般都是放在一片空间下统一管理的,系统对内核层具有操作权限
在了解了这些之后,相信大家应该就能明白了,匿名管道是建立在内核层的,并且几乎所有的进程间通信技术,都是利用进程内核层(共享内存)完成进程通信
想要创建匿名管道,我们就要用到一个函数:pipe函数
头文件 | #include |
功能 | 创建一个匿名管道 |
语法 | int pipe(int fds[2]); |
返回值 | 创建成功返回0,创建失败返回-1 |
参数介绍:相信大家没有看懂 "int pipe(int fds[2]);"中的这个int fds[2],我们来介绍一下这个参数,这个参数里存放的是两个文件描述符,fds[0]是文件的读权限,fds[1]是文件的写权限
接着,进程1使用pipe函数创建一个管道,得到管道的读写权限,结果如下图所示:
这时候一段有趣的对话就出现了:
进程1:大哥,我这边管道创建好了,读写的那两个文件描述符我也拿到了,你快来用这个管道啊!
进程2:老弟呀,大哥我知道你啥都搞好了,但是我这边没有那两个文件描述符来对管道进行读写啊
进程1:那还不简单,大哥你也用pipe函数创建一个管道获取文件描述符呗!
进程2:老弟呀,我要用pipe函数了,我这不就创建了一个新的管道了嘛,我还是对你创建的那个管道没有读写权限啊
进程1:那咋整啊大哥,老弟我有一大堆话想通过管道跟你说呢
进程2:老弟啊,现在就只有一个办法了!
进程1:啥办法呀大哥?
进程2:这样,你通过fork函数把你那两个文件描述符传过来,但是注意啊,一定要把pipe函数放在fork函数之前啊!否则我也会用pipe函数创建管道了
进程1:行吧,但是这样咱俩以后就得各论各的了,这样吧,以后,我管你叫哥,你管我叫爸!
进程2:我***********************
相信看完上面这段对话以后,大家也就明白为什么我们前面所说的:匿名管道只能完成亲缘进程之间的通信,fork函数创建的当然是亲缘进程了
我们来实现一个父写子读的功能
#include
#include
#include
#include
#include
#include
#define message "Hello mother fucker!"
int main(void)
{
//定义一个数组存放文件描述符
int fds[2];
//最好不要用变量来接fds,因为你只得到了里面的数值,pipe返回的fds里面有文件描述符,
//很可能你不并不能用接取到的变量来访问管道
pipe(fds);
printf("管道创建成功,这是一个父写子读的管道!\n");
//创建父子进程
pid_t pid;
pid = fork();
//父进程进入该判断
if(pid > 0)
{
//关闭读权限
close(fds[0]);
printf("信息准备发送,已关闭父端读管道的权限\n");
write(fds[1] , message , strlen(message));
//数据写入,关闭写权限
close(fds[1]);
printf("信息已经发送,已关闭父端写管道的权限\n");
//父进程阻塞回收子进程
pid_t zpid;
while(zpid = wait(NULL))
{
if(zpid > 0)
{
printf("父进程No.%d 已成功回收子进程No.%d\n" , getpid() , zpid);
printf("父进程已成功退出!\n");
exit(0);
}
else
printf("子进程仍不可回收,父进程持续阻塞等待回收中!\n");
}
}
//子进程创建成功后进入该判断
else if(pid == 0)
{
//关闭写权限
close(fds[1]);
printf("子进程准备读取管道内的消息,已关闭子端写管道的权限\n");
//定义信息接收缓冲区
char buffer[1024];
//清空缓存内的消息
bzero(buffer , sizeof(buffer));
//读取管道缓冲区内的信息放入buffer中
read(fds[0] , buffer , sizeof(buffer));
//关闭读权限
close(fds[0]);
printf("子进程已经读取到管道内的消息,消息为: %s ,已关闭子端读管道的权限\n",buffer);
exit(0);
printf("子进程先于父进程退出,成为僵尸态进程!\n");
}
//如果子进程创建失败,进入该判断
else
{
perror("进程创建失败!\n");
return 0;
}
return 0;
}
运行结果:
相信有一些细心的同学发现了,在父进程向管道写入数据之前,我们关闭了父进程读的权限,在子进程读取管道数据之前,我们关闭了子进程写的权限,我们为什么这么着急关闭这两个进程用不到的权限呢?这里就涉及到一个问题:指向管道的描述符问题,也就是文件描述符
这里我们要引出一个概念,也就是管道的引用计数:每当存在一个指向管道的描述符,也就是文件描述符时,管道的引用计数加一。只有当管道的引用计数为0时,管道才能被系统释放
在很多情况下,往往不是一父一子通过匿名管道进行通信,而是很多亲缘进程通信,也就是会有很多文件描述符,但凡有一个进程某个文件描述符没关闭,管道都无法被系统释放而占用内存空间,所以使用匿名管道时要养成尽早关闭无用权限的习惯
四种特殊情况 | 情况简介 |
写阻塞 | 管道缓冲区被写满,无法继续放入数据,从而阻塞写端 |
读阻塞 | 管道缓冲区为空,无数据可读,从而阻塞读端 |
写关闭 | 写端退出,读端读取剩余内容,管道内无内容时,读端读取0 |
读关闭(最特别的情况) | 读端退出,此时写端写入的数据就是垃圾数据,系统发送SIGPIPE信号,杀死写进程 |
相比匿名管道,命名管道的限制要小一些,它可以实现非亲缘进程之前的通信,但是要使用命名管道,我们要先创建管道文件,管道文件有两种创建方式:
- 命令创建:mkfifo 管道文件名
- 函数创建:mkfifo(char* 管道文件名 , int 文件权限)
- 不论是多少个进程要使用同一管道文件来进行通信,这些进程的权限加起来要满足读写权限(比如有100个进程,但他们都只有读权限,没有一个有写权限,那么管道文件就无法被使用)
- 当一个进程内有多个线程在同一读序列时,阻塞只对第一个线程有效,其他读线程自动被设置为非阻塞
要想理解第二种特殊情况,举个排队挂号的例子实在是再合适不过了
假设现在有一个队伍,共有五个人在排队等待挂号,第一个人已经到了挂号窗口,肯定是专心致志的等待拿到号,这就相当于阻塞状态,而还没到窗口的后面四个人肯定是干自己的事,比如玩手机什么的,这就相当于非阻塞状态,等到了窗口再专心等号
这样做的好处就是可以降低等待开销,可以让其他的线程去干一干自己的事
既然是非亲缘进程,自然就要写两个程序,这里我们采用函数创建管道文件的方式
process_1.c(负责写入数据)
#include
#include
#include
#include
#include
#include
#include
#define MSG "儿子,儿子,我是你爸爸!"
int main(void)
{
//创建管道文件
mkfifo("fifo" , 0664);
//写打开
int wfd = open("fifo" , O_WRONLY);
write(wfd , MSG , strlen(MSG));
printf("写进程已经成功发送数据\n");
//关闭文件
close(wfd);
exit(0);
}
process_2.c(负责读取数据)
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
int rfd = open("fifo" , O_RDONLY);
char buffer[1024];
bzero(buffer , sizeof(buffer));
read(rfd , buffer , sizeof(buffer));
printf("读进程已成功接收到消息:%s\n" , buffer);
//关闭文件
close(rfd);
exit(0);
}
运行结果
- 当我们要写入的数据大于管道大小时,就会进行非原子访问,来对数据进行裁剪来使其传入管道中,显而易见,非原子访问的优点就是速度快,管道利用率高,缺点就是要校验数据的完整性和正确性
- 当我们要写入的数据小于管道大小时,就会进行原子访问,将数据整个放入管道中,显而易见,原子访问的优点就是不用担心数据的一致性和完整性,缺点就是速度慢,管道利用率低(因为写入数据比管道小,所以一定有空间没有利用上)
大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答
今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!