朋友们、伙计们,我们又见面了,本期来给大家带来进程间通信相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. 进程间通信
1.1 进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的本质:先得让不同的进程看到同一份资源。这个资源不能由进程双方的任何一方提供,通常是由OS提供。
1.2 进程间通信的分类
管道
- 匿名管道pipe
- 命名管道
System V 进程间通信
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX 进程间通信
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
2. 管道
我们之前在命令行使用的 | 就是一种管道,那么我们在命令行使用管道,它肯定会在底层给我们转化成对应的操作;
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
2.1 匿名管道
- 当一个进程打开一个文件时,会给其分配对应的文件描述符,新打开的文件里面主要的是文件的inode(保存文件属性)、方法集(对应的读写方法)、文件页缓冲区;
- 文件的PCB、描述文件结构体对象都是属于进程管理操作;文件描述符的分配管理属于内存操作,两者互不相干;
- 此时如果调用fork创建子进程时,新创建的子进程根据父进程PCB为模版创建自己的PCB,子进程的文件描述符的指向和父进程指向一致(并不会重新分配);
- 所以父进程打开的文件此时也会被子进程看到,这样子就形成了两个进程看到同一份资源,具备了进程间通信的条件;
- 当父进程想往文件写入数据时,子进程也可以看见,但是父进程向该文件写入时,不需要再刷新到磁盘,然后子进程再从磁盘读入数据,这样效率太低下(访问外设),所以这个文件就是纯内存级文件,这个文件叫做管道文件;
- 该管道文件可以不需要名字,并且用于父子进程通信,该管道叫做匿名管道;
- 管道文件一旦完成资源共享,只允许单向通信。
2.2.1 创建原理(文件描述符角度)
- ① 父进程以读写方式分别打开管道文件(父进程创建管道)
- ② 父进程fork创建子进程
- ③ 父子进程关闭对应的读写端
父进程为什么要以读写方式打开两次呢?
为了完成单向通信,若是只以读方式打开,那么创建子进程的时子进程也会继承读端,关闭哪一个都不能完成通信,以读写方式打开,那么子进程也会继承读写端,此时父进程关闭写端,保留读端,子进程关闭读端,保留写端就可以完成单向通信!
2.2.2 创建原理(内核角度)
- 以读写方式打开两次文件,文件的属性和内容只会存在一份;
- 每个文件结构体对象中都有自己对应的读写位置,所以会存在两个struct file结构体对象;
- 因为它们的属性和内容只有一份,所以这两个struct file都会指向同样的inode、方法集、文件页缓冲区;
- 文件结构体对象中还存在引用计数,用于记录当前有几个文件描述符指向struct file,这样就不会因为一端关闭而影响另一端。
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”
2.2.3 管道的创建
#include <unistd.h> // 功能:创建一无名管道 // 原型 int pipe(int fd[2]); // 参数 // fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端 // 返回值:成功返回0,失败返回错误代码
代码演示:
#include <iostream> #include <cassert> #include <unistd.h> #include <cstring> #include <sys/types.h> #include <sys/wait.h> #define MAX_SIZE 1024 using namespace std; int main() { //1.创建管道 int pipefd[2] = {0}; int n = pipe(pipefd); assert(n == 0); (void)n; //防止告警 cout << "pipefd[0]" << pipefd[0] << "," << "pipefd[1]" << pipefd[1] << endl; //2.创建子进程 pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //子写,父读 //3.父子关闭不需要的fd,形成单通道通信 if(id == 0) { //child close(pipefd[0]); //关闭读,保留写 int cnt = 10; while(cnt--) { char message[MAX_SIZE]; snprintf(message, sizeof(message), "I am child, my id: %d, %d",getpid(),cnt); //通过管道共享信息 write(pipefd[1], message, strlen(message)); sleep(1); } cout << "child close w piont" << endl; exit(0); } //father close(pipefd[1]); //关闭写,保留读 char buffer[MAX_SIZE]; //读取通过管道的信息 while(true) { ssize_t pos = read(pipefd[0], buffer, sizeof(buffer)-1); if(pos > 0) { buffer[pos] = '\0'; cout << "I am your father:" << getpid() << "child say :" << buffer << endl; } else if(pos == 0) { cout << "child quit, me to!" << endl; break; } else cout << "father return val(n): " << n << endl; break; close(pipefd[0]); } //等待子进程 int status = 0; pid_t rid = waitpid(id,&status,0); if(rid == id) { cout << "wait success! child exit sig:" << (status&0x7F) <<endl; } return 0; }
2.2.4 管道的4种情况
- 1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
- 2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
- 3. 写端关闭,读端一直读取, 读端会读到read()返回值为0时,表示读到文件结尾
- 4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止目标进程
2.2.5 管道的5种特性
- 1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
- 2. 匿名管道,默认给读写端要提供同步机制
- 3. 管道数据是面向字节流的
- 4. 管道的生命周期是随进程的
- 5. 管道是单向通信的,是半双工通信的一种特殊情况。
2.2 命名管道
命名管道用于两个毫不相干的进程进行通信
在命令行中创建命名管道的指令叫做:mkfifo
创建好命名管道我们就可以让两个毫不相干的进程进行通信:
2.2.1 创建原理
首先两个毫不相干的进程如何看到这个管道呢?
首先命名管道有唯一的路径,并且还有文件名,所以进程就可以通过唯一的路径和文件名来找到这个管道文件,所以使用路径 + 文件名就可以让不同的进程看到同一份资源。
命名管道和匿名管道的创建原理是一样的。
2.2.2 管道的创建
代码级别的命名管道的创建使用的是:mkfifo函数
创建成功返回0,失败返回-1,错误码被设置。
#include <iostream> #include <sys/types.h> #include <sys/stat.h> #include <cstring> #include <cerrno> #define FILENAME ".fifo" int main() { int n = mkfifo(FILENAME, 0666); // 在当路径下创建管道文件,文件权限是0666 if(n < 0) { std::cerr << "errno " << errno << "errstring " << strerror(errno) << std::endl; return 1; } return 0; }
2.2.3 进程通信
两个进程使用命名管道进行通信操作是和文件操作一样的一个进程向文件写入,另一个进程向文件读取。
server.cc
#include <iostream> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <cstring> #include <cerrno> #include <unistd.h> #define FILENAME "fifo" #define SIZE 1024 int main() { // 1. 创建管道文件 int n = mkfifo(FILENAME, 0666); // 在当路径下创建管道文件,文件权限是0666 if (n < 0) { std::cerr << "errno " << errno << "errstring " << strerror(errno) << std::endl; return 1; } // 2. 打开管道文件 int rfd = open(FILENAME, O_RDONLY); if (rfd < 0) { std::cerr << "errno: " << errno << " , errstring " << strerror(errno) << std::endl; return 1; } std::cout << "open fifo success..." << std::endl; // 3, 读取信息 char buffer[SIZE]; while (true) { ssize_t s = read(rfd, buffer, sizeof(buffer) - 1); // 预留一个空间加上'\0' if (s > 0) // 读取成功按照字符串的方式打印 { buffer[s] = '\0'; std::cout << "Client say# " << buffer << std::endl; } else if (s == 0) // 写端关闭,读端就要退出 { std::cout << "client quit, server quit too!" << std::endl; break; } } // 4. 关闭管道文件 close(rfd); std::cout << "close fifo success..." << std::endl; return 0; }
client.cc
#include <iostream> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <cstring> #include <cerrno> #include <unistd.h> #include <string> #define FILENAME "fifo" int main() { // 1. 打开管道文件 int wfd = open(FILENAME, O_WRONLY); if (wfd < 0) { std::cerr << "errno" << errno << " , errsting:" << strerror(errno) << std::endl; return 1; } std::cout << "open fifo success... write" << std::endl; // 2. 向管道文件写入 std::string message; while (true) { std::cout << "Please Enter# "; std::getline(std::cin, message); ssize_t s = write(wfd, message.c_str(), message.size()); if (s < 0) { std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl; break; } } // 3. 关闭文件管道 close(wfd); std::cout << "close fifo success..." << std::endl; return 0; }
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!