Linux网络编程 - 在服务器端运用进程间通信之管道(pipe)
一 進程間通信的基本概念
1.1 對進程間通信的基本理解
進程間通信(Inter Process Communication,簡稱 IPC)
進程間通信意味著兩個不同進程間可以交換數據,為了實現這一點,操作系統內核需要提供兩個進程可以同時訪問的內存空間,即在內核中開辟一塊緩沖區。整個數據交換過程如下圖所示:
圖1? 進程間通信從上圖 1-1 可以看出,只要有兩個進程可以同時訪問的內存空間,就可以通過此空間交換數據。但我們知道,進程具有完全獨立的內存結構,就連通過 fork 函數創建的子進程也不會與其父進程共享內存空間。因此,進程間通信只能在操作系統內核區開辟這種共享內存緩沖區。
《拓展》關于進程間通信的機制請參見下面博文鏈接
Linux進程之進程間通信
二 Linux 的管道(pipe)
2.1 管道的基本概念
管道(pipe) 也稱為匿名管道,是Linux下最常見的進程間通信方式之一,它是在兩個進程之間實現一個數據流通的通道。
基于管道的進程間通信結構模型如下圖2所示。
圖2? 基于管道的進程間通信模型?????????為了完成進程間通信,需要創建管道。管道并非屬于進程的資源,而是和套接字一樣,屬于操作系統(也就不是 fork 函數的復制對象)。所以,兩個進程通過操作系統內核提供的內存空間進行通信。
2.2 管道的特點
Linux 的管道具有以下特點:
- 管道沒有名字,所以也稱為匿名管道。
- 管道是半雙工的通信方式,數據只能向一個方向流動;需要雙向通信時,需要建立起兩個管道。(缺點1)
- 管道只能用在父子進程或兄弟進程之間(即具有親緣關系的進程)。(缺點2)
- 管道單獨構成一種獨立的文件系統,管道對于管道兩端的進程而言,就是一個文件,但它不是普通文件,它不屬于某種文件系統,而是自立門戶,單獨構成一種文件系統,并且只存在于內存中。
- 數據的讀出和寫入:一個進程向管道中寫入的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,并且每次都是從緩沖區的頭部讀出數據。
- 管道的緩沖區是有限的(管道只存在于內存中,在管道創建時,為緩沖區分配一個頁面大小)。
- 管道中所傳遞的數據是無格式的字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式。例如,多少字節算作一個消息(或命令、記錄等)。
2.3 管道的實現方法
??????? 當一個進程創建一個管道時,Linux 系統內核為使用該管道準備了兩個文件描述符:一個用于管道的輸入(即進程寫操作),也就是在管道中寫入數據;另一個用于管道的輸出(即進程讀操作),也就是從管道中讀出數據,然后對這兩個文件描述符調用正常的系統調用(write、read函數),內核利用這種抽象機制實現了管道這一特殊操作。如下圖 3 所示。
圖3? 管道的結構- ?管道結構的說明
fd0:從管道中讀出數據時使用的文件描述符,即管道出口,用于進程的讀操作(read),稱為讀管道文件描述符。
fd1:向管道中寫入數據時使用的文件描述符,即管道入口,用于進程的寫操作(write),稱為寫管道文件描述符。
??????? 如果一個管道只與一個進程相聯系,可以實現進程自身內部的通信,這個一般用在進程內線程間的通信(自己遇到過)。
??????? 通常情況下,一個創建管道的進程接著就會創建子進程,由于子進程是復制父進程所有資源創建出的進程,因此子進程將從父進程那里繼承到讀寫管道的文件描述符,這樣父子進程間的通信管道就建立起來了。如下圖 4 所示。
圖4? 父進程與子進程之間的管道《父子進程管道半雙工通信說明》
- 父進程的 fd[0] = 子進程的 f[0],即表示這兩個文件描述符都是標識同一個管道的出口端。
- 父進程的 fd[1] = 子進程的 f[1],即表示這兩個文件描述符都是標識同一個管道的入口端。
《父子進程數據傳輸方向》
父進程 —> 子進程的數據傳輸方向:父進程的 fd[1] —> 管道 —> 子進程的 fd[0]
子進程 —> 父進程的數據傳輸方向:子進程的 fd[1] —> 管道 —> 父進程的 fd[0]
??????? 例如,數據從父進程傳輸給子進程時,則父進程關閉讀管道的文件描述符 fd[0],子進程關閉寫管道的文件描述符 fd[1],這樣就建立了從父進程到子進程的通信管道,如下圖 5 所示。
圖5? 從父進程到子進程的管道?2.4 管道的讀寫操作規則
??????? 在建立了一個管道之后即可通過相應的文件 I/O 操作函數(例如 read、write 等)來讀寫管道,以完成數據的傳遞過程。
??????? 需要注意的是由于管道的一端已經關閉,在進行相應的操作時,需要注意以下三個要點:
- 如果從一個寫描述符(fd[1])關閉的管道中讀取數據,當讀完所有的數據后,read 函數返回0,表明已到達文件末尾。嚴格地說,只有當沒有數據繼續寫入后,才可以說到達了完末尾,所以應該分清楚到底是暫時沒有數據寫入,還是已經到達文件末尾,如果是前者,讀進程應該等待。若為多進程寫、單進程讀的情況將更加復雜。
- 如果向一個讀描述符(fd[0])關閉的管道中寫數據,就會產生 SIGPIPE 信號。不管是否忽略這個信號,還是處理它,write 函數都將返回 -1。
- 常數 PIPE_BUF 規定了內核中管道緩沖的大小,所以在寫管道中要注意一點。一次向管道中寫入 PIPE_BUF 或更少的字節數據時,不會和其他進程寫入的內容交錯;反之,當存在多個寫管道的進程時,向其中寫入超過 PIPE_BUF 個字節數據時,將會產生內容交錯現象,即覆蓋了管道中的已有數據。
三 管道的操作
3.1 管道的創建
Linux 內核提供了函數 pipe 用于創建一個管道,對其標準調用格式說明如下:
- pipe() — 創建一個匿名管道。
【編程實例】使用 pipe 函數創建管道。在一個進程中使用管道的示例。
- pipe.c
- 運行結果
$ gcc pipe.c -o pipe
$ ./pipe
write data to pipe: This is a test!
read data from pipe: This is a test!
pipe read_fd: 3, write_fd: 4
《注意》在關閉一個管道時,必須對管道的兩端都執行 close 操作,也就是說要對管道的兩個文件描述符都進行 close 操作。
3.2 通過管道實現進程間通信
????????當父進程調用 pipe 函數時將創建管道,同時獲取對應于管道出入口兩端的文件描述符,此時父進程可以讀寫同一管道,也就是本示例程序中那樣。但父進程的目的通常是與子進程進行數據交換,因此需要將管道入口或出口中的其中一個文件描述符傳遞給子進程。如何傳遞呢?答案就是調用 fork 函數。
- 在父子進程中使用管道的詳細步驟
1、在父進程中調用 pipe 函數創建一個管道。
2、在父進程中調用 fork 函數創建一個子進程。
3、在父進程中關閉不使用的管道一端的文件描述符,然后調用對應的寫操作函數,例如 write,將對應的數據寫入管道。
4、在子進程中關閉不使用的管道一端的文件描述符,然后調用對應的讀操作函數,例如 read,將對應的數據從管道中讀出。
5、在父子進程中,調用 close 函數,關閉管道的文件描述符。
【編程實例】在父子進程中使用管道。在父進程中創建一個管道,并調用 fork 函數創建一個子進程,父進程將一行字符串數據寫入管道,在子進程中,從管道讀出這個字符串并打印出來。
- pipe_fatherson.c
- 運行結果
$ gcc pipe_fatherson.c -o pipe_fatherson
[wxm@centos7 pipe]$ ./pipe_fatherson
Parent Proc, fds[0]=3, fds[1]=4
Child Proc, fds[0]=3, fds[1]=4
Who are you?
Who are you?
《代碼說明》
- 第14行:在父進程中調用 pipe 函數創建管道,fds 數組中保存用于讀寫 I/O 的文件描述符。
- 第18行:接著調用 fork 函數。子進程將同時擁有通過第14行 pipe 函數調用獲取的2個文件描述符,從上面的運行結果可以驗證這一點。注意!復制的并非管道,而是用于管道 I/O 的文件描述符。至此,父子進程同時擁有管道 I/O 的文件描述符。
- 第27、33行:父進程通過第27行代碼,向管道寫入字符串;子進程通過第33行代碼,從管道接收字符串。
- 第36、39行:第36行代碼,子進程結束運行前,關閉管道的讀出端文件描述符;第39行代碼,父進程(也是主進程)結束運行前,關閉管道的寫入端文件描述符。
- 在兄弟進程中使用管道
??????? 在兄弟進程中使用管道進行數據通信的方法和在父子進程中類似,只是將對管道進行操作的兩個進程更換為兄弟進程即可,在父進程中則關閉該管道的 I/O 文件描述符。
【編程實例】值兄弟進程中使用管道的應用實例。首先在主進程(也就是父進程)中創建一個管道和兩個子進程,然后在第1個子進程中將一個字符串通過管道發送給第2個子進程,第2個子進程從管道中讀出數據,然后將該數據輸出到屏幕上。
- pipe_brother.c
- 運行結果
$ gcc pipe_brother.c -o pipe_brother
[wxm@centos7 pipe]$ ./pipe_brother
Parent Proc, fds[0]=3, fds[1]=4
Child1 Proc, fds[0]=3, fds[1]=4
Child2 Proc, fds[0]=3, fds[1]=4
Hello,I`m your brother!
Hello,I`m your brother!
Child1 proc eixt, pid=4679
Child proc send 1
Child2 proc eixt, pid=4680
Child proc send 2
《代碼說明》
- 第54、58、62行:在父進程中調用 waitpid 函數,等待子進程的終止,如果沒有終止的子進程也不會進入阻塞狀態,而是返回0。當子進程1結束運行時,函數返回該子進程的進程ID,執行第58行的代碼;同理,當子進程2結束運行時,函數返回該子進程的進程ID,執行第62行的代碼。
3.3 通過管道實現進程間雙向通信
下面創建2個進程和1個管道進行雙向數據交換的示例,其通信方式如下圖6所示。
圖6? 管道雙向通信模型1?從圖6可以看出,通過一個管道可以進行雙向數據通信。但采用這種模型時需格外注意。先給出示例,稍后再分析討論。
- pipe_duplex.c
- 運行結果
$ gcc pipe_duplex.c -o pipe_duplex
$ ./pipe_duplex
Parent porc output: Who are you?
Child proc output: Thank you for your message
??????? 運行結果和我們預想的一樣:子進程向管道中寫入字符串 str1,父進程從管道中讀出該字符串;父進程向管道中寫入字符串 str2,子進程從管道中讀出該字符串。如果我們將第 27 行的代碼注釋掉,運行結果會是怎樣呢?
$ ./pipe_duplex
Child proc output: Who are you?
從上面的運行結果和進程狀態可以看出,進程 pipe_duplex 陷入了 死鎖狀態(<defunct>),產生的原因是什么呢?
“向管道中傳遞數據時,先讀的進程會把管道中的數據取走。”
????????數據進入管道后成為無主數據。也就是通過 read 函數先讀取數據的進程將得到數據,即使該進程將數據傳到了管道。因此,注釋掉第 27 行代碼將產生問題。在第 28 行,子進程將讀回自己在第 26 行向管道發送的數據。結果,父進程調用 read 函數后將無限期等待數據進入管道,導致進程陷入死鎖。
??????? 從上述示例中可以看到,只用一個管道進行進程間的雙向通信并非易事。為了實現這一點,程序需要預測并控制運行流程,這在每種系統中都不同,可以視為不可能完成的任務。既然如此,該如何進行雙向通信呢?
“創建兩個管道。”
??????? 非常簡單,一個管道無法完成雙向通信任務,因此需要創建兩個管道,各自負責不同的數據流動方向即可。其過程如下圖 7 所示。
圖7? 雙向通信模型2???????? 由上圖 7 可知,使用兩個管道可以避免程序流程的不可預測或不可控制因素。下面采用上述模型改進 pipe_duplex.c 程序。
- pipe_duplex2.c
- 運行結果
$ gcc pipe_duplex2.c -o pipe_duplex2
$ ./pipe_duplex2
Parent porc output: Who are you?
Child proc output: Thank you for your message
- 程序說明
1、子進程 ——> 父進程:通過數組 fds1 指向的管道1進行數據交互。
2、父進程 ——> 子進程:通過數組 fds2 指向的管道2進行數據交互。
四 在網絡編程中運用管道實現進程間通信
上一節我們學習了基于管道的進程間通信方法,接下來將其運用到網絡編程代碼中。
4.1 保存消息的回聲服務器端
下面我們擴展上一篇博文中的服務器端程序 echo_mpserv.c,添加如下功能:
“將回聲客戶端傳輸的字符串按序保存到文件中。”
????????我們將這個功能任務委托給另外的進程。換言之,另行創建進程,從向客戶端提供服務的進程讀取字符串信息。這就涉及到進程間通信的問題。為此,我們可以使用上面講過的管道來實現進程間通信過程。下面給出示例程序。該示例可以與任意回聲客戶端配合運行,但我們將使用前一篇博文中介紹過的 echo_mpclient.c。
【提示】服務器端程序 echo_mpserv.c 和 客戶端程序 echo_mpclient.c,請參見下面的博文鏈接獲取。
Linux網絡編程 - 多進程服務器端(2)
- echo_storeserv.c
- 代碼說明
- 第55、56行:第55行創建管道,第56行創建負責保存數據到文件中的子進程。
- 第57~72行:這部分代碼是第56行創建的子進程運行區域。該代碼執行區域從管道出口端 fds[0] 讀取數據并保存到文件中。另外,上述服務器端并不終止運行,而是不斷向客戶端提供服務。因此,數據在文件中累計到一定程度即關閉文件,該過程通過第63行的 for 循環完成。
- 第99行:第87行通過 fork 函數創建的子進程將復制第55行創建的管道的文件描述符數組 fds。因此,可以通過管道入口端 fds[1] 向管道傳遞字符串數據。
- 運行結果
- 服務器端:echo_storeserv.c
$ gcc echo_storeserv.c -o storeserv
[wxm@centos7 echo_tcp]$ ./storeserv 9190
New client connected from address[127.0.0.1:60534], conn_id=6
New child proc ID: 5589
New client connected from address[127.0.0.1:60536], conn_id=6
New child proc ID: 5592
remove proc id: 5586
client[127.0.0.1:60534] disconnected, conn_id=6
remove proc id: 5589
client[127.0.0.1:60536] disconnected, conn_id=6
remove proc id: 5592
- 客戶端1:echo_mpclient.c
$ ./mpclient 127.0.0.1 9190
Connected...........
One
Message from server: One
Three
Message from server: Three
Five
Message from server: Five
Seven
Message from server: Seven
Nine
Message from server: Nine
Q
[wxm@centos7 echo_tcp]$
- 客戶端2:echo_mpclient.c
$ ./mpclient 127.0.0.1 9190
Connected...........
Two
Message from server: Two
Four
Message from server: Four
Six
Message from server: Six
Eight
Message from server: Eight
Ten
Message from server: Ten
Q
[wxm@centos7 echo_tcp]$
- 查看 echomsg.txt 文件內容
[wxm@centos7 echo_tcp]$ cat echomsg.txt
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
[wxm@centos7 echo_tcp]$
《提示》觀察示例 echo_storeserv.c 后,可以發現在 main 函數中,代碼內容太長,有點影響代碼閱讀和理解。我們其實可以嘗試針對一部分功能以函數為模塊單位重構代碼,有興趣的話,可以試一試,讓代碼結構更加緊湊、美觀。
五 多進程并發服務器端總結
??????? 前面我們已經實現了多進程并發服務器端模型,但它只是并發服務器模型中的其中之一。如果我們有如下的想法:
“我想利用進程和管道編寫聊天室程序,使多個客戶端進行對話,應該從哪著手呢?”
??????? 若想僅用進程和管道構建具有復雜功能的服務器端,程序員需要具備熟練的編程技術和經驗。因此,初學者應用該模型擴展程序并非易事,希望大家不要過于拘泥。以后要說明的另外兩種并發服務器端模型在功能上更加強大,同時更容易實現我們的想法。
??????? 在實際網絡編程開發項目中,幾乎不會用到多進程并發服務器端模型,因為它并不是一種高效的并發服務器模型,不適合實際應用場景。即使我們在實際開發項目中不會利用多進程模型構建服務器端,但這些內容我們還是有必要學習和掌握的。
????????最后跟大家分享一句他人的一條學習編程經驗之談:“即使開始時只需學習必要部分,但最后也會需要掌握所有的內容。”
《提示》另外兩種比較高效的并發服務器端模型為:I/O 復用、多線程服務器端。
六 習題
1、什么是進程間通信?分別從概念上和內存的角度進行說明。
答:從概念上講,進程間通信是指兩個進程之間交換數據的過程。從內存的角度上講,就是兩個進程共享的內存,通過這個共享的內存區域,可以進行數據交換,而這個共享的內存區域是在操作系統內核區中開辟的。
2、進程間通信需要特殊的IPC機制,這是由操作系統提供的。進程間通信時為何需要操作系統的幫助?
答:兩個進程之間要想交換數據,需要一塊共享的內存,但由于每個進程的地址空間都是相互獨立的,因此需要操作系統的幫助。也就是說,兩個進程共享的內存空間必須由操作系統來提供。
3、“管道”是典型的IPC技術。關于管道,請回答如下問題。
a. 管道是進程間交換數據的路徑。如何創建該路徑? 由誰創建?
b. 為了完成進程間通信,2個進程需同時連接管道。那2個進程如何連接到同一管道?
c. 管道允許進行2個進程間的雙向通信。雙向通信中需要注意哪些內容?
- a:在父進程(或主進程)中調用 pipe 函數創建管道。實際管道的創建主體是操作系統,管道不是屬于進程的資源,而是屬于操作系統的資源。
- b:pipe 函數通過傳入參數返回管道的出入口兩端的文件描述符。當調用 fork 函數創建子進程時,這兩個文件描述符會被復制到子進程中,因此,父子進程可以同時訪問同一管道。
- c:數據進入管道后就變成了無主數據。因此,只要有數據流入管道,任何進程都可以讀取數據。因此,要合理安排管道中數據的寫入和讀出順序。
4、編寫示例復習IPC技術,使2個進程相互交換3次字符串。當然,這兩個進程應具有父子關系,各位可指定任意字符串。
答:問題剖析:兩個父子進程要互相交換數據,可以通過管道方式實現進程間通信,而通過創建兩個管道可以實現進程間的雙向通信。我們假設是子進程先向父進程發送消息,然后父進程回復消息,如此往復3次后結束運行。
- pipe_procipc.c
- 運行結果
$ gcc pipe_procipc.c -o pipe_procipc
[wxm@centos7 pipe]$ ./pipe_procipc
Child send message: Hi,I`m child proc
Parent recv message: Hi,I`m child proc
Parent resp message: Hi,I`m parent proc
Child recv message: Hi,I`m parent proc
Child send message: Nice to meet you
Parent recv message: Nice to meet you
Parent resp message: Nice to meet you, too
Child recv message: Nice to meet you, too
Child send message: Good bye!
Parent recv message: Good bye!
Parent resp message: Bye bye!
Child recv message: Bye bye!
[wxm@centos7 pipe]$
參考
《TCP-IP網絡編程(尹圣雨)》第11章 - 進程間通信
《Linux C編程從基礎到實踐(程國鋼、張玉蘭)》第9章 - Linux的進程同步機制——管道和IPC
《TCP/IP網絡編程》課后練習答案第一部分11~14章 尹圣雨
總結
以上是生活随笔為你收集整理的Linux网络编程 - 在服务器端运用进程间通信之管道(pipe)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 苹果的傲慢与堕落,从iPhone XS的
- 下一篇: Pacman