1.進程間通信?的
2.管道
2.1 匿名管道
-----通常用來實現 父子通信
創建子進程時,需要把父進程的進程內容全部拷貝一份,但文件管理是不需要拷貝的
但是我們把父進程的文件描述符表給拷貝下來了,文件描述符表里是一堆指針,他們仍然指向父進程打開的那些文件
- 這也是為什么之前運行子進程會在同一個屏幕上打印內容,
因為父子進程用的是同一個顯示器文件
,自然在同一個屏幕上打印咯, - 和c++中遇到的
淺拷貝
十分相似
2.2 原理
2.2 管道樣例
#include <iostream>
#include <unistd.h>using namespace std;
int main()
{int fd[2]={0};//這里使用fd模擬文件描述符表,忽略了0,1,2即標準輸入stdin,標準輸出stdout,標準錯誤stderrint n=pipe(fd);//pipe函數需要頭文件unistd.hif(n<0)//運行失敗會是n<0{cout<<"error"<<endl;return 1;}cout<<"fd[0]:"<<fd[0]<<endl;cout<<"fd[1]:"<<fd[1]<<endl;return 0;
}
最終結果是:
- 因為0,1,2即標準輸入,標準輸出,標準錯誤一直在被打開,所以只能分配3,4
完整父子進程管道代碼:
#include <iostream> // 標準輸入輸出(cout, endl)
#include <unistd.h> // 提供 pipe(), fork(), close(), read(), write(), sleep() 等系統調用
#include <cstdio> // 提供 printf() 等 C 標準 I/O 函數
#include <cstring> // 提供字符串處理函數(如 memset)
#include <sys/types.h> // 提供 pid_t 等數據類型定義
#include <sys/wait.h> // 提供 waitpid() 函數using namespace std;// 子進程向管道寫入數據的函數
void childwrite(int wfd) {char c = 0; // 寫入的字符(這里固定為 0)int cnt = 0; // 計數器,記錄寫入次數while (true) {write(wfd, &c, 1); // 向管道寫入 1 字節(實際寫入的是 '\0')printf("child: %d\n", cnt++); // 打印寫入次數}
}// 父進程從管道讀取數據的函數
void fatherread(int rfd) {char buffer[1024]; // 讀取緩沖區while (true) {sleep(100); // 父進程休眠 100 秒(實際會被 read() 打斷)buffer[0] = 0; // 清空緩沖區(可選)// 從管道讀取數據(最多讀 sizeof(buffer)-1 字節,預留 1 字節給 '\0')ssize_t n = read(rfd, buffer, sizeof(buffer)-1);if (n > 0) { // 讀取成功buffer[n] = 0; // 手動添加字符串結束符 '\0'std::cout << "child say: " << buffer << std::endl; // 打印讀取的內容} else if (n == 0) { // 管道寫端關閉(子進程退出)std::cout << "n : " << n << std::endl;std::cout << "child 退出,我也退出";break;} else { // 讀取錯誤break;}break; // 測試時提前退出循環(實際應去掉)}
}int main() {// 1. 創建管道int fd[2] = {0}; // fd[0]:讀端,fd[1]:寫端int n = pipe(fd); // 調用 pipe() 創建匿名管道if (n < 0) { // 創建失敗cout << "error" << endl;return 1;}cout << "fd[0]:" << fd[0] << endl; // 打印讀端 fdcout << "fd[1]:" << fd[1] << endl; // 打印寫端 fd// 2. 創建子進程pid_t pid = fork(); // 調用 fork() 創建子進程if (pid == 0) { // 子進程邏輯close(fd[0]); // 關閉讀端(子進程只寫)childwrite(fd[1]); // 調用子進程寫入函數close(fd[1]); // 關閉寫端(實際不會執行到這里)exit(0); // 子進程退出}sleep(5); // 父進程休眠 5 秒(等待子進程寫入數據)close(fd[1]); // 關閉寫端(父進程只讀)fatherread(fd[0]); // 調用父進程讀取函數close(fd[0]); // 關閉讀端// 等待子進程退出int status = 0;int ret = waitpid(pid, &status, 0); // 阻塞等待子進程結束if (ret > 0) { // 子進程已退出// 打印子進程退出狀態(高 8 位是退出碼,低 7 位是終止信號)printf("exit code: %d, exit signal: %d\n", (status>>8)&0xFF, status&0x7F);sleep(5); // 父進程再休眠 5 秒(觀察用)}return 0;
}
2.3 五種特性
2.4 四種通信情況
blog.csdnimg.cn/direct/eeef895593df4fd08b31442035e93198.png)
3.進程池的模擬
3.1 hpp文件的使用
#ifndef __PROCESS_POOL_HPP__ // 頭文件保護宏(雙下劃線風格)
#define __PROCESS_POOL_HPP__#include <iostream> // 系統頭文件用尖括號<>// 函數聲明/定義
void test() {std::cout << "test" << std::endl; // 直接使用std::前綴
}#endif // __PROCESS_POOL_HPP__
-函數的聲明和定義可以放在一塊寫,注意頭兩行和末尾一行 是格式
3.2 進程池代碼實現
ProcessPool.hpp:
#ifndef __PROCESS_POOL_HPP__ // 頭文件保護宏,防止重復包含
#define __PROCESS_POOL_HPP__#include <iostream> // 標準輸入輸出
#include <cstdlib> // C標準庫(替代stdlib.h的C++版本)
#include <vector> // 動態數組容器
#include <unistd.h> // POSIX API(pipe/fork/close等)
#include <sys/wait.h> // 進程等待相關函數
#include "Task.hpp" // 自定義任務管理頭文件// Channel類:管理單個子進程的通信通道
class Channel
{
public:// 構造函數:初始化寫端fd和子進程IDChannel(int fd, pid_t id) : _wfd(fd), _subid(id){// 生成通道名稱(格式:channel-[fd]-[pid])_name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);}// 析構函數(空實現,資源通過Close()顯式釋放)~Channel() {}// 向子進程發送任務碼void Send(int code){int n = write(_wfd, &code, sizeof(code));(void)n; // 顯式忽略返回值(避免編譯器警告)}// 關閉寫端文件描述符void Close(){close(_wfd); // 關閉管道寫端}// 等待子進程退出,回收子進程,避免僵尸進程出現void Wait(){pid_t rid = waitpid(_subid, nullptr, 0); // 阻塞等待(void)rid; // 顯式忽略返回值}// Getter方法int Fd() { return _wfd; } // 獲取寫端fdpid_t SubId() { return _subid; } // 獲取子進程PIDstd::string Name() { return _name; } // 獲取通道名稱private:int _wfd; // 管道寫端文件描述符pid_t _subid; // 子進程PIDstd::string _name; // 通道標識名稱
};// ChannelManager類:管理所有子進程通道
class ChannelManager
{
public:ChannelManager() : _next(0) {} // 初始化輪詢索引// 添加新通道void Insert(int wfd, pid_t subid){_channels.emplace_back(wfd, subid); // 原地構造Channel對象,加入channel數組}// 輪詢選擇下一個通道(簡單負載均衡)Channel &Select(){auto &c = _channels[_next];_next = (_next + 1) % _channels.size(); // 環形選擇return c;}// 打印所有通道信息void PrintChannel(){for (auto &channel : _channels){std::cout << channel.Name() << std::endl;}}// 關閉所有子進程管道void StopSubProcess(){for (auto &channel : _channels){channel.Close();//關掉讀std::cout << "關閉: " << channel.Name() << std::endl;}}// 回收所有子進程void WaitSubProcess(){for (auto &channel : _channels){channel.Wait();std::cout << "回收: " << channel.Name() << std::endl;}}~ChannelManager() {} // 析構函數(vector自動釋放)private:std::vector<Channel> _channels; // 存儲所有Channel對象int _next; // 輪詢索引
};const int gdefaultnum = 5; // 默認子進程數量// ProcessPool類:主進程池實現
class ProcessPool
{
public:// 構造函數:初始化進程數并注冊任務ProcessPool(int num) : _process_num(num){_tm.Register(PrintLog); // 注冊日志任務_tm.Register(Download); // 注冊下載任務_tm.Register(Upload); // 注冊上傳任務}//把這三個函數指針全部加入函數指針數組中// 子進程工作循環void Work(int rfd){while (true){int code = 0;ssize_t n = read(rfd, &code, sizeof(code));//從rfd中讀任務嗎,和channel的send函數相對應,正常一次讀4字節if (n > 0) // 成功讀取{if (n != sizeof(code)) continue; // 數據不完整則繼續讀取std::cout << "子進程[" << getpid() << "]收到任務碼: " << code << std::endl;_tm.Execute(code); // 執行對應任務,就是三個函數之一,上傳,下載。。。。}else if (n == 0) // 管道關閉(父進程終止){std::cout << "子進程退出" << std::endl;break;}else // 讀取錯誤{std::cerr << "讀取錯誤" << std::endl;break;}}}// 啟動進程池bool Start(){for (int i = 0; i < _process_num; i++){// 1. 創建管道int pipefd[2] = {0};if (pipe(pipefd) < 0) return false; // 創建失敗// 2. 創建子進程pid_t subid = fork();if (subid < 0) return false; // fork失敗if (subid == 0) // 子進程分支{close(pipefd[1]); // 關閉寫端Work(pipefd[0]); // 進入工作循環close(pipefd[0]);exit(0); // 正常退出}else // 父進程分支{close(pipefd[0]); // 關閉讀端_cm.Insert(pipefd[1], subid); // 記錄通道信息}}return true;}// 調試用:打印所有通道void Debug() { _cm.PrintChannel(); }// 運行任務(主進程調用)void Run(){int taskcode = _tm.Code(); // 1. 獲取任務碼auto &c = _cm.Select(); // 2. 選擇子進程std::cout << "選擇子進程: " << c.Name() << std::endl;c.Send(taskcode); // 3. 發送任務std::cout << "發送任務碼: " << taskcode << std::endl;}// 停止進程池void Stop(){_cm.StopSubProcess(); // 關閉所有管道_cm.WaitSubProcess(); // 回收所有子進程}~ProcessPool() {} // 析構函數private:ChannelManager _cm; // 通道管理器int _process_num; // 子進程數量TaskManager _tm; // 任務管理器
};#endif
Task.hpp:
// 防止頭文件被重復包含的編譯器指令(現代C++替代#ifndef的方式)
#pragma once// 標準輸入輸出庫(用于cout等)
#include <iostream>
// 動態數組容器(用于存儲任務函數指針)
#include <vector>
// 時間相關函數(用于隨機數種子初始化)
#include <ctime>// 定義函數指針類型:無參數、無返回值的函數,名字是task_t!!!!!
typedef void (*task_t)(); 調試用任務函數
// 打印日志任務函數
void PrintLog()
{std::cout << "我是一個打印日志的任務" << std::endl;
}// 下載任務函數
void Download()
{std::cout << "我是一個下載的任務" << std::endl;
}// 上傳任務函數
void Upload()
{std::cout << "我是一個上傳的任務" << std::endl;
}
//// 任務管理類
class TaskManager
{
public:// 構造函數:初始化隨機數種子TaskManager(){srand(time(nullptr)); // 用當前時間初始化隨機數生成器}// 注冊任務函數:將函數指針存入vectorvoid Register(task_t t){_tasks.push_back(t); // 添加到任務列表末尾}// 生成隨機任務碼:返回[0, 任務數量-1]的隨機數int Code(){return rand() % _tasks.size(); // 取模保證不越界}// 執行任務:根據code調用對應的函數void Execute(int code){// 檢查code是否合法(防御性編程)if(code >= 0 && code < _tasks.size()){_tasks[code](); // 通過函數指針調用任務,就是上面三個打印,上傳,下載函數}// 注意:未處理非法code的情況(可添加錯誤處理)}// 析構函數(當前為空實現)~TaskManager(){}private:std::vector<task_t> _tasks; // 存儲所有注冊的任務函數指針
};
makefile:
process_pool:Main.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f process_pool
main.cc:
#include "ProcessPool.hpp"int main()
{// 創建進程池對象ProcessPool pp(gdefaultnum);// 啟動進程池pp.Start();//剛開始就是建立5個子進程和通道,但通道內沒有內容即任務碼,所以子進程的work會被卡住。// 自動派發任務int cnt = 10;while(cnt--){pp.Run();//往子進程里去發放任務碼,子進程開始work,也就是開始調用manager的Execute函數,就是在三個上傳,下載函數中隨機選一個來執行sleep(1);}// 回收,結束進程池pp.Stop();// 關閉所有管道-即回收父進程的wfd---使用close函數關掉所有channel中的wfd//回收所有子進程----調用waitpid函數return 0;
}
小問題:
如果我每關一個wfd,回收一個子進程會怎樣? 會在第一個子進程回收時阻塞!!!-------------------------------------- read()沒有返回0
- 第一次產生子進程,父進程文件描述符表分配3,4(三是讀,四是寫),子進程先是拷貝父類的內容,所以子進程也是(三是讀,四是寫),然后關閉不需要的fd,父進程關閉3,子進程關閉4
- 第二次產生子進程,父進程分配文件描述符3,5(
因為4已經在上次的過程中被占用
,三是讀,五是寫),同理,子進程也是(三是讀,五是寫),然后再次關閉不需要的fd,父進程關閉3,子進程關閉5 ,要注意的是,第二次產生的子進程會繼承父進程的4,即二號子進程的4是指向第一個子進程管道的寫端的!!!!!!
- 所以第一次,父進程的wfd被關閉后,寫端并沒有完全關閉(因為剩余的四個子進程都繼承了4號)寫端計數器還有4,read函數就不會返回0,自然就沒辦法結束第一個子進程,自然就沒辦法使用wait函數回收,導致阻塞
- 解決辦法-----倒著關,因為只有最后一個子進程的寫端是由父進程一人持有的,父進程關了那就是真的關了,可以讓read直接返回0,完成回收----以此類推
- 也可以在子進程創立時,遍歷channel數組,把里面的wfd都關了,說白了就是把繼承下來的寫端全關了,這樣所有的寫端都只由父進程持有
4.命名管道