Linux程式執行緒學習筆記:程式間通訊 之 管道
周銀輝
中秋和國慶長假連起來耍了19天,平時學的點東西都快全忘了,那麼趕緊補習下Linux吧。
所謂“程式間通訊(IPC,inter-process communication)”,按照其目的講就是讓程式之間能夠“共享資料”,“傳輸資料”,“事件通知”,“程式通知”等,我所知道的一共有“管道” “訊號” “訊息(報文)” “共享記憶體” “套接字” 這幾種方式,我們會挨個挨個說,今天就說說管道。
所謂管道嘛,顧名思義類似於我們生活中的水管,只不過其中流動的是“資料”或者說一個一個位元組,只能單向流動的我們稱為“半雙工”,能雙向流動的稱為“全雙工”,其有兩個端點,資料流入的那一端稱為“寫端”,反之則稱為“讀端”,這兩個端點實際上是兩個“描述字”。這樣的管道可以連線在兩個程式之間,成為資料傳輸的通道。
按照是否有名字,管道分為“未命名管道”也就是你經常看到的pipe,以及“有名管道”也就是你經常看到的FIFO(first in first out, 和資料結構中的FIFO一樣,其也是按照“先進先出”的法則傳輸資料)。
1,pipe
pipe一詞雖然是“管道”二字的英文翻譯,但在這裡其不是管道的統稱,而是一種最基本和簡單的管道形式:未命名管道。由於其沒有名字(或者說id之類的),所以其無法在兩個毫無干系的兩個程式間使用。試想一下,程式A建立了一個管道,程式B無法去找到該管道並使用它,因為沒有任何可拿去查詢的憑據,連名字都沒有... 但在一種特殊情況下其是有用的,如下圖,假設程式A建立了一個管道:
其中的箭頭代表資料寫入和讀出,很明顯,程式A可以從管道的寫端將資料寫入,然後再從讀端讀出,但這似乎沒有什麼意義。
但,如果在管道建立以後,我們將程式A進行一次fork(),有意思的事情發生了, 子程式會複製父程式的大部分資訊,這些資訊裡當然包含了代表了管道讀端和寫端的兩個“描述字”(但管道仍然只有一份,就像被兩個程式讀寫的某個硬碟檔案只有一份一樣),所以其就演變成下圖這個樣子:
如果此時,我們將上圖中左上角以及右下角的兩個箭頭拋棄掉(相當於是說將這兩個箭頭所對應的描述字關閉), 那麼就如下所示咯:
哈,注意到了嗎?我們建立了一個從程式A流向程式B的資料通道。
那麼,將上述過程寫成程式碼的形式就很簡單了。
首先是 int pipe(int f[2]) 這個函式,其需要標頭檔案<unistd.h>,這個函式將建立一個未命名管道,並將管道的讀端描述字包含在f[0]中,將寫端描述字放在f[1]中,然後你就可以像利用普通檔案描述字一樣來讀寫資料了。
然後是fork函式,不多講,不瞭解的同學一定要先搞清楚了,比如看這篇文章。
再次是int close(int fd) 函式, 其需要標頭檔案<unistd.h>,其用於關閉指定的檔案描述字。
最後是write和read函式, 其需要標頭檔案<unistd.h>,用於讀寫資料。
OK,上程式碼:
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define BUFF_SZ 256
int main()
{
printf("app start...\n");
pid_t pid;
int pipe_fd[2];
char buf[BUFF_SZ];
const char data[] = "hi, this is the test data";
int bytes_read;
int bytes_write;
//clear buffer, all bytes as 0
memset(buf, 0, sizeof(buf));
//creat pipe
if(pipe(pipe_fd) < 0)
{
printf("[ERROR] can not create pipe\n");
exit(1);
}
//fork an new process
if(0 == (pid=fork()))
{
//close the write-point of pipe in child process
close(pipe_fd[1]);
//read bytes from read-point of pipe in child process
if((bytes_read = read(pipe_fd[0], buf, BUFF_SZ)) > 0)
{
printf("%d bytes read from pipe : '%s'\n", bytes_read, buf);
}
//close read-point of pipe in child process
close(pipe_fd[0]);
exit(0);
}
//close read-point of pipe in parent process
close(pipe_fd[0]);
//write bytes to write-point of pipe in parent process
if((bytes_write = write(pipe_fd[1], data, strlen(data))))
{
printf("%d bytes wrote to pipe : '%s'\n", bytes_write, data);
}
//close write-point of pipe in parent process
close(pipe_fd[1]);
//wait child process exit
waitpid(pid, NULL, 0);
printf("app end\n");
return 0;
}
執行輸出為:
25 bytes wrote to pipe : 'hi, this is the test data'
25 bytes read from pipe : 'hi, this is the test data'
app end
2,FIFO
與“無名管道”不同的是,FIFO擁有一個名稱來標誌它,所謂的名稱實際上就是一個路徑,比如“/tmp/my_pipe”,其對應到磁碟上的一個管道檔案,如果我們用file命令來檢視其檔案型別的話,會得到如下輸出:
my_fifo: fifo (named pipe)
為了簡化對FIFO的理解,我們可以這樣來假想:程式A在磁碟上建立了一個名為my_pipe的檔案,並向其中寫入一些資料,然後程式B開啟該檔案,並將資料從檔案中讀出,這樣我們便實現了程式A和程式B之間的通訊。大致原理如此,只不過FIFO做了更精細的一些操作,以便實現起來更可靠。
另外,我們需要知道的是,FIFO是單向(半雙工)傳輸資料的。
函式 int mkfifo (char* path, mode_t mode) 負責建立FIFO管道,其需要標頭檔案<sys/stat.h>,引數path即要建立的管道檔案存放位置,mode引數即檔案許可權,更多的參考這裡。
FIFO管道建立完成以後,便可以使用open函式來開啟它,然後進行讀寫操作了。
看下面這個簡單的demo,其將測試資料由程式A傳遞給程式B(為防止混淆視線,我將一些條件判斷和異常處理程式碼刪掉了):
先建立一個程式A,其負責建立FIFO管道,並向其中寫入一些資料:
* process A: create FIFO and write data
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#define FIFO_NAME "/tmp/my_fifo"
int main()
{
int pipe_fd;
//if the pipe file do not exist
if (access(FIFO_NAME, F_OK) == -1)
{
//creat FIFO pipe file
mkfifo(FIFO_NAME, 0777);
}
//open FIFO pipe file.
//this will be brocked until some one open another end point(read-point) of this pipe
pipe_fd = open(FIFO_NAME, O_WRONLY);
//write data into pipe
write(pipe_fd, "hi, this is a test", PIPE_BUF);
//close FIFO pipe file descriptor
close(pipe_fd);
return 0;
}
然後建立程式B,它從管道中讀取資料並顯示出來:
* process B: read data from FIFO
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
int main()
{
int pipe_fd;
char buffer[BUFFER_SIZE + 1];
//reset all bytes in buffer as '\0'
memset(buffer, '\0', sizeof(buffer));
//open FIFO pipe file.
//this will be brocked until some one open another end point(write-point) of this pipe
pipe_fd = open(FIFO_NAME, O_RDONLY);
if(read(pipe_fd, buffer, BUFFER_SIZE) > 0)
{
printf("data from FIFO : %s\n", buffer);
}
//close pipe file descriptor
close(pipe_fd);
return 0;
}
執行下程式便會發現,無論是先執行A或是B,先執行起來的都會等待另外一個,這時open函式第二個引數的原因,我們可以新增O_NONBLOCK選項來取消阻塞。關於open函式,更多的看這裡。
下面這個demo比較有意思,在程式A中敲入字元,其會立即傳遞給程式B(無需等待Enter鍵),然後程式B會將它顯示出來,關於如何取消控制檯對Enter鍵的等待,可以參考這裡。
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <termios.h>
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
static struct termios oldt;
//restore terminal settings
void restore_terminal_settings(void)
{
tcsetattr(0, TCSANOW, &oldt); /* Apply saved settings */
}
//make terminal read 1 char at a time
void disable_waiting_for_enter(void)
{
struct termios newt;
//Save terminal settings
tcgetattr(0, &oldt);
//init new settings
newt = oldt;
//change settings
newt.c_lflag &= ~(ICANON | ECHO);
//apply settings
tcsetattr(0, TCSANOW, &newt);
//make sure settings will be restored when program ends
atexit(restore_terminal_settings);
}
int main()
{
int pipe_fd;
int res;
char buffer[BUFFER_SIZE + 1];
memset(buffer, '\0', sizeof(buffer));
//if the pipe file do not exist
if (access(FIFO_NAME, F_OK) == -1)
{
//creat FIFO pipe file
res = mkfifo(FIFO_NAME, 0777);
if (res != 0)
{
fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);
exit(EXIT_FAILURE);
}
}
//open FIFO pipe file.
//this will be brocked until some one open another end point(read-point) of this pipe
pipe_fd = open(FIFO_NAME, O_WRONLY);
//if FIFO pipe file open sucessfully
if (pipe_fd != -1)
{
printf("input something and press RETURN\n");
char ch;
disable_waiting_for_enter();
while ((ch = getchar()) != '\n')
{
buffer[0] = ch;
//write data into pipe
res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1)
{
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
}
//close FIFO pipe file descriptor
close(pipe_fd);
}
else
{
exit(EXIT_FAILURE);
}
printf("Process %d finish\n", getpid());
exit(EXIT_SUCCESS);
}
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
int main()
{
int pipe_fd;
int res;
char buffer[BUFFER_SIZE + 1];
//reset all bytes in buffer as '\0'
memset(buffer, '\0', sizeof(buffer));
//open FIFO pipe file.
//this will be brocked until some one open another end point(write-point) of this pipe
pipe_fd = open(FIFO_NAME, O_RDONLY);
if (pipe_fd != -1)
{
printf("data read from buffer : \n");
//read all data from pipe file by BUFFER_SIZE each time
do
{
res = read(pipe_fd, buffer, BUFFER_SIZE);
if(res>0)
{
printf("%s", buffer);
fflush(stdout);
}
}while(res > 0);
//close pipe file descriptor
close(pipe_fd);
}
else
{
exit(EXIT_FAILURE);
}
printf("\nProcess %d finish\n", getpid());
exit(EXIT_SUCCESS);
}