匿名管道:
概述:
用於Unix系列系統。單向數據通道,寫端寫的數據在被讀端讀取之前會被操作系統緩存。雙向管道需要通過創建兩個單向管道實現
之所以是匿名的。是因為匿名管道不存在於文件系統中,隨着使用它的進程結束而結束,沒有名稱。沒有特別指明的話,管道指匿名管道。
管道為多個文件創建了臨時的直接連接,這使得整合起來的管道整體性能比各個程序分別運行要高。這種直接連接使得程序可以同時運行,並且允許數據直接在它們之間連續的傳輸而不必將數據傳到臨時文件中或是顯示器上然后等待前一個程序執行完后一個才可以執行。如果寫入程序寫的快於讀取程序,寫入程序就會被阻塞並等待數據被讀取;相反的,讀取程序就會被阻塞等待數據被寫入(如果設置為阻塞讀寫的話)。
文件描述符:當打開文件之后,系統會為其維護一個描述文件的實體,相應的,這個實體會有一個整數作為其描述符,通過這個整數就可以訪問這個文件描述實體。所以在通過文件描述符使用文件的功能中,可以通過改變文件描述符實際指向的內容來實現輸入輸出流的改變。使用fopen()返回的文件結構體struct FILE(即struct _IO_FILE)中的_fileno字段表示文件描述符。文件描述符0/1/2分別為標准輸入輸出錯誤流,所以新打開的文件會從3開始使用並隨着打開的文件增長
在程序中使用管道
Unix系列系統通過pipe()函數創建新的管道。包含在頭文件unistd.h中。原型:int pipe(int filedes[2]);
返回值:成功返回0,失敗返回-1
參數:一個2個元素的文件描述符數組,成功創建的話,函數將在其中分別放置讀端(filedes[0])和寫端(filedes[1])
read()向寫端寫,read()向讀端讀。參數為文件描述符、存放位置、讀/寫大小。默認情況下讀取是阻塞的,只要有寫端是打開的,就會一致阻塞地等待需要的數據
從管道中讀取:
讀取時 管道中 字節數(p) |
至少有一個進程有打開的寫端 | 沒有進程有打開的寫端 | ||
阻塞讀 | 非阻塞讀 | |||
至少一個寫端進程在sleep | 沒有寫端進程在sleep | |||
p=0 | 如果管道不為空就從中取n字節數據然后返回n,否則等待直到有數據 | 阻塞等待直到有數據,然后獲取數據並返回其大小 | 返回-EAGAIN | 返回0 |
0<p<n | 獲取p字節然后返回p | |||
p>=n | 獲取n個字節返回n,在管道中剩下(p-n)個字節 |
通常情況下,一個進程創建了管道之后會fork()一個子進程,並分別的在父子進程中進行讀寫。但是這樣父子進程就會都有讀端和寫端,都可以進行讀和寫。在某些Unix系統中,管道實現為全雙工模式。但是POSIX標准規定只能是單工模式,一個進程只能使用一個文件描述符。Linux遵循了POSIX規范,但是沒有強制要求進程一定要關閉不用的一端,而是將這項工作留給了開發者
原子寫:
遵循POSIX協議的系統,單次寫只要寫入的字節數沒有超過PIPE_BUF的限制,寫操作就是是原子的。默認情況下,如果管道中沒有足夠的空間保存寫入的數據(這次寫入的<=PIPE_BUF,但總和>PIPE_BUF),寫操作就會被阻塞直到有足夠的空間。此外,如果單次寫入的字節數超過了PIPE_BUF就不能保證寫是原子的
有兩種方式獲取PIPE_BUF的大小。POSIX要求每個PIPE_BUF至少需要512字節,Linux中是4096字節
- 包含頭文件<limits.h>使用PIPE_BUF,但是如果頭文件是過時的,就只能用下面的方法獲取准確的值
- 調用fpathconf()獲取一個打開的文件描述符的屬性值。long fpathconf(int filedes, int name);返回指定的文件描述符的指定配置選項的值
管道的實際容量可能會比PIPE_BUF大,但是沒有系統參數指明管道的總容量,可以用程序檢測
關於阻塞I/O和管道:
如果write()向一個沒有任何讀進程連接的管道寫數據,SIGPIPE信號量會被發送到寫進程,默認的信號處理函數會直接終止進程。如果實現了自己的處理函數,在處理完SIGPIPE信號量之后,write()會返回-1,然后errno被設置為EPIPE
在有其他進程向管道寫的時候,如果唯一的讀進程關閉了讀端,所有的寫進程都會執行上一條規則
只要有寫端沒有關閉,讀端就會一直阻塞地等待
向滿的管道(現有數據加上需要寫入的數據量)寫數據會阻塞寫進程直到有足夠的可用空間
和讀文件不同的是,從管道讀數據之后數據就不再存在在管道中。所以即便有多個讀進程從同一個管道讀也不會有任何兩個進程讀到相同的數據
只要管道中的字節數不超過PIPE_BUF,寫就是原子的
進程不能對管道執行seek()(復位讀寫文件的偏移位置)
popen()/pclose():
包含在頭文件stdio.h中,封裝好了一部分創建管道的操作
FILE *popen(const char *command, const char *type);會創建一個管道,然后fork一個子進程,子進程為command指定的程序。type可以是"w"或"r",如果是"r",該函數會返回一個管道的讀端,該管道的寫端會連到command對應的子進程標准輸出流。如果是"w",會返回一個管道的寫端,該管道的讀端會連到command對應子進程的標准輸入流。
int pclose(FILE *stream);用popen()打開的文件指針只能用此函數關閉
命令行層面的管道:
基礎:
標准流重定向(stdin/stdout/stderr)
- Stdin(0):在執行程序的時候,可以在命令后面使用"<"操作符來指定標准輸入文件。重定向或者管道的數據是匿名的,接收程序無法得知數據來源
- Stdout(1):在執行程序的命令后面使用">"操作符來指定標准輸出文件。">>"命令在已有的文件后面增量的添加內容,只使用">"會覆蓋已存在文件的內容。值得注意的是,當重定向或使用管道的時候,實際存放的數據總是一致的,但是在輸出到屏幕上的時候可能會有些許的不同。因為顯示屏的寬是已知的,而重定向的位置是未知的。所以重定向的時候最安全的方式是一個元素一行,而在屏幕上可能是所有的字符串在一行
- Stderr(2):默認情況下重定向的流是標准I/O流,想要重定向ERROR輸出流,需要指定。這三個輸出流有三個數字與之對應,在">"操作符之前加上數字2就可以重定向stderr流。如果想要將stderr和stdout重定向到同一個文件,可以先把stderr重定向到stdout,然后將stdout重定向到文件。2>&1,通過在數字前面加"&"指明這是個流而不是文件名
Error流:默認情況下,管道中的所有程序的error流會合並在一起並發送到console中。但是許多的shell都有其他的語法來控制這個流程,比如csh shell使用"|&"代替"|"來指定標准錯誤流需要和標准輸出流合並然后重定向到下一個程序
用程序模擬shell命令">":
命令:ls > list,ls命令是將當前文件夾下的文件列表輸出到顯示器上,此命令將輸出重定向到list文件
shell執行的指令:
- Fork()一個子進程
- 在子進程中close()文件描述符1(標准輸出的文件描述符)
- 在子進程中open()文件list(和O_CREAT標志一起)
- 子進程exec()命令ls
以上過程能夠實現重定向的原因:Fork的子進程在關閉標准輸出的時候,其對應的文件描述符1就被釋放,之后使用open()命令打開文件list,list就會使用可用的文件描述符1。子進程再執行ls命令的時候,ls仍會去尋找文件描述符1,因為默認情況下它就代表的是標准輸出,但是實際上指向的是文件list,所以就會輸出到list。與此同時,shell主進程的標准輸入輸出仍保持未改變
用程序模擬shell命令"|":
需要采取某種方式將管道一端連接到前一個程序的標准輸出,管道的另一端連接到后一個的標准輸入
系統調用dup()和系統調用dup2():dup2可以代替dup
dup():int dup(int oldfd)
Dup()復制文件描述符指向的內容,在成功調用之后,新舊文件描述符可以通用。它們指向相同打開的文件描述實體,所以即便文件發生了改變,新舊文件描述符都會引用新的文件
但是dup的問題是,它返回的是最小的可用文件描述符。那么一個進程如果關閉了標准輸出,然后dup了管道的寫端,標准輸出的文件描述符就會被使用作為寫端的拷貝,所以在進程想要執行標准輸出的時候,就會輸出到管道的寫端
實現的方式:父進程P創建子進程C,需要實現父進程的標准輸出向子進程的標准輸入寫數據,需要創建管道,寫端連到父進程的標准輸出,讀端連到子進程的標准輸入。
父進程:關閉stdout和fd[0],此時此進程中最小的文件描述符就是stdout,dup(fd[1]),那么此時標准輸出就和fd[1]一致,向標准輸出寫就相當於向fd[0]寫
子進程:關閉stdin和fd[1],此時此進程中最小的文件描述符就是stdin,dup(fd[0]),那么此時標准輸入就和fd[0]一致,從標准輸入讀就相當於向fd[1]讀
問題:
父進程不會等待子進程,因為父進程用execlp()取代了它自己。避免這種情況的方式是創建兩個子進程分別用於讀/寫
將標准輸入輸出連接到管道是分為兩步的,這兩步之間有間隔,所以可能在進程關閉了標准輸入輸出后在將管道連到其上之前有一個信號量到來,其處理函數關閉了一個文件描述符,那么之后dup()返回的文件描述符就會是剛剛關閉的,而不是標准輸入輸出
程序:
switch(fork()){
case -1:{
printf("Error:cannot fork a process.\n");
return -1;
}
case 0:{
close(fd[0]);
dup2(fd[1],fileno(stdout));
close(fd[1]);
close(fd[1]);
return 1;
}
default:{
close(fd[1]);
dup2(fd[0],fileno(stdin));
close(fd[0]);
fgets(message,27,stdin);
return 2;
}
}
int dup2(int oldfd, int newfd);
因為dup()存在的問題,dup2()被創建。
將oldfd的內容拷貝到newfd中,如果newfd之前是打開的,會先關閉再拷貝。整個操作是原子的
程序:
switch(fork()){
case -1:{
perror("Failed to fork:");
exit(3);
}
case 0:{/* parent process */
close(fd[0]);/* close read end */
dup2(fd[1],fileno(stdout));/* set stdout as write end */
close(fd[1]);/* close useless copy of write end */
if(execlp(argv[1],argv[1],NULL) == -1)
perror("Failed to execute parameter1:\n");
exit(4);
}
default:{/* child process */
close(fd[1]);/* close write end */
dup2(fd[0],fileno(stdin));/* set stdin as read end */
close(fd[0]);/* close useless copy of read end */
if(execlp(argv[2],argv[2],NULL) == -1)
perror("Failed to execute parameter2:\n");
exit(5);
}
}
命名管道:
匿名管道的缺點:
- 管道只能在有共同祖先的進程之間使用,比如父子進程
- 管道會隨着使用管道的進程結束而結束,所以每次使用的時候都要創建
二者的異同:
- 在打開、關閉、讀、寫方面,命名管道和匿名管道的操作是相同的
- 命名管道的存在形式是文件系統中的目錄入口,所以相關的有訪問權限和所有者
- 命名管道可以在不相關的進程之間使用
- 命名管道可以在shell層面或是程序層面進程刪除和創建
命令層中的命名管道:
Mknod:此命令用於創建設備特殊文件,所以也可以用於創建管道
需要注意的是,創建特殊文件要在Linux文件系統中才可以,不能在微軟的文件系統下。
使用方式:mknod filename p
filename是想要創建的命名管道名,p告知mknod命令創建的是一個命名管道
之后其他的程序執行的時候就可以通過訪問這個文件來使用管道了
Mkfifo
Mkfifo [option]… NAME,創建名稱為NAME的命名管道,如果有多個NAME,就會分別創建對應的命名管道
在程序中使用命名管道
可以使用系統調用mknod()或是庫函數mkfifo()。但是在mknod()的Linux手冊中說明此命令不能用於創建目錄,如果想要創建目錄應該使用mkdir(2)創建目錄,用mkfifo(3)創建管道。所以我們將使用mdfifo()來創建管道,相比於mknod(),mkfifo()還有一個優點是不用超級用戶權限
使用時需要頭文件sys/types.h和sys/stat/h,int mkfifo(const char *pathname, mode_t mode);。按照慣例,命名管道名稱使用大寫字母
Public FIFO和private FIFO:沒有特定的函數使一個FIFO成為public的,public的含義是創建的管道名被廣而告之,client程序都可以訪問它。而private是指創建的管道只會被其創建進程以及特定的被告知管道名的進程可以使用