1.溫故知新
? ? ? ? ? ? ? 在上一篇博客我們知道了動態庫是怎么樣進行鏈接的,我們知道我們的.o文件,可執行文件都是我們的ELF格式的文件,是ELF文件,里面就有ELF header,程序頭表,節,還有節頭表,我們鏈接器編譯的時候需要合并節,節頭表告訴編譯器怎么合并,當加載進內存的時候又會根據程序頭表,程序頭表告訴節怎么合并成我們的段,我們ELF header里面有我們程序的入口,但是不和我們想象中的先程序我們的main函數,而是先去執行我們的_start,它里面會為我們實現動態庫的地址重定向。我們動態庫是程序加載的時候才從磁盤加載到內存里的,然后我們規定我們的代碼區是不可以更改的,在數據區創建GOT表來實現我們的地址重定向,為了節省內存,我們動態庫函數加載也是采用了延時加載的手段,就是先把函數在虛擬地址開辟好,當你調用的時候發現虛擬地址沒有映射物理地址,發生中斷,陷入內核,內核幫助我們從磁盤加載要用的庫函數建立物理和虛擬的映射關系,實現我們的動態庫函數調用,至于靜態庫,鏈接的時候就已經完成地址的重定向,直接把我們的庫搞到可執行里,不需要再搞這個復雜的一套。
? ? ? ? 下面,我們介紹一下我們的進程間通信,我們知道進程是具有獨立性的,進程之間的任務執行不會相互干擾,但是我們有的時候需要我們進程間通信,為了實現進行間通信我們創建出了管道的概念。下面來詳細說明一下。
2.進程間通信
? ? ? ? ? ?首先我們來說明:進程間通信本質就是讓不同的進程看到同一份資源,并且拿到這份資源,但是由于我們的進程獨立性,我們要做到這個并不是很容易。
????????
我們進程間通信有很多目的,比如共享資源,比如控制進程等等。
管道是什么呢?管道就是我們進行進程間通信的工具。
管道的定義是:管道是一個基于文件系統的一個內存級的實現進程間單向通信的文件
管道的底層原理是:我們知道父進程創建子進程,子進程會把父進程的PCB和struct file自己拷貝一份,所以子進程的也會指向父進程指向的文件,因為這個原因我們父子進程打印都往一個屏幕上打印,因為我們的屏幕文件被共享了,而我們現在父子進程就可以看見同一個文件的文件緩沖區了,文件緩沖區就是一個管道,但是有一個問題是我們上面打開的文件大部分是普通文件,是普通文件就要把文件緩沖區的內容往磁盤里寫,但是我們管道創建本質是為了讓我們父進程把它的資源交給子進程,不需要往磁盤做IO刷新,而且我們文件的讀寫位置只有一個,父進程往管道里100個字節,但是子進程讀從100開始讀,它是讀不到數據的,讀寫共享是不方便我們讀取寫入的,不方便通信。所以我們的解決方法是如果是管道,我們把我們的管道文件也拷貝一份給子進程,并且這個文件不往磁盤做IO,這樣我們就解決了我們的讀寫位置重疊,和我們的往磁盤寫入的問題。
這個圖反映了我們的讀寫位置是一樣的問題
管道的原理就是把我們的file原來不需要拷貝,然后我們多拷貝一份,就解決了我們的讀寫位置一樣的問題,它們的pos就不一樣了。
但是我們的文件緩沖區,文件inode和文件的操作表都是一樣的。
至于怎么做到pos不共享但是緩沖區共享,還是因為我們復制了一份我們的file
這種文件是不需要打開我們的磁盤文件的,IO磁盤直接被干掉了。
管道的實現是讓我們父進程以讀寫方式打開一個管道,然后子進程會繼承,然后父子進程根據需要關閉一個讀端和寫端,因為我們的管道就是進行單向通信的,天然氣管道,暖氣管道都是單向的。
所以我們管道也是單向通信即可。
說了這么多,你給我說管道的原理就是讓我們不同的進程看到同一個資源,這個管道不和磁盤進行IO,子進程繼承的時候會拷貝一份file實現我們的pos的讀寫分離。那么我們怎么在我們的語言中使用管道進行進程間通信?
下面我們進行實現一下,首先我們的調用是pipe,參數是piprfd,這個是我們的輸出型參數。
/ 父進程先創建管道,讓子進程去復制int pipefd[2] = {0};int n = pipe(pipefd);
創建好讀寫管道之后讓我們的父子進程關閉對應的管道就可以進行進程間通信了。
然后我們再來介紹一下管道的特性:匿名管道用于具有血緣關系的進程之間。它是單向通信的,這個好理解,我們想一下生活中的管道基本都是單向通信的,還有是管道的生命周期隨進程,這個也好理解,管道本質也是一個文件,只不過它是為了通信而創立的,進程沒了,也就不需要通信了,它自然就沒了,操作系統不會讓廢棄的管道占用資源,管道也自帶同步機制,比如我們的管道里如果沒有東西了,read會阻塞,如果我們管道被寫滿了數據,就不會再進行寫入了,如果我們的寫端關閉,我們的read會返回0,表示讀到文件結尾了,如果讀端關閉,寫端正常,我們的操作系統會殺掉進程,因為我們的讀端不讀數據,你再往管道里面寫數據就沒有意義了。
我們再來提一個原子性的概念,我們學過化學,知道最小的就是原子構成的,所以原子在我們的計算機中說人話就是這個操作不可以被打斷,很多人還是不怎么理解,我們打個比方,我們的a++,這操作其實要分很多步驟去完成,我先得吧我們的a從內存搞到CPU,然后CPU對a再進行加法操作,然后再把我們的結果寫回到我們的內存,才完成了一次++,所以它的++動作就不是原子性的了,它是可以被打斷的,比如剛把我們的a加載到內存準備++,這個時候我們的線程被切走了,不就沒完成動作了嗎?然而我們的原子性就是一下就完成,不可以被打斷的,就是原子性操作。
而我們的管道當寫入字節少于一定大小的話,寫入就是原子性的。
通過我們管道的這些特性比如我們的阻塞特性,管道沒數據,讀端會阻塞,管道寫滿了就不會再寫入了,我們可以讓父進程來控制它的子進程。
我們父進程可以用什么時候寫入數據來控制子進程來執行自己的任務,我父進程不往里面寫,你就讀不到數據,你讀不到數據,你就會阻塞在那里。
還有就是我們父進程和子進程規定,寫入4字節整數,整數不同,表示的任務不同。
我們可以采用輪詢或者隨機數的形式給不同的子進程進行寫入,避免有的進程很忙,有的進程很閑。不準偷懶!!!
#include <sys/types.h>
#include <unistd.h>
#include <unistd.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
#include <cstdio>
#include <string>
#include <vector>
#include"Task.hpp"
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
#include "Task.hpp"
// typedef std::function<void (int fd)> func_t;
using callback_t = std::function<void(int fd)>;//返回值,參數
class channel
{
public:channel(int wfd, string name, pid_t id): _wfd(wfd), _name(name), _id(id){}~channel(){}int Fd() { return _wfd; }std::string Name() { return _name; }pid_t Target() { return _id; }void Close() { close(_wfd); }void Wait(){pid_t rid = waitpid(_id, nullptr, 0);}
private:int _wfd; // 父進程寫入的文件描述符pid_t _id; // 子進程的idstring _name; // 子進程的名字
};class processpool
{
public:processpool(int num) : _num(num){}~processpool(){}bool initpocesspool(callback_t cb){for (int i = 0; i < _num; i++){// 父進程先創建管道,讓子進程去復制int pipefd[2] = {0};int n = pipe(pipefd);pid_t id = fork();if (id < 0)return false;if (id == 0) // 這個函數邏輯只有子進程進來,父進程不進來{// 子進程// 要讀取,關閉寫入close(pipefd[1]);// 子進程要干什么事情啊cb(pipefd[0]);exit(0);}else{// 父進程完成對子進程的描述+組織// 父進程// 要寫入,關閉讀取close(pipefd[0]);string name = "channel-" + to_string(i);_channels.emplace_back(pipefd[1], name, id);}}return true;}// 輪詢控制子進程void processcontrol(){int count=5;int index = 0;while (count--){// 1. 選擇一個通道(進程)int who = index;index++;index %= _channels.size();// 2. 選擇一個任務,隨機int x = index % tasks.size(); // [0, 3]// 3. 任務推送給子進程write(_channels[who].Fd(), &x, sizeof(x));//往哪個文件里寫,寫的內容的地址,寫的內容大小//當任務被寫入的時候,子進程要去讀sleep(1);}}void WaitSubProcesses(){for(auto e:_channels){e.Close();}for(auto e:_channels){e.Wait();}}
private:int _num; // 有多少個子進程vector<channel> _channels; // 將子進程塞進vector,用channel描述,_chanells組織,先描述,再組織
};
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <functional>
// 4種任務
// task_t[4];using task_t = std::function<void()>;void Download()
{std::cout << "我是一個downlowd任務" << std::endl;
}void MySql()
{std::cout << "我是一個 MySQL 任務" << std::endl;
}void Sync()
{std::cout << "我是一個數據刷新同步的任務" << std::endl;
}void Log()
{std::cout << "我是一個日志保存任務" << std::endl;
}std::vector<task_t> tasks;class Init
{
public:Init()//構造函數的初始化666{tasks.push_back(Download);tasks.push_back(MySql);tasks.push_back(Sync);tasks.push_back(Log);}
};Init ginit;//當實例化出對象的時候這個4個任務的插入將同步完成!!!利用了實例化的特性
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
#include <cstdio>
#include <string>
#include <vector>
#include "processpool.hpp"
int main()
{// 1.chuangjian processpoolprocesspool pp(5);// 2.進行初始化pp.initpocesspool([](int fd){while(true){//這個邏輯只有子進程能進來父進程往下一步走了int code = 0;//std::cout << "子進程阻塞: " << getpid() << std::endl;ssize_t n = read(fd, &code, sizeof(code)); // 阻塞讀取// 確保讀取到完整數據std::cout << "子進程被喚醒: " << getpid() << std::endl;tasks[code]();
// } else if (n == 0) {
// break; // 父進程關閉管道,子進程退出
// } else {
// perror("read");
// break;
// }if(n == 0){//父進程std::cout << "子進程應該退出了: " << getpid() << std::endl;break;}else{std::cerr << "read fd: " << fd << ", error" << std::endl;break;}}
});//3.進行進程池的控制,控制權在父進程手里pp.processcontrol();// 4. 結束線程池pp.WaitSubProcesses();std::cout << "父進程控制子進程完成,父進程結束" << std::endl;// //創建指定的子進程數// for(int i=0;i<count;i++)// {// //創建管道// int pipefd[2]={0};// int n=pipe(pipefd);// //2.創建子進程// pid_t id=fork();// if(id==0)// {// //子進程// close(pipefd[1]);//子進程關閉寫端// exit(0);// }// else// {// close(pipefd[0]);//父進程關閉讀端// }// }return 0;
}
還有就是我們是否注意過我們的子進程什么時候會退出,是我們的父進程的寫端關閉,根據我們的管道特性,寫端關閉我們的讀端讀到0就退出了,但是這個時候我們會有個bug,你看我們父進程創立了一個子進程,它搞了一個讀管道,當我們第一次創建進程,它是對的,父進程關閉了它的讀端,子進程關閉了它的寫端,但是第二次就不太對了,你看我創建子進程子進程會拷貝我的文件描述符表一份,它也會把我第一次創建的指向第一次創建子進程寫端的文件也會拷貝過去,這就導致我們關閉寫端的時候沒有關閉完全,子進程無法讀到0,它也無法退出,進程卡bug了,所以我們需要在創建我們子進程的時候順便把我們上次的寫端關掉就好了。
怎么拿到父進程上次的寫端,從我們的channel里拿就好了,我們每次創建子進程的時候都遍歷一遍我們的_channels就好了,里面就有我們的父進程的寫端,讓子進程關閉了就好了。
for(auto &e:_channels)close(e.Fd());