這是《UNIX網絡編程 卷2:進程間通訊》(W.Richard Stevens)的讀書筆記以及批註。html
pipe
函數返回兩個
文件描述符,前者用於讀管道,後者用於寫管道
這意味着,可使用write
、read
、open
來像操做文件同樣讀寫管道。linux
父進程建立一個管道,而後
fork
出子進程,接着父進程關閉這個管道的讀出端,子進程關閉同一管道寫出端,造成了從父進程流向子進程的單向管道。
fork
出的子進程顯然和父進程具備親緣關係,因此子進程會持有和父進程徹底相同的fd
文件描述符副本(做者在Page.25倒數第三段中寫到:「……有內核維護的打開文件的文件描述符……只在單個進程內有意義……假如說文件描述符4……對於可能在另外一個與本進程無親緣關係的進程中打開在文件描述符4上的文件而言根本沒有意義」)。shell
若是父子進程均不關閉管道任何一端,此時若是從子進程發送消息,同時從父子進程都開始讀取,以下所示,會怎樣呢?:編程
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #define MAXLEN 15 int main(int argc,char **argv){ int fd[2]; pid_t cpid; //child process pid ssize_t n; //indeed read-in size pipe(fd); //get pipe char buff[MAXLEN]; if((cpid=fork())==0){ //child process write(fd[1],"hello",6); printf("\nIn Child Process:Write ends\n"); while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Child Process:message is %s\n",buff); } printf("\nIn Child Process:Read ends\n"); exit(0); }else{ //parent process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Father Process:message is %s\n",buff); } printf("\nIn Father Process:Read ends\n"); waitpid(cpid,NULL,0); exit(0); } }
#include<unistd.h>
:pipe
,read
,write
#include<sys/types.h>
、#include<sys/wait.h>
:waitpid
fd[0]
寫出端編譯運行上面的程序,結果有如:segmentfault
In Child Process:Write ends In Child Process:message is hello
或服務器
In Child Process:Write ends In Father Process:message is hello
語句順序可能相反,可是不出意外的,它們都會在此時阻塞。也就是說,子進程經過write
寫入fd[1]
的數據可能被子進程或父進程的read
讀入,可是任何一個進程讀入後,兩個進程同時在下一輪或本輪的read
進入了阻塞狀態。
對於read
函數的說明是:網絡
ssize_t read(int fd,void * buf ,size_t count)
read()
會把參數fd
所指的文件傳送count
個字節到buf
指針所指的內存中。若參數count
爲0
,則read()
不會有做用並返回0
。返回值爲實際讀取到的字節數,若是返回0
,表示已到達 文件尾或是 無可讀取的數據,此外,文件 讀寫位置會隨讀取到的字節 移動。
讀取管道的信息時,文件讀寫指針也會發生移動,因此任何一個進程讀完其中的數據後,沒有向緩衝區填入新的數據,那麼任何試圖從空管道中讀取數據的進程由於沒有數據可讀(file pointer indicates this scenario.),都會被阻塞在這裏。併發
值得注意的是,管道與通常文件不一樣的是,文件老是要有EOF
的,這標誌着讀取過程的結束;而管道由於寫入信息具備偶發性,因此,當管道沒有數據時是read
不能馬上返回的,由於發送方的數據隨時可能到達,那麼read
如何知道讀取結束了呢?顯然,只有發送進程明確關閉了寫入端close(fd[1])
,此時讀取方纔能得到一個EOF
。
而在這裏,由於父子進程是有親緣關係的進程,因此兩者持有的fd
符其實是同一個內存緩衝區域,因此管道的讀入讀出端口實際上被兩個進程引用着。這意味着,就算子進程發送數據出去後馬上關閉寫出端口,父進程仍持有寫入端的描述符,從而兩個進程的read
都沒法關閉,由於內核會認爲父進程隨時可能從該寫入端寫入數據。socket
爲了印證這一點,能夠修改代碼爲:函數
//child process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Child Process:message is %s\n",buff); close(fd[1]); } ... //parent process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Father Process:message is %s\n",buff); close(fd[1]); }
不管哪個進程從管道中讀入了數據,它都會馬上關閉本身的寫入端。假如子進程發送的消息被子進程讀到了,從而關閉了子進程的寫入端。那麼父進程在執行read
時就會被阻塞,由於此時管道的惟一寫入端就在父進程手中!對於父進程,這陷入一個悖論:「我必須等待,由於我不知道何時會給本身發送消息」。
所以,最佳實踐是,讓發送方一開始就關閉讀入端口,讓接收方一開始就開始關閉寫出端口。從而保證單向管道只有惟一的讀入寫出端口。
另外,write
操做對管道和FIFO的操做,若是寫入數據量小於PIPE_BUF
,那麼就保證是原子的,不然不保證是原子的,這意味着若是發送方發送大量數據(遠遠多於PIPE_BUF
)後,發送/接收雙方同時都開始收,那麼可能會形成數據紊亂,由於哪一個進程在何時開始讀取是不定的:
#define MAXLEN 1048576 //修改成大量數據 //child process write(fd[1],str,MAXLEN); printf("\nIn Child Process:Write ends\n"); while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Child Process:message length=%d\n",(int)strlen(buff)); } //parent process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Father Process:message length=%d\n",(int)strlen(buff)); }
執行結果可能以下:
In Child Process:Write ends In Child Process:message length=65535 In Father Process:message length=983039 ...(blocking)
子進程接收到65536
字節、父進程接收到983040
字節數據,這是由於strlen
函數統計時沒有吧'/0'
算在其中,它們加和剛好是1048576
。
做者在Page.36下方寫有:「對於管道的read,只要該管道存在一些數據就會立刻返回,沒必要等待到達所請求的字節數」。
更多參考:
對於客戶端-服務器模型,使用單向的管道是很難完成數據的交互,由於涉及到收發雙方的數據同步問題,極端例子是發送方發出的數據被本身收到了,這樣致使管道內無數據可讀,此後兩者陷入阻塞。
解決這個問題的一個方法是使用兩根管道,方向相反,這樣,服務器進程一開始在管道A讀入端陷入阻塞,等待客戶端的數據到來;客戶端從管道A發送數據後,開始在管道B的讀入端等待服務器,服務器處理完數據後,將反饋信息順着管道B的發出端口發出,完成一次信息交換。
因此做者的代碼中有:
client(pipeB_readin_fd,pipeA_wirteout_fd); server(pipeA_readin_fd,pipeB_writeout_fd);
FIFO的名字只有經過調用
unlink
才能從文件系統中刪除。
FIFO的建立流程是:
mkfifo
建立FIFO,若返回-1,到第2步,不然到第3步;unlink
函數的定義是:
#include <unistd.h> int unlink(const char *pathname);Delete a name and possibly the file it refers to.
unlink()
deletes a name from the filesystem.
If that name was the last link to a file and no processes have the file open, the file is deleted and the space it was using is made available for reuse.
If the name was the last link to a file but any processes still have the file open, the file will remain in existence until the last file descriptor referring to it is closed.
If the name referred to a symbolic link, the link is removed.
▲ If the name referred to a socket, FIFO, or device, the name for it is removed but processes which have the object open may continue to use it.
在這篇筆記中使用父子進程用FIFO模擬了客戶端-服務器模型用以交互信息。
特別注意open
一個FIFO時,若是是以只讀方式打開,那麼若是這個FIFO尚未以只寫方式打開過得話就要陷入阻塞。若是發送方發送完消息不close
掉FIFO,那麼讀端將在read
FIFO時陷入阻塞,假如寫端此時也在等待讀端反饋,那麼頗有可能就會陷入死鎖。
關於圖4-21,第一列「當前操做」表示如今準備要進行的操做,第二列「管道或FIFO現有打開操做」表示在某個進程中已經完成的操做。例如:
- 當前操做:
open
FIFO只讀- 管道或FIFO現有打開操做:FIFO不是打開來寫
- 返回:阻塞:阻塞到FIFO打開來寫爲止
這表示,某FIFO已經在某個進程中打開,且沒有設置
O_WRONLY
,即沒有寫端可以對該FIFO進行寫操做,那麼若是如今對該FIFO執行open
操做且使用O_RDONLY
表示讀,則會陷入阻塞,由於沒有寫端,天然讀不出任何東西。
選項O_NONBLOCK
表示非阻塞,加上這個選項後,表示open
調用是非阻塞的,若是沒有這個選項,則表示open
調用是阻塞的。
在這種一對多的服務器-客戶端中,代碼實現的是迭代服務器,即一次只服務於一個客戶,其餘客戶須要等待。一個誤區在於,可能認爲一個客戶端寫入/tmp/fifo.serv
的FIFO後,哪怕FIFO中還有空域區域,其餘客戶端就在write
函數中阻塞,不會寫入東西,一直等待服務器處理完該客戶端消息後纔會開始寫入。
事實上注意到第16行讀取的函數是Readline
,也就是說,多個客戶端能夠併發地向同一個FIFO中寫入數據,只有當FIFO滿了之後其餘客戶端纔會在write中阻塞,而服務器徹底是根據一行一行的讀取內容,由於一行表明了一個客戶端請求。由於在例子中的假設中,客戶端請求老是小於PIPE_BUF
,因此寫入操做老是原子性的。
另外,在這裏雖然服務器對服務器FIFO只進行讀操做(寫操做由客戶端進行),可是仍然持有一個對該FIFO的寫端(dummyfd
),從而,就算沒有任何一個客戶端存在,由於存在該FIFO的寫端,服務器得以在read
中阻塞,等待着消息的到達,而不是遇到EOF
而關閉FIFO。
對客戶端FIFO的unlink
由客戶端完成,服務器端只要close
客戶端FIFO,就可以使得客戶端在read
中讀到EOF
而結束。
能夠在shell中使用命令mkfifo
來建立FIFO
在本頁有一個命令行例子:
echo "$Pid message" > /tmp/fifo.serv (間隔至關長的時間後) cat < /tmp/fifo.$Pid ....(服務器應答消息)
兩個命令之間能夠間隔至關長的時間,可是仍然可以得到服務器消息。錯誤的理解是:服務器讀取/tmp/fifo.serv
內的請求後做處理,將處理結果寫入/tmp/fifo.$Pid
FIFO中,而後服務器進程就關閉了。從而客戶端進程能夠在任什麼時候候從/tmp/fifo.$Pid
讀取。
實際是,服務器讀取客戶端請求後處理,可是要將處理結果寫入客戶端的/tmp/fifo.$Pid
FIFO以前必需要進行open
,然而在此時客戶端還沒有對FIFO進行只讀打開,沒有讀端,因此服務器在open
中阻塞,所以,在cat < /tmp/fifo.$Pid
命令以前的時間中,服務器進程始終在阻塞,沒有結束。直到客戶端打開FIFO時,服務器才寫入,而後客戶端才能讀取。
若是管道、FIFO所有被close
(沒有讀端也沒有寫端,即文中的「最終close
」),那麼管道、FIFO的數據都被丟棄。
系統對 管道和FIFO 的惟一限制 是:
OPEN_MAX
:一個進程在任意時刻打開的最大描述符數PIPE_BUF
:可原子的寫入任何一個 管道和FIFO 的最大數據量。
OPEN_MAX
能夠經過sysconf
函數來查詢,查詢的宏是_SC_OPEN_MAX
。在這個網站能夠查看到該函數的詳細狀況。PIPE_BUF
能夠經過pathconf
函數來查詢,查詢的宏是_PC_PIPE_BUF
。在這個網站能夠查看到該函數的詳細狀況。pathconf
函數pathconf
函數的接口定義是:
#include <unistd.h> long fpathconf(int fd, int name); long pathconf(const char *path, int name);
其做用是得到文件名path
/文件描述符fd
的名爲name
的配置的值。
這些值在unistd.h
頭文件中也定義了相關的宏能夠獲取,可是這些宏只是規定了這些值的最小值,是靜態不可變的。若是應用想要獲取實時的值(這些值可能發生變更),那麼就要調用這兩個函數。
其中name
能夠指定爲已經預約好的宏名,例如:
name = _PC_PIPE_BUF
: 能夠 原子的寫到FIFO管道中的最大字節數。對於fpathconf
,fd
參數要是管道或FIFO的描述符;而對於pathconf
,path
是一個FIFO路徑或者是目錄名,若是是目錄名,那麼返回的值就是建立在該目錄下的FIFOs的最大字節數。
能夠看出,Posix認爲PIPE_BUF
是一個pathname variable,它的值可能會隨着指定的路徑名而發生變化。
這裏值得注意的是pathconf
函數的返回值,幫助手冊寫的是:
pathconf
函數的返回值是以下狀況中的一種:
- 若是出現錯誤,那麼返回-1,而且使用
errno
全局變量來指示錯誤緣由。- 若是
name
是關於限制最大/最小這方面性質的配置名,且其限制值是不明確的(indeterminate),那麼返回-1,而且errno
不變(爲了將這種狀況和上一種區分開來,請先設置errno
變量爲0,調用完函數後再檢查當-1返回時errno
是不是非零值便可)。- 若是
name
參數是受支持的配置選項名,那麼返回一個正值,不然返回-1。- 不然,其選項或限制的值將被返回。這個(動態返回的)值比與之相關的定義在頭文件
unistd.h
或limits.h
中的用於描述該應用的(靜態)值更寬泛(not be more restrictive)
注意到前兩點,就能夠明白爲何做者在wrapunix.c
中將包裹pathconf
的包裹函數定義爲
//in wrapunix.c by W.Richard Stevens long Pathconf(const char *pathname, int name) { long val; errno = 0; /* in case pathconf() does not change this */ if ( (val = pathconf(pathname, name)) == -1) { if (errno != 0) err_sys("pathconf error"); else err_sys("pathconf: %d not defined", name); } return(val); }
同時還注意到了errno
這個變量。errno
是記錄系統的最後一次錯誤代碼。代碼是一個int
型的值,在errno.h
頭文件中定義。在博客《Linux errno詳解》一文中,做者寫道:
Linux中 系統調用的錯誤都存儲於errno
中,errno
由操做 系統維護,存儲 就近發生的錯誤,即下一次的錯誤碼會 覆蓋掉上一次的錯誤。只有當 系統調用 或者 調用lib 函數時出錯, 纔會置位errno。
可使用定義在string.h
中的strerror
函數來根據errno
得到錯誤說明字符串,或是經過定義在stdio.h
頭文件中的perror
函數來把系統調用錯誤信息字符串發送到標準輸出。這篇幫助文檔給出了可能的錯誤代碼。
sysconf
函數sysconf
函數接口是
#include <unistd.h> long sysconf(int name);
是用來在運行時得到配置信息。在這篇幫助手冊中詳細說明了sysconf
函數。若是name
是 _SC_OPEN_MAX
,那麼它表示一個進程最多能打開的文件數。其返回狀況與上面的pathconf
是徹底一致的。
因此做者定義了相似了包裹函數
long Sysconf(int name) { long val; errno = 0; /* in case sysconf() does not change this */ if ( (val = sysconf(name)) == -1) { if (errno != 0) err_sys("sysconf error"); else err_sys("sysconf: %d not defined", name); } return(val); }
使用以下語句便可完成查詢:
printf("PIPE_BUF=%ld, OPEN_MAX=%ld\n", Pathconf(argv[1],_PC_PIPE_BUF),Sysconf(_SC_OPEN_MAX));
mq_open
、mq_close
、mq_unlink
函數unlink
和close
的區別:筆記:磁盤分區、文件系統、連接。若是調用mq_unlink
時,那麼指定的隊列就會被從系統刪除,可是注意,若是此時其連接計數(或引用計數)不爲0,說明仍有進程在使用它,它就是「懸空的」,這就意味着,它已經被系統除名,已經不能經過它的名字找到它,可是在正在使用它的進程中仍是可以正常使用(由於持有它的描述符,能夠理解爲指針)。當最後一個mq_close
關閉它後,它的引用計數就變成了0,說明它已經徹底不能被找到,這個時候隊列就會被析構,徹底消失。
getopt
函數的解釋參考:使用 getopt() 進行命令行處理。書中實例代碼中使用這個函數來處理命令行輸入的命令字符串並得到相關設置,使用while
是不斷處理命令字符串中的選項,當該命令字符串被解析完,while就會退出(getopt
返回-1)。也就是說,它只能一次處理一個命令字符串。