詳見: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版本的多樣性,電子電氣工程協會(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):主要作爲進程間以及同一進程不同線程之間的同步手段。