1、概述
管道是最初的UNIX IPC形式,由於管道沒有名字,所以只能在用於有親緣關係的進程(所謂的親緣關係是指進程間有共同的祖先)。FIFO則被稱為具名管道。
先說明fork,exec,_exit函數對管道及fifo函數的影響:
fork:子進程取得父進程的管道以及fifo描述字的拷貝。
exec:所有代開的描述字依舊開啟,除非已經設定描述字的FD_CLOEXEC位。
_exit:關閉所有開啟的描述字,最後一個描述字關閉時刪除管道與FIFO中的資料。
管道和FIFO涉及的函數有:pipe, mkfifo, fcntl, open, read, write, close, unlink.
2、管道函數及說明:int pipe(int fd[2]);
(1)、這個函數提供一個單向管道。fd是輸出參數,fd[0]用來讀出,fd[1]用來寫入(這裡的讀出和寫入是的賓語是管道)。函數成功返回0,錯誤返回-1.
注意:a、linux的system call(相當於系統API)函數的傳回值是成功返回0,返回其他的就表示出錯,而windows的api函數則剛好相反,錯誤返回0,成功返回其他。b、這裡不寫函數的標頭檔,要知道它用到那些標頭檔,在類unix下用man來查看即可,如:man 2 pipe,2表示pipe是一個系統調用,如果是c函數庫裡面的函數則按3。
(2)、管道的典型用途如下(用來作為父子進程訊息的共用):
a、建立管道1(int fd1[2]) 和管道2(int fd2[2]);
b、fork建立子進程
c、父進程關閉管道1的讀出端和2的寫入端(fd1[0],fd2[1])
d、子進程關閉管道2的讀出端和1的寫入端(fd1[1],fd2[0])
代碼如下:
- int pipe1[2];
- int pipe2[2];
- pid_t childPid;
- pipe(pipe1);
- pipe(pipe2);
- if( ( childPid = fork()) == 0 ) /* child process */
- {
- close(pipe1[1]);
- close(pipe2[0]);
- ... /* your code */
- }
- /* parent process */
- close(pipe1[0]);
- close(pipe2[1]);
- ...
- waitpid(childPid, NULL, 0);
- ...
子進程在這裡用來專門做一件事情,像伺服器。而父進程就相當於用戶端。如是,子進程在完成任務之後就exit(0),那麼但子進程終止時候變成殭屍進程(zombie),核心給父進程發送一個訊號SIGCHILD,但是父進程沒有撲抓,預設行為是忽略;父進程後面調用到waitpid的時候就可以等待childPid進程的終止,取得終止狀態。如果沒有waitpid而直接返回,則父進程返回後那個殭屍子進程將成為託孤給init進程的孤兒進程。核心將為init進程發送SIGCHILD訊號。
這樣子,就變成: 客戶-->管道fd1-->伺服器;伺服器-->fd2-->客戶
3、fifo 有名管道(fifo的意思是:先進先出)
也是一個半雙工的單向資料流,但是有一個路徑名與之相連。函數如下:
int mkfifo(const char *pathname, mode_t mode);
說明:
a、mkfifo函數已經隱含制訂了O_CREAT | O_EXCL也就是說如果pathname那個管道已經存在,則返回一個錯誤值EEXIST,如果想開啟一個已經存在的管道,用open()即可。
b、由於fifo是先進先出的,所以write函數總是往管道末尾寫入資料,而read函數總是從管道的開頭返回資料,如果用lseek定位檔案指標,會返回ESPIPE錯誤。
應用:
- /* server.c */
- ...
- if( ( mkfifo(FIFO1, FILE_MODE ) < 0 ) && (errno != EEXIST ) )
- /* error handle and exit(1) */
- if( ( mkfifo(FIFO2, FILE_MODE ) < 0 ) && (errno != EEXIST ) )
- {
- unlink(FIFO1);
- /* error handle and exit(1) */
- }
- readfd = open(FIFO1, O_RDONLY, 0);
- writefd = open(FIFO2, O_WRONLY, 0);
- /* your code */
- exit(0);
這是伺服器處理序的FIFO,其中FIFO1,FIFO2可以定義一個宏來指定,比如,#define FIFO1 "/temp/fifo.1"(路徑名隨意,不過由於是臨時的檔案,一般放在/temp目錄下,許可權比較低嘛)
開啟檔案之後就可以用read,write函數往管道讀寫資訊了,FIFO1,FIFO2就是像檔案描述符。
再看client的代碼:
- /* client.h */
- writefd = open(FIFO1, O_WRONLY, 0);
- readfd = open(FIFO2, O_RDONLY, 0);
- /* your code */
- close(writefd);
- close(readfd);
- unlink(FIFO1);
- unlink(FIFO2);
- exit(0);
說明:a、通常管道是由伺服器建立,由用戶端銷毀(unlink函數),close只是關閉管道而已。b、注意clinet.h上的那兩個open和server.h上的那兩個open的順序,否則會引起死結(見後面)
4、阻塞態下的規則(阻塞態是預設的狀態):
(1)、管道和FIFOopen函數的返回結果:
writefd = open(FIFO1, O_WRONLY, 0); 這個是用唯寫方式開啟管道,如果FIFO1此時已經有別的進程以唯讀方式開啟(就是說在這條代碼運行之前,已經有代碼open(FIFO1, O_RDONLY, 0)運行),則此函數返回成功,否則,將會阻塞到有別的進程以唯讀方式開啟FIFO1為止。反過來也一樣。
(2)、read函數作用於管道和FIFO 的返回結果:
如果FIFO1為空白(就是說裡面沒有資料):該管道以唯讀方式開啟,則返回0;FIFO1以唯寫方式開啟,則阻塞到FIFO1有資料或者是FIFO1不再以寫方式開啟為止。
(3)、write函數作用於管道和FIFO的返回結果:
如果FIFO1沒有以唯讀方式開啟,則給進程產生SIGPIPE訊號(預設行為是終止該進程);如果已經以唯讀方式開啟,則見下。
(4)、write操作的原子性:
如果寫入資料的位元組數小於等於PIPE_BUF,則該函數保證其原子性;否則不能保證。
現在來看一下上面那個程式,伺服器代碼先運行,因此:當起運行到readfd = open(FIFO1, O_RDONLY, 0);的時候,還沒有任何進程以O_WRONLY方式開啟FIFO1,進程阻塞在這裡;
然後用戶端代碼開始運行,但運行到這個地方的時候:writefd = open(FIFO1, O_WRONLY, 0);伺服器阻塞的地方開始釋放,而在用戶端,因為FIFO1在伺服器已經是以O_RDONLY開啟了,所以繼續運行。
如果用戶端的這兩個open交換一個順序,那麼readfd = open(FIFO2, O_RDONLY, 0);先運行,則由於FIFO2還沒有以O_WRONLY方式開啟,所以用戶端也阻塞,客戶和伺服器都阻塞,大家都在等對方的資源,這種情況我們稱之為死結(deadlock)
5、非阻塞態下的規則:
(1)、非阻塞態的設定。
a、調用open時可以指定 readfd = open(FIFO1, O_RDONLY | O_NONBLOCK );
b、如果readfd已經開啟,則可以用fcntl來設定O_NONBLOCK標誌。
(2)、對open操作的影響:
如果當前操作是wrfd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0); 那麼如果是FIFO1在此以前沒有用O_RDONLY方式開啟過,返回ENOXIO錯誤,否則都成功返回。
(3)、對空管道或空FIFO read操作的影響:read(readfd, ...); 如果該readfd對應的FIFO用O_WRONLY開啟返回0,否則返回EAGAIN。
(4)、對write操作的影響同阻塞態下。
6、技巧:單個伺服器多個客戶時候,伺服器中有連續的兩行代碼:
readfifo = open(SERV_FIFO, O_RDONLY, 0);
dummy = open(SERV_FIFO, O_WRONLY, 0);
作用:a、伺服器運行到readfifo,阻塞,直到有客戶用O_WRONLY開啟SERV_FIFO為止。然後因為SERV_FIFO已經以O_RDONLY開啟,因此這個dummy成功返回。
b、到客戶完成任務,關閉SERV_FIFO時,SERV_FIFO變成空,因此,伺服器在運行到read語句的時候,阻塞知道下一個客戶以O_WRONLY方式開啟SERV_FIFO為止(也就是直到有下一個使用者請求為止)
7、Dos攻擊(拒絕服務型攻擊)
有見上面的阻塞,有個攻擊方法就是說,發送一條請求,但是從來不開啟自己的FiFO,讓伺服器死等。伺服器一直阻塞,沒辦法用了。
當然,現在的伺服器都是並發性伺服器,最多隻能阻塞他的一個子進程;但是即使在這種情況下一樣可以進行Dos攻擊,方法是發送大量的請求,以至於伺服器開闢的進程達到極限,而且每個子進程都用上面的辦法來阻塞。(此時不但伺服器處理序被阻塞了,整個伺服器系統都被阻塞了)。
8、位元組流
管道和FIFO是以位元組流的方式來傳遞資訊的,類似於TCP。那麼怎樣區分資訊的界限呢?有下面三種常用的技巧:
a、帶內特殊終止符:分隔標誌(類似與SLIP的標記)
b、顯示長度在資訊頭:像TCP協議棧實現。
c、每次串連一個記錄:像HTTP1.0協議實現。