✅<1>主页::我的代码爱吃辣
📃<2>知识讲解:Linux——进程间通信——管道通信
☂️<3>开发环境:Centos7
💬<4>前言:进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。
目录
一.什么是进程间通信
二.进程间通信目的
三.进程间通信发展
四.什么是管道
五.匿名管道
六.父子进程管道通信
1.匿名管道的场景与特点
2. 用fork来共享管道原理
七.基于匿名管道实现进程池
一.什么是进程间通信
进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。
二.进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
三.进程间通信发展
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
四.什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
五.匿名管道
头文件:#include <unistd.h>
功能:创建一无名管道。
原型:int pipe(int fd[2]);
参数:fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端。
返回值:成功返回0,失败返回错误代码。
测试代码:
pipe.cc:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int fds[2]; // f[0]管道读端,f[1]管道写端
char buf[100];
int len;
// 创建管道
if (pipe(fds) == -1)
perror("make pipe"), exit(1);
// read from stdin
while (fgets(buf, 100, stdin))
{
len = strlen(buf);
// 写入管道
if (write(fds[1], buf, len) != len)
{
perror("write to pipe");
break;
}
memset(buf, 0, sizeof(buf));
// 从管道中读取
if ((len = read(fds[0], buf, 100)) == -1)
{
perror("read from pipe");
break;
}
// 写入显示器
if (write(1, buf, len) != len)
{
perror("write to stdout");
break;
}
}
}
makefile:
pipe:pipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf pipe
测试结果:
六.父子进程管道通信
我们知道fork之后,子进程会继承父进程的代码,数据会发生写时拷贝,那么父进程的文件描述符会不会被继承呢?会的。那么父进程创建的管道,其中的两个文件描述符一个指向管道的读端,一个指向管道的写端,也会被子进程继承。
1.匿名管道的场景与特点
管道的特点:
- 管道只具有单向通信的功能。
- 管道的本质是文件,因为fd的生命周期是随进程的,所以管道的生命周期也是随进程的。
- 管道通信,通常用来进行具有“血缘”关系的进程,进行进程间的通信。常用父子进程通信,
- 在管道的通信中,写入的次数,和读取的次数,不是严格匹配的,读写次数没有强相关,是面向字节流。
- 具有一定的协同能力,如果写端没有写入,读端会被阻塞——自带同步机制。
特殊场景:
- 如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待。
- 如果我们writer端将管道写满了,我们还能写吗?不能
- 如果我关闭了写端,读取完毕管道数据,在读,就会read返回0,表明读到了文件结尾
- 写端一直写,读端关闭,会发生什么呢?没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程!OS会通过信号来终止进程,(13)SIGPIPE。
测试代码:
#include <iostream>
#include <string>
#include <cerrno>
#include <cassert>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
// 让不同的进程看到同一份资源!!!!
// 任何一种任何一种进程间通信中,一定要 先 保证不同的进程之间看到同一份资源
int pipefd[2] = {0}; // pipefd[0] 读端, pipe[1]写端
// 1. 创建管道
int n = pipe(pipefd);
if (n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 读端
std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 写端
// 2. 创建子进程
pid_t id = fork();
assert(id != -1);
if (id == 0) // 子进程
{
// 3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[0]);
// 4. 开始通信 -- 结合某种场景
const std::string namestr = "hello,我是子进程";
int cnt = 1;
char buffer[1024];
while (true)
{
snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());
write(pipefd[1], buffer, strlen(buffer));
sleep(1);
}
// 退出时关闭打开的文件描述符
close(pipefd[1]);
exit(0);
}
// 父进程
// 3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
// 4. 开始通信 -- 结合某种场景
char buffer[1024];
int cnt = 0;
while (true)
{
// sleep(10);
// sleep(1);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
else if (n == 0)
{
std::cout << "我是父进程, 读到了文件结尾" << std::endl;
break;
}
else
{
std::cout << "我是父进程, 读异常了" << std::endl;
break;
}
sleep(1);
if (cnt++ > 5)
break;
}
// 父进程读端关闭,子进程会收到13号信号
close(pipefd[0]);
// 回收子进程的僵尸状态
int status = 0;
waitpid(id, &status, 0);
std::cout << "子进程pid:" << id << ",收到的信号sig: " << (status & 0x7F) << std::endl;
sleep(1);
return 0;
}
测试结果:
2. 用fork来共享管道原理
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
七.基于匿名管道实现进程池
当没有数据可读时,read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
如果我们使用,父进程来控制写端,子进程进行读取,发送数据让子进程执行特定的任务,我们就可以实现对子进程的控制。
代码:
CtrlProc.cc:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
using namespace std;
#define NUM_PROC 5
struct child_pip
{
child_pip(int fd, pid_t pid)
: _fd(fd), _pid(pid)
{
}
~child_pip()
{
}
int _fd;
pid_t _pid;
};
void WaitCommand()
{
Task task;
int command;
while (1)
{
size_t n = read(0, &command, sizeof(int));
if (n == 4) // 读取成功
{
task.funcs[command]();
}
else if (n == 0) // 读取失败
{
break;
}
else
{
break;
}
}
}
void creatproc(vector<child_pip> &child_pip_v)
{
for (int i = 0; i < NUM_PROC; i++)
{
// 1.创建管道
int pipfd[2];
pipe(pipfd);
// 2.创建子进程
pid_t pid = fork();
if (pid < 0)
perror("fork");
// 我们想让子进程从管道读,父进程向管道写
if (pid == 0) // 子进程
{
// 3.关闭不必要的文件描述符
close(pipfd[1]);
// 3.1重定向,将来子进程指向0号文件描述符读取
dup2(pipfd[0], 0);
WaitCommand();
exit(0);
}
// 父进程
// 3.关闭不必要的文件描述符
cout << "子进程pid:" << pid << endl;
close(pipfd[0]);
// 建立好子进程与管道的映射,父进程的写端口,和子进程pid
child_pip_v.push_back(child_pip(pipfd[1], pid));
}
}
void ctrlproc(vector<child_pip> &child_pip_v)
{
while (1)
{
int command = 0;
cin >> command;
if (command == -1)
break;
int i = rand() % NUM_PROC;
write(child_pip_v[i]._fd, &command, sizeof(int));
}
}
void waitproc(vector<child_pip> &child_pip_v)
{
int status = 0;
for (int i = 0; i < child_pip_v.size(); i++)
{
close(child_pip_v[i]._fd);
}
// sleep(5);
for (int i = 0; i < child_pip_v.size(); i++)
{
waitpid(child_pip_v[i]._pid, &status, 0);
cout << "子进程:" << child_pip_v[i]._pid << "退出" << endl;
}
}
int main()
{
vector<child_pip> child_pip_v;
creatproc(child_pip_v);
ctrlproc(child_pip_v);
waitproc(child_pip_v);
return 0;
}
Task.cc:
#include <vector>
#include <iostream>
#include <unistd.h>
using namespace std;
typedef void (*fun_t)();
void beatxyy()
{
cout << "子进程:" << getpid() << ",执行数据库" << endl;
}
void beatxyf()
{
cout << "子进程:" << getpid() << ",写入日志" << endl;
}
void beatwy()
{
cout << "子进程:" << getpid() << ",读取网卡" << endl;
}
void beatwj()
{
cout << "子进程:" << getpid() << ",刷新缓冲区" << endl;
}
void beatxjy()
{
cout << "子进程:" << getpid() << ",数据比对" << endl;
}
struct Task
{
Task()
{
funcs.push_back(beatxyy);
funcs.push_back(beatxyf);
funcs.push_back(beatwy);
funcs.push_back(beatwj);
funcs.push_back(beatxjy);
}
vector<fun_t> funcs;
};
makefile:
CtrlProc:CtrlProc.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf CtrlProc
测试结果:
注意:
为什么这里的waitproc我们要分开成两个循环,如果一个循环,文件描述符会无法关闭完,子进程也就无法退出。
我们关闭了第一个文件描述符,第一个管道由于继承问题,第一个管道还会有后面的子进程也会指向。最终导致我们只能有最后一个子进程先退出了,其他子进程进程陆续退出,此时进程等待也已经结束了,除了最后一个子进程。其他子进程的僵尸状态都没有被回收。