目录
一.通信背景
3.进程间通信目的
4.进程间通信发展
5.进程间通信分类
二.管道——匿名管道
1.匿名管道原理
2.匿名管道特点
3.打开进程读写的系统接口:pipe
4.管道的代码-演示pipe通信的基本过程-匿名管道
5.实现业务处理的管道代码-父进程控制子进程
6.进程池应用-多进程业务
进程池应用代码:
7.匿名管道特征总结:
三.管道——命名管道
1.命名管道原理
2.mkfifo 创建命名管道
3.命名管道应用代码:
makefile
comm.h
clientFifo.cpp
serverFifo.cpp
1.进程是具有独立性的!——导致进程间想交互数据,成本会非常高
进程为什么要通信:需要多进程进行协同处理一件事情
2.不要以为,进程独立了,就是彻底独立,有时候,我们需要双方能够进行一定程度的信息交互
因为进程有独立性。
所以通信之前,我们需要让不同的进程看到同一份资源(文件,内存块..)
我们要学的进程间通信,不是告诉我们如何通信。而是解决:他们两个如何先看到同一份资源!
资源的不同决定了不同种类的通信方式!
第一种通信方式—>管道(提供共享资源的一种手段)
父进程写入文件的缓冲区,子进程去缓冲区里读,就实现了两个进程看到同一份资源,这个用于通信的内存级文件就叫做管道(普通级文件是为了保存数据到磁盘的)。struct file中有inode,inode中有一个联合体可以来表示自己是管道/块设备/文件/字符
都是单向的!
管道传输资源的——这个资源就是 数据
进程间通信管道——>单向的,传输数据的!
进程间通信管道是单向的过程图:
①为什么父进程要分别打开读和写? ——答:为了让子进程继承,让子进程不用再打开了!(如果父进程只是打开写,子进程也会继承写,那就没人读了,让子进程读的话还得再打开读)
②为什么父子要关闭对应的读写?——答:管道必须是单向通信的。父进程关闭读,子进程关闭写,就构成单向的父写子读了。
③谁决定,父子关闭什么读写? ——答:不是由管道本身决定的, 是由你的需求决定的!
man 2 pipe :pipe底层封装open,open了两次,第一次O_RDONLY,第二次O_WRONLY,把读写描述符分别放在数组pipefd[0],pipefd[1]。 返回值 ——> 成功返回0,失败返回-1
①当父进程没有写入数据的时候,子进程在等。所以,父进程写入之后,子进程才能read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主!
②父进程和子进程读写管道的时候(是有一定的顺序性的!)。那么父子进程各自printf的时候,会有顺序吗?——答:无序。printf就是向显示器写入,也是文件,但是缺乏访问控制。
③父进程和子进程读写管道的时候:
——管道内部,没有数据,reader就必须阻塞等待(read),等管道有数据(阻塞等待就是当前进程的 task_ struct 放入等待队列中,R->S/D/T)
——管道内部,如果数据被写满,writer就必须阻塞等待(write)等待管道中有空间
因为pipe内部自带访问控制机制——同步和互斥机制
#include
#include
#include
#include
#include
#include
//演示pipe通信的基本过程 -- 匿名管道
int main()
{
// 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)
{
// child
// 子进程来进行读取, 子进程就应该关掉写端
close(pipefd[1]);
#define NUM 1024
char buffer[NUM];
while(true)
{
cout << "时间戳: " << (uint64_t)time(nullptr) << endl;
// 子进程没有带sleep,为什么子进程你也会休眠呢??
memset(buffer, 0, sizeof(buffer));
sleep(100);
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(s > 0)
{
//读取成功
buffer[s] = '\0';
cout << "子进程收到消息,内容是: " << buffer << endl;
}
else if(s == 0) //当read返回0时说明写端已关闭,所以要退出
{
cout << "父进程写完了,我也退出啦" << endl;
break;
}
else{
//Do Nothing
}
}
close(pipefd[0]);
exit(0);
}
else
{
// parent
// 父进程来进行写入,就应该关掉读端
close(pipefd[0]);
const char *msg = "你好子进程,我是父进程, 这次发送的信息编号是: ";
int cnt = 0;
// while(cnt < 5)
while(1)
{
char sendBuffer[1024];
sprintf(sendBuffer, "%s : %d", msg, cnt);
//sleep(30); // 这里是为了一会看现象明显
write(pipefd[1], sendBuffer, strlen(sendBuffer)); //要不要+1 1,0
cnt++;
cout << "cnt: " << cnt << endl;
}
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;
}
assert处只是换了一种写法,原写法是:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef void (*functor)();
vector functors; // 方法集合
// for debug
unordered_map 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);
}
2. 父进程控制子进程
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)
{
// 3. 关闭不需要的文件fd
// child,read
close(pipefd[1]);
// 4. 业务处理
while (true)
{
uint32_t operatorType = 0;
// 如果有数据,就读取,如果没有数据,就阻塞等待, 等待任务的到来
ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));
if (s == 0)
{
cout << "我要退出啦,我是给人打工的,老板都走了...." << endl;
break;
}
//这里if(s == sizeof(uint32_t));换一种断言的写法
assert(s == sizeof(uint32_t));
// debug 模式下:assert断言,是有效的
// release 模式:断言就没有了
// 一旦断言没有了,s变量就是:只被定义但没有被使用。
//release模式下只定义不使用,可能会有warning
(void)s; //保证s变量被使用过
if (operatorType < functors.size())
{
functors[operatorType]();
}
else
{
cerr << "bug? operatorType = " << operatorType << std::endl;
}
}
close(pipefd[0]);
exit(0);
}
else
{
srand((long long)time(nullptr));
// parent,write - 操作
// 3. 关闭不需要的文件fd
close(pipefd[0]);
// 4. 指派任务
int num = functors.size();
int cnt = 10;
while (cnt--)
{
// 5. 形成任务码
uint32_t commandCode = rand() % num;
std::cout << "父进程指派任务完成,任务是: " << info[commandCode] << " 任务的编号是: " << cnt << std::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;
}
注意:关闭写段在这里统一在最后回收资源时关闭。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef void (*functor)();
vector functors; // 方法集合
// for debug
unordered_map 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 std::pair elem;
int processNum = 5;
void work(int blockFd)
{
cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
// 子进程核心工作的代码
while (true)
{
// a.阻塞等待 b. 获取任务信息
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if(s == 0) break;
assert(s == sizeof(uint32_t));
(void)s;
// c. 处理任务
if(operatorCode < functors.size()) functors[operatorCode]();
}
cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
}
// [子进程的pid, 子进程的管道fd]
void blanceSendTask(const vector &processFds)
{
srand((long long)time(nullptr));
while(true)
{
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;
}
}
int main()
{
loadFunctor();
vector assignMap;
// 创建processNum个进程
for (int i = 0; i < processNum; i++)
{
// 定义保存管道fd的对象
int pipefd[2] = {0};
// 创建管道
pipe(pipefd);
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程读取, r -> pipefd[0]
close(pipefd[1]);
// 子进程执行
work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
//父进程做的事情, pipefd[1]
close(pipefd[0]);
elem e(id, pipefd[1]);
assignMap.push_back(e);
}
cout << "create all process success!" << std::endl;
// 父进程, 派发任务
blanceSendTask(assignMap);
// 回收资源
for (int i = 0; i < processNum; i++)
{
if (waitpid(assignMap[i].first, nullptr, 0) > 0)
cout << "wait for: pid=" << assignMap[i].first << " wait success!"
<< "number: " << i << "\n";
close(assignMap[i].second); 注意!!关闭写段在这里统一关闭
}
}
(命令行管道|,其实就是匿名管道)
(1)管道只能用来进行具有血缘关系的进程之间,进行进程间通信。常用于父子通信
(2)管道只能单向通信(内核实现决定的),半双工的一 种特殊情况(半双工:某时某刻只要一个人在说一个人在听)
(3)管道自带同步机制(pipe满—> writer等 ; pipe空—> reader等) --自带访问控制
(4)管道是面向字节流的----现在还解释不清楚--先写的字符,一定是先被读取的,没有格式边界,需要用户来定义区分内容的边界
[sizeof (uint32_ t)] ---- 网络tcp,我们自定义协议的
(5)管道的生命周期--管道是文件--进程退出了,曾经打开的文件会怎么办?退出--管道的生命周期随进程。与该文件相关的所有进程退出,此文件的引用计数--到0,文件也会关闭。
只能父子(血缘)通信? ?毫不相干的进程之间进行通信,可以吗? ?——可以,用命名管道
进程间通信的本质是:不同的进程要看到同一份资源。
匿名管道原理:通过利用 fork创建子进程,子进程能继承父进程的文件描述符表的特性 实现的。
命名管道原理:磁盘中的内存把 文件描述的结构体struct_file 加载到内存中,进程1打开此文件,引用计数加1;进程2打开此文件,遍历内核发现文件存在,引用计数变为2,两个进程可以看到同一份资源。两进程通信时在内存中通信,数据不会刷新到磁盘上,此文件只是一种符号
通过一个fifo文件->有路径->具有唯一性->通过路径,找到同一个资源!
磁盘
int mkfifo(const char *pathname, mode_t mode);
作用:创建一个文件(命名管道),路径pathname就是这个文件的路径(./.fifo)。
pathname:你要制作的文件的文件路径。mode:你要制作的文件的权限
返回值:创建成功返回0,创建失败返回-1
.PHONY:all
all: clientFifo serverFifo
clientFifo:clientFifo.cpp
g++ -Wall -o $@ $^ -std=c++11
serverFifo:serverFifo.cpp
g++ -Wall -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf clientFifo serverFifo .fifo
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define IPC_PATH "./.fifo"
//写入
#include "comm.h"
using namespace std;
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)
{
//abcd\n\0
line[strlen(line) - 1] = '\0';
write(pipeFd, line, strlen(line));
}
else
{
break;
}
}
close(pipeFd);
cout << "客户端退出啦" << endl;
return 0;
}
//读取
#include "comm.h"
using namespace std;
int main()
{
umask(0);
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 << "客户退出啦,我也退出把";
break;
}
else
{
//do nothing
cout << "read: " << strerror(errno) << endl;
break;
}
}
close(pipeFd);
cout << "服务端退出啦" << endl;
unlink(IPC_PATH);
return 0;
}
注意点①:serverFifo.cpp中最后几句容易忘写:
close(pipeFd);
cout << "服务端退出啦" << endl;
unlink(IPC_PATH);