从本章开始,我们开始学习进程通信相关的知识,本章将来详细探讨一下管道,学习匿名管道和命名管道的原理和代码实现等相关操作。目标已经确定,接下来就要搬好小板凳,准备开讲了…
在我们之前的学习中,我们知道进程是具独立性的。但是不要以为进程独立了,就是彻底独立,有时候,我们需要进程间能够进行一定程度的信息交互。
进程间通信目的:
需要多进程进行协同处理一件事情(并发处理)。单纯的数据传输,一个进程想把数据发给另一个进程。多进程之间共享同样的资源。一个进程想让另一个进程做其他的事情,进程控制。
举一个通信的例子:
在我们刚学Linux时,就接触过竖划线|
的操作,那么究竟什么是管道呢?
Unix
中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个 “管道”。两个进程看到同一份资源才具备通信的条件:
其中【管道 】就提供了共享资源的一种手段!
如何才能让两个进程看到同一份资源?
生活中的管道大多数都是单向的,进程通信中的管道数据传输也是单向的。
进程通信的核心思想:让两个进程获取到同一份资源
创建子进程,子进程是以父进程为模板,代码共享,数据要发生写时拷贝,文件描述符的映射表也拷贝了一份,并且内容也拷贝到子进程中了。
Linux中可以通过特定的系统调用来判断文件是普通文件还是管道文件:
如果设计的时候就设计成,如果是普通文件就往磁盘上写,如果是管道文件也往缓冲区里写,但是就
不再往磁盘上刷新了。如果是管道,就把它和对应的磁盘去关联。
匿名管道主要用于父子进程之间的通信,用pipe
接口来创建管道:
pipe
封装了open
, open
了两次。输出型参数:
我们需要传入一个由两个整型元素组成的数组作为参数,例如 int fd[2]。这个数组被称为pipe函数的输出型参数,它用于接收pipe函数返回的两个文件描述符。
具体来说,fd[0] 是管道的读端文件描述符,用于从管道中读取数据;fd[1] 是管道的写端文件描述符,用于向管道中写入数据。
注意:
连通一个管道:
我们以子进程关闭写端,父进程关闭读端为例:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 演示pipe通信的基本过程 -- 匿名管道
int main()
{
// 1. 创建管道
int pipefd[2] = { 0 };
if(pipe(pipefd) != 0)
{
cerr << "pipe erro" << endl;
return 1;
}
// 2. 创建子进程
pid_t id = fork();
if(id < 0)
{
cerr << "fork error" << endl;
return 2;
}
else if(id == 0)
{
// 子进程
// 让子进程来进行读取,子进程就应该关掉写端
close(pipefd[1]);
#define NUM 1024
char buffer[NUM];
while(true)
{
cout << "时间戳" << (uint64_t)time(nullptr) << endl;
// 子进程没有带sleep,为什么子进程也会休眠呢??
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(s > 0)
{
// 读取成功
buffer[s] = '\0';
cout << "子进程收到消息,内容是:" << buffer << endl;
}
else if(s == 0)
{
cout << "父进程写完了,我也退出了!" << endl;
break;
}
else
{
//do noting
}
}
close(pipefd[0]);
exit(0);
}
else
{
// 父进程
// 让父进程进行写入,父进程就应该关掉读端
close(pipefd[0]);
const char* msg = "你好子进程,我是父进程,这次发送的信息编号是";
int cnt = 0;
while(cnt < 5)
{
char sendBuffer[1024];
sprintf(sendBuffer, "%s : %d", msg, cnt);
write(pipefd[1], sendBuffer, strlen(sendBuffer));
sleep(1);
cnt++;
}
close(pipefd[1]);
cout << "父进程写完了" << endl;
}
pid_t res = waitpid(id, nullptr, 0);
if(res > 0)
{
cout << "等待子进程成功" << endl;
}
// 0 -> 嘴巴 -> 读(嘴巴)
// 1 -> 笔 -> 写
// cout << "fd[0]" << pipefd[0] << endl;
// cout << "fd[1]" << pipefd[1] << endl;
return 0;
}
通过文件接口对pipefd
返回的两个文件描述符,进行read/write
,就能让父进程写进管道的字符串被子进程从管道读取到了:
管道内数据,写满了就不能再写了,读完了就不能再读了,这样就保证了管道内数据的合理性。
task_struct
放入等待队列中,并将状态从R设置为S/D/T!wait_queue_head_t
,一个链表结构。read
操作,当管道中没有数据可读时,read
函数会阻塞等待,直到有数据可读或管道被关闭。read
函数,父进程写入休眠1秒并不会导致read
函数立即返回0,而是等待父进程写入数据。read
函数时会等待父进程写入数据,并不会判断为文件读取完毕。
- 如果管道的写入端已经关闭(所有写入端都关闭),但读取端仍然打开,那么读取端的 read() 调用将会阻塞等待,直到有数据可读或者管道被关闭。
- 反之,如果管道的读取端已经关闭(所有读取端都关闭),而写入端仍然打开,那么写入端的 write() 调用可能会引发信号 SIGPIPE 或返回错误。
我们结合上述所学知识,就可以简单写一个通过通信管道父进程给子进程派发任务执行的代码了。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 父进程控制子进程
typedef void (*functor)();
vector<functor> functors; // 方法集合
//for debug
unordered_map<uint32_t, string> info;
void f1()
{
cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void f2()
{
cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void f3()
{
cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void loadFunctor()
{
info.insert({functors.size(), "处理日志的任务"});
functors.push_back(f1);
info.insert({functors.size(), "备份数据任务"});
functors.push_back(f2);
info.insert({functors.size(), "网络连接的任务"});
functors.push_back(f3);
}
int main()
{
// 0. 加载任务列表
loadFunctor();
// 1. 创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 2. 创建子进程
pid_t id = fork();
if (id < 0)
{
// 创建失败
cerr << "fork error" << endl;
return 2;
}
else if (id == 0)
{
// 子进程,read - 读取
// 3. 关闭不需要的文件fd
close(pipefd[1]);
// 子进程不断根据收到的信息,执行对应的方法
// 如果没有人往管道中写,此时子进程就卡在了read这里等待别人分配任务
while (true)
{
uint32_t operatorType = 0;
// 从fd为pipefd[0]的文件里读sizeof(uint32_t)个字节的内容,写到operatorType中去
// 如果有数据就读取,如果没有数据就阻塞等待,等待任务的到来。
ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));
if (s == 0)
{
cout << "我要退出了..." << endl;
break;
}
assert(s == sizeof(uint32_t));
(void)s;
// 走到这里一定是一个成功的读取
if (operatorType < functors.size())
{
functors[operatorType]();
}
else
{
cerr << "bug? operatorType = " << operatorType << endl;
}
}
close(pipefd[0]);
exit(0);
}
else if (id > 0)
{
srand((long long)time(nullptr));
// 父进程,write - 操作
// 3. 关闭不需要的文件fd
close(pipefd[0]);
// 4. 指派任务
int num = functors.size();
int cnt = 10;
while (cnt--)
{
// 5. 形成任务码
uint32_t commandCode = rand() % num;
cout << "父进程指派任务完成,任务是:" << info[commandCode] << "任务的编号是: " << cnt << endl;
// 向指定的进程下达执行任务的操作
write(pipefd[1], &commandCode, sizeof(uint32_t));
sleep(1);
}
close(pipefd[1]);
pid_t res = waitpid(id, nullptr, 0);
if (res) cout << "wait success" << endl;
}
return 0;
}
编码小细节:
由于在这里我们并不使用这个值,所以加上(void)前缀可以告诉编译器我们明确地不打算使用它,以避免产生未使用变量的警告信息。
控制多个进程:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 进程池
typedef void (*functor)();
vector<functor> functors; // 方法集合
//for debug
unordered_map<uint32_t, string> info;
void f1()
{
cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void f2()
{
cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void f3()
{
cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]"
<< "执行时间是[" << time(nullptr) << "]\n"
<< endl;
}
void loadFunctor()
{
info.insert({functors.size(), "处理日志的任务"});
functors.push_back(f1);
info.insert({functors.size(), "备份数据任务"});
functors.push_back(f2);
info.insert({functors.size(), "网络连接的任务"});
functors.push_back(f3);
}
// 第一个int32_t: 进程pid,第二个int32_t: 该进程对应的管道写端fd
typedef pair<int32_t, int32_t> elem;
int processNum = 5;
void work(int blockFd)
{
// 子进程核心工作的代码
while (true)
{
// a. 阻塞等待 b. 获取任务信息
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if (s == 0) break;
cout << "进程[" << getpid() << "]" << "开始工作" << endl;
assert(s == sizeof(uint32_t));
(void)s;// 编程小技巧
if (operatorCode < functors.size())
{
// c. 处理任务
functors[operatorCode]();
}
else
{
cerr << "bug? operatorCode = " << operatorCode << endl;
}
}
cout << "进程[" << getpid() << "]" << "结束工作" << endl;
}
// [子进程的pid, 子进程的管道fd]
void blanceSendTask(const vector<elem>& processFds)
{
srand((long long)time(nullptr));
// 随机给某个进程派发随机某个任务:
// uint32_t cnt = 10;
// while (cnt--)
// {
// sleep(1);
// // 选择一个进程,选择进程是随机的,没有压着一个进程给任务
// // 较为均匀的将任务给所有的子进程 -- 负载均衡
// uint32_t pick = rand() % processFds.size();
// // 选择一个任务
// uint32_t task = rand() % functors.size();
// // 把任务给一个指定的进程
// write(processFds[pick].second, &task, sizeof(task));
// // 打印对应的提示信息
// cout << "父进程指派任务->" << info[task] << "给进程: "
// << processFds[pick].first << "编号: " << pick << endl;
// }
// 将这几个进程创建的管道的写端给挨个关上
// for(int i = 0; i < processFds.size(); i++)
// {
// close(processFds[i].second);
// }
// 给这几个进程挨个派发随机任务;
for (int i = 0; i < processFds.size(); i++)
{
sleep(1);
int j = rand() % functors.size();
write(processFds[i].second, &j, sizeof(int));
close(processFds[i].second);
}
}
int main()
{
// 加载任务列表
loadFunctor();
vector<elem> assignMap;
// 创建processNum个进程
for (int i = 0; i < processNum; i++)
{
// 定义管道保存fd的对象
int pipefd[2] = { 0 };
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程执行,read -> pipefd[0]
close(pipefd[1]);
// 子进程执行
work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else if(id > 0)
{
// 父进程做的事情,pipefd[1]
close(pipefd[0]);
elem e(id, pipefd[1]);
// 将创建出来的子进程的pid存起来
assignMap.push_back(e);
}
}
cout << "creat all process success!\n" << endl;
// 父进程,派发任务
blanceSendTask(assignMap);
// 回收资源
for (int i = 0; i < processNum; i++)
{
if (waitpid(assignMap[i].first, nullptr, 0) > 0)
cout << "wait for:" << assignMap[i].first << " wait success! "
<< " number: " << i << endl;
}
cout << "----------------------------程序结束----------------------------" << endl;
return 0;
}
总体来说,这段代码实现了一个简单的进程池,通过负载均衡的方式将任务分发给子进程进行处理。
|
命令,其实就是一个匿名管道:
我们来查看一下进程状态:
我们看到这两个进程同属于一个父进程,这就说明sleep
进程是一对兄弟进程。
PID
不同,PPID
相同,说明有相同的父进程。由父子之间的通信转化成兄弟之间的通信:
|
时,我们左边当一条命令,右边当一条命令。fork
两次创建子进程,让这两个子进程各自继承对应的文件描述符。cat mytest
做输出重定向,对wc -l
做输入重定向。在 Linux 中,符号 “|” 表示管道(pipeline),用于将一个命令的输出连接到另一个命令的输入。在使用 “|” 时,前一个进程的标准输出会被连接到后一个进程的标准输入。这意味着前一个进程是写端,后一个进程是读端。
类似于创建匿名管道:
创建命名管道时候,要指明路径,和umask
值,为了防止默认umask
的扰乱,我们一开始将`umask``置为0。
umask(0);
if(mkfifo("./.fifo", 0600) != 0)//当返回值不为0的时候,代表出现了错误
{
cerr << "mkfifo error" << endl;
return 1;
}
管道文件是以p开头的:
通过管道实现的,两个终端虽然不一样,但是cat是进程,echo也是个进程,这两个进程都属于操作系统,写和读是同一个文件:
匿名管道之间的通信是基于父子进程继承的关系来实现的。而让两个毫不相干的进程实现进程通信则是命名管道做的事情。
命名管道,进程间通信的本质是:不同的进程要看到同一份资源。
匿名管道:子进程继承父进程。
命名管道:通过一个fifo文件
有路径就具有唯一性,通过路径,就能找到同一个资源!
只要都通过对应的管道文件所在的路径,就能保证使用路径的唯一性,就能够打开同一个文件。
只要打开的是同一个文件在内核里用的就是同一个struct file
,那么指向的就是同一个inode
,用的就是同一个缓冲区。
此时就看到了同一个资源。
命名管道是让两个进程之间是看到同一个文件,这个文件做了符号处理,相当于管道文件(通信时,数据不会刷新到磁盘上),操作系统一看到这个文件就知道了,这个文件的数据不用刷新到磁盘上,所以此时就在内存里,就有了管道。
头文件:
#pragma
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define IPC_PATH "./.fifo"
using namespace std;
客户端:
#include "comm.h"
// 写入
int main()
{
int pipeFd = open(IPC_PATH, O_WRONLY);
if (pipeFd < 0)
{
cerr << "open: " << strerror(errno) << endl;
return 1;
}
#define NUM 1024
char line[NUM];
// 进行通信
while (true)
{
printf("请输入你的消息# ");
fflush(stdout);
memset(line, 0, sizeof(line));
// fgets -> C语言的函数 -> line结尾自动添加\0
if (fgets(line, sizeof(line), stdin) != nullptr)
{
line[strlen(line) - 1] = '\0';
write(pipeFd, line, strlen(line));
}
else
{
break;
}
}
close(pipeFd);
cout << "客户端退出了" << endl;
return 0;
}
服务端:
#include "comm.h"
// 读取
int main()
{
umask(0);
// server创建好了,client就不用创建了
if (mkfifo(IPC_PATH, 0600) != 0)
{
cerr << "mkfifo error" << endl;
return 1;
}
int pipeFd = open(IPC_PATH, O_RDONLY);
if (pipeFd < 0)
{
cerr << "open fifo error" << endl;
return 2;
}
#define NUM 1024
// 正常的通信过程
char buffer[NUM];
while (true)
{
ssize_t s = read(pipeFd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = '\0';
cout << "客户端->服务器#" << buffer << endl;
}
else if (s == 0)
{
cout << "客户退出了,我也推出了" << endl;
break;
}
else
{
// do nothing
cout << "read: " << strerror(errno) << endl;
}
}
close(pipeFd);
cout << "服务端退出了" << endl;
// 跑完之后删除管道
unlink(IPC_PATH);
return 0;
}
必须server先跑,才能出现管道文件: