進程-IPC 管道 (一)
詳見:https://github.com/ZhangzheBJUT/linux/blob/master/IPC(%E4%B8%80).md
一 IPC 概述
進程間通信就是在不同進程之間傳播或交換信息,那麽不同進程之間存在著什麽兩方都能夠訪問的介質呢?進程的用戶空間是互相獨立的,一般而言是不能互相訪問的,唯一的例外是共享內存區。系統空間是“公共場所”。所以內核顯然能夠提供這種條件,例如以下圖所看到的。
除此以外,那就是兩方都能夠訪問的外設了。兩個進程能夠通過磁盤上的普通文件交換信息。或者通過“註冊表”或其他數據庫中的某些表項和記錄交換信息。
linux下的進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟件公布中心)。它們在進程間通信方面的側重點有所不同。前者對Unix早期的進程間通信手段進行了系統的改進和擴充,形成了 system
V IPC
。通信進程局限在單個計算機內;後者則跳過了該限制。形成了基於套接字 socket
的進程間通信機制。
Linux 則把兩者繼承了下來,例如以下圖示:
Unix IPC包含: 管道、FIFO、信號; System V IPC包含:System V消息隊列、System V信號燈、System V共享內存區; Posix IPC包含: Posix消息隊列、Posix信號燈、Posix共享內存區。
有兩點須要簡單說明一下: 1.因為Unix版本號的多樣性。電子電氣project協會(IEEE)開發了一個獨立的Unix標準,這個新的ANSI Unix標準被稱為計算 機環境的可移植性操作系統界面(PSOIX)。
現有大部分Unix和流行版本號都是遵循POSIX標準的,而Linux從一開始就遵循POSIX標準。 2.BSD並非沒有涉足單機內的進程間通信(socket本身就能夠用於單機內的進程間通信)。其實。非常多Unix版本號的單 機IPC留有BSD的痕跡,如4.4BSD支持的匿名內存映射、4.3+BSD對可靠信號語義的實現等等。
上圖給出了linux 所支持的各種IPC手段,為了避免概念上的混淆,在盡可能少提及Unix的各個版本號的情況下,全部問題的討論終於都會歸結到linux環境下的進程間通信上來。而且。對於linux所支持通信手段的不同實現版本號(如對於共享內存來說,有Posix共享內存區以及System V共享內存區兩個實現版本號)以下將主要介紹Posix API。
linux下進程間通信的幾種主要手段:
- 管道(Pipe)及命名管道(named pipe):管道可用於具有親緣關系進程間的通信;命名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還同意無親緣關系進程間的通信。
- 信號(Signal):信號是比較復雜的通信方式,用於通知接受進程有某種事件發生。除了用於進程間通信外,進程還可以發送信號給進程本身;linux 除了支持Unix早期信號語義函數signal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上。該函數是基於BSD的,BSD為了實現可靠信號機制,又可以統一對外接口,用sigaction函數又一次實現了signal函數)。
- 信號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。
- 共享內存:使得多個進程能夠訪問同一塊內存空間。是最快的可用IPC形式,是針對其他通信機制執行效率較低而設計的。
它往往與其他通信機制,如信號量結合使用,來達到進程間的同步及相互排斥。
- 消息隊列:消息隊列是消息的鏈接表。包含Posix消息隊列和system V消息隊列。有足夠權限的進程能夠向隊列中加入消息,被賦予讀權限的進程則能夠讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道僅僅能承載無格式字節流以及緩沖區大小受限等缺點。
- 套接字(Socket):更為一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的。但如今一般能夠移植到其他類Unix系統上。Linux和System V的變種都支持套接字。
二 管道
當從一個進程連接數據流到還有一個進程時,使用術語管道(pipe)。
一般是把一個進程的輸出通過管道連接到還有一個進程的輸入。
對於shell命令來說,命令的連接是通過管道操作符來完畢的。例如以下所看到的:
cmd1 | cmd2 shell負責安排兩個命令的標準輸入和標準輸出 cmd1的標準輸入來自終端鍵盤 cmd1的標準輸出傳遞給cmd2,作為它的標準輸入 cmd2的標準輸出連接到終端屏幕
shell所做的工作實際上是對標準輸入和標準輸出流進行又一次連接,使數據流從鍵盤輸入通過兩個命令終於輸出到屏幕上。
2.1.poen與pclose函數
函數原型:
#include <stdio.h>
FILE *popen(const char*command,const char *open_mode);
int pclose(FILE *stream_to_close);
函數描寫敘述:
popen 函數同意一個程序將還有一個程序作為新進程來啟動。並能夠傳遞數據給它或者通過它來接收數據。 command 字符串是要運行的程序名和對應的參數。這個命令被送到 /bin/sh 以 -c 參數運行, 即由 shell來運行。
open_mode 必須為"r"或者"w",二者僅僅能選擇一個,函數的返回值FILE*文件流指針。通過經常使用的stdio庫函數 (如fread)來讀取被調用程序的輸出。
假設open_mode是"w",調用程序就能夠用fwrite調用向被調用程序發送 數據。而被調用程序能夠在自己的標準輸入上讀取數據。 補:/bin/sh -c Read commands from the command_string operand instead of from the standard input. Special parameter 0 will be set from the command_name operand and the positional parameters ($1, $2, etc.) set from the remaining argument operands.
讀取外部程序的輸出:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
FILE *read_fp;
char buffer[BUFSIZ+1];
int chars_read;
memset(buffer,‘\0‘,sizeof(buffer));
read_fp = popen("uname -a","r");
if (read_fp !=NULL)
{
chars_read = fread(buffer,sizeof(char),BUFSIZ,read_fp);
if (chars_read>0)
{
printf("output was:-\n%s\n",buffer);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
將輸出發送到外部程序:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
FILE *write_fp;
char buffer[BUFSIZ+1];
sprintf(buffer,"Once upon a time ,thera was ...\n");
write_fp = popen("od -c","w");
if (write_fp != NULL)
{
fwrite(buffer,sizeof(char),strlen(buffer),write_fp);
pclose(write_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
註意:popen()函數的返回值是一個普通的標準I/O流,且它僅僅能用pclose()函數來關閉,而不是fclose()。
popen函數執行一個程序時。它首先啟動shell,即系統中的sh命令,然後將command字符串作為一個參數傳遞給它,由shell來負責分析命令字符串。它同意我們通過popen啟動很復雜的shell命令。使用shell的一個不太好的影響是。針對每一個popen的調用,不僅要啟動一個被請求的程序,還要啟動一個shell。即每一個popen調用將多啟動兩個進程,從節省資源的角度來看,popen函數的調用成本略高,並且對目標命令的調用比正常方式要慢一些。
pclose調用僅僅在popen啟動的進程結束後才返回。假設調用pclose時它仍在執行,pclose調用將等待該進程的結束。
2.2.pipe函數
底層pipe函數,通過這個函數在兩個程序之間傳遞數據不須要啟動一個shell來解釋請求命令,它同一時候還提供了對讀寫數據的很多其它控制。
函數原型:
#include <unistd.h>
int pipe(int file_descriptor[2]);
函數描寫敘述:
pipe 函數參數是一個由兩個整數類型的文件描寫敘述符組成的數組的指針。
該函數在數組中填上兩個新的文件描寫敘述符後返回0,假設 失敗則返回-1,並設置error來表明失敗的原因。 常見的錯誤: EMFILE:進程使用的文件描寫敘述符過多 ENFILE:系統的文件表已滿 EFAULT:文件描寫敘述符無效 兩個返回的文件描寫敘述符以一種特殊的方式連接起來,寫到file_descriptor[1]的全部數據都能夠從file_descriptor[0]讀出來。
數據基於先進先出的原則進行處理,意味著假設你把1,2,3寫到file_descriptor[1],從file_descriptior[0]讀取到的數據也 是1,2,3。
註意:調用pipe函數時在內核中開辟一塊緩沖區(稱為管道)用於通信,它有一個讀端一個寫端,然後通過file_descriptor
參數傳出給用戶程序兩個文件描寫敘述符。file_descriptor[0]指向管道的讀端,file_descriptor[1]
指向管道的寫端。
在用戶程序看起來管道就像一個打開的文件,通過read(file_descriptor[0]
)或者write(file_descriptor[1]
)來向這個文件讀寫數據,事實上是在讀寫內核緩沖區。兩個文件描寫敘述符被強制規定file_descriptor[0]
僅僅能指向管道的讀端。假設進行寫操作就會出現錯誤;同理 file_descriptor[1]
僅僅能指向管道的寫端。假設進行讀操作就會出現錯誤。pipe使用的是文件描寫敘述符而不是文件流,所以必須使用底層的read和write調用來訪問數據,而不是用文件流函數fread和fwrite。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "1233";
char buffer[BUFSIZ+1];
memset(buffer,‘\0‘,sizeof(buffer));
if (pipe(file_pipes)==0)
{
data_processed = write(file_pipes[1],some_data,strlen(some_data));
printf("Wrote %d bytes \n",data_processed);
data_processed = read(file_pipes[0],buffer,BUFSIZ);
printf("Read %d bytes \n",data_processed);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
程序說明: 這個程序用數組 file_pipes
中的兩個文描寫敘述符創建一個管道,然後它用文件描寫敘述符 file_pipes[1]
向管道中寫數據
,再從 file_pipes[0]
讀回數據。
管道的真正優勢體如今:兩個進程之間傳遞數據。 當程序用fork調用創建新進程時。原先打開的文件描寫敘述符仍將保持打開狀態。假設在原先的進程中創建一個管道,然後再調用fork創建新進程,即能夠通過管道在兩個進程之間傳遞數據,例如以下圖所看到的:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int data_processed;
int file_pipes[2];
const char some_data[] = "1233";
char buffer[BUFSIZ+1];
pid_t fork_result;
memset(buffer,‘\0‘,sizeof(buffer));
if (pipe(file_pipes)==0)
{
fork_result = fork();
if (fork_result ==-1)
{
fprintf(stderr,"Fork failed");
exit(EXIT_FAILURE);
}
if (fork_result ==0)
{
close(file_pipes[1]);
sleep(2);
data_processed = read(file_pipes[0],buffer,BUFSIZ);
printf("Read %d bytes :%s \n",data_processed,buffer);
}
else
{
close(file_pipes[0]);
data_processed = write(file_pipes[1],some_data,strlen(some_data));
printf("Wrote %d bytes \n",data_processed);
}
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
程序說明: 程序首先用pipe函數創建一個管道。接著用fork調用創建一個新進程。假設fork調用成功,父進程首先關閉讀操作符,然後寫數據到管道中,而子進程首先關閉寫操作符,然後從管道中讀取數據。父子進程都在僅僅調用了一次write或read之後就退出。
其原理例如以下圖所看到的:
系統維護一個文件的文件描寫敘述符表計數,父子進程都各自有指向同樣文件的文件描寫敘述符,當關閉一個文件描寫敘述符時,對應計數減1,當這個計數減到0時,文件就被關閉。因此盡管父進程關閉了其文件描寫敘述符 file_pipes[0]
,可是這個文件的文件描寫敘述符計數還沒等於0,所以子進程還能夠讀取。也能夠這麽理解,父進程和子進程都有各自的文件描寫敘述符,盡管在父進程中關閉了file_pipes[0]
,可是對子進程中的 file_pipes[0]
沒有影響。
註:1.文件表中的每一項都會維護一個引用計數。標識該表項被多少個文件描寫敘述符(fd)引用,在引用計數為0的時候,表項才會被刪除。所以調用close(fd)關閉子進程的文件描寫敘述符,僅僅會降低引用計數。可是不會使文件表項被清除。所以父進程依然能夠訪問。
2.當沒有數據可讀時。read調用一般會堵塞。即它將暫停進程來等待直到有數據到達為止。
假設管道的還有一端已被關閉。也就是說沒有進程打開這個管道並向它寫數據了,此時read調用將會被堵塞。註意,這與讀取一個無效的文件描寫敘述符不同。read把無效的文件描寫敘述符看做一個錯誤並返回-1.
在pipe使用中,也能夠在子進程中執行一個與其父進程全然不同的還有一個程序,而不是只執行一個同樣的程序。這個可由exec調用來實現。在上面的樣例中,由於子進程本身有 file_pipes
數據的一個副本。所以這並不成為問題。但經過exec調用後,原來的進程已經被新的子進程替換了。為解決問題,能夠將文件描寫敘述符(實際上是一個數字)作為一個參數傳遞給exec啟動程序。具體實現見:pipe3.c 和 pipe4.c
2.3.命名管道FIFO
無名管道僅僅能用在父子進程之間,這些程序由一個共同的祖先進程啟動。但假設想在不同進程之間交換數據,這就不太方便。而這能夠使用FIFO文件來完畢在不相關的進程之間交換數據,它通常叫做命名管道(named pipe)。
命名管道是一種特殊類型的文件,它在文件系統中以文件名稱的形式存在,但它的行為卻和已經看到過的沒有名字的管道類似。
使用以下兩個函數能夠創建一個FIFO文件:
int mkfifo(const char*filename,mode_t mode)
int mknod(const char* filename,mode_t mode | S_IFIFO,(dev_t)0);
命名管道的一個很實用特點是:因為它們出如今文件系統中,所以它們能夠像尋常的文件名稱一樣在命令中使用,使用FIFO僅僅是為了單向傳遞數據。
用法例如以下:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int res = mk fifo("/tmp/my_fifo",0777);
if (res == 0)
{
printf("FIFO created.\n");
}
exit(EXIT_SUCCESS);
}
查看執行結果:
註:輸出結果中的第一個字符為p,表示這是一個管道文件。不同的用戶掩碼最後得到的文件訪問權限是不同的(我的用戶掩碼為0002)。
與通過pipe調用創建管道不同,FIFO是以命名文件的形式存在,而不是打開的文件描寫敘述符。所以對它進行讀寫操作必須先打開它。FIFO也用open和close函數打開和關閉。 對FIFO文件來說。傳遞給open調用的是FIFO的路徑名,而不是一個正常的文件。
打開一個FIFO文件的方法例如以下:
open(const char*path,O_RDONLY);
在這樣的情況下,open調用將堵塞。除非有一個進程以寫的方式打開同一個FIFO,否則它不會返回。
open(const char* path,O_RDONLY|O_NONBLOCK)
在這樣的情況下,即使沒有其他進程以寫方式打開FIFO,open調用也將成功並立馬返回。
open(const char *path,O_WRONLY)
在這樣的情況下。open調用將會堵塞,直到有一個進程以讀方式打開一個FIFO為止。
open(const char*path,O_WRONLY|O_NONBLOCK)
這個函數調用總是立馬返回,但假設沒有一個進程以讀方式打開FIFO文件,open調用將返回一個錯誤而且FIFO也不會被打開。
假設確實有一個進程以讀方式打開FIFO文件,那麽我們就能夠通過它返回的文件描寫敘述符對這個FIFO文件進行讀寫操作。
註意:
- 使用open打開FIFO文件程序不能以O_RDWR模式打開FIFO文件進行讀寫操作。這樣做的後果是未明白定義。假設確實須要在程序之間雙向傳遞數據,最好使用一對FIFO。
-
O_NONBLOCK
分別搭配O_RDONLY
和O_WRONLY
在效果上是不同的,假設沒有進程以讀方式打開管道。非堵塞寫方式的open調用將失敗。但非堵塞讀方式的open調用總是成功。close調用的行為並不受
O_NONBLOCK
標誌的影響。#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #define FIFO_NAME "/tmp/my_fifo" int main(int argc,char* argv[]) { int res; int open_mode = 0; int i; if (argc <2) { fprintf(stderr, "Usage:%s <some combination of O_RDONLY O_WRONLY O_NONBLOCK>\n",*argv ); exit(EXIT_FAILURE); } for (i = 1; i < argc; ++i) { if (strncmp(*++argv,"O_RDONLY",8) == 0) { open_mode |= O_RDONLY; } if (strncmp(*argv,"O_WRONLY",8) == 0 ) { open_mode |= O_WRONLY; } if (strncmp(*argv,"O_NONBLOCK",10) == 0) { open_mode |= O_NONBLOCK; } } if (access(FIFO_NAME,F_OK) == -1) { res = mkfifo(FIFO_NAME,0777); if (res !=0) { fprintf(stderr,"Could not create fifo %s\n",FIFO_NAME); exit(EXIT_FAILURE); } } printf("Process %d opening FIFO\n",getpid()); res = open(FIFO_NAME,open_mode); printf("Process %d result %d\n",getpid(),res); sleep(5); if (res != -1) { (void)close(res); } printf("Process %d finished\n",getpid()); exit(EXIT_SUCCESS); }
使用 O_NONBLOCK
模式會影響到對FIFO的read和write調用。
小結:
1. 從FIFO中讀取數據
- 假設有進程寫打開FIFO,且當前FIFO為空,則對於設置了堵塞標誌的讀操作來說。將一直堵塞下去,直到有數據能夠讀時才繼續運行;對於沒有設置堵塞標誌的讀操作來說,則返回0個字節,當前errno值為EAGAIN,提示以後再試。
- 對於設置了堵塞標誌的讀操作來說。造成堵塞的原因有兩種:
1、當前FIFO內有數據,但有其他進程在讀這些數據;
2、FIFO本身為空。
解堵塞的原因是:FIFO中有新的數據寫入,不論寫入數據量的大小。也不論讀操作請求多少數據量,僅僅要有數據寫入就可以。 - 讀打開的堵塞標誌僅僅對本進程第一個讀操作施加作用。假設本進程中有多個讀操作序列,則在第一個讀操作被喚醒並完畢讀操作後,其他將要運行的讀操作將不再堵塞,即使在運行讀操作時,FIFO中沒有數據也一樣(此時,讀操作返回0)。
- 假設沒有進程寫打開FIFO。則設置了堵塞標誌的讀操作會堵塞。
- 假設FIFO中有數據。則設置了堵塞標誌的讀操作不會由於FIFO中的字節數少於請求的字節數而堵塞,此時。讀操作會返回FIFO中現有的數據量。
2. 從FIFO中寫入數據
FIFO的長度是須要考慮的一個非常重要因素。
系統對任一時刻在一個FIFO中能夠存在的數據長度是有限制的。它由#define PIPE_BUF
定義。在頭文件limits.h
中。在Linux和其他類UNIX系統中。它的值一般是4096字節。Red
Hat Fedora9 下是4096,但在某些系統中它可能會小到512字節。
盡管對於僅僅有一個FIFO寫進程和一個FIFO讀進程而言,這個限制並不重要,但僅僅使用一個FIFO並同意多個不同進程向一個FIFO讀進程發送寫請求的情況是非經常見的。
假設幾個不同的程序嘗試同一時候向FIFO寫數據,是否能保證來自不同程序的數據塊不相互交錯就非常關鍵了。
也就是說,必須保證每一個寫操作“原子化”。
-
對於設置了堵塞標誌的寫操作:
- 當要寫入的數據量不大於
PIPE_BUF
時,linux將保證寫入的原子性。假設此時管道空暇緩沖區不足以容納要寫入的字節數,則進入睡眠,直到當緩沖區中可以容納要寫入的字節數時,才開始進行一次性寫操作。即寫入的數據長度小於等於
PIPE_BUF
時,那麽或者寫入所有字節。或者一個字節都不寫入。它屬於一個一次性行為,詳細要看FIFO中是否有足夠的緩沖區。 - 當要寫入的數據量大於
PIPE_BUF
時,linux將不再保證寫入的原子性。FIFO緩沖區一有空暇區域。寫進程就會試圖向管道寫入數據,寫操作在寫完所有請求的數據後返回。
- 當要寫入的數據量不大於
-
對於沒有設置堵塞標誌的寫操作:
- 當要寫入的數據量不大於
PIPE_BUF
時,linux將保證寫入的原子性。假設當前FIFO空暇緩沖區可以容納請求寫入的字節數,寫完後成功返回。假設當前FIFO空暇緩沖區不可以容納請求寫入的字節數,則返回EAGAIN錯誤。提示以後再寫。 - 當要寫入的數據量大於
PIPE_BUF
時。linux將不再保證寫入的原子性。在寫滿全部FIFO空暇緩沖區後,寫操作返回。
- 當要寫入的數據量不大於
- 信號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。
進程-IPC 管道 (一)