> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。
> 目标:理解进程通信----管道通信
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:Linux初阶
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
🌟前言
当我们创建两个进程,希望它们可以相互沟通,我的内容你可以读,我的内容你可以写,这种联系我们就需要我们的管道,这个管道就可以达成两个进程或者更多进程相互通信,那这个关系在Linux中是如何实现的呢,我们又该如何理解呢?
⭐主体
学习【Linux】静态库和动态库咱们按照下面的图解:
🌙 什么是进程间通信
💫 进程间通信介绍
什么是进程间通信?
进程具有独立性,每个进程都有自己的PCB,所以进程间需要通信,并且通信的成本一定不低(通信的本质:OS需要直接或者间接给通信双方的进程提供“内存空间”,并且要通信的进程,必须看到一份公共的资源)
图解:
如何去通信?
- 采用标准的做法:System V进程间通信(聚焦在本地通信,如共享内存)、POSIX进程间通信(让通信过程可以跨主机)。
- 采用文件的做法:管道-基于文件系统(匿名管道、命名管道)
管道通信的特点:
是面向字节流、占用内存空间、只能单向传输、有固定的大小和缓冲区等。
总结:
而我们所说的不同通信种类本质就是:上面所说的资源,是OS中的哪一个模块提供的。如文件系统提供的叫管道通信;OS对应的System V模块提供的…,(成本不低是因为我们需要让不同的进程看到同一份资源。)
💫 进程间通信目的
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)
为什么要有进程间通信?
有时候我们需要多进程协同的,完成某种业务内容。比如管道。
图解:
💫 进程间通信分类
进程间通信分类:(匿名管道和命名管道)
- 匿名管道是只能在父子进程间使用的,它通过pipe()函数创建,并返回两个文件描述符,一个用于读,一个用于写。
- 命名管道是可以在任意进程间使用的,它通过mkfifo()或mknod()函数创建一个特殊的文件,然后通过open()函数打开,并返回一个文件描述符,用于读或写。
🌙 管道的实现和理解
💫 管道介绍
概念:
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
分析:
任何一个文件包括两套资源:1.file的操作方法 2.有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信,这种方式提供的文件称为管道文件。管道文件本质就是内存级文件,不需要IO。
两个进程如何看到同一个管道文件:fork创建子进程完成
管道创建时分别以读和写方式打开同一个文件(如果只读或者只写,子进程也只会继承只读或只写,父子双方打开文件的方式一样,无法完成单向通信);父进程创建子进程,父进程以读写打开,子进程也是以读写打开(一般而言,管道只用来进行单向数据通信);关闭父子进程不需要的文件描述符,完成通信。
💫 匿名管道
概念:
我们通过文件名区分文件,但是如果当前进程的文件没有名字,这样的内存级文件称为匿名管道。让两个进程看到同一个文件,通过父进程创建子进程,子进程继承文件地址的方式,看到同一个内存级文件,此时内存级文件没有名称就是匿名管道了。匿名管道能用来父进程和子进程之间进行进程间通信。
1.pipe
作用及其使用:
- 创建一个管道只需要调用pipe,注意头文件,返回值,以及函数的参数。
- 头文件为#include ,调用成功返回0,调用失败返回-1。参数是输出型参数。
SYNOPSIS #include <unistd.h> int pipe(int pipefd[2]); DESCRIPTION pipe() creates a pipe,pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
创建Makefile文件:
mypipe:mypipe.cc g++ mypipe.cc -o mypipe .PHONY:clean clean: rm -f mypipe
创建管道文件,打开读写端:
#include <iostream> #include <unistd.h> #include <cassert> using namespace std; int main() { int fds[2]; int n = pipe(fds); assert(n == 0); //0,1,2->3,4 //[0]:读取 [1]:写入 cout<<"fds[0]:"<<fds[0]<<endl;//3 cout<<"fds[1]:"<<fds[1]<<endl;//4 return 0; }
因此,fds[0]:3代表读取,fds[1]:4代表写入;
fork子进程:
#include <iostream> #include <unistd.h> #include <cassert> #include <sys/types.h> #include <sys/wait.h> using namespace std; int main() { int fds[2]; int n = pipe(fds); assert(n == 0); //fork pid_t id = fork(); assert(id>=0); if(id==0) { //子进程通信 exit(0); } //父进程通信 n = waitpid(id,nullptr,0); assert(n==id); return 0; }
关闭父子进程不需要的文件描述符,完成通信:(子进程写入,父进程读取)
#include <iostream> #include <unistd.h> #include <cassert> #include <sys/types.h> #include <sys/wait.h> #include <string> #include <cstdio> #include <cstring> #include <unistd.h> using namespace std; int main() { int fds[2]; int n = pipe(fds); assert(n == 0); //fork pid_t id = fork(); assert(id>=0); if(id==0) { //子进程通信:子进程进行写入,关闭读 close(fds[0]); //通信 const char*s = "这是子进程,正在进行通信"; int cnt = 0; while(true) { cnt++; char buffer[1024]; snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid()); //写端写满的时候,在写会阻塞,等对方进行读取 write(fds[1],buffer,strlen(buffer));//系统接口 sleep(1);//一秒写一次 } //退出前关闭子进程 close(fds[1]); exit(0); } //父进程通信:父进程进行读取,关闭写 close(fds[1]); //通信 while(true) { char buffer[1024]; //管道中如果没有数据,读端在读,默认会直接阻塞当前正在读取的进程 ssize_t s = read(fds[0],buffer,sizeof(buffer)-1); if(s>0) buffer[s] = 0; cout<<"Get Message# "<<buffer<<"|mypid:"<<getpid()<<endl; } n = waitpid(id,nullptr,0); assert(n==id); //结束前关闭 close(fds[0]); return 0; }
2.读写特征
管道读写特征:
- 1.读快写慢
分析:
- 子进程休眠时,不在写入,父进程在读取(如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程)
代码:
#include <stdio.h> #include <unistd.h> #include <assert.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h> #include <string.h> #include <stdlib.h> int main() { int fds[2]; int n = pipe(fds); assert(n == 0); pid_t id = fork(); assert(id >= 0); if (id == 0) // 子进程 { // 子进程通信,关闭子进程的读取端,即子进程进行写入 close(fds[0]); const char *s = "你好,我是子进程,正在进行通信"; int cnt = 0; while (1) { cnt++; char buffer[1024]; snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid()); write(fds[1], buffer, strlen(buffer)); sleep(50); // 每一秒写一次 } close(fds[1]); // 退出子进程前关闭文件写入端 exit(0); } // 父进程 close(fds[1]); // 父进程关闭写入端,即父进程进行读取 while (1) { char buffer[1024]; printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"); ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); printf("888888888888888888888888888888888888!!\n"); if (s > 0) buffer[s] = 0; printf("Get Message : %s | mypid = %d\n", buffer, getpid()); } n = waitpid(id, NULL, 0); assert(n == id); close(fds[0]); // 退出程序前,关闭读取端 return 0; }
- 2.读慢写快
分析:
- 读取管道的进程一直不进行读取,而写端一直在写入。写端可以向管道内写入,但是管道是固定大小的缓冲区,不断的只写不读管道会被写满。满了以后就不能再写入了,此时写端会处于阻塞状态。
文件mypipe.cc
#include <stdio.h> #include <unistd.h> #include <assert.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h> #include <string.h> #include <stdlib.h> int main() { int fds[2]; int n = pipe(fds); assert(n == 0); pid_t id = fork(); assert(id >= 0); if (id == 0) // 子进程 { // 子进程通信,关闭子进程的读取端,即子进程进行写入 close(fds[0]); const char *s = "你好,我是子进程,正在进行通信"; int cnt = 0; while (1) { cnt++; char buffer[1024]; snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid()); write(fds[1], buffer, strlen(buffer)); printf("count: %d\n", cnt); } close(fds[1]); // 退出子进程前关闭文件写入端 exit(0); } // 父进程 close(fds[1]); // 父进程关闭写入端,即父进程进行读取 while (1) { sleep(50); // 父进程不读 char buffer[1024]; ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); printf("Get Message : %s | mypid = %d\n", buffer, getpid()); } n = waitpid(id, NULL, 0); assert(n == id); close(fds[0]); // 退出程序前,关闭读取端 return 0; }
拓展:
如果休息sleep(2),这种情况,写端是将数据塞到管道内,管道读取是安装指定大小读取(并非一行一行的读取,最初安装一行来读取是因为写入的慢,一次只写一行数据,数据就被读取了)。
- 3.写入关闭,读到0
子进程写入端关闭:
#include <stdio.h> #include <unistd.h> #include <assert.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h> #include <string.h> #include <stdlib.h> int main() { int fds[2]; int n = pipe(fds); assert(n == 0); pid_t id = fork(); assert(id >= 0); if (id == 0) // 子进程 { // 子进程通信,关闭子进程的读取端,即子进程进行写入 close(fds[0]); const char *s = "你好,我是子进程,正在进行通信"; int cnt = 0; while (1) { cnt++; char buffer[1024]; snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid()); write(fds[1], buffer, strlen(buffer)); printf("count: %d\n", cnt); break; } close(fds[1]); // 退出子进程前关闭文件写入端 exit(0); } // 父进程 close(fds[1]); // 父进程关闭写入端,即父进程进行读取 while (1) { sleep(2); // 父进程不读 char buffer[1024]; ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("Get Message : %s | mypid = %d\n", buffer, getpid()); } else if (s == 0) // 写入端关闭,读到文件末尾了 { printf("read: %d\n", s); break; // 关闭读取端 } } n = waitpid(id, NULL, 0); assert(n == id); close(fds[0]); // 退出程序前,关闭读取端 return 0; }
- 4. 读取端关闭,写入端直接关闭
关闭读取端后,写入端就没有意义了,因此OS会给写入的进程发送信号,终止该进程。
#include <stdio.h> #include <unistd.h> #include <assert.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h> #include <string.h> #include <stdlib.h> int main() { int fds[2]; int n = pipe(fds); assert(n == 0); pid_t id = fork(); assert(id >= 0); if (id == 0) // 子进程 { // 子进程通信,关闭子进程的读取端,即子进程进行写入 close(fds[0]); const char *s = "你好,我是子进程,正在进行通信"; int cnt = 0; while (1) { cnt++; char buffer[1024]; snprintf(buffer, sizeof buffer, "child -> parent say:%s [%d], [%d]", s, cnt, getpid()); write(fds[1], buffer, strlen(buffer)); printf("count: %d\n", cnt); } close(fds[1]); // 退出子进程前关闭文件写入端 printf("子进程关闭写入端\n"); exit(0); } // 父进程 close(fds[1]); // 父进程关闭写入端,即父进程进行读取 while (1) { sleep(2); // 父进程不读 char buffer[1024]; ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("Get Message : %s | mypid = %d\n", buffer, getpid()); } break; // 关闭读取端 } close(fds[0]); // 退出程序前,关闭读取端 printf("父进程关闭读取端\n"); n = waitpid(id, NULL, 0); assert(n == id); return 0; }
【Linux】进程通信----管道通信(下) https://developer.aliyun.com/article/1565748