赞
踩
目录
什么是进程间通信?
我们在学习了进程的相关知识后,知道,进程的运行是具有独立性的,每个进程都有自己独立的PCB,也都有只属于自己的地址空间,进程之间是互不干扰的。
所以说,进程之间要通信的话,难度是非常大的。
进程间通信的本质:操作系统需要直接或者间接给通信双方的进程提供一段 “内存空间”,并且要让不同的进程看到同一份资源(内存空间)。而这个内存空间不应该属于任何一个进程,而应该由操作系统来维护。
所以,进程间通信的最大成本就是:操作系统在设计的原生层面,进程是互相独立的,但是现在需要让它们之间进行通信。那么应该如何让不同的进程看到同一份资源。
~ 数据传输:一个进程需要将它的数据发送给另一个进程。
~ 资源共享:多个进程之间共享同样的资源。
~ 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
~ 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
~ 管道:1、匿名管道pipe 2、命名管道
~ System V IPC:1、System V 消息队列 2、System V 共享内存 3、System V 信号量
~ POSIX IPC:1、消息队列 2、共享内存 3、信号量 4、互斥量 5、条件变量 6、读写锁
进程间通信的必要性:如果只是使用单进程,那么也就无法使用并发能力,更加无法实现多进程协同工作。
本篇文章我们来具体讲一讲,管道的相关知识。
什么是管道?
1、管道是Unix中最古老的进程间通信的形式。
2、我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
管道只能进行单向通信,用来传输资源:数据。

我们按下面的步骤来分析:
1、首先,父进程分别以读和写的方式,打开了一个文件。
2、然后,父进程fork创建了一个子进程,我们知道,子进程会继承父进程,指向同一个文件。
3、我们规定,父进程对文件写入,子进程对文件读取。关闭父子进程各自不需要的功能。即:父进程关闭读的文件描述符,子进程关闭写的文件描述符。
最终,通过该文件就实现了单向通信。这就是管道。管道的本质其实就是内存级文件。所以两个进程间通信的数据,不需要刷新到磁盘里。
为什么能够这样做?
文件有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信。
管道一般用来进行具有血缘关系的进程(父子进程)之间的通信。
管道分为匿名管道和命名管道。下面我们就来具体讲一讲它们的内容。
通过文件名区分文件,但是如果当前进程的文件没有名字,这样的内存级文件称为匿名管道。匿名管道能用来父进程和子进程之间进行进程间通信。
匿名管道文件在磁盘上也没有实体,它只存在于内存中。
函数:pipe,调用成功返回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.
我们首先创建管道文件,打开读写端:在pipefd数组中,pipefd[0]代表读,pipefd[1]代表写。
- #include <iostream>
- #include <cassert>
- #include <unistd.h>
-
- using namespace std;
-
- int main()
- {
- // 创建管道
- int pipefd[2] = {0};
- int n = pipe(pipefd);
- assert(n != -1);
- (void)n;
-
- cout << "pipefd[0]: " << pipefd[0] << endl;
- cout << "pipefd[1]: " << pipefd[1] << endl;
-
- return 0;
- }


接着,我们fork创建子进程, 然后,我们关闭父子进程不需要的文件描述符,完成通信框架的建立:
- #include <iostream>
- #include <cassert>
- #include <unistd.h>
-
- using namespace std;
-
- int main()
- {
- // 创建管道
- int pipefd[2] = {0};
- int n = pipe(pipefd);
- assert(n != -1);
- (void)n;
-
- pid_t id = fork();
- assert(id != -1);
- if(id == 0)
- {
- //子进程
- close(pipefd[1]);
-
- exit(0);
- }
-
- //父进程
- close(pipefd[0]);
-
- return 0;
- }

最后,我们通过父进程给子进程发送消息来检测通信:
- #include <iostream>
- #include <cassert>
- #include <unistd.h>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/wait.h>
-
- using namespace std;
-
- int main()
- {
- // 创建管道
- int pipefd[2] = {0};
- int n = pipe(pipefd);
- assert(n != -1);
- (void)n;
-
- // 创建子进程
- pid_t id = fork();
- assert(id != -1);
- if (id == 0)
- {
- // 子进程
- close(pipefd[1]);
- char buffer[1024];
- while (true)
- {
- ssize_t mess = read(pipefd[0], buffer, sizeof(buffer));
- if (mess > 0)
- {
- buffer[mess] = 0;
- cout << "father process say: " << buffer << endl;
- }
- }
-
- exit(0);
- }
-
- // 父进程
- close(pipefd[0]);
- string message = "i am father ->";
- char buff[1024];
- int count = 0;
- while (true)
- {
- snprintf(buff, sizeof(buff), "%s[%d]: %d", message.c_str(), getpid(), count++);
- write(pipefd[1], buff, sizeof(buff));
- sleep(1);
- }
-
- pid_t ret = waitpid(id, nullptr, 0);
- assert(ret > 0);
- (void)ret;
-
- close(pipefd[1]);
- return 0;
- }


管道的特点:
1、管道是用来进行具有血缘关系的进程间的通信,常用于父子进程。
2、管道能够让进程间协同,提供了访问控制。
3、管道是面向字节流的。
4、管道的生命周期随进程,进程退出,管道释放。
5、管道是单向通信,是半双工通信的特殊情况。
6、内核会对管道操作进行互斥与同步。
下面我们针对管道的特点2,来进行演示:
~ 读快写慢
其余代码不变,我们仅让父进程在每次写入后,休眠10秒再写。


我们发现,父进程休眠期间,子进程会等待父进程写入后再读取。(如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程)
~ 读慢写快
我们让父进程不停地写入。

子进程休眠5秒后再读取。

拿着管道读端不读,写端一直在写:写端往管道里写,而管道是有大小的,不断往写端写,会被写满。

管道是固定大小的缓冲区,当管道被写满,就不能再写了。此时写端会阻塞。而休眠结束后,子进程就会开始读取,这时父进程才能继续写入。
~ 关闭写入端
父进程写入端关闭:写完第一次后,父进程休眠6秒,然后结束循环,关闭写入端。

父进程作为写入的一方,关闭了写入端,子进程作为读取的一方,read会返回0,表示读到了文件的结尾。这时我们让子进程结束循环,并退出。


~ 关闭读端
管道是单向的:读端关闭,再写入就没有意义了,这时OS会终止写端,会给写进程发送信号,终止写端。
为了方便观察,我们让父进程读取,子进程写入。
- #include <stdio.h>
- #include <unistd.h>
- #include <string.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- int main()
- {
- int pipefd[2] = { 0 };
- if (pipe(pipefd) < 0)
- {
- perror("pipe");
- return 1;
- }
- pid_t id = fork();
- if (id == 0){
- //child
- close(pipefd[0]);
- const char* msg = "hello father, I am child...";
- int count = 10;
- while (count--){
- write(pipefd[1], msg, strlen(msg));
- sleep(1);
- }
- close(pipefd[1]);
- exit(0);
- }
- //father
- close(pipefd[1]);
- close(pipefd[0]);
- int status = 0;
- waitpid(id, &status, 0);
- printf("child get signal:%d\n", status & 0x7F);
- return 0;
- }



操作系统向子进程发送的是SIGPIPE信号将子进程终止。
上面我们所讲的匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
而如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。
一个进程打开了一个文件后,Linux内核里会有文件的struct file结构。如果还有第二个进程要打开这个文件,那么只要路径相同,那么两个进程打开的文件一定是同一个且是唯一的,那么第二个进程不需要继续创建struct file对象,因为OS会识别到打开的文件被打开了。
此时两个进程就看到了同一份资源,该文件也不需要把数据刷新到磁盘上去,不需要IO。
所以说,命名管道是真实存在于系统路径下的,只是它只有属性,没有内容。它能够使用open,close函数被打开或关闭。
mkfifo:也可以作为命令直接在指定路径下创建命名管道。mkfifo 命名管道名称
- NAME
- mkfifo - make FIFOs (named pipes)
-
- SYNOPSIS
- #include <sys/types.h>
- #include <sys/stat.h>
- int mkfifo(const char *pathname, mode_t mode);
- RETURN VALUE
- On success mkfifo() returns 0. In the case of an error, -1 is returned (in which case, errno is set appropriately).
下面我们就来使用一下:我们有三个文件,server.cc(读取端) ,client.cc(写入端) 是两个源文件,它们运行后会是两个不同且无关系的进程,我们来进行它们之间的通信。comm.h是一个头文件。
comm.h
- #include<iostream>
- #include<string>
- #include<unistd.h>
- #include<cstdio>
- #include<sys/types.h>
- #include<sys/stat.h>
- #include<fcntl.h>
-
- using namespace std;
-
- string ipcPath = "./fifo.ipc";
- #define MODE 0666
- #define SIZE 128
server.cc
- #include "comm.hpp"
-
- int main()
- {
- // 1.创建命名管道
- int ret = mkfifo(ipcPath.c_str(), MODE);
- if (ret == -1)
- {
- perror("mkfifo");
- exit(1);
- }
-
- // 2.正常文件操作:通信
- int fd = open(ipcPath.c_str(), O_RDONLY);
- if (fd < 0)
- {
- perror("open");
- exit(2);
- }
- char buffer[SIZE];
- while (true)
- {
- ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
- if (s > 0)
- {
- cout << "client say :" << buffer << endl;
- }
- else if (s == 0)
- {
- cerr << "read the end of file -> client quit, server quit too" << endl;
- break;
- }
- else
- {
- perror("read");
- break;
- }
- }
-
- close(fd);
- return 0;
- }

client.cc
- #include "comm.hpp"
-
- int main()
- {
- // 1.打开命名管道
- int fd = open(ipcPath.c_str(), O_WRONLY);
- if (fd < 0)
- {
- perror("open");
- exit(1);
- }
-
- string buffer;
- while (true)
- {
- cout << "please enter message ->" << endl;
- getline(cin, buffer);
- write(fd, buffer.c_str(), buffer.size());
- }
-
- close(fd);
- return 0;
- }

通信演示:

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。