一、進(jìn)程間通信介紹
1.進(jìn)程間為什么要進(jìn)行通信?
進(jìn)程間通信的是為了協(xié)調(diào)不同的進(jìn)程,使之能在一個操作系統(tǒng)里同時運(yùn)行,并相互傳遞、交換信息。
2.進(jìn)程間通信的目的包括:
數(shù)據(jù)傳輸:一個進(jìn)程需要將它的數(shù)據(jù)發(fā)送給另一個進(jìn)程;
資源共享:多個進(jìn)程間共享同樣的資源;
通知事件:一個進(jìn)程需要向另一個或一組進(jìn)程發(fā)送消息,通知它們發(fā)生了某種事情,比如進(jìn)程終止時需要通知其父進(jìn)程;
進(jìn)程間通信(IPC)是一組編程接口,讓程序員能夠協(xié)調(diào)不同的進(jìn)程,使之能在一個操作系統(tǒng)里同時運(yùn)行,并相互傳遞、交換信息;
進(jìn)程控制:有些進(jìn)程希望完全控制另一個進(jìn)程的執(zhí)行(如Debug進(jìn)程),此時控制進(jìn)程希望能夠攔截另一個進(jìn)程的所有陷入和異常,并能夠及時知道它的狀態(tài)改變。
3.進(jìn)程間通信的分類:
(1)管道:1、匿名管道pipe;2、命名管道m(xù)kfifo
(2)System V IPC:1、System V 消息隊列;2、System V 共享內(nèi)存;3、System V 信號量。
(3) POSIX IPC:1、消息隊列;2、共享內(nèi)存;3、信號量;4、互斥量;5、條件變量;6、讀寫鎖。
那么管道是如何進(jìn)行通信的?
二、管道
什么是管道?
管道是一種通信機(jī)制,通常用于進(jìn)程間的通信,它表現(xiàn)出來的形式將前面每一個進(jìn)程的輸出(stdout)直接作為下一個進(jìn)程的輸入(stdin)。
1.匿名管道
匿名管道是一種進(jìn)程間通信機(jī)制,它僅限于本地父子進(jìn)程之間通信,結(jié)構(gòu)簡單,類似于一根水管,一端進(jìn)水另一端出水(單工)也就是只能夠單向通信。匿名管道的作用之一是輸出重定向。
所以用匿名管道pipe通信大概分為四部:1、創(chuàng)建匿名管道;2、創(chuàng)建子進(jìn)程;3、關(guān)閉不需要的文件描述符;4、進(jìn)行通信。
pipe函數(shù),linux中輸入 man 2 pipe,可以查看pipe函數(shù),如下:
#include <unistd.h>
功能:創(chuàng)建一無名管道
原型
int pipe(int pipefd[2]);
參數(shù)
pipefd:文件描述符數(shù)組,其中fd[0]表示讀端, fd[1]表示寫端
返回值:成功返回0,失敗返回-1和錯誤代碼
1.1父進(jìn)程和一個子進(jìn)程之間的通信
接下來試著寫一下,父進(jìn)程向管道當(dāng)中寫“i am father”, 子進(jìn)程從管道當(dāng)中讀出內(nèi)容, 并且打印到標(biāo)準(zhǔn)輸出;先創(chuàng)建管道, 進(jìn)而創(chuàng)建子進(jìn)程, 父子進(jìn)程使用管道進(jìn)行通信。
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
//創(chuàng)建匿名管道
int pipefd[2] = {0}; //文件描述符數(shù)組
int n = pipe(pipefd);
if (n < 0) //如果失敗,則打印錯誤碼
{
cout << "pipe error, " << errno << ": " << strerror(errno) << endl;
}
cout << "讀端:" << pipefd[0] << endl;
cout << "寫端:" << pipefd[1] << endl; //打印出讀寫端的文件描述符
//創(chuàng)建子進(jìn)程
pid_t id = fork(); //子進(jìn)程中返回0,父進(jìn)程返回子進(jìn)程id,出錯返會-1
assert(id != -1);
if (id == 0)
{
// child(子進(jìn)程)
close(pipefd[1]); //關(guān)閉不需要的文件描述符,子進(jìn)程需要讀,所以關(guān)閉管道寫端
//pipefd數(shù)組中0表示讀端;1表示寫端
char buffer[1024];//不管讀數(shù)據(jù)還是寫數(shù)據(jù),都需要緩沖區(qū),所以buffer相當(dāng)于一個緩沖區(qū)
while (true)
{
sleep(1);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1); //read函數(shù)
//sizeof為什么-1? 從文件中讀取數(shù)據(jù)到buffer中,因?yàn)閏語言中字符串的結(jié)束
//標(biāo)志是'\0',而文件中的數(shù)據(jù)結(jié)束尾不需要'\0',所以buffer的內(nèi)存要比讀出
//數(shù)據(jù)內(nèi)存大'\0';相反,往文件中寫入數(shù)據(jù)時,要注意去掉字符串末尾的'\0',
//字符串末尾的'\0'在文件中會變成亂碼(^@)
if (n > 0)
{
buffer[n] = '\0';
cout << "我是子進(jìn)程,讀取到父進(jìn)程的數(shù)據(jù): " << buffer << endl;
}
else if (n == 0)
{
cout << "我是子進(jìn)程,讀到了文件結(jié)尾" << buffer << endl;
break;
}
else
{
cout << "我是子進(jìn)程,讀取異常" << endl;
}
}
close(pipefd[0]); //關(guān)閉子進(jìn)程中管道的讀端
exit(0);
}
// father(父進(jìn)程)
close(pipefd[0]); //父進(jìn)程寫入,關(guān)閉讀端
int cnt = 1; //用來記錄寫了多少次
const string str = "i am father"; //要寫入的內(nèi)容
char buffer[1024];
while (true)
{
sleep(1);
snprintf(buffer, sizeof(buffer), "%s, 計數(shù)器:%d", str.c_str(), cnt++);//將數(shù)據(jù)寫入到buffer中
write(pipefd[1], buffer, strlen(buffer));//再將buffer中的數(shù)據(jù)寫入管道中
}
close(pipefd[1]); //關(guān)閉父進(jìn)程中管道的寫端
return 0;
}
下面是一些open,write,close等函數(shù)和文件描述符等有關(guān)概念:
系統(tǒng)方面對文件的打開,讀寫和關(guān)閉(open,write,close等用法)
文件描述符
運(yùn)行結(jié)果如下:
由下述代碼看現(xiàn)象:
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
cout << "pipe error, " << errno << ": " << strerror(errno) << endl;
}
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// child
int cnt = 5;
close(pipefd[1]);
char buffer[1024];
while (--cnt)
{
sleep(1);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
cout << buffer << endl;
}
else if (n == 0)
{
cout << "我是子進(jìn)程,讀到了文件結(jié)尾" << buffer << endl;
break;
}
else
{
cout << "我是子進(jìn)程,讀取異常" << endl;
}
}
close(pipefd[0]);
exit(0);
}
// father
close(pipefd[0]);
int cnt = 1;
const string str = "i am father";
char buffer[1024];
while (true)
{
sleep(1);
snprintf(buffer, sizeof(buffer), "%s, 計數(shù)器:%d", str.c_str(), cnt++);
write(pipefd[1], buffer, strlen(buffer));
}
close(pipefd[1]);
return 0;
}
由上可知:
寫端一直寫,讀端關(guān)閉OS會殺死一直在寫入的進(jìn)程。
將上述代碼進(jìn)行小的修改,就可以得到如下現(xiàn)象,代碼就不在展示了,四種現(xiàn)象如下:
1.如果read讀取完畢了所有的管道數(shù)據(jù),如果對方不發(fā),那么就只能等待;
2.如果我們writer端將管道寫滿了,那么就不能再寫。管道的內(nèi)存是有一定大小的;
3.如果我關(guān)閉了寫端,讀取完畢管道數(shù)據(jù),再讀,就會read返回0,表明讀到了文件結(jié)尾;
4.寫端一直寫,讀端關(guān)閉,會發(fā)生什么呢?沒有意義。OS不會維護(hù)無意義,低效率,或者浪費(fèi)資源的事情。OS會殺死一直在寫入的進(jìn)程! OS會通過信號來終止進(jìn)程,13) SIGPIPE。
1.2父進(jìn)程和多個子進(jìn)程之間的通信
父進(jìn)程寫數(shù)據(jù),子進(jìn)程讀取數(shù)據(jù),父進(jìn)程和多個子進(jìn)程之間的通信,首先創(chuàng)建管道,代碼如下:
void createProcess(vector<EndPoint> &end_points)
{
for (int i = 0; i < CHILDNUMS; ++i)
{
// 1.先創(chuàng)建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0); // pipe函數(shù)成功返回0
(void)n; // linux中,定義一個變量,但是沒使用,所以強(qiáng)轉(zhuǎn)為void。(gcc編譯可能報錯)
// 2.創(chuàng)建子進(jìn)程
pid_t id = fork();
assert(id != -1); // fork失敗返回-1
if (id == 0)
{
// 子進(jìn)程
// 關(guān)閉不需要的文件描述符,假設(shè)父進(jìn)程寫,子進(jìn)程讀
close(pipefd[1]); // pipe中1 為寫端
char buffer[1024];
while (true)
{
//...進(jìn)行讀取
......
}
close(pipefd[0]); // 關(guān)閉讀端
exit(0); // 關(guān)閉讀端,子進(jìn)程退出
}
// 父進(jìn)程
close(pipefd[0]);
end_points.push_back(EndPoint(id, pipefd[1])); //將文件描述符管理起來
}
}
其中需要注意的是創(chuàng)建子進(jìn)程,子進(jìn)程是父進(jìn)程的拷貝,所以當(dāng)多次fork后,子進(jìn)程中含有父進(jìn)程指向管道的寫端,如下圖:
所以在創(chuàng)建管道時,需要關(guān)閉多余的管道,改進(jìn)代碼如下:
void createProcess(vector<EndPoint> &end_points)
{
vector<int> fds;
for (int i = 0; i < CHILDNUMS; ++i)
{
// 1.先創(chuàng)建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0); // pipe函數(shù)成功返回0
(void)n; // linux中,定義一個變量,但是沒使用,所以強(qiáng)轉(zhuǎn)為void。(gcc編譯可能報錯)
// 2.創(chuàng)建子進(jìn)程
pid_t id = fork();
assert(id != -1); // fork失敗返回-1
if (id == 0)
{
//關(guān)閉子進(jìn)程多余的fd
for(auto &fd : fds)
close(fd);
// 子進(jìn)程
// 關(guān)閉不需要的文件描述符,假設(shè)父進(jìn)程寫,子進(jìn)程讀
close(pipefd[1]); // pipe中1 為寫端
char buffer[1024];
while (true)
{
//...進(jìn)行讀取
......
}
close(pipefd[0]); // 關(guān)閉讀端
exit(0); // 關(guān)閉讀端,子進(jìn)程退出
}
// 父進(jìn)程
close(pipefd[0]);
end_points.push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
完整代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-565217.html
#include <iostream>
#include <vector>
#include <string>
#include <string.h> //strlen
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
#define CHILDNUMS 3 // 子進(jìn)程的個數(shù)
class EndPoint
{
public:
EndPoint(pid_t id, int fd)
: _child_id(id), _write_fd(fd)
{
}
public:
pid_t _child_id;
int _write_fd;
};
void createProcess(vector<EndPoint> &end_points)
{
vector<int> fds;
for (int i = 0; i < CHILDNUMS; ++i)
{
// 1.先創(chuàng)建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0); // pipe函數(shù)成功返回0
(void)n; // linux中,定義一個變量,但是沒使用,所以強(qiáng)轉(zhuǎn)為void。(gcc編譯可能報錯)
// 2.創(chuàng)建子進(jìn)程
pid_t id = fork();
assert(id != -1); // fork失敗返回-1
if (id == 0)
{
//關(guān)閉子進(jìn)程多余的fd
for(auto &fd : fds)
close(fd);
// 子進(jìn)程
// 關(guān)閉不需要的文件描述符,假設(shè)父進(jìn)程寫,子進(jìn)程讀
close(pipefd[1]); // pipe中1 為寫端
char buffer[1024];
while (true)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
cout << "我是pid=%d的子進(jìn)程," << getpid() << ";接受到父進(jìn)程的指令是:" << buffer << endl;
}
else if (n == 0)
{
cout << "我是子進(jìn)程pid=" << getpid() << ",讀到了文件結(jié)尾,并且父進(jìn)程寫端已關(guān)閉" << endl;
break;
}
else
{
cout << "我是子進(jìn)程,讀取異常" << endl;
}
sleep(1);
}
close(pipefd[0]); // 關(guān)閉讀端
exit(0); // 關(guān)閉讀端,子進(jìn)程退出
}
// 父進(jìn)程
close(pipefd[0]);
end_points.push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
void ctrlProcess(const vector<EndPoint> &end_points)
{
// 對每個子進(jìn)程下發(fā)任務(wù)
char buffer[1024];
int command = 0;
int cnt = 0;
while (true)
{
cnt %= end_points.size();
cout << "輸如命令號:";
cin >> command;
if (command == 0)
break;
snprintf(buffer, sizeof(buffer), "pid=%d的子進(jìn)程處理%d任務(wù)", end_points[cnt]._child_id, command);
write(end_points[cnt]._write_fd, buffer, strlen(buffer));
++cnt;
sleep(1);
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
// 需要讓子進(jìn)程全部退出 --- 只需要讓父進(jìn)程關(guān)閉所有的write_fd并且要回收子進(jìn)程的僵尸狀態(tài)
for (int end = 0; end < end_points.size(); ++end)
{
cout << "父進(jìn)程讓子進(jìn)程退出:" << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
waitpid(end_points[end]._child_id, nullptr, 0); //waitpid不需要查看子進(jìn)程退出信息,所以傳nullptr
cout << "父進(jìn)程回收了子進(jìn)程:" << end_points[end]._child_id << endl;
}
}
int main()
{
cout << "命令號為0,則退出進(jìn)程" << endl;
// 需要記錄子進(jìn)程的pid和父進(jìn)程的寫端
vector<EndPoint> end_points;
// 1. 先進(jìn)行構(gòu)建控制結(jié)構(gòu), 父進(jìn)程寫入,子進(jìn)程讀取
createProcess(end_points);
// 2.控制每個子進(jìn)程
ctrlProcess(end_points);
// 3. 處理所有的退出問題
waitProcess(end_points);
return 0;
}
運(yùn)行結(jié)果如下:
管道的特點(diǎn):文章來源地址http://www.zghlxwxcb.cn/news/detail-565217.html
- 只能用于具有共同祖先的進(jìn)程(具有親緣關(guān)系的進(jìn)程)之間進(jìn)行通信;
- 通常,一個管道由一個進(jìn)程創(chuàng)建,然后該進(jìn)程調(diào)用fork,此后父、子進(jìn)程之間就可應(yīng)用該管道;
- 管道提供流式服務(wù);
- 一般而言,進(jìn)程退出,管道釋放,所以管道的生命周期隨進(jìn)程;
- 內(nèi)核會對管道操作進(jìn)行同步與互斥;
- 管道是半雙工的,數(shù)據(jù)只能向一個方向流動;需要雙方通信時,需要建立起兩個管道。
到了這里,關(guān)于進(jìn)程間通信之匿名管道的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!