hello,各位读者大大们你们好呀
系列专栏:【Linux初阶】
✒️✒️本篇内容:进程间通信介绍,管道概述,匿名管道+应用,命名管道+应用
作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
数据传输
:一个进程需要将它的数据发送给另一个进程(进程具有独立性,通信成本不低)。资源共享
:多个进程之间共享同样的资源。通知事件
:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。进程控制
:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。匿名管道
和命名管道
。我们知道,进程间通信需要一定的成本,那么这个成本是怎么产生的呢?这就要我们理解通信的本质了:1.OS直接或间接给通信双方的进程提供“内存空间”
;2.要通信的进程,必须看到同一份公共的资源
。
这个很好理解,因为进程具有独立性,所以需要有一个公共的地方让两个进程完成信息或资源的传输。我们所学的不同的通信种类,就是以操作系统提供的不同模块进行区别分类的!
任何一个文件(struct file)都有 file的操作方法
、属于自己的内核缓冲区
、strcut Page[]
。当我们进行 fork创建子进程的时候,我们会将父进程的 task_strcut
和 文件描述符表
拷贝一份,子进程文件描述符表指向的文件在不做修改的情况下和父进程文件描述符表指向的文件相同。此时,我们就能让父子两个不同的进程看到同一份资源了。
在能看到同一份资源的基础上,如果父进程可以向文件缓冲区中写入,子进程能从文件缓冲区中读取,不就完成了进程间的通信
了吗。我们将操作系统提供的这个内核级文件,我们称之为管道文件
。
进程间通信的数据不会刷新到磁盘,而是在OS内部完成
,因为刷新到磁盘会大大降低通信的速度。管道文件是一个内存级文件,通信只需要在OS内部完成,因此我们可以不关心它在磁盘的路径,甚至这个文件可以是虚拟出来的。
下面我们一起来认识一下管道通信的基本原理:
上图的步骤执行完之后,就可以实现将父进程的数据传输到子进程中去了。通过对上面图解的学习,我们不难知道:管道通常只能进行单向数据通信!
单向通信相比于双向通信更简单且更具有依赖性。
最终,设计者们根据这个方案的特性(有出入口、单向传输),为这个方案/方法取名为 - 管道
。
我们知道通信的实现需要两个步骤:1.让不同的进程拥有一份共享资源
;2.通信
。在文章之前的内容中,我们已经对第一点的原理进行了讲解,下面我将带着大家重一起学习管道的具体实现(开辟共享资源+通信)。
匿名管道:目前能用来进行父子进程间的通信
。
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
因为文件描述符0,1,2已经被占用,因此在理想情况下也需要用3,4代表读取和写入端,由于我们不清楚系统申请读写的顺序,所以我们习惯上直接使用fd[0]表示读端, fd[1]表示写端。
读取和写入端巧记:[0]: 读取,嘴巴,读书的;[1]: 写入,钢笔,写的;
代码示例:
#include
#include //snprintf
#include //write
#include
#include
#include //sleep wait
#include
#include
using namespace std;
// 父进程进行读取,子进程进行写入
int main()
{
// 第一步:创建管道文件,打开读写端
int fds[2];
int n = pipe(fds);
assert(n == 0);
// 第二步: fork
pid_t id = fork();
assert(id >= 0);
if (id == 0)
{
// 子进程进行写入 - 关闭读
close(fds[0]);
// 子进程的通信代码
const char* s = "我是子进程,我正在给你发消息";
int cnt = 0;
while (true)
{
cnt++;
char buffer[1024]; // 只有子进程能看到!
snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());//格式化输出
// 写端写满的时候,再写会阻塞,等对方进行读取!
write(fds[1], buffer, strlen(buffer));
cout << "count: " << cnt << endl;
//sleep(2); - 用于测试写端阻塞
}
// 子进程
close(fds[1]); // 子进程关闭写端fd
cout << "子进程关闭自己的写端" << endl;
exit(0);
}
// 父进程进行读取
close(fds[1]);
// 父进程的通信代码
while (true)
{
//sleep(2); - 用于测试读端阻塞
char buffer[1024];
// 如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); //buffer如果满了,s代表下一个未被使用的字节数
if (s > 0)
{
buffer[s] = 0;
cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;
}
else if (s == 0)
{
//读到文件结尾 - 读完之后s为0 - 跳出循环结束读取
cout << "read: " << s << endl;
break;
}
break;
}
close(fds[0]);
cout << "父进程关闭读端" << endl;
int status = 0;
n = waitpid(id, &status, 0);
assert(n == id);
cout << "pid->" << n << " : " << (status & 0x7F) << endl;
return 0;
}
读写特征:
管道是一个固定大小的缓冲区
。
读慢、写快
:写端将缓冲区写满的时候,再写(写端)会阻塞,需要等对方进行读取!
读快、写慢
:如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
读关闭、一直写
:为避免资源浪费,OS会关闭写端,即给写进程发送信号,终止写端。
———— 我是一条知识分割线 ————
我们在命令行中经常会使用这样的指令
cat file.txt | grep "hello"
它实质上就是通过树划线对指令进行了分割,让操作系统 fork两个子进程对分割好的两条指令进行实现,再通过兄弟管道之间的进程通信,最终变成我们看到的样子的!
我们可以用匿名管道设计一个简易的进程池
,即用一个辅助进程控制其他进程完成工作。在父进程(写端)我们通过给不同的子进程(读端)传输不同的命令操作码(commandCode),让对应的子进程根据命令操作码实现不同的功能,我们让命令操作码为 4字节。
场景示例:我们可以给 2号进程发送命令操作码和对应的运算数据,唤醒 2号进程之后,让进程对命令操作码对应的功能(运算)进行实现。
我们希望:我们可以将我们的任务均衡的下发给每一个子进程,让子进程执行 - 负载均衡(单机)。
代码示例
processpool:processPool.cc
g++ - o $@ $ ^ -std = c++11
.PHONY:clean
clean :
rm - rf processpool
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MakeSeed() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234)//随机数种子
#define PROCSS_NUM 10
///子进程要完成的某种任务 -- 模拟一下/
// 函数指针 类型
typedef void (*func_t)();
void downLoadTask()
{
std::cout << getpid() << ": 下载任务\n"
<< std::endl;
sleep(1);
}
void ioTask()
{
std::cout << getpid() << ": IO任务\n"
<< std::endl;
sleep(1);
}
void flushTask()
{
std::cout << getpid() << ": 刷新任务\n"
<< std::endl;
sleep(1);
}
void loadTaskFunc(std::vector<func_t>* out) //加载方法表
{
assert(out);
out->push_back(downLoadTask);
out->push_back(ioTask);
out->push_back(flushTask);
}
/下面的代码是一个多进程程序//
class subEp // Endpoint
{
public:
subEp(pid_t subId, int writeFd)
: subId_(subId), writeFd_(writeFd)
{
char nameBuffer[1024]; //定义对象名称
snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
name_ = nameBuffer;
}
public:
static int num; //用于区别不同对象(整体)
std::string name_; //管道+子进程的整体名称
pid_t subId_; //子进程pid
int writeFd_; //父进程写端的文件描述符
};
int subEp::num = 0;
int recvTask(int readFd) //读端接口
{
int code = 0;
ssize_t s = read(readFd, &code, sizeof code);
if (s == 4) return code;
else if (s <= 0) return -1;
else return 0;
}
void sendTask(const subEp& process, int taskNum)
{
std::cout << "send task num: " << taskNum << " send to -> " << process.name_ << std::endl;
int n = write(process.writeFd_, &taskNum, sizeof(taskNum));
assert(n == sizeof(int));
(void)n;
}
void createSubProcess(std::vector<subEp>* subs, std::vector<func_t>& funcMap)
{
std::vector<int> deleteFd; //管道+子进程整体选择(可以选择不同整体)
for (int i = 0; i < PROCSS_NUM; i++)
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
(void)n;
// 父进程打开的文件,是会被子进程共享的
// 你试着多想几轮 - 子进程会继承上一个子进程的写端
pid_t id = fork();
if (id == 0)
{
for (int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]); //关闭从上一个子进程继承而来的写端
// 子进程, 进行处理任务
close(fds[1]);
while (true)
{
// 1. 获取命令码,如果没有发送,我们子进程应该阻塞
int commandCode = recvTask(fds[0]);
// 2. 完成任务
if (commandCode >= 0 && commandCode < funcMap.size())
funcMap[commandCode]();
else if (commandCode == -1) break;
}
exit(0);
}
close(fds[0]);
subEp sub(id, fds[1]); //构造一个整体对象
subs->push_back(sub); //将新整体链接到vector中
deleteFd.push_back(fds[1]); //保存文件描述符,方便以后关闭从上一个子进程继承而来的写端
}
}
void loadBlanceContrl(const std::vector<subEp>& subs, const std::vector<func_t>& funcMap, int count)
{
int processnum = subs.size();
int tasknum = funcMap.size();
bool forever = (count == 0 ? true : false);
while (true)
{
// 1. 选择一个子进程 --> std::vector -> index - 随机数(负载均衡)
int subIdx = rand() % processnum;
// 2. 选择一个任务 --> std::vector -> index
int taskIdx = rand() % tasknum;
// 3. 任务发送给选择的进程
sendTask(subs[subIdx], taskIdx);
sleep(1); //防止发送过快
if (!forever)
{
count--;
if (count == 0) break;
}
}
// write quit -> read 0
for (int i = 0; i < processnum; i++) close(subs[i].writeFd_); // waitpid();
}
void waitProcess(std::vector<subEp> processes)
{
int processnum = processes.size();
for (int i = 0; i < processnum; i++)
{
waitpid(processes[i].subId_, nullptr, 0);
std::cout << "wait sub process success ...: " << processes[i].subId_ << std::endl;
}
}
int main()
{
MakeSeed(); //生成随机数种子
// 1. 建立子进程并建立和子进程通信的信道, 有bug的,但是不影响我们后面编写
// 1.1 加载方法表
std::vector<func_t> funcMap;
loadTaskFunc(&funcMap);
// 1.2 创建子进程,并且维护好父子通信信道
std::vector<subEp> subs;
createSubProcess(&subs, funcMap);
// 2. 走到这里就是父进程, 控制子进程,负载均衡的向子进程发送命令码
int taskCnt = 3; // 0: 永远进行,>0: 父进程发送几次
loadBlanceContrl(subs, funcMap, taskCnt);
// 3. 回收子进程信息
waitProcess(subs);
return 0;
}
命名管道
。$ mkfifo filename
———— 我是一条知识分割线 ————
命名管道也可以从程序里创建,相关函数有:
#include
#include
int mkfifo(const char *filename,mode_t mode);
这里我们先要搞清楚一个概念,命名管道如何让不同的进程,看到同一份资源?
可以让不同的进程打开指定名称(路径+文件名)的同一文件
。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NAMED_PIPE "/tmp/mypipe"
bool createFifo(const std::string& path)
{
umask(0);
int n = mkfifo(path.c_str(), 0600);
if (n == 0)
return true;
else
{
std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;
return false;
}
}
void removeFifo(const std::string& path)
{
int n = unlink(path.c_str()); //删除管道
assert(n == 0); // debug , release 里面就没有了
(void)n;
}
#include "comm.hpp"
int main()
{
std::cout << "client begin" << std::endl;
int wfd = open(NAMED_PIPE, O_WRONLY);
std::cout << "client end" << std::endl;
if (wfd < 0) exit(1);
//write
char buffer[1024];
while (true)
{
std::cout << "Please Say# ";
fgets(buffer, sizeof(buffer), stdin); // abcd\n
if (strlen(buffer) > 0) buffer[strlen(buffer) - 1] = 0; //去掉输入的\n
ssize_t n = write(wfd, buffer, strlen(buffer));
assert(n == strlen(buffer));
(void)n;
}
close(wfd);
return 0;
}
#include "comm.hpp"
int main()
{
bool r = createFifo(NAMED_PIPE);
assert(r);
(void)r;
std::cout << "server begin" << std::endl;
int rfd = open(NAMED_PIPE, O_RDONLY);
std::cout << "server end" << std::endl;
if (rfd < 0) exit(1);
//read
char buffer[1024];
while (true)
{
ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "client->server# " << buffer << std::endl;
}
else if (s == 0)
{
std::cout << "client quit, me too!" << std::endl;
break;
}
else
{
std::cout << "err string: " << strerror(errno) << std::endl;
break;
}
}
close(rfd);
// sleep(10);
removeFifo(NAMED_PIPE);
return 0;
}
注意:命名管道文件必须读端、写端都打开才能开始运行
注意:当我们关闭写端,读端会跟着关闭(代码实现的!)
进程间通信介绍 & 管道 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!