目錄
為什么要進行進程間通信?
匿名管道的具體實現
pipe創建內存級文件形成管道
pipe的簡單使用
匿名管道的四種情況和五種特性
四種情況
五種特性
PIPE_BUF
命令行管道 |
功能代碼:創建進程池
為什么要進行進程間通信?
1.數據傳輸:一個進程需要將它的數據發送給另一個進程,比如我們有兩個進程,一個負責獲取數據,另一個負責處理數據,這時第一個進程就要將獲取到的數據交給第二個進程
2.資源共享:多個進程間共享同樣的資源
3.通知事件:一個進程需要給其他進程發送消息,通知他們發生了某種事件
4.進程控制:有些事件需要完全控制另一個進程,比如我們在使用gdb調試時,gdb就是一個進程,它控制了我們要調試的進程
進程之前是有互相傳遞信息的需求,但是進程之間又是獨立的,一個進程不可能去另一個進程的地址空間中取信息,所以這就要求操作系統去提供一塊交換數據的空間來供進程之間使用。
OS提供空間有不同的樣式,這就有了不同的通信方式:
1.管道(分為匿名和命名)
2.共享內存
3.消息隊列
4.信號量
那么我們就先來談一談匿名管道
匿名管道的具體實現
在談之前,我們要有一些之前的知識作為理論基礎,就是父進程創建子進程PCB和文件描述符表是要拷貝一份的,并且里邊的值不會進行修改,就相當于淺拷貝;而管理文件的結構體對象不會拷貝。因為前者是跟進程相關的,而后者是跟文件系統相關的。我們把這段話用圖來描述就是這樣的:
通過這樣的操作父子進程就可以看到同一塊文件的緩沖區了,這樣進程就可以讀寫了,但是兩個文件由讀又寫容易發生混亂,所以我們一般關掉一個進程的讀端,關掉另一個進程的寫端,這樣就實現了單向通信,就是因為它是單向通信,就像管道一樣,所以這樣的通信方式就被命名為管道。
pipe創建內存級文件形成管道
我們上面的操作是基于一個實實在在的磁盤文件的,我們必須得這樣嗎?肯定不是的,OS就提供了一個系統調用負責提供一個內存級的文件,它沒有名字,只能通過文件描述符來訪問,這個系統調用叫pipe()
它的參數是一個輸出型參數,就是pipe這個函數把內存級文件的讀寫文件描述符放到這個數組中,我們來取來用。
并且,規定0下標放的是r方法,1下標放的是w方法。
由上面我們可以看出,匿名管道是只能具有血緣關系的進程之間使用,因為文件描述符表是要靠父進程創建子進程拷貝下來的。
pipe的簡單使用
那么下面我們就寫一段代碼來驗證上面所說的內容,并且演示管道究竟應該如何使用,下面的代碼就是子進程往管道里寫,父進程往管道里讀
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
void writer(int wfd)
{const char *str = "i am child,this is the ";int cnt = 0;char buffer[128] = {0};while (1){snprintf(buffer, sizeof(buffer), "%s%d message", str, cnt);write(wfd, buffer, strlen(buffer));sleep(1);cnt++;}
}
void reader(int rfd)
{while (1){char buffer[1024] = {0};read(rfd, buffer, sizeof(buffer));printf("I am father,I get a message:%s\n", buffer);}
}int main()
{int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0)return 1;pid_t id = fork();if (id == 0){// 子進程負責wclose(pipefd[0]);writer(pipefd[1]);exit(0);}// 父進程負責rclose(pipefd[1]);reader(pipefd[0]);return 0;
}
匿名管道的四種情況和五種特性
有了上面的一些基本使用,下面我們來演示一下管道的四種情況以及說明五種特性
四種情況
第一種:管道中沒有數據,并且子進程不關閉自己的寫端,這時父進程會進行阻塞等待,直到管道中有數據
第二種:子進程一直寫,父進程不讀,但是父進程不關閉讀端,當管道被寫滿時就要進行阻塞等待,直到管道中的數據被讀出去才會繼續寫
我們就讓子進程一次寫一個字符,看看它一共能寫多少個字符
這里printf如果不給換行的話一定要fflush,否則有的打印的東西會在緩沖區中打印不出來
我們可以看到最終是打印到了65536byte,正好是64kb,我們就可以推斷出管道的大小是64kb
第三種:子進程不寫了并且關掉了寫端,這時讀端讀完了管道中的數據后,read的返回值就為0,這時我們就可以人為的退出了,這和第一種情況是不同的第一種情況是阻塞等待
我們讓子進程寫10秒就退出,read返回值為0父進程就退出
第四種:讓寫端一直寫,但是讀端不讀并且關閉讀端,這時的結果就是寫端也會退出,因為沒人讀了寫就沒意義了。
至于說寫端是如何退出的呢?其實是收到了退出信號,我們也可以通過wait的方式來看一下退出信號是什么
我們讓寫端一直寫,讀端讀5秒后退出,然后通過wait的方式獲取子進程(寫端)的退出信號
五種特性
通過上面的一些介紹,我們就可以總結出管道的五種特性:
1.自帶同步機制:寫滿了就不寫了,等待讀,等待它們之間的同步,讀不到就不讀了,等待寫
2.具有血緣關系的進程間進行通信
3.pipe是面向字節流的:我可以一個字符一個字符的寫,同時可以一下讀很多個字節,就是說讀的次數和寫的次數之間是沒有關系的,它們是面向管道中的數據的
4.進程退出,管道自動釋放,文件的生命周期是隨著進程的
5.管道只能單向通信,就是一個寫,一個讀,這也叫半雙工
PIPE_BUF
PIPE_BUF是一個常量,它是由大小的
就是說:每次寫入管道的字節數如果小于這個值,那么就認為本次寫入是原子的(安全的),就是保證內容是完整的,不會被分割
命令行管道 |
之前我們說的命令 | ,本質上就是這篇博客說的pipe
比如?
就是同時創建三個進程,兩個進程之間創建好管道,第一個進程的輸出當作第二個進程的輸入,第二個進程的輸出當作第三個進程的輸入,最終效果是睡眠三秒
功能代碼:創建進程池
進程池就是一次創建多個進程,然后父進程負責分發任務給各個子進程。各個子進程處理完一個任務后還可以處理下一個任務,而不需要創建新的進程,這樣就減少了創建和銷毀進程的開銷。
我們之前寫bash的時候就是有一個任務bash就創建子進程,然后子進程進行進程程序替換,執行完后就退出了。
我們今天寫的呢就是創建子進程后子進程一直等待父進程的命令然后執行任務,執行完任務后繼續等待。
//task.hpp
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <vector>
#include<ctime>
using namespace std;typedef void(*task)();
void task1()
{cout<<"do task one successfully"<<endl;
}
void task2()
{cout<<"do task two successfully"<<endl;
}void task3()
{cout<<"do task three successfully"<<endl;
}
task tasks[3]={task1,task2,task3};//processpool.cc
#include "task.hpp"void Usage(char *argv)
{cout << "please input : " << argv << " and processnum\n";
}
enum error
{USAGE_failed = 1,PIPE_failed,
};class channel
{
public:channel(int wfd, int id, int n): _wfd(wfd), _id(id), _name("channel-" + to_string(n)){}void print(){cout << "name is " << _name << "id is " << _id << " wfd is " << _wfd << endl;}int wfd(){return _wfd;}~channel() {}private:int _wfd;pid_t _id;string _name;
};class processpool
{
public:processpool(int size): _size(size){for (int i = 1; i <= size; i++){int pipefd[2] = {0};int ret = pipe(pipefd);if (ret == -1){cout << "pipe failed : errno is " << errno << "error describe is " << strerror(errno) << endl;exit(PIPE_failed);}pid_t id = fork();if (id == 0){// 子進程for (int j = pipefd[0] + 1; j <= pipefd[1]; j++){close(j);}// 等待任務while (1){int buffer = 0;int n = read(pipefd[0], &buffer, sizeof(buffer));if (n != 0){cout << "child" << i << " " << getpid() << " "; tasks[buffer]();//執行任務}if (n == 0)break;}exit(0);}channels.push_back({pipefd[1], id, i});close(pipefd[0]);}}void print(){for (auto &e : channels){e.print();}}void get_wfd(vector<int> &f){for (auto &e : channels){f.push_back(e.wfd());}}private:vector<channel> channels;int _size;
};
void give_task(vector<int> &wfd)
{int n = wfd.size();for (int i = 0; i < 100; i++){int tasknum = rand() % (sizeof(tasks) / sizeof(tasks[0]));write(wfd[i % n], &tasknum, sizeof(tasknum));//按順序選擇管道,派發隨機任務sleep(1);}
}int main(int argc, char *argv[])
{srand((unsigned int)time(nullptr));if (argc != 2){Usage(argv[0]);return USAGE_failed;}int processnum = stoi(argv[1]);processpool pool(processnum);//創建進程池vector<int> wfd;pool.get_wfd(wfd);//都可以去給哪個文件描述符給任務give_task(wfd);//發送任務return 0;
}