之前我们编写的代码大多是单进程任务。单进程无法使用进程的并发功能,也无法实现多进程的协同工作。
因此我们要实现多进程任务,而多进程的关键就是进程间通信,进程间通信包括:
进程间通信的本质是:让不同的进程共享同一份资源,即读写同一块内存空间。该内存空间不能隶属于任何一个进程,而是要由操作系统提供,否则将违背进程的独立性。
管道:Linux中最古老的进程间通信形式,是Linux文件系统原生支持的。
匿名管道pipe
命名管道
System V IPC:更侧重于本地进程间通信
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC:基于网络的进程间通信:既能进行本地通信,又能进行网络通信。使代码具有高扩展,高使用性。
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
什么是管道?
父进程创建管道:分别以读写方式打开同一个管道文件(打开两次)。
调用fork(),创建子进程。
在创建子进程时,操作系统会将父进程的文件描述符表拷贝(写时拷贝)到子进程中。这意味着子进程会继承父进程所有打开的文件描述符,包括文件、管道、套接字等。
注意:进程管理和文件管理是两个独立的模块。创建子进程时,进程相关的内核数据结构(task_struct,映射页表,mm_struct,files_struct)基本都要进行写时拷贝。而文件相关的内核数据结构(file结构体)不会进行拷贝。
构建单向通信的信道:父子进程各自关闭不需要的读写端;发送方关闭读端,接收方关闭写端。
在进程通信的过程中,管道文件中的数据需要写入到磁盘(落盘,持久化)吗?
总结:
方式一:通过命令使用匿名管道构建进程间通信,匿名管道的指令符号是 “|”
方式二:匿名管道也可以由进程创建,创建匿名管道的系统调用是pipe
使用系统调用pipe进行父子进程间通信:
测试代码:
#include .....
int main(){
//1. 使用pipe系统调用创建管道
int pipefd[2]={0};
int ret = pipe(pipefd);
assert(ret != -1); //在release模式下,assert语句被删除,ret成了未使用的变量。
(void)ret; //强转void是为了解除编译警告。
#ifdef DEBUG //条件编译:调试时编译
//使用编译指令定义宏:g++ -D DEBUG
//加#注释定义:g++ #-D DEBUG
cout << "pipefd[0]: " << pipefd[0] << endl;
cout << "pipefd[1]: " << pipefd[1] << endl;
#endif
//2.创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程是通信接收方
//3.构建单向通信的信道
close(pipefd[1]); //关闭写端
//4.接收信息
char r_buff[1024]; //读缓冲区
while(true)
{
//如果写入的一方没有关闭写端:管道中有数据就读,没有数据就阻塞等待
ssize_t sz = read(pipefd[0], r_buff, sizeof(r_buff)-1);
if(sz>0)
{
r_buff[sz] = '\0';
printf("I'm child process! pid:%d ppid:%d\n", getpid(), getppid());
cout << "father# " << r_buff << endl;
}
else if(sz == 0) //如果写入的一方已经关闭写端:read会返回0,表示读到了文件的结尾!
{
cout << "writer quit(father), reader quit(child)!" << endl;
break;
}
}
close(pipefd[1]); //子进程退出前关闭读端
exit(0);
}
//父进程是通信发送方
//3.构建单向通信的信道
close(pipefd[0]); //关闭读端
//4.发送信息
char w_buff[1024]; //写缓冲区
size_t cnt = 0;
string message = "我是父进程,正在向子进程发送消息!";
while(true)
{
snprintf(w_buff, sizeof(w_buff), "%s pid:%d cnt:%d", message.c_str(), getpid(), ++cnt);
write(pipefd[1], w_buff, strlen(w_buff)); //注意:使用strlen不要+1,不写入末尾的\0
if(cnt == 10)
{
cout << "writer quit(father)!" << endl;
break;
}
sleep(1);
}
close(pipefd[1]); //父进程退出前关闭写端
pid_t cpid = waitpid(id, nullptr, 0);
assert(ret != -1);
cout << "child process quit! child_pid: " << cpid << endl;
return 0;
}
运行结果:
进程池(Process Pool)是一种常见的进程管理技术,它可以提高程序的并发性和效率。
进程池通常由一组预先创建好的子进程组成,这些子进程可以被主进程动态地分配和管理。主进程将任务分配给子进程,子进程执行任务并将结果返回给主进程。当任务完成后,子进程不会退出,而是继续等待下一个任务的到来。
进程池的主要优点是可以避免频繁地创建和销毁进程,从而提高程序的效率。由于子进程已经预先创建好了,因此可以避免每次创建进程时的开销,如内存分配、资源初始化等。此外,进程池还可以避免进程过多导致系统资源耗尽的问题,从而提高程序的稳定性。
进程池通常用于处理大量的短时间任务,如网络服务器中的请求处理、数据处理等。通过进程池,可以将任务分配给多个子进程并行处理,从而提高程序的并发性和效率。
示例代码:
//process_pool.cc 进程池的实现代码
#include "task.hpp"
#include ...
const size_t PROCESS_NUM = 5; //创建子进程的数量
uint32_t WaitCommand(int rfd){
uint32_t comid = 0; //严格限制comid的数据类型是32位int,保证在不同平台下运行。
ssize_t sz = read(rfd, &comid, sizeof(comid)); //如果对方不发送命令,就阻塞等待
if(sz == 0) //如果写端fd被关闭:则read返回0,此时子进程退出。
{
return -1;
}
assert(sz == sizeof(uint32_t));
return comid;
}
void SendAndWakeup(pid_t cpid, int wfd, uint32_t cmdid){
write(wfd, &cmdid, sizeof(cmdid));
printf("main process: call %d execute %s through %d\n", cpid, desc[cmdid].c_str(), wfd);
}
int main(){
srand(time(nullptr));
LoadHandler();
//1.创建一个进程信道表:pid-pipefd
vector<pair<pid_t, int>> slots;
//2.循环创建子进程并建立管道通信,填写进程信道表
for(size_t i = 0; i < PROCESS_NUM; ++i)
{
//2.1 创建管道
int pipefd[2] = {0};
int ret = pipe(pipefd);
assert(ret!=-1);
(void)ret;
//2.2 创建子进程
pid_t id = fork();
assert(id!=-1);
if(id == 0)
{
//3. 子进程接收并执行命令
close(pipefd[1]); //子进程关闭写端
while(true)
{
uint32_t comid = WaitCommand(pipefd[0]);
if(comid >= 0 && comid < cmdset.size()) //如果comid合法就执行命令
{
cmdset[comid]();
}
else if(comid == -1) //写端关闭,子进程退出
{
break;
}
else
{
cout << "非法的命令ID!" << endl;
}
}
close(pipefd[1]); //子进程退出前关闭读端
exit(0);
}
//2.3 填写进程信道表
close(pipefd[0]); //父进程关闭读端
slots.emplace_back(id, pipefd[1]);
}
//父进程选择进程发送命令
while(true)
{
//随机任务
//随机选择一个任务
uint32_t cmdid = rand() % cmdset.size();
//选择一个进程 ,采用随机数的方式,选择进程来完成任务,随机数方式的负载均衡
int choice = rand() % slots.size();
//把任务派发给指定的进程
SendAndWakeup(slots[choice].first, slots[choice].second, cmdid);
sleep(1);
//手动指派任务
// int select;
// uint32_t cmdid;
// cout << "############################################" << endl;
// cout << "# 1. show funcitons 2.send command #" << endl;
// cout << "############################################" << endl;
// cout << "Please Select> ";
// cin >> select;
// if (select == 1)
// ShowHandler();
// else if (select == 2)
// {
// cout << "Enter Your Command> ";
// // 选择任务
// cin >> cmdid;
// // 选择进程
// int choice = rand() % slots.size();
// // 把任务给指定的进程
// SendAndWakeup(slots[choice].first, slots[choice].second, cmdid);
// }
// else
// {
// break;
// }
}
//父进程退出前关闭所有管道的写端,对应的子进程都会退出
for(const auto& slot : slots)
{
close(slot.second);
}
// 回收所有的子进程信息
for(const auto& slot : slots)
{
pid_t cpid = waitpid(slot.first, nullptr, 0);
assert(cpid != -1);
cout << "child process[" << cpid << "] exit!" << endl;
}
return 0;
}
//task.hpp 任务处理函数的实现代码
#include ...
typedef function<void()> func;
vector<func> cmdset; cmdset; //命令集:用他调用指定函数
unordered_map<uint32_t, string> desc; //命令描述:用他打印指定函数的字符串描述
void ReadMySQL(){
printf("sub process[%d]: 执行访问数据库任务\n\n", getpid());
}
void AnalyzeURL(){
printf("sub process[%d]: 执行URL解析任务\n\n", getpid());
}
void Encrypt(){
printf("sub process[%d]: 执行加密任务\n\n", getpid());
}
void Save(){
printf("sub process[%d]: 执行数据保存任务\n\n", getpid());
}
//向命令集中加载命令
void LoadHandler(){
uint32_t cmdid = 0;
cmdset.push_back(ReadMySQL);
desc.emplace(cmdid++, "ReadMySQL: 读取数据库");
cmdset.push_back(AnalyzeURL);
desc.emplace(cmdid++, "AnalyzeURL: URL解析");
cmdset.push_back(Encrypt);
desc.emplace(cmdid++, "Encrypt: 加密计算");
cmdset.push_back(Save);
desc.emplace(cmdid++, "Save: 数据保存");
}
//打印所有的命令描述
void ShowHandler(){
for(const auto& iter : desc)
{
printf("cmdid: %d cmddesc: %s\n", iter.first, iter.second.c_str());
}
}
运行结果(执行随机任务):
匿名管道(pipe)应用的一个限制就是只能用于具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。
命名管道与匿名管道不同:
命名管道与普通磁盘文件不同:
提示:命名管道和匿名管道的文件存在形式不同,创建打开方式不同。但是他们构建进程间通信的思路和原理是相同的。
方式一:命名管道可以从命令行上创建,使用指令mkfifo创建命名管道:
方式二:命名管道也可以由进程创建,创建命名管道的系统调用是mkfifo:
注意:以上两种方式只能用于创建FIFO文件,需要调用open函数才能以读或写的形式打开FIFO文件。
命名管道的打开规则:
common.h & log.hpp:
//common.h:server.cxx和client.cxx的公共头文件
#ifndef _COMMON_H_
#define _COMMON_H_
#include ...
using namespace std;
#define MODE 0666 //文件权限码
#define BUFF_SIZE 128 //缓冲区的大小
//server和client打开同一个命名管道
const char* ipcPath = "./fifo.ipc"; //命名管道的所在路径
#endif
//log.hpp:实现打印日志信息的代码
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include ...
using namespace std;
enum MsgTypeID{
DEBUG,
NOTICE,
WORNING,
ERROR
};
const char* MsgTypeName[] = {"DEBUG", "NOTICE", "WORNING", "ERROR"};
void PrintLog(char* msg, MsgTypeID id){
printf("%u | %s: %s\n", (unsigned)time(nullptr), MsgTypeName[id], msg);
}
#endif
server.cxx
//server.cxx
#include "common.h"
#include "log.hpp"
void GetMessage(int fd)
{
//测试一中的进程间通信代码
//...
}
int main()
{
//1.创建命名管道
int ret = mkfifo(ipcPath, MODE);
if(ret == -1) return errno;
PrintLog("创建命名管道成功!", DEBUG);
//2.打开管道文件
int fd = open(ipcPath, O_RDONLY);
if(ret == -1) return errno;
PrintLog("打开管道文件成功!", DEBUG);
//3.进程间通信
//测试一:服务端直接处理客户端信息
char rbuff[BUFF_SIZE];
while(true)
{
ssize_t sz = read(fd, rbuff, sizeof(rbuff)-1);
if(sz>0)
{
//读取到数据
rbuff[sz] = 0;
printf("[%d] clinet say> %s\n", getpid(), rbuff);
}
else if(sz == 0)
{
//end of file
printf("[%d] read end of file, clien quit, server quit too!\n", getpid());
break;
}
else
{
//read error
exit(errno);
}
}
//测试二:服务端创建多个子进程接收处理客户端信息
// int process_num = 3;
// for(int i = 0; i < process_num; ++i)
// {
// pid_t id = fork();
// if(id == -1) exit(errno);
// if(id == 0)
// {
// GetMessage(fd);
// exit(0);
// }
// }
// //等待子进程退出
// for(int i = 0; i < process_num; ++i)
// {
// pid_t cpid = waitpid(-1, nullptr, 0);
// }
//4.关闭文件
close(fd);
PrintLog("关闭管道文件!", DEBUG);
//5.删除命名管道
unlink(ipcPath);
PrintLog("删除管道文件!", DEBUG);
return 0;
}
client.cxx
//client.cxx
#include "common.h"
int main()
{
//1.打开管道文件
int fd = open(ipcPath, O_WRONLY);
if(fd == -1) return errno;
//2.进程间通信
//char wbuff[BUFF_SIZE];
string wbuff(BUFF_SIZE, 0);
while(true)
{
cout << "please enter message line:" << endl;
getline(cin, wbuff);
write(fd, wbuff.c_str(), wbuff.size());
}
//3.关闭文件
close(fd);
return 0;
}
测试一:服务端直接处理客户端信息
当client端(写端)退出时,server端(读端)读取到管道文件末尾,read返回0,server端也退出。(访问控制)
提示:和匿名管道一样,为了保证进程通信协同,命名管道也提供了访问控制。具体规则请参考匿名管道。
测试二:服务端创建多个子进程接收处理客户端信息。(所有的子进程都继承了主进程打开的管道文件)
从client端发送的消息被server端子进程抢占式接收(可以发现每次输出的子进程pid是随机)
提示:一个管道可以同时具有多个读端,管道的所有读端竞争式的获取资源。