进程间通信之管道
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信之管道相关的知识,希望对你有一定的参考价值。
目录
前言
我们知道进程之间具有独立性,进程之间互不影响。但是,进程间通信回事两个进程之间关联起来,一个进程的状态可能会影响另一个进程。但是着并不说明进程之间就不具备独立性了。
我们要实现进程间通信最重要的一点就是,要让通信的进程指向同一块资源(内存)。
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发售那个给另外一个进程。
- 资源共享:多个进程之间需要共享同样的资源。
- 通知事件:一个进程需要向另外一个或者一组进程发送消息,通知它们发生了某种事件(比如:进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时告知它的状态改变。
一.管道
管道本身是一个文件。用管道实现进程间的通信,实际上是通过文件来实现进程间的通信。
管道又分为匿名管道和命名管道,顾名思义,匿名管道,创建的管道名字是不知道的,命名管道,创建管道的名字是知道的。
注意:管道实现通信只能进程单向通信。一个进程读,一个进程写。
1.1 匿名管道
1.1.1 匿名管道的使用场景
匿名管道通常使用于有亲缘关系的进程之间,常用于父子进程。看完原理再来理解一下这句话。
有亲缘关系的进程,会继承同一个祖先进程的部分内容。其中files_struct是继承祖先进程的。可以使两亲缘关系的进程指向同一文件。
所以为什么子进程会默认打开stdin,stdout,stderr,但是子进程并没有指向open操作?
只需要祖先进程打开了stdin,stdout,stderr就好了,子进程会拷贝父进程内容。
1.1.2 匿名管道实现通信的原理
- 管道也是一个文件,站在文件的角度来理解管道实现通信的原理。
管道是一个文件,当一个进程以读和写的方式打开一个管道。再创建一个子进程,子进程会以父进程为模板,拷贝父进程的部分内容。此时file_strcut里的数组(文件描述符与文件的映射关系)会是父进程的拷贝。此时,父子进程都指向了管道文件(同一块空间),并且子进程也是以读写方式打开的该文件(因为子进程会继承父进程代码,父进程再创建子进程之前以读写方式打开的文件),如果将一个进程对文件进行写,一个进程对文件进行读,由于来给你进程指向同一空间,所以读进程拿到的数据就是写进程写进去的数据。此时就完成了对文件的通信。
- 站在内核角度,理解管道实现进程间通信的原理
父子进程指向同一个文件,该文件在内存中占用空间,于是两进程指向了同一块空间。
文件加载带内存需要开辟空间,一个进程如何找到文件的内存进行读写的?
task_struct有一个files_struct指针,可以找到files_struct,files_struct里有一个数组,数组下标对应文件描述符,可以找到对应文件struct_file。这样就找到文件了。
struct_file中有一个struct_path,可以找到对应目录,目录中保存文件名和inode的对应关系,就可以找到文件的inode。文件inode中有一个struct address_space,进入里面有struct radix_tree_root page_tree,就可以找到对应内存空间。
再调用struct_file里的const struct file_operations *f_op指针,调用对应的读写函数。就实现了进程间的通信
1.1.3 怎么创建管道
使用int pipe(int fd[2]),系统调用。
//头文件
#include <unistd.h>
//pipe系统调用是以读写的方式打开一个管道文件
int pipe(int pipefd[2]);
/*返回值:打开成功0,失败返回异常信号
参数:pipefd[2]是两个输出型参数,保存的是文件描述符
fd[0]读打开的管道文件描述符,fd[1]写打开管道的文件描述符*/
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main(){
5 int fd[2]={0};
6 //以读写方式打开管道
7 int res=pipe(fd);
8 if(res==0){
9 printf("fd[0]:%d,fd[1]:%d\\n",fd[0],fd[1]);
10
11 }
12 else{
13 perror("pipe error");
14 exit(1);
15 }
16
17 return 0;
18 }
1.1.4 怎么实现进程间通信
父进程创建一个全局变量,再创建子进程,可以实现进程间通信吗?
答案是不能,因为只有父子进程对全局变量有写入动作,就会进行写时拷贝,父子进程全局变量的内存空间就不是同一块空间了。
代码实现子进程写,父进程读。
子进程先关闭以都打开管道的文件描述符,使用write系统调用往管道写入数据。
父进程关闭以写打开的管道的文件描述符,使用read系统调用从管道读取数据。
注意:父子进程共享一个管道文件,管道文件只有一个,是因为打开方式不同所以有多个文件描述符。
pipe返回的文件描述符fd父子进程是共享的,因为父子进程没有对fd进行修改不会进行写时拷贝。
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<stdlib.h>
5 int main(){
6 int fd[2]={0};
7 //以读写方式打开管道
8 int res=pipe(fd);
9 if(res==0){
10 pid_t id = fork();
11 if(id==0){
12 //子进程,写
13 close(fd[0]);//关闭读
14 const char *buf="i am child...\\n";
15 while(1){//不断写
16 write(fd[1],buf,strlen(buf));
17 }
18
19 }
20 else if(id>0){
21 //父进程,读
22 close(fd[1]);//关闭读
23 char buf[256];
24 while(1){//不断读
25 ssize_t n=read(fd[0],buf,sizeof(buf)-1);
26 if(n>0){//读到数据
27 buf[n]=0;//最后加一个'\\0'
28 printf("%s",buf);
29 }
30
31 }
32 }
33 else{
34 //不成功
35 perror("fork error");
36 exit(1);
37 }
38 }
39 else{
40 perror("pipe error");
41 exit(1);
42 }
43
44 return 0;
45 }
1.1.5 管道通信额四种情况
- 当读进程进行读操作时,当读条件不满足,读进程进入阻塞状态。
读条件不满足:管道里没有数据或者说写端没有往管道写数据。
读进程进入阻塞状态:PCB的状态设置为S,该进程从运行队列进入等待队列,等待管道中有数据。
改变上面代码:子进程写数据时延时5s,父进程读数据没有时间限制
由于写进程比读进程慢,读进程在读时,大部分时间,管道是空的,此时读进程会进入阻塞状态,等待管道中有数据。
- 当写进程进行读操作时,当写条件不满足,写进程进入阻塞状态。
写条件不满足:管道满了的时候
写进程进入阻塞状态:PCB的状态设置为S,该进程从运行队列进入等待队列,等待管道中可以写入数据。
改变上面代码:子进程写数据没有时间限制,父进程读数据延时5s。
此时写进程块,读进程慢。一开始写进程往管道了写了很多数据并且将道写满了。
此时已经不符合写的条件,子进程进入阻塞状态。父进程来读管道里的数据,
待管道可以写数据时,子进程再进入运行状态,往管道写数据。
进程间同步的概念:一个进程快导致另一个进程也快,一个进程慢导致另一个进程也慢。一个进程受到控制,另外一个进程也受到控制,着就叫进程间同步。
- 如果写进程关闭写文件描述符,读进程会一直读到管道结尾。
这里介绍一下read系统调用的返回值,如果返回0,读到文件结尾,返回值不为0,表示实际读到值的个数。
再改一下上面的代码:
将写进程先往管道了写一些数据后关闭写文件描述符,再读进程进行读数据
读进程读数据也不是一行一行读的,读到缓冲区后,缓冲区满了再刷新出来。
这里说明,如果将写文件描述符关闭,读进程最终一定会读到管道的结尾。因为已经没有进程往文件中写入数据了。
- 如果读进程关闭读文件描述符,写进程可能会被系统直接杀死,系统会传入13好信号SIGPIPE,杀死进程,进程异常退出。
改一下上面的代码:
通过监控进程状态发现:
读进程退出读文件描述符,系统传13号信号杀死写进程。并不是将写进程变成僵尸状态,上面是因为写进程是子进程,但是父进程没有退出。
所以一般先将写进程关闭写文件描述符,再关闭读进程的读文件描述符。
总结一下四种情况:
读写进程都不关闭文件描述符,但是如果一个进程快一个进程慢,需要快的进程以阻塞方式等慢的进程。
读写进程其中一个关闭文件描述符,关闭写文件描述符,读进程会读到管道结尾。关闭读文件描述符,系统可能会传13号信号杀死写进程。
1.1.6 匿名管道的特征
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常一个管道由一个进程创建,然后进程调用fork,父子进程之间就可以进程通信了。
- 管道提供流式服务,就是一个数据一个数据传,相比较数据包,多个数据构成数据包,以整个数据包为单位。
- 进程退出管道释放,管道声明周期随进程的
- 内核对管道的操作式同步和互斥的。同步一个进程快,另外一个也快,一个慢,另外一个也慢。互斥是,一个再写,另外一个不会正好读,一个在读,不会另外一个进程正好写。
- 管道是半双工的,数据只能一个方向流动,需要双方通信时,需要建立两个管道。
1.2 命名管道
从上面我们知道匿名管道只能用于具有同一祖先(具有亲缘关系)的进程。
命名管道可以用于不相关的两进程之间进行通信。
命名管道是一个特殊的管道
先说明,上面关于管道的使用时的四种情况在这里一样适用,特点也符合。
1.2.1 命名管道的原理
其实原理和匿名管道差不多,只是需要先创建一个命名管道。再一个进程以读或者写的方式来打开该管道文件,再另外一个进程不需要创建管道,只需要以写或者读的方式来打开管道文件。再调用读写系统调用来往文件写或者读,来进行进程间通信。
两进程分别对同一管道文件分别用读或写的方式打开,两进程看到同一文件(资源)。
不需要创建子进程,可以是两个不相关的进程。
1.2.2 创建命名管道
- 使用命令行 mkfifo 文件名
- 使用函数int mkfifo(const char *filename,mode_t mode)
//头文件
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//返回值:成功返回0,失败返回-1
//参数:filename 路径+文件名
// mode:权限
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5
6 int main(){
7 umask(0);//设置掩码为0
8 int res=mkfifo("./fifo",0644);
9 if(res==-1){
10 perror("mkfifo error");
11 exit(2);
12 }
13 return 0;
14 }
1.2.3 命名管道和匿名管道的区别
匿名管道只能用于具有亲缘关系的进程,命名管道剋用于不相关的进程之间。
匿名管道用pipe创建并且会以读写方式打开匿名管道文件。
命名管道用mkfifo来创建命名管道文件,再用open来通过读或者写的方式打开文件。
1.2.4 实现进程间通信
用命令行实现进程间的通信:
用命名管道系统调用实现client和server进程的通信。
client.c文件代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<fcntl.h>
5 #include<stdlib.h>
6 #include<sys/types.h>
7 #include<sys/stat.h>
8
9 int main(){
10
11 int fd=open("./fifo",O_WRONLY);//写的方式打开进程
12 if(fd==-1){
13 perror("open error");
14 exit(-1);
15 }
16 char str[60];
17 while(1){
18 printf("please enter message #");
19 fflush(stdout);
20 ssize_t n=read(0,str,sizeof(str)-1);//从标准输入中获取字符串
21 if(n>0){
22 str[n]=0;
23 write(fd,str,n);//写道管道中
24 }
25 }
26
27 return 0;
28 }
server.c文件代码
服务器读端怎么退出?
管道的生命周期是随进程的,用户端(client)写端进程退出,服务器端(server)读端最终会读到管道的结尾。然后再退出进程即可。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<fcntl.h>
5 #include<stdlib.h>
6 #include<sys/types.h>
7 #include<sys/stat.h>
8
9 int main(){
10 umask(0);//设置掩码为0
11 int res=mkfifo("./fifo",0644);//创建一个命名管道
12 if(res==-1){
13 perror("mkfifo error");
14 exit(2);
15 }
16
17 int fd=open("./fifo",O_RDONLY);//只读的方式打开管道
18 if(fd==-1){
19 perror("open errord");
20 exit(-1);
21 }
22 while(1){
23 char str[256]={0};
24 ssize_t n=read(fd,str,sizeof(str)-1);//读管道里的内容
25 if(n>0){
26 str[n]=0;
27 printf("%s",str);
28 }
29 else if(n==0){//写进程退出,管道里的数据读完,退出进程
30 printf("client quit,me too\\n");
31 break;
32 }
33 else{
34
35 }
36 }
37
38 return 0;
39 }
服务器不仅可以将用户输入的数据往显示器上打,还可以往文件上打,首先将要打入的文件打开,适用dup2(1,打开文件的文件描述符) ,就会将信息往文件里打了。
#include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<fcntl.h>
5 #include<stdlib.h>
6 #include<sys/types.h>
7 #include<sys/stat.h>
8
9 int main(){
10 umask(0);//设置掩码为0
11 int res=mkfifo("./fifo",0644);//创建一个命名管道
12 if(res==-1){
13 perror("mkfifo error");
14 exit(2);
15 }
16
17 int fd=open("./fifo",O_RDONLY);//只读的方式打开管道
18 int od=open("./test.txt",O_WRONLY| O_CREAT, 0644);//打开一个文件
19 if(fd==-1){
20 perror("open errord");
21 exit(-1);
22 }
23 dup2(od,1);//重新向
24 close(od);
25 while(1){
26 char str[256]={0};
27 ssize_t n=read(fd,str,sizeof(str)-1);//读管道里的内容
28 if(n>0){
29 str[n]=0;
30
31 printf("%s",str);
32 }
33 else if(n==0){//写进程退出,管道里的数据读完,退出进程
34 printf("client quit,me too\\n");
35 break;
36 }
37 else{
38
39 }
40 }
41
42 return 0;
43 }
命名管道文件到底是什么?
实际上命名管道fifo只是一个标志,实际系统会在内存开辟一段空间给管道,往管道里写入就是往这块内存中写入,从管道里读,就是从内存中读。
因为如果是真的在硬盘上创建一个文件,再从往硬盘文件写和读,就是往外设写和读,就是系统的I/O了。这样的时间效率回很低。
1.3 总结
匿名管道还是命名管道都是通过文件的方式,来让两个进程看到同一份资源。通过一个进程对文件进行写操作,一个文件进程读操作,来实现进程间的通信。
管道实现进程通信是单向的。同步和互斥的。
匿名管道适用于具有血缘关系的进程,命名管道可以用于不相关的进程。
理解一个命令:who | wc - l
中间的'|'是一个匿名管道,who和wc是两个进程,'|'系统通过pipe创建一个匿名管道,bash创建子进程who,bash再创建子进程wc,who和wc是兄弟进程。who和wc都会继承bash的匿名管道文件。who和wc看到统一资源,who往管道写数据,wc从管道读数据。往后数据写完退出,wc读数据到最后也退出了。
以上是关于进程间通信之管道的主要内容,如果未能解决你的问题,请参考以下文章