概念:程序是一個獨立的資源分配單位,不同程序之間有關聯,不能在一個程序中直接存取另一個程序的資源。
通訊目的:
如何實現程序通訊?
要讓兩個不同的程序實現通訊,前提條件是讓它們看到同一份資源。所以要想辦法讓他們看到同一份資源,就需要採取一些手段,可以分為下面幾種。
1.管道
2.System V IPC
3.POSIX IPC
概念:我們把一個程序連線到另一個程序的一個資料流稱為一個「管道」。
管道的特點:
int pipe(int pidefd[2]);
功能:建立無名管道
引數:pipefd:為int型別陣列的首地址,其存放了管道的檔案描述符pipefd[0]、pipefd[1]
當一個管道建立的時候,他會建立兩個檔案描述符fd[0]和fd[1]。其中fd[0]固定用於讀管道,而fd[1]固定用於寫管道。
返回值:成功:0 失敗:-1
匿名管道建立原理:
呼叫pipe函數後,OS會在fd_array陣列中分配兩個檔案描述符給管道,一個是讀,一個是寫,並把這兩個檔案描述符放到使用者傳進來的陣列中,fd[0]代表管道讀端,fd[1]代表管道寫端。這樣一個管道就建立好了。
範例演示:
範例1:觀察兩個檔案描述符的值
#include <stdio.h>
#include <unistd.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1){
// 管道建立失敗
perror("make piep");
//用於退出程序
exit(-1);
}
// 成功返回0
// pipefd[0] 代表讀端
// pipefd[1] 代表寫端
printf("fd[0]:%d, fd[1]:%d\n", pipefd[0], pipefd[1]);
return0;
}
執行結果如下:
顯然,pipefd這個陣列裡面放的是兩個檔案描述符,分別是3和4,因為0,1,2檔案描述符在程序建立的時候會由系統自動建立。
範例2:嘗試使用管道讀寫資料
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1){
// 管道建立失敗
perror("make piep");
exit(-1);
}
char buf[64] = "hello world";
// 寫資料
write(pipefd[1], buf, sizeof(buf)/sizeof(buf[0]));
// 讀資料
memset(buf,0,sizeof(buf));// 清空buf
ssize_t s = read(pipefd[0], buf, 11);
buf[s] = '\0';
printf("%s\n", buf);
return 0;
}//成功輸出hello world
可以看見對管道的操作,實際上就是對兩個讀寫檔案的操作,本質就是對檔案的操作和使用。
Linux下一切皆檔案,看待管道,其實時可以像看待檔案一樣。且管道和檔案使用方法是一致的。管道的生命週期隨程序。
原理:匿名管道是提供給有親緣關係兩個程序進行通訊的。所以我們可以在建立管道之後通過fork函數建立子程序,這樣父子程序就看到同一份資源,且父子程序都有這個管道的讀寫檔案描述符。我們可以關閉父程序的讀端,關閉子程序的寫端,這樣子程序往管道里面寫資料,父程序往管道里面讀資料,這樣兩個程序就可以實現通訊了。
原理解讀:
fork函數呼叫成功後,將為子程序申請PCB和使用者記憶體空間,子程序是父程序的副本,在使用者空間將複製父程序使用者空間所有的資料(程式碼段、資料段、BBS、棧、堆,實際上是複製的父程序的虛擬空間的地址),子程序從父程序繼承下列屬性:有效使用者、組號、行程群組號、環境變數、訊號處理方式設定、訊號遮蔽集合、當前工作目錄、根目錄、檔案模式掩碼、檔案大小限制和開啟的檔案描述符(特別注意:共用同一檔案表項)。
共用同一檔案表項就造成了一種現象,父子程序無論誰對檔案進行操作,那麼另外一個程序的檔案表也會受到相同的影響。
從圖中可以看出,雖然在子程序的表項中式複製了關於開啟檔案的資訊,但是他們是共用檔案表的,所以如果一個程序對檔案指標進行移動,那麼肯定會影響到另外的程序。
思考:這是不是和寫時拷貝相違背了,為什麼檔案表就能共用了呢?
要知道在linux原始碼中,每個程序都存在一個PCB結構體,每個PCB中,存放了一個結構體指標指向一個我們理解為檔案描述符的結構體struct file,而這個結構體裡,才存了檔案的id,值得注意的是,這個結構體裡有一個指標才是指向真正檔案的。檔案系統存在於磁碟當中,對磁碟的操作作業系統不會拷貝一份檔案給子程序,相反,像那些臨時建立存放於堆區和棧區的資料,作業系統會採用寫時拷貝,進行復制。
總結:父子程序共用檔案表,對檔案表進行的任何操作都會對父子程序造成相同的影響,與寫時拷貝進行區分。
父子程序通過建立匿名管道通訊具體過程如下:
1.父程序建立管道(管道建立要在程序建立之前)
2.fork建立子程序(子程序繼承父程序的管道檔案描述符)
3.關閉父程序的寫段,子程序的讀端
範例演示: 子程序每隔1秒往管道里面寫資料,父程序每隔1秒往管道里讀資料
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1){
// 管道建立失敗
perror("make piep");
exit(-1);
}
pid_t id = fork();
if (id < 0){
perror("fork failed");
exit(-1);
}
else if (id == 0){
// child
// 關閉讀端
close(pipefd[0]);
const char* msg = "I am child...!\n";
//int count = 0;
// 寫資料
while (1){
ssize_t s = write(pipefd[1], msg, strlen(msg));
printf("child is sending message...\n");
sleep(1);
}
}
else{
// parent
close(pipefd[1]);
char buf[64];
while (1){
ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
if (s > 0){
buf[s] = '\0';// 字串後放一個'\0'
printf("father get message:%s", buf);
}
else if (s == 0){
// 讀到檔案結尾 寫端關閉檔案描述符 讀端會讀到檔案結尾
printf("father read end of file...\n ");
}
sleep(1);
}
}
return 0;
}
執行結果如下:
讀寫規則總結:
注意:O_NONBLOCK是非阻塞的標誌位,指定管道對我們的操作要麼成功,要麼立刻返回錯誤,不被阻塞。
只能用於具有共同祖先的程序(具有親緣關係的程序)之間進行通訊;通常,一個管道由一個程序創
建,然後該程序呼叫fork,此後父、子程序之間就可應用該管道。
管道提供流式服務。也就是你想往管道里讀寫多少資料是根據自身來定的
一般而言,程序退出,管道釋放,所以管道的生命週期隨程序
一般而言,核心會對管道操作進行同步與互斥
管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道
半雙工是指傳輸過程中同時只能向一個方向傳輸,一方的資料傳輸結束之後,另外一方再回應。雙方傳輸資料是不可以同時進行的
全雙工是指兩方能同時傳送和接受資料。在這種情況下就沒有擁堵的危險,資料的傳輸也就更快
概念:無名管道,由於沒有名字,所以只能用於親緣關係的程序通訊。為了克服這個缺點,提出了命名管道(FIFO)。
命名管道不同於無名管道之處在於它提供了一個路徑名與之關聯,以FIFO的檔案形式存在於檔案系統中,這樣,即使與FIFO的建立程序不存在親緣關係的程序,只要可以存取該路徑,就能夠彼此通過FIFO相互通訊,因此,通過FIFO不相關的程序也能交換資料。
1.通過命令建立命名管道
mkfifo filename
2.通過函數建立命名管道
int mkfifo(const char *pathname, mode_t mode);
功能:建立命名管道
引數:pathname:普通的路徑名,也就是建立後FIFO的名字。
mode:檔案的許可權,與開啟普通檔案的open函數中的mode引數類似。
返回值:成功:0 (狀態碼) 失敗:如果檔案已經存在,則會出錯返回-1
程式碼範例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO "./fifo"
int main()
{
umask(0);
// 建立管道
int ret = mkfifo(FIFO, 0666);
if (ret == -1){
perror("make fifo");
exit(-1);
}
}
執行結果如下:
上面說過,管道其實就是一種特殊的檔案,管道檔案大小是0,因為上面介紹過,管道檔案的內容都存放在記憶體當中。
一旦建立了一個FIFO,就可以用open開啟它,常見的檔案I/O都可以作用於FIFO檔案。
FIFO嚴格的遵循先進先出的原則,對管道以及FIFO的讀總是從開始處返回資料,對它們的寫則是把資料新增到末尾。
讀寫規則:
讀管道
(2)若寫端沒有全部關閉,read阻塞等待
寫管道
(2)若管道沒滿,write將資料寫入,並返回實際寫入的位元組數
接下來我會使用命名管道實現簡單的版本聊天。
talkA.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
//先讀後寫
//以唯讀的方式開啟管道1
//以只寫的方式開啟管道2
define SIZE 1024
int main()
{
int fdr = -1;
int fdw = -1;
int ret = -1;
char buf[SIZE];
//以唯讀的方式開啟管道1
fdr = open("fifo1",O_RDONLY);
if(-1==fdr)
{
perror("open");
return 1;
}
printf("以唯讀的方式開啟管道1....\n");
//以只寫的方式開啟管道2
fdw = open("fifo2",O_WRONLY);
if(-1==fdw)
{
perror("open");
return 1;
}
printf("以只寫的方式開啟管道2....\n");
//迴圈讀寫
while(1)
{
//讀管道1
memset(buf,0,SIZE);
ret = read(fdr,buf,SIZE);
if(ret<=0)
{
perror("read");
break;
}
printf("read:%s\n",buf);
//寫管道2
memset(buf,0,SIZE);
fgets(buf,SIZE,stdin);
//去掉最後一個換行符
if('\n'==buf[strlen(buf)-1])
buf[strlen(buf)-1]=0;
//寫管道
ret = write(fdw,buf,strlen(buf));
if(ret<=0)
{
perror("write");
break;
}
printf("write ret:%d\n",ret);
}
//關閉檔案描述符
close(fdr);
close(fdw);
}
talkB.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
//以唯讀的方式開啟管道2
//以只寫的方式開啟管道1
#define SIZE 1024
int main()
{
int fdr = -1;
int fdw = -1;
int ret = -1;
char buf[SIZE];
//以只寫的方式開啟管道1
fdw = open("fifo1",O_WRONLY);
if(-1==fdw)
{
perror("open");
return 1;
}
printf("以只寫的方式開啟管道1....\n");
//以唯讀的方式開啟管道2
fdr = open("fifo2",O_RDONLY);
if(-1==fdr)
{
perror("open");
return 1;
}
printf("以唯讀的方式開啟管道2....\n");
//迴圈讀寫
while(1)
{
//寫管道1
memset(buf,0,SIZE);
fgets(buf,SIZE,stdin);
//去掉最後一個換行符
if('\n'==buf[strlen(buf)-1])
buf[strlen(buf)-1]=0;
//寫管道
ret = write(fdw,buf,strlen(buf));
if(ret<=0)
{
perror("write");
break;
}
printf("write ret:%d\n",ret);
//讀管道2
memset(buf,0,SIZE);
ret = read(fdr,buf,SIZE);
if(ret<=0)
{
perror("read");
break;
}
printf("read:%s\n",buf);
}
//關閉檔案描述符
close(fdr);
close(fdw);
}
執行結果如下:可以實現阻塞式的資料讀取
當兩個程序通訊的時候,我們檢視fifo的大小:
可以發現,管道的大小沒有發生變化。其實兩個程序通訊是在記憶體中進行的,並沒有把資料寫到管道中,因為管道只是一個符號性的檔案。如果是在管道寫資料,那麼IO次數會很多,效率太低了。