Linux系列
文章目錄
- Linux系列
- 前言
- 一、進程通信的目的
- 二、進程通信的原理
- 2.1 進程通信是什么
- 2.2 匿名管道通訊的原理
- 三、進程通訊的使用
- 總結
前言
Linux進程間同通訊(IPC)是多個進程之間交換數據和協調行為的重要機制,是我們學習Linux操作系統中比較重要的一個模塊。
一、進程通信的目的
Linux 進程間通信(IPC)的主要目的是讓多個獨立的進程能夠協作、共享資源、交換數據或同步操作。這是現代操作系統中多任務、并行計算和分布式系統的核心基礎。以下是具體的目的和場景:
- 數據傳輸:一個進程需要將它的數據發送給另一個進程
- 協同工作:兩進程需要更具對方的執行結果來協同完成某種任務
- 資源同步與互斥:避免多個進程同時訪問共享資源導致數據競爭或狀態不一致
二、進程通信的原理
2.1 進程通信是什么
進程通信的本質其實就是多個進程可以同時訪問同一份“資源”。對于進程來說“資源”就是空間中存儲的數據,所以要實現進程通信我們只需要讓多個進程可以同時訪問同一塊空間即可。這里我們就以兩個進程為例:
這時我們只需要做到讓進程1
向這塊空間中寫數據,進程2
可以將進程1
寫入的數據從空間中取出,這樣就完成了通訊工作。那么這塊空間該由誰提供呢?由于進程必須保證獨立性(也就是說不論這個空間由哪個進程提供,另一個進程都不能對它訪問,否則就破壞了獨立性),也就注定了這塊共享空間只能由第三方提供---------操作系統,那么這塊空間就必須由操作系統管理了,空間的創建到釋放都是由操作系統來完成的,所以我們對這塊空間的訪問,就變成了對操作系統的訪問,而進程(代表用戶)要想訪問操作系統,只能通過系統調用接口進行。
為了滿足通訊需求,一般操作系統都會有一個獨立的通信模塊。今天我們就來介紹基于文件級別的通訊方式------管道
2.2 匿名管道通訊的原理
根據上面的分析,我們首先要讓兩個進程看到同一份資源:
我們利用父進程創建子進程時的特點,這樣就可以讓兩個進程(父和子)看到同一分空間,而我們的管道就是文件,它是內核文件,并不會向磁盤刷新內容,父子進程對他進行訪問時并不需要在目錄中查詢,而是直接通過文件描述符查找,所以管道文件不用定義名字,因此被稱為匿名管道。
到了這里我們僅解決了,讓兩個進程看到同一份空間,但是管道文件不支持以讀寫方式打開,也就是說我們上面的介紹,兩個進程(父子進程)只能向管道中讀,或只能向管道中寫,而進程通訊一般為一個進程寫,一個進程讀。
所以進程在打開管道文件時,都會分別以讀、寫兩種方式打開,當我們讓父進程讀取數據,子進程寫入數據的形式通訊時(這個用戶自己控制),父進程就關閉對應的寫方式,子進程關閉對應的讀方式,此時子進程就可以向管道文件的緩沖區寫入數據供父進程讀取。到了這里,我們就建立了進程通信的信道。
通過上面的介紹我們可以知道匿名管道通信的特點為:
- 只能進行單向通信
- 通信進程間需要有血緣關系,常用于父子
三、進程通訊的使用
首先我們先來認識需要用到的系統調用:
這個系統調用需要傳遞一個存儲兩個元素的整形數組,該參數為輸出型參數,第一個元素為以讀形式打開的文件描述符,第二從元素為以寫形式打開的文件描述符。下面我們先實現一個簡單的通信:
示例1:
include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<string>
#include<cstring>
using namespace std;int main()
{int pipefd[2];int n=pipe(pipefd);if(n<0)return 1;pid_t id=fork();//創建子進程if(id==0)//子進程{string str="Hello father process\n";close(pipefd[0]);//子關閉讀權限write(pipefd[1],str.c_str(),strlen(str.c_str()));//向管道中寫入數據exit(0);}close(pipefd[1]);//父關閉寫權限char tmp[100]={0};read(pipefd[0],tmp,100);//從管道中讀取數據cout<<tmp;return 0;
}
程序執行結果:
這給代碼主要幫助我們理解上面介紹的,關閉文件描述符以及pipe()
參數的作用。接下來我們會通過下面的代碼示例,對我們的管道通信的四種情況進行總結。
情況1:讀寫端正常,管道為空,讀端就要阻塞
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstdio>
#include<cstring>
using namespace std;#define N 2
#define NUM 1024void Write(int wfd)
{char buff[NUM];string str="Hello ,I am child,pid:";pid_t self=getpid();int num=0;while(true){sleep(2);buff[0]=0;snprintf(buff,sizeof(buff),"%s%d---%d",str.c_str(),self,num++);//將要寫入數據格式化到buffwrite(wfd,buff,strlen(buff));//將buff中的數據寫入管道}
}void Read(int rfd)
{char buff[NUM];while(true){buff[0]=0;int n=read(rfd,buff,sizeof(buff));//從管道中讀取數據if(n>0){cout<<"father get message:"<<buff<<endl;}else if(n==0){cout<<"father read fail"<<endl;}else break;}
}
int main()
{int pipefd[N];int n=pipe(pipefd);if(n<0)return 1;//調用失敗程序返回pid_t id=fork();//創建子進程if(id<0)return 1;if(id==0)//child{close(pipefd[0]);//關閉讀端Write(pipefd[1]);close(pipefd[1]);//這一步有沒有都無所謂exit(0);}//fatherclose(pipefd[1]);//關閉寫端Read(pipefd[0]);close(pipefd[0]);return 0;
}
想要的效果不好展示,大家可以自己跑以下看看程序執行過程
上述代碼,子進程在進程在向管道中寫入數據時,每間隔兩秒寫入一次,而父進程會阻塞等待,當子進程寫入完成后父進程才會讀取并返回。我們可以看到程序并沒有執行cout<<"father read fail"<<endl;
這也證明了父進程會進行阻塞等待,等待子進程寫入。
個人感覺這已經能說明問題了,不知道你能不能get到
情況二:讀寫端正常,管道被寫滿,寫端就要阻塞
接下來我們對上面代碼進行一點小改動:
void Write(int wfd)
{char buff[NUM];string str="Hello ,I am child,pid:";pid_t self=getpid();int num=0;while(true){buff[0]=0;snprintf(buff,sizeof(buff),"%s%d---%d",str.c_str(),self,num++);//將要寫入數據格式化到buffwrite(wfd,buff,strlen(buff));//將buff中的數據寫入管道cout<<"已經寫入了:"<<num<<"條信息"<<endl;//記錄寫入次數}}
void Read(int rfd)
{char buff[NUM];while(true){buff[0]=0;sleep(10);int n=read(rfd,buff,sizeof(buff));//從管道中讀取數據if(n>0){cout<<"father get message:"<<buff<<endl;}else if(n==0){cout<<"father read fail"<<endl;}else break;}
}
我們讓父進程等待10
秒再讀取,子進程一直寫入。
此時你會看到當子進程入定數量的數據后,就會停止寫入進入阻塞狀態,等待父進程的讀取,父進程讀取成功后,子進程才能繼續寫入,其原因就直管道被寫滿了。
情況三:讀端正常,寫端關閉,讀端就會讀到0,表明讀到了文件(pipe)結尾,不會阻塞
void Write(int wfd)
{char buff[NUM];string str="Hello ,I am child,pid:";pid_t self=getpid();int num=0;int cnt=5;//寫入五次while(cnt--){sleep(1);buff[0]=0;snprintf(buff,sizeof(buff),"%s%d---%d",str.c_str(),self,num++);//將要寫入數據格式化到buffwrite(wfd,buff,strlen(buff));//將buff中的數據寫入管道}cout<<"我是寫端,我關閉了"<<endl;
}void Read(int rfd)
{char buff[NUM];while(true){buff[0]=0;sleep(1);int n=read(rfd,buff,sizeof(buff));//從管道中讀取數據if(n>0){cout<<"father get message:"<<buff<<endl;}else if(n==0){cout<<"father read fail"<<endl;}else break;}
}
這個圖效果就很好了,當寫端關閉時,讀端讀取到文件末尾返回0
執行cout<<"father get message:"<<buff<<endl;
,子進程退出變為僵尸。
情況四:寫端正常,讀端關閉,操作系統通過信號殺掉正在寫入的進程。
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> //stdlib.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define N 2
#define NUM 1024using namespace std;// child
void Writer(int wfd)
{string s = "hello, I am child";pid_t self = getpid();int number = 0;char buffer[NUM];while (true){sleep(1);// 構建發送字符串buffer[0] = 0; // 字符串清空, 只是為了提醒閱讀代碼的人,我把這個數組當做字符串了snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);// cout << buffer << endl;// 發送/寫入給父進程, system callwrite(wfd, buffer, strlen(buffer)); // strlen(buffer) + 1???}
}// father
void Reader(int rfd)
{char buffer[NUM];int cnt = 0;while(true){buffer[0] = 0; // system callssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlenif(n > 0){buffer[n] = 0; // 0 == '\0'cout << "father get a message[" << getpid() << "]# " << buffer << endl;}else if(n == 0) {printf("father read file done!\n");break;}else break;cnt++;if(cnt>5) break;// cout << "n: " << n << endl;}
}int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if (n < 0)return 1;// child -> w, father->rpid_t id = fork();if (id < 0)return 2;if (id == 0){// childclose(pipefd[0]);// IPC codeWriter(pipefd[1]);close(pipefd[1]);exit(0);}// fatherclose(pipefd[1]);// IPC codeReader(pipefd[0]); // 讀取5sclose(pipefd[0]);cout << "father close read fd: " << pipefd[0] << endl;sleep(5); //為了觀察僵尸int status = 0;pid_t rid = waitpid(id, &status, 0); if(rid < 0) return 3;cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl;sleep(5);cout << "father quit" << endl;return 0;
}
可以看到當讀端關閉后,操作系統就會將使用13
號信號殺掉寫端。
總結
1、具有血緣關系的進程才能使用匿名管道通訊
2、管道只能單向通信
3、父子進程是會進程協同的,同步與互斥的------保護管道文件的數據安全
4、管道是門面向字節流的
5、管道是基于文件的,而文件的生命周期是隨進程的