深入了解linux系統(tǒng)—— 進(jìn)程池

前言:

本篇博客所涉及到的代碼以同步到本人gitee:進(jìn)程池· 遲來的grown/linux – 碼云 – 開源中國(guó)

一、池化技術(shù)

在之前的學(xué)習(xí)中,多多少少都聽說過池,例如內(nèi)存池,線程池等等。

那這些池到底是干什么的呢?池又指的是什么呢?

這種思想就好比在vector動(dòng)態(tài)數(shù)組擴(kuò)容一樣,是按照2或者1.5倍進(jìn)行擴(kuò)容,這樣減少開辟空間的開銷從而提高效率。

二、進(jìn)程池原理

我們也了解了進(jìn)程間通信——管道,可以實(shí)現(xiàn)進(jìn)程之間的通信技術(shù);那現(xiàn)在我們可不可以利用父進(jìn)程管理所有的子進(jìn)程,并且讓子進(jìn)程去完成某些任務(wù)呢?

深入了解linux系統(tǒng)—— 進(jìn)程池

那進(jìn)程池又是什么東西呢?

池化技術(shù)是預(yù)先創(chuàng)建資源,通過復(fù)用來提升系統(tǒng)性能和資源利用率。

這里子進(jìn)程要執(zhí)行任務(wù),如果這里父進(jìn)程要傳輸信息給子進(jìn)程時(shí)再去創(chuàng)建進(jìn)程,子進(jìn)程執(zhí)行完任務(wù)之后就退出;這勢(shì)必存儲(chǔ)非常多的系統(tǒng)調(diào)用,而系統(tǒng)調(diào)用也是有成本的。那這里我們就可以預(yù)先創(chuàng)建多個(gè)子進(jìn)程,讓這些子進(jìn)程等待父進(jìn)程傳輸信息;執(zhí)行完任務(wù)后繼續(xù)等待父進(jìn)程傳輸信息。

這樣我們預(yù)先創(chuàng)建進(jìn)程,讓這些進(jìn)程執(zhí)行任務(wù)而不是在要執(zhí)行任務(wù)時(shí)再創(chuàng)建進(jìn)程;并且執(zhí)行完任務(wù)的子進(jìn)程還可以繼續(xù)完成下一個(gè)任務(wù),這樣通過復(fù)用進(jìn)程來通過系統(tǒng)性能和進(jìn)制資源的利用率。

三、進(jìn)程池實(shí)現(xiàn)

了解了進(jìn)程池原理,現(xiàn)在來看它應(yīng)該如何去實(shí)現(xiàn):

首先,我們要預(yù)先創(chuàng)建一個(gè)進(jìn)程池,并把它管理起來。其次,父進(jìn)程要通過傳輸信息來控制子進(jìn)程完成不同的任務(wù);父進(jìn)程就要發(fā)送信息,子進(jìn)程就要接受信息并執(zhí)行任務(wù)最后,進(jìn)程池能夠被創(chuàng)建出來,當(dāng)然也要能夠被釋放(銷毀)。1. 描述進(jìn)程池

我們要預(yù)先創(chuàng)建一個(gè)進(jìn)程池,并且要將它管理起來;那就要像將這個(gè)進(jìn)程池描述出來;如何描述并管理這個(gè)進(jìn)程池呢?

這里我們要實(shí)現(xiàn)的本質(zhì)還是要進(jìn)行父子進(jìn)程間通信,要讓父進(jìn)程發(fā)信息來控制子進(jìn)程;

先描述

那站著父進(jìn)程的角度:

所以就可以設(shè)計(jì)一個(gè)channel類用來描述一個(gè)管道文件;那這個(gè)類具有哪些屬性呢?

wfd:父進(jìn)程中寫端的文件描述符pid:管道文件對(duì)應(yīng)子進(jìn)程的pid,后續(xù)用來回收子進(jìn)程代碼語(yǔ)言:JavaScript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

class channel{public:    channel(int wfd, int pid)        : _wfd(wfd), _pid(pid)    {    }    ~channel() {}private:    int _wfd;    int _pid;};

再組織

一個(gè)父進(jìn)程它要?jiǎng)?chuàng)建多個(gè)管道文件也就是多個(gè)子進(jìn)程,就要將這些子進(jìn)程管理起來;

所以,這里可以設(shè)計(jì)一個(gè)channel_manage類來講管道文件channel管理起來。

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

class channel_manage{public:    channel_manage()    {    }    ~channel_manage()    {    }private:    std::vector<channel> _channels;};

描述進(jìn)程池

在進(jìn)程池中,一定是存在上面的組織管道文件channel_manage,因?yàn)槲覀円獙?duì)其進(jìn)行管理;

在進(jìn)程池中還可能存在其他信息:進(jìn)程負(fù)載情況,進(jìn)程狀態(tài)等等。

這里就只記錄進(jìn)程的數(shù)量。

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

class channel_pool{public:    const int NUM = 5; // 進(jìn)程池中進(jìn)程數(shù)量    channel_pool()        : _processnum(NUM)    {}    ~channel_pool()    {}private:    channel_manage _cm;    int _processnum;};

2. 初始化進(jìn)程池

描述出了進(jìn)程池channel_pool,現(xiàn)在我們能夠根據(jù)這個(gè)channel_pool創(chuàng)建出一個(gè)進(jìn)程池對(duì)象,但是創(chuàng)建出來的這個(gè)進(jìn)程池對(duì)象里面什么是都沒有,一個(gè)進(jìn)程都沒有。

所以,我們就要對(duì)進(jìn)程池進(jìn)行初始化:那如何初始化呢?(這里暫定進(jìn)程池中進(jìn)程個(gè)數(shù)為5個(gè))

簡(jiǎn)單來說初始化進(jìn)程池時(shí)就要將所有的子進(jìn)程創(chuàng)建出來,那創(chuàng)建完子進(jìn)程,子進(jìn)程應(yīng)該做什么呢,父進(jìn)程又該做什么呢?

這里創(chuàng)建子進(jìn)程就是為了讓子進(jìn)程完成某個(gè)任務(wù),所以創(chuàng)建子進(jìn)程之后(記得關(guān)閉不用的文件描述符),就讓子進(jìn)程等待父進(jìn)程發(fā)送信息;在子進(jìn)程接受到信息之后再去完成任務(wù)。而創(chuàng)建完子進(jìn)程之后,父進(jìn)程關(guān)閉不用的文件描述符,然后就要在_cm中新增一個(gè)管道文件channel對(duì)象。

而channel在channel_manage中,所以channel_manage就要通過新增channel對(duì)象的方法。

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

//這里只顯示新增方法和成員變量class channel_manage{public:    void _insert(int wfd, int pid)    {        _channels.emplace_back(wfd,pid);    }private:    std::vector<channel> _channels;};class channel_pool{public:    void work(int rfd){};//任務(wù)方法    void _init()    {        for(int i = 0;i<_processnum;i++)        {            int fd[2];            int n = pipe(fd);            if(n < 0)            {                std::cerr<<"pipe failed"<<std::endl;                exit(1);            }            //創(chuàng)建子進(jìn)程            int id = fork();            if(id < 0)            {                std::cerr<<"fork failed"<<std::endl;                exit(1);            }            else if(id == 0)            {                //child                close(fd[1]);//關(guān)閉寫端                _work(fd[0]);//等到父進(jìn)程發(fā)送信息                close(fd[0]);                exit(1);            }            //parent            close(fd[0]);            //在_cm中新增channel對(duì)象            _cm._insert(fd[1],id);        }    }private:    channel_manage _cm;    int _processnum;};

3. 子進(jìn)程接受信息

完成了上述操作,現(xiàn)在進(jìn)程池被創(chuàng)建出來,也被初始化了。

這里子進(jìn)程是直到管道文件的文件描述符的就是fd[0];所以子進(jìn)程就要在fd[0]文件描述符對(duì)應(yīng)的管道文件中讀取數(shù)據(jù),然后根據(jù)讀取到的數(shù)據(jù)來執(zhí)行不同的任務(wù)。

那父子之間發(fā)送什么樣的信息呢?

這里就采用一個(gè)整數(shù)對(duì)應(yīng)一個(gè)任務(wù),讓父進(jìn)程發(fā)送一個(gè)整數(shù)給子進(jìn)程。

子進(jìn)程讀取數(shù)據(jù):

讀取數(shù)據(jù)失敗就返回-1;寫端退出就返回0;返回值大于0 :表示讀取到的實(shí)際字節(jié)數(shù)。(當(dāng)讀取到的字節(jié)數(shù)不等于4,就表示不是按照int讀取的,就指讀取到一個(gè)位置信息)。代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

    void work(int rfd)    {        while (true)        {            int massage = 0;            int n = read(rfd, &massage, sizeof(massage));            if (n < 0)            {                std::cerr << "read failed" << std::endl;                exit(1);            }            else if (n == 0)            {                std::cout << "exit, because write exit" << std::endl;                break;            }            else if (n != 4)            {                std::cout << "unkonw massage : " << massage << std::endl;            }            // 讀取成功,執(zhí)行任務(wù)            std::cout << "receive massage : " << massage << std::endl;        }    }

4. 父進(jìn)程發(fā)送信息

實(shí)現(xiàn)子進(jìn)程接受信息,現(xiàn)在來看父進(jìn)程發(fā)送信息;如何發(fā)送呢?

這里父進(jìn)程發(fā)送信息無(wú)非就以下三個(gè)問題:

給哪一個(gè)進(jìn)程發(fā)送信息?發(fā)送什么信息?如何發(fā)送信息?

選擇一個(gè)進(jìn)程發(fā)送信息

發(fā)送什么信息?

如何發(fā)送信息

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

class channel{public:    bool _send(int taskcode)    {        int n = write(_wfd, &taskcode, sizeof(taskcode));        if (n < 0)        { // 寫入失敗            std::cerr << "write failed" << std::endl;            return false;        }        // 寫入成功        return true;    }private:    int _wfd;    int _pid;};class channel_manage{public:    channel &select()    {        auto &ret = _channels[_next];        _next++;        _next %= _channels.size();        return ret;    }private:    std::vector<channel> _channels;    int _next = 0;};class channel_pool{public:    void send(int taskcode)    {        // 選擇一個(gè)進(jìn)程        auto &c = _cm._select();        // 任務(wù)碼由上層調(diào)用決定        // 發(fā)送信息        c._send(taskcode);    }private:    channel_manage _cm;    int _processnum;};

到這里,進(jìn)程池就可以大致的運(yùn)行起來了,這里簡(jiǎn)單測(cè)試一下

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

//test.cc#include "channelpool.hpp"int main(){    srand((int)time(nullptr));    channel_pool cp;    cp._init();    cp.Print(); // 輸出進(jìn)程池中的所有進(jìn)程信息    while (1)    {        int taskcode = rand() % 5;        cp.send(taskcode);        std::cout << std::endl;        sleep(1);    }    return 0;}
深入了解linux系統(tǒng)—— 進(jìn)程池

進(jìn)程池也是能夠正常運(yùn)行,子進(jìn)程也能夠接受到父進(jìn)程發(fā)送的信息。

5. 回收進(jìn)程池資源

做完上述的內(nèi)容,這里的進(jìn)程池就大致可以運(yùn)行起來;

但是現(xiàn)在還缺少一個(gè)步驟,那就是回收進(jìn)程池的資源。

如何回收進(jìn)程池的資源呢?

首先,要關(guān)閉父進(jìn)程中所有的管道文件的文件描述符。其次就是父進(jìn)程等待子進(jìn)程退出,回收子進(jìn)程。

而_wfd文件描述符、_pid子進(jìn)程pid都封裝在channel中;

如何關(guān)閉管道文件,如何等待子進(jìn)程退出,這都要channel來提供。

而我們想要通過進(jìn)程池對(duì)象調(diào)用回收函數(shù),那channel_manage也要提供對(duì)應(yīng)的關(guān)閉文件和等待子進(jìn)程退出的函數(shù)。

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

class channel{public:    void _close()    {        close(_wfd);    }    void _wait()    {        wait(nullptr);    }private:    int _wfd;    int _pid;};class channel_manage{public:    void _close()    {        for (auto &channel : _channels)        {            channel._close();            std::cout << "關(guān)閉管道文件 : " << channel.getname() << std::endl;        }    }    void _wait()    {        for (auto &channel : _channels)        {            channel._wait();            std::cout << "等待子進(jìn)程退出 : " << channel.getname() << std::endl;        }    }private:    std::vector<channel> _channels;    int _next = 0;};class channel_pool{public:    void _quit()    {        //關(guān)閉所有w端文件        _cm._close();        //回收子進(jìn)程        _cm._wait();    }private:    channel_manage _cm;    int _processnum;};

這里就發(fā)送一次信息然后退出,測(cè)試一下:

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

//test.cc#include "channelpool.hpp"int main(){    srand((int)time(nullptr));    channel_pool cp;    cp._init();    cp.Print(); // 輸出進(jìn)程池中的所有進(jìn)程信息    int cnt = 1;    while (cnt--)    {        int taskcode = rand() % 5;        cp.send(taskcode);        std::cout << std::endl;    }    cp._quit();    sleep(10);    return 0;}
深入了解linux系統(tǒng)—— 進(jìn)程池

四、隱藏的問題

在上述的代碼中存在一個(gè)隱藏的問題:

現(xiàn)在來看如果這樣回收進(jìn)程池資源:

代碼語(yǔ)言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

 class channel{public:    void _close()    {        close(_wfd);    }    void _wait()    {        wait(nullptr);    }private:    int _wfd;    int _pid;};class channel_manage{public:    void _quit()    {        for (auto &channel : _channels)        {            channel._close();            std::cout << "關(guān)閉管道文件 : " << channel.getname() << std::endl;            channel._wait();            std::cout << "等待子進(jìn)程退出 : " << channel.getname() << std::endl;        }    }private:    std::vector<channel> _channels;    int _next = 0;};class channel_pool{public:    void _quit()    {        //關(guān)閉所有w端文件        _cm._close();        //回收子進(jìn)程        _cm._wait();    }private:    channel_manage _cm;    int _processnum;};

這樣關(guān)閉一個(gè)寫端,等待一個(gè)子進(jìn)程退出。

深入了解linux系統(tǒng)—— 進(jìn)程池

我們會(huì)發(fā)現(xiàn),程序卡到了這里,這是為什么呢?

這樣父進(jìn)程在創(chuàng)建子進(jìn)程時(shí),這個(gè)子進(jìn)程的文件描述符表中是存儲(chǔ)前面創(chuàng)建的管道文件的w端;這樣我們調(diào)用channel類的_close就只關(guān)閉了父進(jìn)程的w端,在其他進(jìn)程中還存在管道文件的w端。這樣子進(jìn)程就會(huì)阻塞到read出,等待w端關(guān)閉,而父進(jìn)程就等待子進(jìn)程退出,這樣程序就卡在了這樣。

深入了解linux系統(tǒng)—— 進(jìn)程池

而第一個(gè)創(chuàng)建的子進(jìn)程,文件描述符4指向自己的w端,然后被關(guān)閉了。第二個(gè)創(chuàng)建的子進(jìn)程,4指向第一個(gè)子進(jìn)程對(duì)應(yīng)管道文件的w端。第三個(gè)創(chuàng)建的子進(jìn)程,4指向第一個(gè)子進(jìn)程管道文件的w端,5指向第二個(gè)子進(jìn)程對(duì)應(yīng)管道文件的w端。…

這樣,在回收進(jìn)程池的資源時(shí),關(guān)閉父進(jìn)程第一個(gè)子進(jìn)程的w端,此時(shí)第二、第三…個(gè)子進(jìn)程的文件描述符中都存在第一個(gè)子進(jìn)程對(duì)應(yīng)管道文件的w端。

解決方法:

在回收進(jìn)程池資源時(shí),從最后一個(gè)被創(chuàng)建的子進(jìn)程開始回收。 因?yàn)橐粋€(gè)子進(jìn)程中只存在比自己創(chuàng)建的早的子進(jìn)程的w端;所以最后一個(gè)被創(chuàng)創(chuàng)建的子進(jìn)程的w端只有父進(jìn)程存在。 在創(chuàng)建子進(jìn)程時(shí),關(guān)閉比該子進(jìn)程創(chuàng)建早的子進(jìn)程的w端。

對(duì)于第一種方法這里就不演示了,來看第二種方法

對(duì)于第二種方法可能存在疑問:子進(jìn)程和父進(jìn)程是同時(shí)執(zhí)行的,那子進(jìn)程不會(huì)把自己的w段關(guān)閉嗎?那不就同一個(gè)文件描述符關(guān)閉2次了嗎?

實(shí)現(xiàn)第二種方法也很簡(jiǎn)單,只需在創(chuàng)建子進(jìn)程之后,讓子進(jìn)程調(diào)用一次channel_massage的_close函數(shù)即可。

這里就不演示了。

到這里,本篇文章內(nèi)容就結(jié)束了

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊11 分享