文章目录
- 前言
- 一、进程间通信
- 1.概念
- 2.目的
- 3.进程间通信分类
- 二、管道
- 1.管道介绍
- 2.管道分类
- 1.匿名管道
- pipi
- 创建管道文件,打开读写端
- fork子进程
- 关闭父进程的读取端,关闭子进程的写入端
- 读写特征
- 管道特征
- 2.命名管道
- mkfifo
- 创建管道文件
- 删除管道文件
- 通信
- 三、匿名管道和命名管道的区别和联系
- 总结
前言
管道是Linux中最古老的进程间通信的方式,本文介绍了进程间通信的相关概念,主要介绍了匿名管道和命名管道。
一、进程间通信
1.概念
什么是进程间通信?
进程具有独立性,每个进程之间是互不干扰的状态,但是一个大的项目,不会只让一个进程独立完成所有工作,所以进程间是一定会有通信的情况,同时进程间通信的成本一定不低(通信的本质:OS需要直接或间接给通信双方的进程提供“内存空间”,而要通信的进程,必须看到一份公共的资源)。
成本不低的原因:我们需要让不同的进程看到同一份资源。
2.目的
进程间通信的目的是:
- 数据传输:一个进程需要将它的数据发送给另一个进程;
- 资源共享:多个进程之间共享同一个资源;
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某件事(例如:子进程终止时,要通知父进程);
- 进程控制:有的进程希望完全控制另一个进程大的执行(例如:Debug进程)。
为什么需要进程间通信?
有时候我们需要多进程协同,共同完成某种业务内容。例如:管道。
3.进程间通信分类
我们所说的不同通信种类本质上是按照:上面所说的资源是OS中的哪一个模块提供的来划分的。如:文件系统提供的叫做管道通信;OS对应的System V模块提供的……
- 采用标准的做法:System V进程间通信(聚焦在本地通信,如共享内存)、POSIX进程间通信(让通信过程可以跨主机);
- 采用文件的做法:管道——基于文件系统(匿名管道、命名管道)。
本文主要介绍的是管道。
二、管道
1.管道介绍
- 管道是Unix中最古老的进程间通信的方式,我们把连接两个进程的数据流称为“管道”。
- 任何一个文件包括两套资源:1.file的操作方法;2.属于自己的内核缓冲区。所以父子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以在对应的文件缓冲区读取,这样就可以完成进程间通信。这种方式中被子进程写入和父进程读取的文件,我们称为管道文件。管道文件本质就是内存级文件(不需要IO)。
- 两个进程如何看到同一个管道文件?
fork创建子进程,管道创建时,要分别以读和写的方式打开同一个文件(如果父进程是以只读或只写的方式(其中一种方式)打开文件,子进程也只会继承只读或者只写,父子双方打开文件的方式一样,导致无法完成单向通信)
父子进程正确进行管道通信的方式: - 父进程创建管道,同时具有读写权限。
- 父进程创建子进程,子进程继承管道以及对管道的读写
- 父进程关闭读取端,子进程关闭写入端。自此父进程只能向管道中写入,子进程只能从管道中读取,完成了父子进程的单向通讯。
2.管道分类
管道根据是否具有文件名,分为匿名管道和有名管道。
1.匿名管道
通过父进程创建子进程,子进程继承文件地址的方式,让父子进程看到同一个内存级文件,该内存级文件没有名称,则就称为匿名管道。匿名管道可以用来进行父进程和子进程之间的进程间通信。
pipi
pipi创建一个管道,只需要调用pipe系统调用。
它的头文件是unistd.h
;调用成功就返回0,调用失败就返回-1;其参数是输出型参数。
创建管道文件,打开读写端
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 int main()
5 {
6 int fds[2];
7 int n = pipe(fds);
8 assert(n == 0);
9 printf("fds[0]:%d\n",fds[0]);
10 printf("fds[1]:%d\n",fds[1]);
11 return 0;
12 }
因此,fds[0]:3代表读取,fds[1]:4代表写入;
fork子进程
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 int main()
5 {
6 int fds[2];
7 int n = pipe(fds);
8 assert(n == 0);
9 pid_t id = fork();
10 assert(id >= 0);
11 if(id == 0)//子进程
12 {
13 //子进程通信
14 exit(0);
15 }
16 //父进程
17 n = waitpid(id, NULL, 0);
18 assert(n == id);
19 return 0;
20 }
关闭父进程的读取端,关闭子进程的写入端
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<stdlib.h>
10 int main()
11 {
12 int fds[2];
13 int n = pipe(fds);
14 assert(n == 0);
15 pid_t id = fork();
16 assert(id >= 0);
17 if(id == 0)//子进程
18 {
19 //子进程通信,关闭子进程的读取端,即子进程进行写入
20 close(fds[0]);
21 const char* s = "你好,我是子进程,正在进行通信";
22 int cnt = 0;
23 while(1)
24 {
25 cnt++;
26 char buffer[1024];
27 snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]",s,cnt,getpid());
28 write(fds[1], buffer, strlen(buffer));
29 sleep(1);//每一秒写一次
30 }
31 close(fds[1]);//退出子进程前关闭文件写入端
32 exit(0);
33 }
34 //父进程
35 close(fds[1]);//父进程关闭写入端,即父进程进行读取
36 while(1)
37 {
38 char buffer[1024];
39 ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
40 if(s > 0) buffer[s] = 0;
41 printf("Get Message : %s | mypid = %d\n", buffer, getpid());
42 }
43 n = waitpid(id, NULL, 0);
44 assert(n == id);
45 close(fds[0]);//退出程序前,关闭读取端
46 return 0;
47 }
读写特征
- 读快,写慢
子进程休眠时,不再写入,父进程仍在读取(如果管道内没有数据,而读端在读取,则会默认直接阻塞当前正在读取的进程);
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<stdlib.h>
10 int main()
11 {
12 int fds[2];
13 int n = pipe(fds);
14 assert(n == 0);
15 pid_t id = fork();
16 assert(id >= 0);
17 if(id == 0)//子进程
18 {
19 //子进程通信,关闭子进程的读取端,即子进程进行写入
20 close(fds[0]);
21 const char* s = "你好,我是子进程,正在进行通信";
22 int cnt = 0;
23 while(1)
24 {
25 cnt++;
26 char buffer[1024];
27 snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]",s,cnt,getpid());
28 write(fds[1], buffer, strlen(buffer));
29 sleep(50);//每一秒写一次
30 }
31 close(fds[1]);//退出子进程前关闭文件写入端
32 exit(0);
33 }
34 //父进程
35 close(fds[1]);//父进程关闭写入端,即父进程进行读取
36 while(1)
37 {
38 char buffer[1024];
39 printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
40 ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
41 printf("888888888888888888888888888888888888!!\n");
42 if(s > 0) buffer[s] = 0;
43 printf("Get Message : %s | mypid = %d\n", buffer, getpid());
44 }
45 n = waitpid(id, NULL, 0);
46 assert(n == id);
47 close(fds[0]);//退出程序前,关闭读取端
48 return 0;
49 }
2.读慢写快
读取管道的进程一直不进行读取,而写端一直在写入。写端可以向管道内写入,但是管道是固定大小的缓冲区,不断的只写不读管道会被写满。满了以后就不能再写入了,此时写端会处于阻塞状态。
文件test.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<stdlib.h>
10 int main()
11 {
12 int fds[2];
13 int n = pipe(fds);
14 assert(n == 0);
15 pid_t id = fork();
16 assert(id >= 0);
17 if(id == 0)//子进程
18 {
19 //子进程通信,关闭子进程的读取端,即子进程进行写入
20 close(fds[0]);
21 const char* s = "你好,我是子进程,正在进行通信";
22 int cnt = 0;
23 while(1)
24 {
25 cnt++;
26 char buffer[1024];
27 snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]",s,cnt,getpid());
28 write(fds[1], buffer, strlen(buffer));
29 printf("count: %d\n",cnt);
30 }
31 close(fds[1]);//退出子进程前关闭文件写入端
32 exit(0);
33 }
34 //父进程
35 close(fds[1]);//父进程关闭写入端,即父进程进行读取
36 while(1)
37 {
38 sleep(50);//父进程不读
39 char buffer[1024];
40 ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
41 if(s > 0) buffer[s] = 0;
42 printf("Get Message : %s | mypid = %d\n", buffer, getpid());
43 }
44 n = waitpid(id, NULL, 0);
45 assert(n == id);
46 close(fds[0]);//退出程序前,关闭读取端
47 return 0;
48 }
如果,让父进程只sleep(2)
的话,读取的速度稍微比较慢:
这种情况,写端是将数据塞到管道内,管道读取是安装指定大小读取(并非一行一行的读取,最初安装一行来读取是因为写入的慢,一次只写一行数据,数据就被读取了)。
- 写入端关闭,则读取到0(管道末尾)读取端关闭
文件test.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<stdlib.h>
10 int main()
11 {
12 int fds[2];
13 int n = pipe(fds);
14 assert(n == 0);
15 pid_t id = fork();
16 assert(id >= 0);
17 if(id == 0)//子进程
18 {
19 //子进程通信,关闭子进程的读取端,即子进程进行写入
20 close(fds[0]);
21 const char* s = "你好,我是子进程,正在进行通信";
22 int cnt = 0;
23 while(1)
24 {
25 cnt++;
26 char buffer[1024];
27 snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]",s,cnt,getpid());
28 write(fds[1], buffer, strlen(buffer));
29 printf("count: %d\n",cnt);
30 break;
31 }
32 close(fds[1]);//退出子进程前关闭文件写入端
33 exit(0);
34 }
35 //父进程
36 close(fds[1]);//父进程关闭写入端,即父进程进行读取
37 while(1)
38 {
39 sleep(2);//父进程不读
40 char buffer[1024];
41 ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
42 if(s > 0)
43 {
44 buffer[s] = 0;
45 printf("Get Message : %s | mypid = %d\n", buffer, getpid());
46 }
47 else if(s == 0)//写入端关闭,读到文件末尾了
48 {
49 printf("read: %d\n", s);
50 break;//关闭读取端
51 }
52 }
53 n = waitpid(id, NULL, 0);
54 assert(n == id);
55 close(fds[0]);//退出程序前,关闭读取端
56 return 0;
57 }
4. 读取端关闭,写入端直接关闭
关闭读取端后,写入端就没有意义了,因此OS会给写入的进程发送信号,终止该进程。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<assert.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<stdlib.h>
10 int main()
11 {
12 int fds[2];
13 int n = pipe(fds);
14 assert(n == 0);
15 pid_t id = fork();
16 assert(id >= 0);
17 if(id == 0)//子进程
18 {
19 //子进程通信,关闭子进程的读取端,即子进程进行写入
20 close(fds[0]);
21 const char* s = "你好,我是子进程,正在进行通信";
22 int cnt = 0;
23 while(1)
24 {
25 cnt++;
26 char buffer[1024];
27 snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]",s,cnt,getpid());
28 write(fds[1], buffer, strlen(buffer));
29 printf("count: %d\n",cnt);
30 }
31 close(fds[1]);//退出子进程前关闭文件写入端
32 printf("子进程关闭写入端\n");
33 exit(0);
34 }
35 //父进程
36 close(fds[1]);//父进程关闭写入端,即父进程进行读取
37 while(1)
38 {
39 sleep(2);//父进程不读
40 char buffer[1024];
41 ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
42 if(s > 0)
43 {
44 buffer[s] = 0;
45 printf("Get Message : %s | mypid = %d\n", buffer, getpid());
46 }
47 break;//关闭读取端
48 }
49 close(fds[0]);//退出程序前,关闭读取端
50 printf("父进程关闭读取端\n");
51 n = waitpid(id, NULL, 0);
52 assert(n == id);
53 return 0;
54 }
管道特征
- 管道的生命周期是根据进程的,进程退出,则管道释放;
- 管道可以用来进行具有血缘关系的进程间通信(常用于父子进程);
- 管道是面向字节流的;
- 半双工——单向管道(特殊);
- 互斥与同步机制——对共享资源进行保护的方案。
2.命名管道
匿名管道的限制就是只能在具有血缘关系的进程间通信,那么如果是两个毫不相干的进程间通信交互呢?
如果我们想要在两个不相关的进程之间进行通信,我们可以使用FIFO文件,它被称为命名管道。(命名管道是一种特殊类型的文件)
mkfifo
在当前路径下创建命名管道:
mkfifo named_pipe
创建管道文件
comm.hpp文件(同一份资源)
1 #pragma once
2 #include<iostream>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 #include<fcntl.h>
6 #include<cassert>
7 #include<cstring>
8 #include<cerrno>
9 using namespace std;
10 #define NAMED_PIPO "/tmp/mypipe.name"
11 #include<string>
12 bool createFIFO(const string &path)
13 {
14 umask(0);
15 int n = mkfifo(path.c_str(), 0666);
16 if(n ==0) return true;
17 else
18 {
19 cout<<"errno:"<<errno<<"err string:"<<strerror(errno)<<endl;
20 }
21 return false;
22 }
server.cc文件(读取端)
1 #include"comm.hpp"
2 int main()
3 {
4 bool ret = createFIFO(NAMED_PIPO);
5 assert(ret == true);
6 (void)ret;
7 return 0;
8 }
删除管道文件
unlink
头文件:unistd.h
;参数为const char* path;返回值:如果删除成功返回0,如果删除失败返回-1
使用:
在comm.hpp中封装删除函数
23 void removeFIFO(const string &path)
24 {
25 int n = unlink(path.c_str());
26 assert(n == 0);
27 (void)n;//避免因为没有使用n导致爆警告
28 }
在文件server中调用删除函数
1 #include"comm.hpp"
2 int main()
3 {
4 bool ret = createFIFO(NAMED_PIPO);
5 assert(ret == true);
6 (void)ret;
7
8 removeFIFO(NAMED_PIPO);
9 return 0;
10 }
到此管道文件的创建和删除就完成了,接下来我们进入通信阶段:
通信
clinet.cc文件(写入端)
1 #include"comm.hpp"
2 int main()
3 {
4 printf("HHHHHH\n");
5 int wfd = open(NAMED_PIPO, O_WRONLY);
6 if(wfd < 0) exit(1);
7 char buffer[1024];
8 while(1)
9 {
10 cout<<"Please say:";
11 fgets(buffer, sizeof buffer, stdin);
//if(strlen(buffer) > 0) buffer[strlen(buffer) - 1] = 0;
12 ssize_t s = write(wfd, buffer, strlen(buffer));
13 assert(s == strlen(buffer));
14 (void)s;
15 }
16 close(wfd);
17 return 0;
18 }
server.cc文件(读取端)
1 #include"comm.hpp"
2 int main()
3 {
4 bool ret = createFIFO(NAMED_PIPO);
5 assert(ret);
6 (void)ret;
7 int rfd = open(NAMED_PIPO, O_RDONLY);
8 if(rfd < 0) exit(1);
9
10 char buffer[1024];
11 while(1)
12 {
13 ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);
14 if(s > 0)
15 {
16 buffer[s] = 0;
17 cout<<"client -> server"<< buffer<<endl;
18 }
19 else if(s == 0)
20 {
21 cout<<"client quit. me, too"<<endl;
22 break;
23 }
24 else
25 {
26 cout<<"err string:"<<strerror(errno)<<endl;
27 break;
28 }
29 }
30 removeFIFO(NAMED_PIPO);
31 return 0;
32 }
comm.hpp文件(同一份资源)
1 #pragma once
2 #include<iostream>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 #include<fcntl.h>
6 #include<cassert>
7 #include<cstring>
8 #include<cerrno>
9 #include<unistd.h>
10 #include<stdlib.h>
11 #include<stdio.h>
12 using namespace std;
13 #define NAMED_PIPO "/tmp/mypipe.name"
14 #include<string>
15 bool createFIFO(const string &path)
16 {
17 umask(0);
18 int n = mkfifo(path.c_str(), 0666);
19 if(n ==0) return true;
20 else
21 {
22 cout<<"errno:"<<errno<<"err string:"<<strerror(errno)<<endl;
23 }
24 return false;
25 }
26 void removeFIFO(const string &path)
27 {
28 int n = unlink(path.c_str());
29 assert(n == 0);
30 (void)n;//避免因为没有使用n导致爆警告
31 }
运行:
我们发现读端每次会多打印一行空白,实际上是将写入端最后的Enter也作为数据传送了,因此我们将写端缓冲区的最后一个位置内容改为0即可。
if(strlen(buffer) > 0) buffer[strlen(buffer) - 1] = 0;
三、匿名管道和命名管道的区别和联系
它们都是往管道文件里写东西,
两个进程打开同一个文件:站在内核的角度,第二个文件不需要继续创建struct file对象,因为OS会识别到需要打开的文件已经被打开了。在内核中,两个进程此时就看到了同一份资源,有对应文件的操作方法和缓冲区,不需要将数据刷新到磁盘上(不需要IO),所以无论是匿名管道还是命名管道,本质都是内存级文件。
匿名管道是通过继承的方式让两个进程看到一个文件(资源),命名管道是通过让不同的进程打开指定名称(路径+文件名)的文件,来看待同一份资源。所以命名管道是通过文件的文件名来唯一标定资源的唯一性,而匿名管道是通过继承的方式来标定的。
总结
以上就是今天要讲的内容,本文介绍了进程间通信——管道的相关概念。
本文作者目前也是正在学习Linux相关的知识,如果文章中的内有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!