作者丨二營(yíng)長(zhǎng)的意大利炮手
鏈接: https://juejin.im/post/5d7efce9f265da03a049a921?
進(jìn)程間的通信方式:
?? ?每個(gè)進(jìn)程各自有不同的用戶地址空間,任何一個(gè)進(jìn)程的全局變量在另一個(gè)進(jìn)程中都看不到,所以進(jìn)程之間要交換數(shù)據(jù)必須通過內(nèi)核,在內(nèi)核中開辟一塊緩沖區(qū),進(jìn)程1把數(shù)據(jù)從用戶空間拷到內(nèi)核緩沖區(qū),進(jìn)程2再?gòu)膬?nèi)核緩沖區(qū)把數(shù)據(jù)讀走,內(nèi)核提供的這種機(jī)制稱為進(jìn)程間通信(IPC,InterProcess Communication)。
1. 管道/匿名管道(pipe)
? ? ? ?管道是半雙工的,數(shù)據(jù)只能向一個(gè)方向流動(dòng);需要雙方通信時(shí),需要建立起兩個(gè)管道。只能用于父子進(jìn)程或者兄弟進(jìn)程之間(具有親緣關(guān)系的進(jìn)程);管道的實(shí)質(zhì)是一個(gè)內(nèi)核緩沖區(qū),進(jìn)程以先進(jìn)先出的方式從緩沖區(qū)存取數(shù)據(jù),管道一端的進(jìn)程順序的將數(shù)據(jù)寫入緩沖區(qū),另一端的進(jìn)程則順序的讀出數(shù)據(jù)。該緩沖區(qū)可以看做是一個(gè)循環(huán)隊(duì)列,讀和寫的位置都是自動(dòng)增長(zhǎng)的,不能隨意改變,一個(gè)數(shù)據(jù)只能被讀一次,讀出來(lái)以后在緩沖區(qū)就不復(fù)存在了。
2. 有名管道(FIFO)
管道類似與數(shù)據(jù)結(jié)構(gòu)中的隊(duì)列。有名管道不同于匿名管道之處在于它提供了一個(gè)路徑名與之關(guān)聯(lián),以有名管道的文件形式存在于文件系統(tǒng)中,這樣,即使與有名管道的創(chuàng)建進(jìn)程不存在親緣關(guān)系的進(jìn)程,只要可以訪問該路徑,就能夠彼此通過有名管道相互通信,因此,通過有名管道不相關(guān)的進(jìn)程也能交換數(shù)據(jù)。值的注意的是,有名管道嚴(yán)格遵循先進(jìn)先出(first in first out),對(duì)匿名管道及有名管道的讀總是從開始處返回?cái)?shù)據(jù),對(duì)它們的寫則把數(shù)據(jù)添加到末尾。
?無(wú)名管道阻塞問題:無(wú)名管道無(wú)需顯示打開,創(chuàng)建時(shí)直接返回文件描述符,在讀寫時(shí)需要確定對(duì)方的存在,否則將退出。如果當(dāng)前進(jìn)程向無(wú)名管道的一端寫數(shù)據(jù),必須確定另一端有某一進(jìn)程。如果寫入無(wú)名管道的數(shù)據(jù)超過其最大值,寫操作將阻塞,如果管道中沒有數(shù)據(jù),讀操作將阻塞,如果管道發(fā)現(xiàn)另一端斷開,將自動(dòng)退出。
? ? ?有名管道阻塞問題:有名管道在打開時(shí)需要確實(shí)對(duì)方的存在,否則將阻塞。即以讀方式打開某管道,在此之前必須一個(gè)進(jìn)程以寫方式打開管道,否則阻塞。此外,可以以讀寫(O_RDWR)模式打開有名管道,即當(dāng)前進(jìn)程讀,當(dāng)前進(jìn)程寫,不會(huì)阻塞。
3. 信號(hào)(Signal)
信號(hào)是Linux系統(tǒng)中用于進(jìn)程間互相通信或者操作的一種機(jī)制,信號(hào)可以在任何時(shí)候發(fā)給某一進(jìn)程,而無(wú)需知道該進(jìn)程的狀態(tài)。
如果該進(jìn)程當(dāng)前并未處于執(zhí)行狀態(tài),則該信號(hào)就有內(nèi)核保存起來(lái),知道該進(jìn)程回復(fù)執(zhí)行并傳遞給它為止。
如果一個(gè)信號(hào)被進(jìn)程設(shè)置為阻塞,則該信號(hào)的傳遞被延遲,直到其阻塞被取消是才被傳遞給進(jìn)程。
Linux系統(tǒng)中常用信號(hào):
(1)SIGHUP:用戶從終端注銷,所有已啟動(dòng)進(jìn)程都將收到該進(jìn)程。系統(tǒng)缺省狀態(tài)下對(duì)該信號(hào)的處理是終止進(jìn)程。
(2)SIGINT:程序終止信號(hào)。程序運(yùn)行過程中,按Ctrl+C鍵將產(chǎn)生該信號(hào)。
(3)SIGQUIT:程序退出信號(hào)。程序運(yùn)行過程中,按Ctrl+鍵將產(chǎn)生該信號(hào)。
(4)SIGBUS和SIGSEGV:進(jìn)程訪問非法地址。
(5)SIGFPE:運(yùn)算中出現(xiàn)致命錯(cuò)誤,如除零操作、數(shù)據(jù)溢出等。
(6)SIGKILL:用戶終止進(jìn)程執(zhí)行信號(hào)。shell下執(zhí)行kill -9發(fā)送該信號(hào)。
(7)SIGTERM:結(jié)束進(jìn)程信號(hào)。shell下執(zhí)行kill 進(jìn)程pid發(fā)送該信號(hào)。
(8)SIGALRM:定時(shí)器信號(hào)。
(9)SIGCLD:子進(jìn)程退出信號(hào)。如果其父進(jìn)程沒有忽略該信號(hào)也沒有處理該信號(hào),則子進(jìn)程退出后將形成僵尸進(jìn)程。
信號(hào)來(lái)源
信號(hào)是軟件層次上對(duì)中斷機(jī)制的一種模擬,是一種異步通信方式,,信號(hào)可以在用戶空間進(jìn)程和內(nèi)核之間直接交互,內(nèi)核可以利用信號(hào)來(lái)通知用戶空間的進(jìn)程發(fā)生了哪些系統(tǒng)事件,信號(hào)事件主要有兩個(gè)來(lái)源:
硬件來(lái)源:用戶按鍵輸入Ctrl+C退出、硬件異常如無(wú)效的存儲(chǔ)訪問等。
軟件終止:終止進(jìn)程信號(hào)、其他進(jìn)程調(diào)用kill函數(shù)、軟件異常產(chǎn)生信號(hào)。
信號(hào)生命周期和處理流程
(1)信號(hào)被某個(gè)進(jìn)程產(chǎn)生,并設(shè)置此信號(hào)傳遞的對(duì)象(一般為對(duì)應(yīng)進(jìn)程的pid),然后傳遞給操作系統(tǒng);
(2)操作系統(tǒng)根據(jù)接收進(jìn)程的設(shè)置(是否阻塞)而選擇性的發(fā)送給接收者,如果接收者阻塞該信號(hào)(且該信號(hào)是可以阻塞的),操作系統(tǒng)將暫時(shí)保留該信號(hào),而不傳遞,直到該進(jìn)程解除了對(duì)此信號(hào)的阻塞(如果對(duì)應(yīng)進(jìn)程已經(jīng)退出,則丟棄此信號(hào)),如果對(duì)應(yīng)進(jìn)程沒有阻塞,操作系統(tǒng)將傳遞此信號(hào)。
(3)目的進(jìn)程接收到此信號(hào)后,將根據(jù)當(dāng)前進(jìn)程對(duì)此信號(hào)設(shè)置的預(yù)處理方式,暫時(shí)終止當(dāng)前代碼的執(zhí)行,保護(hù)上下文(主要包括臨時(shí)寄存器數(shù)據(jù),當(dāng)前程序位置以及當(dāng)前CPU的狀態(tài))、轉(zhuǎn)而執(zhí)行中斷服務(wù)程序,執(zhí)行完成后在回復(fù)到中斷的位置。當(dāng)然,對(duì)于搶占式內(nèi)核,在中斷返回時(shí)還將引發(fā)新的調(diào)度。

4. 消息(Message)隊(duì)列
消息隊(duì)列是存放在內(nèi)核中的消息鏈表,每個(gè)消息隊(duì)列由消息隊(duì)列標(biāo)識(shí)符表示。
與管道(無(wú)名管道:只存在于內(nèi)存中的文件;命名管道:存在于實(shí)際的磁盤介質(zhì)或者文件系統(tǒng))不同的是消息隊(duì)列存放在內(nèi)核中,只有在內(nèi)核重啟(即,操作系統(tǒng)重啟)或者顯示地刪除一個(gè)消息隊(duì)列時(shí),該消息隊(duì)列才會(huì)被真正的刪除。
另外與管道不同的是,消息隊(duì)列在某個(gè)進(jìn)程往一個(gè)隊(duì)列寫入消息之前,并不需要另外某個(gè)進(jìn)程在該隊(duì)列上等待消息的到達(dá)。延伸閱讀:消息隊(duì)列C語(yǔ)言的實(shí)踐
消息隊(duì)列特點(diǎn)總結(jié):
(1)消息隊(duì)列是消息的鏈表,具有特定的格式,存放在內(nèi)存中并由消息隊(duì)列標(biāo)識(shí)符標(biāo)識(shí).
(2)消息隊(duì)列允許一個(gè)或多個(gè)進(jìn)程向它寫入與讀取消息.
(3)管道和消息隊(duì)列的通信數(shù)據(jù)都是先進(jìn)先出的原則。
(4)消息隊(duì)列可以實(shí)現(xiàn)消息的隨機(jī)查詢,消息不一定要以先進(jìn)先出的次序讀取,也可以按消息的類型讀取.比FIFO更有優(yōu)勢(shì)。
(5)消息隊(duì)列克服了信號(hào)承載信息量少,管道只能承載無(wú)格式字 節(jié)流以及緩沖區(qū)大小受限等缺。
(6)目前主要有兩種類型的消息隊(duì)列:POSIX消息隊(duì)列以及System V消息隊(duì)列,系統(tǒng)V消息隊(duì)列目前被大量使用。系統(tǒng)V消息隊(duì)列是隨內(nèi)核持續(xù)的,只有在內(nèi)核重起或者人工刪除時(shí),該消息隊(duì)列才會(huì)被刪除。
5. 共享內(nèi)存(share memory)
使得多個(gè)進(jìn)程可以可以直接讀寫同一塊內(nèi)存空間,是最快的可用IPC形式。是針對(duì)其他通信機(jī)制運(yùn)行效率較低而設(shè)計(jì)的。
為了在多個(gè)進(jìn)程間交換信息,內(nèi)核專門留出了一塊內(nèi)存區(qū),可以由需要訪問的進(jìn)程將其映射到自己的私有地址空間。進(jìn)程就可以直接讀寫這一塊內(nèi)存而不需要進(jìn)行數(shù)據(jù)的拷貝,從而大大提高效率。
由于多個(gè)進(jìn)程共享一段內(nèi)存,因此需要依靠某種同步機(jī)制(如信號(hào)量)來(lái)達(dá)到進(jìn)程間的同步及互斥。
延伸閱讀:Linux支持的主要三種共享內(nèi)存方式:mmap()系統(tǒng)調(diào)用、Posix共享內(nèi)存,以及System V共享內(nèi)存實(shí)踐

6. 信號(hào)量(semaphore)
信號(hào)量是一個(gè)計(jì)數(shù)器,用于多進(jìn)程對(duì)共享數(shù)據(jù)的訪問,信號(hào)量的意圖在于進(jìn)程間同步。
為了獲得共享資源,進(jìn)程需要執(zhí)行下列操作:
(1)創(chuàng)建一個(gè)信號(hào)量:這要求調(diào)用者指定初始值,對(duì)于二值信號(hào)量來(lái)說(shuō),它通常是1,也可是0。
(2)等待一個(gè)信號(hào)量:該操作會(huì)測(cè)試這個(gè)信號(hào)量的值,如果小于0,就阻塞。也稱為P操作。
(3)掛出一個(gè)信號(hào)量:該操作將信號(hào)量的值加1,也稱為V操作。
為了正確地實(shí)現(xiàn)信號(hào)量,信號(hào)量值的測(cè)試及減1操作應(yīng)當(dāng)是原子操作。為此,信號(hào)量通常是在內(nèi)核中實(shí)現(xiàn)的。Linux環(huán)境中,有三種類型:Posix(可移植性操作系統(tǒng)接口)有名信號(hào)量(使用Posix IPC名字標(biāo)識(shí))、Posix基于內(nèi)存的信號(hào)量(存放在共享內(nèi)存區(qū)中)、System V信號(hào)量(在內(nèi)核中維護(hù))。這三種信號(hào)量都可用于進(jìn)程間或線程間的同步。

兩個(gè)進(jìn)程使用一個(gè)二值信號(hào)量

兩個(gè)進(jìn)程所以用一個(gè)Posix有名二值信號(hào)量

信號(hào)量與普通整型變量的區(qū)別:
(1)信號(hào)量是非負(fù)整型變量,除了初始化之外,它只能通過兩個(gè)標(biāo)準(zhǔn)原子操作:wait(semap) , signal(semap) ; 來(lái)進(jìn)行訪問;
(2)操作也被成為PV原語(yǔ)(P來(lái)源于荷蘭語(yǔ)proberen"測(cè)試",V來(lái)源于荷蘭語(yǔ)verhogen"增加",P表示通過的意思,V表示釋放的意思),而普通整型變量則可以在任何語(yǔ)句塊中被訪問;
信號(hào)量與互斥量之間的區(qū)別:
(1)互斥量用于線程的互斥,信號(hào)量用于線程的同步。這是互斥量和信號(hào)量的根本區(qū)別,也就是互斥和同步之間的區(qū)別。
互斥:是指某一資源同時(shí)只允許一個(gè)訪問者對(duì)其進(jìn)行訪問,具有唯一性和排它性。但互斥無(wú)法限制訪問者對(duì)資源的訪問順序,即訪問是無(wú)序的。
同步:是指在互斥的基礎(chǔ)上(大多數(shù)情況),通過其它機(jī)制實(shí)現(xiàn)訪問者對(duì)資源的有序訪問。
在大多數(shù)情況下,同步已經(jīng)實(shí)現(xiàn)了互斥,特別是所有寫入資源的情況必定是互斥的。少數(shù)情況是指可以允許多個(gè)訪問者同時(shí)訪問資源
(2)互斥量值只能為0/1,信號(hào)量值可以為非負(fù)整數(shù)。
也就是說(shuō),一個(gè)互斥量只能用于一個(gè)資源的互斥訪問,它不能實(shí)現(xiàn)多個(gè)資源的多線程互斥問題。信號(hào)量可以實(shí)現(xiàn)多個(gè)同類資源的多線程互斥和同步。當(dāng)信號(hào)量為單值信號(hào)量是,也可以完成一個(gè)資源的互斥訪問。
(3)互斥量的加鎖和解鎖必須由同一線程分別對(duì)應(yīng)使用,信號(hào)量可以由一個(gè)線程釋放,另一個(gè)線程得到。
7. 套接字(socket)
套接字是一種通信機(jī)制,憑借這種機(jī)制,客戶/服務(wù)器(即要進(jìn)行通信的進(jìn)程)系統(tǒng)的開發(fā)工作既可以在本地單機(jī)上進(jìn)行,也可以跨網(wǎng)絡(luò)進(jìn)行。也就是說(shuō)它可以讓不在同一臺(tái)計(jì)算機(jī)但通過網(wǎng)絡(luò)連接計(jì)算機(jī)上的進(jìn)程進(jìn)行通信。

Socket是應(yīng)用層和傳輸層之間的橋梁。
套接字是支持TCP/IP的網(wǎng)絡(luò)通信的基本操作單元,可以看做是不同主機(jī)之間的進(jìn)程進(jìn)行雙向通信的端點(diǎn),簡(jiǎn)單的說(shuō)就是通信的兩方的一種約定,用套接字中的相關(guān)函數(shù)來(lái)完成通信過程。
套接字特性
套接字的特性由3個(gè)屬性確定,它們分別是:域、端口號(hào)、協(xié)議類型。
(1)套接字的域
它指定套接字通信中使用的網(wǎng)絡(luò)介質(zhì),最常見的套接字域有兩種:
一是AF_INET,它指的是Internet網(wǎng)絡(luò)。當(dāng)客戶使用套接字進(jìn)行跨網(wǎng)絡(luò)的連接時(shí),它就需要用到服務(wù)器計(jì)算機(jī)的IP地址和端口來(lái)指定一臺(tái)聯(lián)網(wǎng)機(jī)器上的某個(gè)特定服務(wù),所以在使用socket作為通信的終點(diǎn),服務(wù)器應(yīng)用程序必須在開始通信之前綁定一個(gè)端口,服務(wù)器在指定的端口等待客戶的連接。
另一個(gè)域AF_UNIX,表示UNIX文件系統(tǒng),它就是文件輸入/輸出,而它的地址就是文件名。
(2)套接字的端口號(hào)
每一個(gè)基于TCP/IP網(wǎng)絡(luò)通訊的程序(進(jìn)程)都被賦予了唯一的端口和端口號(hào),端口是一個(gè)信息緩沖區(qū),用于保留Socket中的輸入/輸出信息,端口號(hào)是一個(gè)16位無(wú)符號(hào)整數(shù),范圍是0-65535,以區(qū)別主機(jī)上的每一個(gè)程序(端口號(hào)就像房屋中的房間號(hào)),低于256的端口號(hào)保留給標(biāo)準(zhǔn)應(yīng)用程序,比如pop3的端口號(hào)就是110,每一個(gè)套接字都組合進(jìn)了IP地址、端口,這樣形成的整體就可以區(qū)別每一個(gè)套接字。
(3)套接字協(xié)議類型
因特網(wǎng)提供三種通信機(jī)制,一是流套接字,流套接字在域中通過TCP/IP連接實(shí)現(xiàn),同時(shí)也是AF_UNIX中常用的套接字類型。流套接字提供的是一個(gè)有序、可靠、雙向字節(jié)流的連接,因此發(fā)送的數(shù)據(jù)可以確保不會(huì)丟失、重復(fù)或亂序到達(dá),而且它還有一定的出錯(cuò)后重新發(fā)送的機(jī)制。
二個(gè)是數(shù)據(jù)報(bào)套接字,它不需要建立連接和維持一個(gè)連接,它們?cè)谟蛑型ǔJ峭ㄟ^UDP/IP協(xié)議實(shí)現(xiàn)的。它對(duì)可以發(fā)送的數(shù)據(jù)的長(zhǎng)度有限制,數(shù)據(jù)報(bào)作為一個(gè)單獨(dú)的網(wǎng)絡(luò)消息被傳輸,它可能會(huì)丟失、復(fù)制或錯(cuò)亂到達(dá),UDP不是一個(gè)可靠的協(xié)議,但是它的速度比較高,因?yàn)樗⒁恍枰偸且⒑途S持一個(gè)連接。
三是原始套接字,原始套接字允許對(duì)較低層次的協(xié)議直接訪問,比如IP、 ICMP協(xié)議,它常用于檢驗(yàn)新的協(xié)議實(shí)現(xiàn),或者訪問現(xiàn)有服務(wù)中配置的新設(shè)備,因?yàn)镽AW SOCKET可以自如地控制Windows下的多種協(xié)議,能夠?qū)W(wǎng)絡(luò)底層的傳輸機(jī)制進(jìn)行控制,所以可以應(yīng)用原始套接字來(lái)操縱網(wǎng)絡(luò)層和傳輸層應(yīng)用。比如,我們可以通過RAW SOCKET來(lái)接收發(fā)向本機(jī)的ICMP、IGMP協(xié)議包,或者接收TCP/IP棧不能夠處理的IP包,也可以用來(lái)發(fā)送一些自定包頭或自定協(xié)議的IP包。網(wǎng)絡(luò)監(jiān)聽技術(shù)很大程度上依賴于SOCKET_RAW。
原始套接字與標(biāo)準(zhǔn)套接字的區(qū)別在于:原始套接字可以讀寫內(nèi)核沒有處理的IP數(shù)據(jù)包,而流套接字只能讀取TCP協(xié)議的數(shù)據(jù),數(shù)據(jù)報(bào)套接字只能讀取UDP協(xié)議的數(shù)據(jù)。因此,如果要訪問其他協(xié)議發(fā)送數(shù)據(jù)必須使用原始套接字。

服務(wù)器端
(1)首先服務(wù)器應(yīng)用程序用系統(tǒng)調(diào)用socket來(lái)創(chuàng)建一個(gè)套接字,它是系統(tǒng)分配給該服務(wù)器進(jìn)程的類似文件描述符的資源,它不能與其他的進(jìn)程共享。
(2)然后,服務(wù)器進(jìn)程會(huì)給套接字起個(gè)名字,我們使用系統(tǒng)調(diào)用bind來(lái)給套接字命名。然后服務(wù)器進(jìn)程就開始等待客戶連接到這個(gè)套接字。
(3)接下來(lái),系統(tǒng)調(diào)用listen來(lái)創(chuàng)建一個(gè)隊(duì)列并將其用于存放來(lái)自客戶的進(jìn)入連接。
(4)最后,服務(wù)器通過系統(tǒng)調(diào)用accept來(lái)接受客戶的連接。它會(huì)創(chuàng)建一個(gè)與原有的命名套接不同的新套接字,這個(gè)套接字只用于與這個(gè)特定客戶端進(jìn)行通信,而命名套接字(即原先的套接字)則被保留下來(lái)繼續(xù)處理來(lái)自其他客戶的連接(建立客戶端和服務(wù)端的用于通信的流,進(jìn)行通信)。
客戶端
(1)客戶應(yīng)用程序首先調(diào)用socket來(lái)創(chuàng)建一個(gè)未命名的套接字,然后將服務(wù)器的命名套接字作為一個(gè)地址來(lái)調(diào)用connect與服務(wù)器建立連接。
(2)一旦連接建立,我們就可以像使用底層的文件描述符那樣用套接字來(lái)實(shí)現(xiàn)雙向數(shù)據(jù)的通信(通過流進(jìn)行數(shù)據(jù)傳輸)。