管道 (Unix)
在類Unix作業系統(以及一些其他借用了這個設計的作業系統,如Windows)中,管道(英語:Pipeline)是一系列將標準輸入輸出連結起來的行程,其中每一個行程的輸出被直接作為下一個行程的輸入。 每一個連結都由匿名管道實現[來源請求]。管道中的組成元素也被稱作過濾程式。
例子
編輯簡單樣例
編輯ls -l | less
在這個例子中,ls
用於在Unix下列出目錄內容,less
是一個有搜尋功能的互動式的文字分頁器。這個管線使得用戶可以在列出的目錄內容比螢幕長時目錄上下翻頁。
以less
結束的管道(或more,這是個相似的分頁工具)是最常被使用的。這讓用戶可以閱覽尚未顯示的大量文字(受可用快取限制,控制台的螢幕大小、螢幕快取大小往往有限,不足以一次先輸出所有輸出內容,也不能自由捲動內容),若少了這工具則這些文字將會捲過終端機而無法閱讀到。換句話說,他們將程式設計師從為自己的軟件開發分頁器的負擔中解放了出來:他們只需要把他們的輸出用過「管道」匯入到less
程式中即可,甚至也可以完全不顧分頁問題,去假定他們的用戶會在需要將輸出分頁的時候自己去這樣做。
複雜樣例
編輯以下是一個管線化的範例,執行由一URL標示的萬維網資源的一種拼寫檢查器。之後是關於這個其作用的說明。注意「\」是用來把這六行轉為一個命令列。
curl "https://en.wikipedia.org/wiki/Pipeline_(Unix)" | \
sed 's/[^a-zA-Z ]/ /g' | \
tr 'A-Z ' 'a-z\n' | \
grep '[a-z]' | \
sort -u | \
comm -23 - /usr/share/dict/words | \
less
- curl 取得該網頁的HTML內容(在有些系統上可以使用wget)。
- sed 移除非空格的字元和網頁內容的字母,並以空格取代之。
- tr 把大寫字母改成小寫字母,並把行列裏的空格換成新行(每個詞現在各佔有獨立的一行)。
- grep 過濾得到那些至少有一個小寫字母的行(刪除空行)。
- sort 將「單詞」(也就是每一個行)按照字母順序排序,並且通過命令列的-u參數來刪除重複的行。
- comm 尋找兩個檔案中的共同行,-23過濾掉只有第二個檔案擁有的行、兩個檔案共有的行,僅僅留下只在第一個檔案中有的行。在檔名的位置上的-參數列示要求comm使用標準輸入(在這個例子裏,他的標準輸入來自於管道上游的標準輸出)作為輸入,而不是以普通檔案作為輸入。最終得到一串沒有出現在/usr/share/dict/words之中的「單詞」(也就是一行)。
- less 允許用戶翻頁瀏覽結果。
這個特殊的「|」字元告訴命令列直譯器(Shell)將前一個命令的輸出通過「管道」匯入到接下來的一行命令作為輸入。也就是說,curl命令的輸出被作為sed命令的輸入,後面的命令也是這樣。
命令列介面中的管線
編輯所有廣泛應用於UNIX和Windows中的shell程式都有特殊的語法構建管線。典型語法是使用ASCII中的垂直線「|」(正是由於這個原因,這個符號常被稱為管道符)。當出現這樣的語法時shell會啟動各個行程,並調整各個行程的標準流之間的連接(還包括安排一些快取)。
錯誤流
編輯通常,管線中的行程的標準錯誤流("stderr")不會通過管道傳輸;它們被合併輸出到控制台。然而,很多Shell提供一些擴充的語法去改變這一行為。比如在csh Shell和bash中,使用「|&」代替「|」來表示錯誤流也需要被合併進入標準輸出,並傳遞給下一個行程。Bourne shell也可以合併錯誤流,通過 2>&1 也可以將錯誤流重新導向到一個不同的檔案。
Pipemill
編輯在一些常用的簡單管線中,shell僅僅只是用管道來連接每個子行程,然後在子行程中執行外部命令。因此shell本身沒有通過管線來處理數據。
然而,shell也有可能直接處理管線數據。構建這樣的語法像這樣:
command | while read var1 var2 ...; do
# process each line, using variables as parsed into $var1, $var2, etc
# (note that this is a subshell: var1, var2 etc will not be available
# after the while loop terminates)
done
... 這樣的語法叫 "pipemill" 。
在程式中創造管道
編輯匿名管道
編輯使用C語言在UNIX中使用pipe(2)系統呼叫時,這個函數會讓系統構建一個匿名管道,這樣在行程中就打開了兩個新的,打開的檔案描述子:一個唯讀端和一個唯寫端。管道的兩端是兩個普通的,匿名的檔案描述子,這就讓其他行程無法連接該管道。
為了避免死結並利用行程的並列執行的好處,有一個或多個管道的UNIX行程通常會呼叫fork(2)
產生新行程。並且每個子行程在開始讀或寫管道之前都會關掉不會用到的管道端。或者行程會產生一個子線程並使用管道來讓線程進行數據交換。
實現代碼:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int
main(int argc, char *argv[])
{
int pipefd[2];
pid_t cpid;
char buf;
if (argc != 2) {
fprintf(stderr, "Usage: %s <string>\n", argv[0]);
exit(EXIT_FAILURE);
}
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* Child reads from pipe */
close(pipefd[1]); /* Close unused write end */
while (read(pipefd[0], &buf, 1) > 0)
write(STDOUT_FILENO, &buf, 1);
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);
} else { /* Parent writes argv[1] to pipe */
close(pipefd[0]); /* Close unused read end */
write(pipefd[1], argv[1], strlen(argv[1]));
close(pipefd[1]); /* Reader will see EOF */
wait(NULL); /* Wait for child */
exit(EXIT_SUCCESS);
}
}
具名管道
編輯具名管道可以通過呼叫mkfifo(2)
或mknod(2)
來構建,當被呼叫時表現為輸入或輸出的檔案。這樣可以允許建立多個管道,並且將其同標準錯誤重新導向或tee
結合起來使用更為有效。
實現代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
char filename[] = "test_fifo";
if (!mkfifo(filename,S_IRUSR | S_IWUSR| S_IRGRP|S_IWGRP)){
pid_t pid = fork();
if (pid == 0){ //child
int fd = open(filename, O_WRONLY);
if (fd < 0)
perror("child open()");
else{
if (strlen(argv[1]) != write(fd, argv[1], strlen(argv[1])))
perror("child write error");
else
close(fd);
}
}
else if (pid > 0){ //father
int fd = open(filename, O_RDONLY);
if (fd < 0)
perror("father open()");
else{
char buffer[200];
int readed = read(fd, buffer, 199);
close(fd);
buffer[readed] = '\0';
printf("%s\n",buffer);
}
}
else
perror("fork()");
}
else
perror("mkfifo() error:");
}
以上代碼在編譯後執行時給出一個參數,子行程會將該參數內容寫入管道(該管道在當前目錄下,檔名為「test_fifo」),父行程從管道中讀取內容並顯示出來
實現
編輯在大多數類UNIX作業系統中,管線上的所有行程同時啟動,輸入輸出流也已經被正確地連接,並且這些行程被排程程式所管理。最為重要的一點就是,所有的UNIX管道和其他管道實現不一樣的地方就是快取的概念:輸出行程可能會以每秒5000 byte的速度輸出,但是接收行程也許每秒只能接收100 byte,但不會有數據遺失。原因就是管道上游的行程的所有輸出都會被放入一個佇列中。當下游行程開始接收數據時,作業系統就會將數據從佇列傳至接收行程,並將傳完的數據從佇列中移除。當快取佇列空間不足時,上游行程會被終止,直到接收行程讀取數據為上游行程騰出空間。在Linux中,快取佇列的大小是65536 byte。
網絡管線
編輯歷史
編輯管道的概念以及垂直線的記號(|)都是由道格拉斯·麥克羅伊發明的,他是早期命令列外殼的作者。他發現他常常將一個程式的輸出作為另一個程式的輸入,於是便發明了「管道。它的想法在1973年被實現,Ken Thompson將管道添加到了UNIX作業系統。[1]這個點子最終被移植到了其他的作業系統,比如DOS、OS/2、Microsoft Windows和BeOS,而且常常使用相同的記號(垂直線)。
雖然管道概念是獨立發展的,但是 Unix 管道相似於、也確實晚於由Ken Lochner在20世紀60年代為Dartmouth Time Sharing System開發的'communication files'。[2][3]
在蘋果Automator(類似管道一樣將多個重複的命令鏈結起來)的那個機械人拿着一根管子的圖示也是對於最初Unix管道概念的紀念。
其他作業系統
編輯其他作業系統的這個特色源自於Unix,例如 Taos 和 MS-DOS,最終成為軟件工程的管道與過濾器設計模式。
參見
編輯參照
編輯- Sal Soghoian on MacBreak Episode 3 "Enter the Automatrix"
外部連結
編輯- History of Unix pipe notation (頁面存檔備份,存於互聯網檔案館)
- Doug McIlroy’s original 1964 memo (頁面存檔備份,存於互聯網檔案館), proposing the concept of a pipe for the first time
- 單一UNIX®規範第7期,由國際開放標準組織發佈 : create an interprocess channel – 系統介面(System Interfaces)參考,
- Pipes: A Brief Introduction (頁面存檔備份,存於互聯網檔案館) by The Linux Information Project (LINFO)
- Unix Pipes – powerful and elegant programming paradigm (Softpanorama) (頁面存檔備份,存於互聯網檔案館)
- Ad Hoc Data Analysis From The Unix Command Line at Wikibooks (頁面存檔備份,存於互聯網檔案館) – Shows how to use pipelines composed of simple filters to do complex data analysis.
- Use And Abuse Of Pipes With Audio Data (頁面存檔備份,存於互聯網檔案館) – Gives an introduction to using and abusing pipes with netcat, nettee and fifos to play audio across a network.
- stackoverflow.com (頁面存檔備份,存於互聯網檔案館) – A Q&A about bash pipeline handling.