因?yàn)闆](méi)答好進(jìn)程間通信,面試掛了...

前言
開(kāi)場(chǎng)小故事

炎炎夏日,張三騎著單車(chē)去面試花了 1 小時(shí),一路上汗流浹背。
結(jié)果面試過(guò)程只花了 5 分鐘就結(jié)束了,面完的時(shí)候,天還是依然是亮的,還得在烈日下奔波 1 小時(shí)回去。
面試五分鐘,騎車(chē)兩小時(shí)。
你看,張三因面試沒(méi)準(zhǔn)備好,吹空調(diào)的時(shí)間只有 5 分鐘,來(lái)回路上花了 2 小時(shí)曬太陽(yáng),你說(shuō)慘不慘?
所以啊,炎炎夏日,為了能延長(zhǎng)吹空調(diào)的時(shí)間,我們應(yīng)該在面試前準(zhǔn)備得更充分些,吹空調(diào)時(shí)間是要自己爭(zhēng)取的。
很明顯,在這一場(chǎng)面試中, 張三在進(jìn)程間通信這一塊沒(méi)復(fù)習(xí)好,雖然列出了進(jìn)程間通信的方式,但這只是表面功夫,應(yīng)該需要進(jìn)一步了解每種通信方式的優(yōu)缺點(diǎn)及應(yīng)用場(chǎng)景。
說(shuō)真的,我們這次一起幫張三一起復(fù)習(xí)下,加深他對(duì)進(jìn)程間通信的理解,好讓他下次吹空調(diào)的時(shí)間能長(zhǎng)一點(diǎn)。

正文
每個(gè)進(jìn)程的用戶地址空間都是獨(dú)立的,一般而言是不能互相訪問(wèn)的,但內(nèi)核空間是每個(gè)進(jìn)程都共享的,所以進(jìn)程之間要通信必須通過(guò)內(nèi)核。

Linux 內(nèi)核提供了不少進(jìn)程間通信的機(jī)制,我們來(lái)一起瞧瞧有哪些?
管道
如果你學(xué)過(guò) Linux 命令,那你肯定很熟悉「|」這個(gè)豎線。
$?ps?auxf?|?grep?mysql
上面命令行里的「|」豎線就是一個(gè)管道,它的功能是將前一個(gè)命令(ps auxf)的輸出,作為后一個(gè)命令(grep mysql)的輸入,從這功能描述,可以看出管道傳輸數(shù)據(jù)是單向的,如果想相互通信,我們需要?jiǎng)?chuàng)建兩個(gè)管道才行。
同時(shí),我們得知上面這種管道是沒(méi)有名字,所以「|」表示的管道稱(chēng)為匿名管道,用完了就銷(xiāo)毀。
管道還有另外一個(gè)類(lèi)型是命名管道,也被叫做 FIFO,因?yàn)閿?shù)據(jù)是先進(jìn)先出的傳輸方式。
在使用命名管道前,先需要通過(guò) mkfifo 命令來(lái)創(chuàng)建,并且指定管道名字:
$?mkfifo?myPipe
myPipe 就是這個(gè)管道的名稱(chēng),基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我們可以用 ls 看一下,這個(gè)文件的類(lèi)型是 p,也就是 pipe(管道) 的意思:
$?ls?-l
prw-r--r--. 1?root????root?????????0?Jul?17?02:45?myPipe
接下來(lái),我們往 myPipe 這個(gè)管道寫(xiě)入數(shù)據(jù):
$?echo?"hello"?>?myPipe??//?將數(shù)據(jù)寫(xiě)進(jìn)管道
?????????????????????????//?停住了?...
你操作了后,你會(huì)發(fā)現(xiàn)命令執(zhí)行后就停在這了,這是因?yàn)楣艿览锏膬?nèi)容沒(méi)有被讀取,只有當(dāng)管道里的數(shù)據(jù)被讀完后,命令才可以正常退出。
于是,我們執(zhí)行另外一個(gè)命令來(lái)讀取這個(gè)管道里的數(shù)據(jù):
$?cat?hello
可以看到,管道里的內(nèi)容被讀取出來(lái)了,并打印在了終端上,另外一方面,echo 那個(gè)命令也正常退出了。
我們可以看出,管道這種通信方式效率低,不適合進(jìn)程間頻繁地交換數(shù)據(jù)。當(dāng)然,它的好處,自然就是簡(jiǎn)單,同時(shí)也我們很容易得知管道里的數(shù)據(jù)已經(jīng)被另一個(gè)進(jìn)程讀取了。
那管道如何創(chuàng)建呢,背后原理是什么?
匿名管道的創(chuàng)建,需要通過(guò)下面這個(gè)系統(tǒng)調(diào)用:
int?pipe(int?fd[2])
這里表示創(chuàng)建一個(gè)匿名管道,并返回了兩個(gè)描述符,一個(gè)是管道的讀取端描述符 fd[0],另一個(gè)是管道的寫(xiě)入端描述符 fd[1]。注意,這個(gè)匿名管道是特殊的文件,只存在于內(nèi)存,不存于文件系統(tǒng)中。

其實(shí),所謂的管道,就是內(nèi)核里面的一串緩存。從管道的一段寫(xiě)入的數(shù)據(jù),實(shí)際上是緩存在內(nèi)核中的,另一端讀取,也就是從內(nèi)核中讀取這段數(shù)據(jù)。另外,管道傳輸?shù)臄?shù)據(jù)是無(wú)格式的流且大小受限。
看到這,你可能會(huì)有疑問(wèn)了,這兩個(gè)描述符都是在一個(gè)進(jìn)程里面,并沒(méi)有起到進(jìn)程間通信的作用,怎么樣才能使得管道是跨過(guò)兩個(gè)進(jìn)程的呢?
我們可以使用 fork 創(chuàng)建子進(jìn)程,創(chuàng)建的子進(jìn)程會(huì)復(fù)制父進(jìn)程的文件描述符,這樣就做到了兩個(gè)進(jìn)程各有兩個(gè)「 fd[0] 與 fd[1]」,兩個(gè)進(jìn)程就可以通過(guò)各自的 fd 寫(xiě)入和讀取同一個(gè)管道文件實(shí)現(xiàn)跨進(jìn)程通信了。

管道只能一端寫(xiě)入,另一端讀出,所以上面這種模式容易造成混亂,因?yàn)楦高M(jìn)程和子進(jìn)程都可以同時(shí)寫(xiě)入,也都可以讀出。那么,為了避免這種情況,通常的做法是:
父進(jìn)程關(guān)閉讀取的 fd[0],只保留寫(xiě)入的 fd[1];
子進(jìn)程關(guān)閉寫(xiě)入的 fd[1],只保留讀取的 fd[0];

所以說(shuō)如果需要雙向通信,則應(yīng)該創(chuàng)建兩個(gè)管道。
到這里,我們僅僅解析了使用管道進(jìn)行父進(jìn)程與子進(jìn)程之間的通信,但是在我們 shell 里面并不是這樣的。
在 shell 里面執(zhí)行 A | B命令的時(shí)候,A 進(jìn)程和 B 進(jìn)程都是 shell 創(chuàng)建出來(lái)的子進(jìn)程,A 和 B 之間不存在父子關(guān)系,它倆的父進(jìn)程都是 shell。

所以說(shuō),在 shell 里通過(guò)「|」匿名管道將多個(gè)命令連接在一起,實(shí)際上也就是創(chuàng)建了多個(gè)子進(jìn)程,那么在我們編寫(xiě) shell 腳本時(shí),能使用一個(gè)管道搞定的事情,就不要多用一個(gè)管道,這樣可以減少創(chuàng)建子進(jìn)程的系統(tǒng)開(kāi)銷(xiāo)。
我們可以得知,對(duì)于匿名管道,它的通信范圍是存在父子關(guān)系的進(jìn)程。因?yàn)楣艿罌](méi)有實(shí)體,也就是沒(méi)有管道文件,只能通過(guò) fork 來(lái)復(fù)制父進(jìn)程 fd 文件描述符,來(lái)達(dá)到通信的目的。
另外,對(duì)于命名管道,它可以在不相關(guān)的進(jìn)程間也能相互通信。因?yàn)槊罟艿?,提前?chuàng)建了一個(gè)類(lèi)型為管道的設(shè)備文件,在進(jìn)程里只要使用這個(gè)設(shè)備文件,就可以相互通信。
不管是匿名管道還是命名管道,進(jìn)程寫(xiě)入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個(gè)進(jìn)程讀取數(shù)據(jù)時(shí)候自然也是從內(nèi)核中獲取,同時(shí)通信數(shù)據(jù)都遵循先進(jìn)先出原則,不支持 lseek 之類(lèi)的文件定位操作。
消息隊(duì)列
前面說(shuō)到管道的通信方式是效率低的,因此管道不適合進(jìn)程間頻繁地交換數(shù)據(jù)。
對(duì)于這個(gè)問(wèn)題,消息隊(duì)列的通信模式就可以解決。比如,A 進(jìn)程要給 B 進(jìn)程發(fā)送消息,A 進(jìn)程把數(shù)據(jù)放在對(duì)應(yīng)的消息隊(duì)列后就可以正常返回了,B 進(jìn)程需要的時(shí)候再去讀取數(shù)據(jù)就可以了。同理,B 進(jìn)程要給 A 進(jìn)程發(fā)送消息也是如此。
再來(lái),消息隊(duì)列是保存在內(nèi)核中的消息鏈表,在發(fā)送數(shù)據(jù)時(shí),會(huì)分成一個(gè)一個(gè)獨(dú)立的數(shù)據(jù)單元,也就是消息體(數(shù)據(jù)塊),消息體是用戶自定義的數(shù)據(jù)類(lèi)型,消息的發(fā)送方和接收方要約定好消息體的數(shù)據(jù)類(lèi)型,所以每個(gè)消息體都是固定大小的存儲(chǔ)塊,不像管道是無(wú)格式的字節(jié)流數(shù)據(jù)。如果進(jìn)程從消息隊(duì)列中讀取了消息體,內(nèi)核就會(huì)把這個(gè)消息體刪除。
消息隊(duì)列生命周期隨內(nèi)核,如果沒(méi)有釋放消息隊(duì)列或者沒(méi)有關(guān)閉操作系統(tǒng),消息隊(duì)列會(huì)一直存在,而前面提到的匿名管道的生命周期,是隨進(jìn)程的創(chuàng)建而建立,隨進(jìn)程的結(jié)束而銷(xiāo)毀。
消息這種模型,兩個(gè)進(jìn)程之間的通信就像平時(shí)發(fā)郵件一樣,你來(lái)一封,我回一封,可以頻繁溝通了。
但郵件的通信方式存在不足的地方有兩點(diǎn),一是通信不及時(shí),二是附件也有大小限制,這同樣也是消息隊(duì)列通信不足的點(diǎn)。
消息隊(duì)列不適合比較大數(shù)據(jù)的傳輸,因?yàn)樵趦?nèi)核中每個(gè)消息體都有一個(gè)最大長(zhǎng)度的限制,同時(shí)所有隊(duì)列所包含的全部消息體的總長(zhǎng)度也是有上限。在 Linux 內(nèi)核中,會(huì)有兩個(gè)宏定義 MSGMAX 和 MSGMNB,它們以字節(jié)為單位,分別定義了一條消息的最大長(zhǎng)度和一個(gè)隊(duì)列的最大長(zhǎng)度。
消息隊(duì)列通信過(guò)程中,存在用戶態(tài)與內(nèi)核態(tài)之間的數(shù)據(jù)拷貝開(kāi)銷(xiāo),因?yàn)檫M(jìn)程寫(xiě)入數(shù)據(jù)到內(nèi)核中的消息隊(duì)列時(shí),會(huì)發(fā)生從用戶態(tài)拷貝數(shù)據(jù)到內(nèi)核態(tài)的過(guò)程,同理另一進(jìn)程讀取內(nèi)核中的消息數(shù)據(jù)時(shí),會(huì)發(fā)生從內(nèi)核態(tài)拷貝數(shù)據(jù)到用戶態(tài)的過(guò)程。
共享內(nèi)存
消息隊(duì)列的讀取和寫(xiě)入的過(guò)程,都會(huì)有發(fā)生用戶態(tài)與內(nèi)核態(tài)之間的消息拷貝過(guò)程。那共享內(nèi)存的方式,就很好的解決了這一問(wèn)題。
現(xiàn)代操作系統(tǒng),對(duì)于內(nèi)存管理,采用的是虛擬內(nèi)存技術(shù),也就是每個(gè)進(jìn)程都有自己獨(dú)立的虛擬內(nèi)存空間,不同進(jìn)程的虛擬內(nèi)存映射到不同的物理內(nèi)存中。所以,即使進(jìn)程 A 和 進(jìn)程 B 的虛擬地址是一樣的,其實(shí)訪問(wèn)的是不同的物理內(nèi)存地址,對(duì)于數(shù)據(jù)的增刪查改互不影響。
共享內(nèi)存的機(jī)制,就是拿出一塊虛擬地址空間來(lái),映射到相同的物理內(nèi)存中。這樣這個(gè)進(jìn)程寫(xiě)入的東西,另外一個(gè)進(jìn)程馬上就能看到了,都不需要拷貝來(lái)拷貝去,傳來(lái)傳去,大大提高了進(jìn)程間通信的速度。

信號(hào)量
用了共享內(nèi)存通信方式,帶來(lái)新的問(wèn)題,那就是如果多個(gè)進(jìn)程同時(shí)修改同一個(gè)共享內(nèi)存,很有可能就沖突了。例如兩個(gè)進(jìn)程都同時(shí)寫(xiě)一個(gè)地址,那先寫(xiě)的那個(gè)進(jìn)程會(huì)發(fā)現(xiàn)內(nèi)容被別人覆蓋了。
為了防止多進(jìn)程競(jìng)爭(zhēng)共享資源,而造成的數(shù)據(jù)錯(cuò)亂,所以需要保護(hù)機(jī)制,使得共享的資源,在任意時(shí)刻只能被一個(gè)進(jìn)程訪問(wèn)。正好,信號(hào)量就實(shí)現(xiàn)了這一保護(hù)機(jī)制。
信號(hào)量其實(shí)是一個(gè)整型的計(jì)數(shù)器,主要用于實(shí)現(xiàn)進(jìn)程間的互斥與同步,而不是用于緩存進(jìn)程間通信的數(shù)據(jù)。
信號(hào)量表示資源的數(shù)量,控制信號(hào)量的方式有兩種原子操作:
一個(gè)是 P 操作,這個(gè)操作會(huì)把信號(hào)量減去 -1,相減后如果信號(hào)量 < 0,則表明資源已被占用,進(jìn)程需阻塞等待;相減后如果信號(hào)量 >= 0,則表明還有資源可使用,進(jìn)程可正常繼續(xù)執(zhí)行。
另一個(gè)是 V 操作,這個(gè)操作會(huì)把信號(hào)量加上 1,相加后如果信號(hào)量 <= 0,則表明當(dāng)前有阻塞中的進(jìn)程,于是會(huì)將該進(jìn)程喚醒運(yùn)行;相加后如果信號(hào)量 > 0,則表明當(dāng)前沒(méi)有阻塞中的進(jìn)程;
P 操作是用在進(jìn)入共享資源之前,V 操作是用在離開(kāi)共享資源之后,這兩個(gè)操作是必須成對(duì)出現(xiàn)的。
接下來(lái),舉個(gè)例子,如果要使得兩個(gè)進(jìn)程互斥訪問(wèn)共享內(nèi)存,我們可以初始化信號(hào)量為 1。

具體的過(guò)程如下:
進(jìn)程 A 在訪問(wèn)共享內(nèi)存前,先執(zhí)行了 P 操作,由于信號(hào)量的初始值為 1,故在進(jìn)程 A 執(zhí)行 P 操作后信號(hào)量變?yōu)?0,表示共享資源可用,于是進(jìn)程 A 就可以訪問(wèn)共享內(nèi)存。
若此時(shí),進(jìn)程 B 也想訪問(wèn)共享內(nèi)存,執(zhí)行了 P 操作,結(jié)果信號(hào)量變?yōu)榱?-1,這就意味著臨界資源已被占用,因此進(jìn)程 B 被阻塞。
直到進(jìn)程 A 訪問(wèn)完共享內(nèi)存,才會(huì)執(zhí)行 V 操作,使得信號(hào)量恢復(fù)為 0,接著就會(huì)喚醒阻塞中的線程 B,使得進(jìn)程 B 可以訪問(wèn)共享內(nèi)存,最后完成共享內(nèi)存的訪問(wèn)后,執(zhí)行 V 操作,使信號(hào)量恢復(fù)到初始值 1。
可以發(fā)現(xiàn),信號(hào)初始化為 1,就代表著是互斥信號(hào)量,它可以保證共享內(nèi)存在任何時(shí)刻只有一個(gè)進(jìn)程在訪問(wèn),這就很好的保護(hù)了共享內(nèi)存。
另外,在多進(jìn)程里,每個(gè)進(jìn)程并不一定是順序執(zhí)行的,它們基本是以各自獨(dú)立的、不可預(yù)知的速度向前推進(jìn),但有時(shí)候我們又希望多個(gè)進(jìn)程能密切合作,以實(shí)現(xiàn)一個(gè)共同的任務(wù)。
例如,進(jìn)程 A 是負(fù)責(zé)生產(chǎn)數(shù)據(jù),而進(jìn)程 B 是負(fù)責(zé)讀取數(shù)據(jù),這兩個(gè)進(jìn)程是相互合作、相互依賴的,進(jìn)程 A 必須先生產(chǎn)了數(shù)據(jù),進(jìn)程 B 才能讀取到數(shù)據(jù),所以執(zhí)行是有前后順序的。
那么這時(shí)候,就可以用信號(hào)量來(lái)實(shí)現(xiàn)多進(jìn)程同步的方式,我們可以初始化信號(hào)量為 0。

具體過(guò)程:
如果進(jìn)程 B 比進(jìn)程 A 先執(zhí)行了,那么執(zhí)行到 P 操作時(shí),由于信號(hào)量初始值為 0,故信號(hào)量會(huì)變?yōu)?-1,表示進(jìn)程 A 還沒(méi)生產(chǎn)數(shù)據(jù),于是進(jìn)程 B 就阻塞等待;
接著,當(dāng)進(jìn)程 A 生產(chǎn)完數(shù)據(jù)后,執(zhí)行了 V 操作,就會(huì)使得信號(hào)量變?yōu)?0,于是就會(huì)喚醒阻塞在 P 操作的進(jìn)程 B;
最后,進(jìn)程 B 被喚醒后,意味著進(jìn)程 A 已經(jīng)生產(chǎn)了數(shù)據(jù),于是進(jìn)程 B 就可以正常讀取數(shù)據(jù)了。
可以發(fā)現(xiàn),信號(hào)初始化為 0,就代表著是同步信號(hào)量,它可以保證進(jìn)程 A 應(yīng)在進(jìn)程 B 之前執(zhí)行。
信號(hào)
上面說(shuō)的進(jìn)程間通信,都是常規(guī)狀態(tài)下的工作模式。對(duì)于異常情況下的工作模式,就需要用「信號(hào)」的方式來(lái)通知進(jìn)程。
信號(hào)跟信號(hào)量雖然名字相似度 66.66%,但兩者用途完全不一樣,就好像 Java 和 JavaScript 的區(qū)別。
在 Linux 操作系統(tǒng)中, 為了響應(yīng)各種各樣的事件,提供了幾十種信號(hào),分別代表不同的意義。我們可以通過(guò) kill -l 命令,查看所有的信號(hào):
$?kill?-l
?1)?SIGHUP???????2)?SIGINT???????3)?SIGQUIT??????4)?SIGILL???????5)?SIGTRAP
?6)?SIGABRT??????7)?SIGBUS???????8)?SIGFPE???????9)?SIGKILL?????10)?SIGUSR1
11)?SIGSEGV?????12)?SIGUSR2?????13)?SIGPIPE?????14)?SIGALRM?????15)?SIGTERM
16)?SIGSTKFLT???17)?SIGCHLD?????18)?SIGCONT?????19)?SIGSTOP?????20)?SIGTSTP
21)?SIGTTIN?????22)?SIGTTOU?????23)?SIGURG??????24)?SIGXCPU?????25)?SIGXFSZ
26)?SIGVTALRM???27)?SIGPROF?????28)?SIGWINCH????29)?SIGIO???????30)?SIGPWR
31)?SIGSYS??????34)?SIGRTMIN????35)?SIGRTMIN+1??36)?SIGRTMIN+2??37)?SIGRTMIN+3
38)?SIGRTMIN+4??39)?SIGRTMIN+5??40)?SIGRTMIN+6??41)?SIGRTMIN+7??42)?SIGRTMIN+8
43)?SIGRTMIN+9??44)?SIGRTMIN+10?45)?SIGRTMIN+11?46)?SIGRTMIN+12?47)?SIGRTMIN+13
48)?SIGRTMIN+14?49)?SIGRTMIN+15?50)?SIGRTMAX-14?51)?SIGRTMAX-13?52)?SIGRTMAX-12
53)?SIGRTMAX-11?54)?SIGRTMAX-10?55)?SIGRTMAX-9??56)?SIGRTMAX-8??57)?SIGRTMAX-7
58)?SIGRTMAX-6??59)?SIGRTMAX-5??60)?SIGRTMAX-4??61)?SIGRTMAX-3??62)?SIGRTMAX-2
63)?SIGRTMAX-1??64)?SIGRTMAX
運(yùn)行在 shell 終端的進(jìn)程,我們可以通過(guò)鍵盤(pán)輸入某些組合鍵的時(shí)候,給進(jìn)程發(fā)送信號(hào)。例如
Ctrl+C 產(chǎn)生
SIGINT信號(hào),表示終止該進(jìn)程;Ctrl+Z 產(chǎn)生
SIGTSTP信號(hào),表示停止該進(jìn)程,但還未結(jié)束;
如果進(jìn)程在后臺(tái)運(yùn)行,可以通過(guò) kill 命令的方式給進(jìn)程發(fā)送信號(hào),但前提需要知道運(yùn)行中的進(jìn)程 PID 號(hào),例如:
kill -9 1050 ,表示給 PID 為 1050 的進(jìn)程發(fā)送
SIGKILL信號(hào),用來(lái)立即結(jié)束該進(jìn)程;
所以,信號(hào)事件的來(lái)源主要有硬件來(lái)源(如鍵盤(pán) Cltr+C )和軟件來(lái)源(如 kill 命令)。
信號(hào)是進(jìn)程間通信機(jī)制中唯一的異步通信機(jī)制,因?yàn)榭梢栽谌魏螘r(shí)候發(fā)送信號(hào)給某一進(jìn)程,一旦有信號(hào)產(chǎn)生,我們就有下面這幾種,用戶進(jìn)程對(duì)信號(hào)的處理方式。
1.執(zhí)行默認(rèn)操作。Linux 對(duì)每種信號(hào)都規(guī)定了默認(rèn)操作,例如,上面列表中的 SIGTERM 信號(hào),就是終止進(jìn)程的意思。Core 的意思是 Core Dump,也即終止進(jìn)程后,通過(guò) Core Dump 將當(dāng)前進(jìn)程的運(yùn)行狀態(tài)保存在文件里面,方便程序員事后進(jìn)行分析問(wèn)題在哪里。
2.捕捉信號(hào)。我們可以為信號(hào)定義一個(gè)信號(hào)處理函數(shù)。當(dāng)信號(hào)發(fā)生時(shí),我們就執(zhí)行相應(yīng)的信號(hào)處理函數(shù)。
3.忽略信號(hào)。當(dāng)我們不希望處理某些信號(hào)的時(shí)候,就可以忽略該信號(hào),不做任何處理。有兩個(gè)信號(hào)是應(yīng)用進(jìn)程無(wú)法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它們用于在任何時(shí)候中斷或結(jié)束某一進(jìn)程。
Socket
前面提到的管道、消息隊(duì)列、共享內(nèi)存、信號(hào)量和信號(hào)都是在同一臺(tái)主機(jī)上進(jìn)行進(jìn)程間通信,那要想跨網(wǎng)絡(luò)與不同主機(jī)上的進(jìn)程之間通信,就需要 Socket 通信了。
實(shí)際上,Socket 通信不僅可以跨網(wǎng)絡(luò)與不同主機(jī)的進(jìn)程間通信,還可以在同主機(jī)上進(jìn)程間通信。
我們來(lái)看看創(chuàng)建 socket 的系統(tǒng)調(diào)用:
int?socket(int?domain,?int?type,?int?protocal)
三個(gè)參數(shù)分別代表:
domain 參數(shù)用來(lái)指定協(xié)議族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本機(jī);
type 參數(shù)用來(lái)指定通信特性,比如 SOCK_STREAM 表示的是字節(jié)流,對(duì)應(yīng) TCP、SOCK_DGRAM ?表示的是數(shù)據(jù)報(bào),對(duì)應(yīng) UDP、SOCK_RAW 表示的是原始套接字;
protocal 參數(shù)原本是用來(lái)指定通信協(xié)議的,但現(xiàn)在基本廢棄。因?yàn)閰f(xié)議已經(jīng)通過(guò)前面兩個(gè)參數(shù)指定完成,protocol 目前一般寫(xiě)成 0 即可;
根據(jù)創(chuàng)建 socket 類(lèi)型的不同,通信的方式也就不同:
實(shí)現(xiàn) TCP 字節(jié)流通信:socket 類(lèi)型是 AF_INET 和 SOCK_STREAM;
實(shí)現(xiàn) UDP 數(shù)據(jù)報(bào)通信:socket 類(lèi)型是 AF_INET 和 SOCK_DGRAM;
實(shí)現(xiàn)本地進(jìn)程間通信:「本地字節(jié)流 socket 」類(lèi)型是 AF_LOCAL 和 SOCK_STREAM,「本地?cái)?shù)據(jù)報(bào) socket 」類(lèi)型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等價(jià)的,所以 AF_UNIX 也屬于本地 socket;
接下來(lái),簡(jiǎn)單說(shuō)一下這三種通信的編程模式。
針對(duì) TCP 協(xié)議通信的 socket 編程模型

服務(wù)端和客戶端初始化
socket,得到文件描述符;服務(wù)端調(diào)用
bind,將綁定在 IP 地址和端口;服務(wù)端調(diào)用
listen,進(jìn)行監(jiān)聽(tīng);服務(wù)端調(diào)用
accept,等待客戶端連接;客戶端調(diào)用
connect,向服務(wù)器端的地址和端口發(fā)起連接請(qǐng)求;服務(wù)端
accept返回用于傳輸?shù)?socket的文件描述符;客戶端調(diào)用
write寫(xiě)入數(shù)據(jù);服務(wù)端調(diào)用read讀取數(shù)據(jù);客戶端斷開(kāi)連接時(shí),會(huì)調(diào)用
close,那么服務(wù)端read讀取數(shù)據(jù)的時(shí)候,就會(huì)讀取到了EOF,待處理完數(shù)據(jù)后,服務(wù)端調(diào)用close,表示連接關(guān)閉。
這里需要注意的是,服務(wù)端調(diào)用 accept 時(shí),連接成功了會(huì)返回一個(gè)已完成連接的 socket,后續(xù)用來(lái)傳輸數(shù)據(jù)。
所以,監(jiān)聽(tīng)的 socket 和真正用來(lái)傳送數(shù)據(jù)的 socket,是「兩個(gè)」 socket,一個(gè)叫作監(jiān)聽(tīng) socket,一個(gè)叫作已完成連接 socket。
成功連接建立之后,雙方開(kāi)始通過(guò) read 和 write 函數(shù)來(lái)讀寫(xiě)數(shù)據(jù),就像往一個(gè)文件流里面寫(xiě)東西一樣。
針對(duì) UDP 協(xié)議通信的 socket 編程模型

UDP 是沒(méi)有連接的,所以不需要三次握手,也就不需要像 TCP 調(diào)用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口號(hào),因此也需要 bind。
對(duì)于 UDP 來(lái)說(shuō),不需要要維護(hù)連接,那么也就沒(méi)有所謂的發(fā)送方和接收方,甚至都不存在客戶端和服務(wù)端的概念,只要有一個(gè) socket 多臺(tái)機(jī)器就可以任意通信,因此每一個(gè) UDP 的 socket 都需要 bind。
另外,每次通信時(shí),調(diào)用 sendto 和 recvfrom,都要傳入目標(biāo)主機(jī)的 IP 地址和端口。
針對(duì)本地進(jìn)程間通信的 socket 編程模型
本地 socket ?被用于在同一臺(tái)主機(jī)上進(jìn)程間通信的場(chǎng)景:
本地 socket 的編程接口和 IPv4 、IPv6 套接字編程接口是一致的,可以支持「字節(jié)流」和「數(shù)據(jù)報(bào)」兩種協(xié)議;
本地 socket 的實(shí)現(xiàn)效率大大高于 IPv4 和 IPv6 的字節(jié)流、數(shù)據(jù)報(bào) socket 實(shí)現(xiàn);
對(duì)于本地字節(jié)流 socket,其 socket 類(lèi)型是 AF_LOCAL 和 SOCK_STREAM。
對(duì)于本地?cái)?shù)據(jù)報(bào) socket,其 socket 類(lèi)型是 AF_LOCAL 和 SOCK_DGRAM。
本地字節(jié)流 socket 和 本地?cái)?shù)據(jù)報(bào) socket 在 bind 的時(shí)候,不像 TCP 和 UDP 要綁定 IP 地址和端口,而是綁定一個(gè)本地文件,這也就是它們之間的最大區(qū)別。
總結(jié)
由于每個(gè)進(jìn)程的用戶空間都是獨(dú)立的,不能相互訪問(wèn),這時(shí)就需要借助內(nèi)核空間來(lái)實(shí)現(xiàn)進(jìn)程間通信,原因很簡(jiǎn)單,每個(gè)進(jìn)程都是共享一個(gè)內(nèi)核空間。
Linux 內(nèi)核提供了不少進(jìn)程間通信的方式,其中最簡(jiǎn)單的方式就是管道,管道分為「匿名管道」和「命名管道」。
匿名管道顧名思義,它沒(méi)有名字標(biāo)識(shí),匿名管道是特殊文件只存在于內(nèi)存,沒(méi)有存在于文件系統(tǒng)中,shell 命令中的「|」豎線就是匿名管道,通信的數(shù)據(jù)是無(wú)格式的流并且大小受限,通信的方式是單向的,數(shù)據(jù)只能在一個(gè)方向上流動(dòng),如果要雙向通信,需要?jiǎng)?chuàng)建兩個(gè)管道,再來(lái)匿名管道是只能用于存在父子關(guān)系的進(jìn)程間通信,匿名管道的生命周期隨著進(jìn)程創(chuàng)建而建立,隨著進(jìn)程終止而消失。
命名管道突破了匿名管道只能在親緣關(guān)系進(jìn)程間的通信限制,因?yàn)槭褂妹艿赖那疤?,需要在文件系統(tǒng)創(chuàng)建一個(gè)類(lèi)型為 p 的設(shè)備文件,那么毫無(wú)關(guān)系的進(jìn)程就可以通過(guò)這個(gè)設(shè)備文件進(jìn)行通信。另外,不管是匿名管道還是命名管道,進(jìn)程寫(xiě)入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個(gè)進(jìn)程讀取數(shù)據(jù)時(shí)候自然也是從內(nèi)核中獲取,同時(shí)通信數(shù)據(jù)都遵循先進(jìn)先出原則,不支持 lseek 之類(lèi)的文件定位操作。
消息隊(duì)列克服了管道通信的數(shù)據(jù)是無(wú)格式的字節(jié)流的問(wèn)題,消息隊(duì)列實(shí)際上是保存在內(nèi)核的「消息鏈表」,消息隊(duì)列的消息體是可以用戶自定義的數(shù)據(jù)類(lèi)型,發(fā)送數(shù)據(jù)時(shí),會(huì)被分成一個(gè)一個(gè)獨(dú)立的消息體,當(dāng)然接收數(shù)據(jù)時(shí),也要與發(fā)送方發(fā)送的消息體的數(shù)據(jù)類(lèi)型保持一致,這樣才能保證讀取的數(shù)據(jù)是正確的。消息隊(duì)列通信的速度不是最及時(shí)的,畢竟每次數(shù)據(jù)的寫(xiě)入和讀取都需要經(jīng)過(guò)用戶態(tài)與內(nèi)核態(tài)之間的拷貝過(guò)程。
共享內(nèi)存可以解決消息隊(duì)列通信中用戶態(tài)與內(nèi)核態(tài)之間數(shù)據(jù)拷貝過(guò)程帶來(lái)的開(kāi)銷(xiāo),它直接分配一個(gè)共享空間,每個(gè)進(jìn)程都可以直接訪問(wèn),就像訪問(wèn)進(jìn)程自己的空間一樣快捷方便,不需要陷入內(nèi)核態(tài)或者系統(tǒng)調(diào)用,大大提高了通信的速度,享有最快的進(jìn)程間通信方式之名。但是便捷高效的共享內(nèi)存通信,帶來(lái)新的問(wèn)題,多進(jìn)程競(jìng)爭(zhēng)同個(gè)共享資源會(huì)造成數(shù)據(jù)的錯(cuò)亂。
那么,就需要信號(hào)量來(lái)保護(hù)共享資源,以確保任何時(shí)刻只能有一個(gè)進(jìn)程訪問(wèn)共享資源,這種方式就是互斥訪問(wèn)。信號(hào)量不僅可以實(shí)現(xiàn)訪問(wèn)的互斥性,還可以實(shí)現(xiàn)進(jìn)程間的同步,信號(hào)量其實(shí)是一個(gè)計(jì)數(shù)器,表示的是資源個(gè)數(shù),其值可以通過(guò)兩個(gè)原子操作來(lái)控制,分別是 P 操作和 V 操作。
與信號(hào)量名字很相似的叫信號(hào),它倆名字雖然相似,但功能一點(diǎn)兒都不一樣。信號(hào)是進(jìn)程間通信機(jī)制中唯一的異步通信機(jī)制,信號(hào)可以在應(yīng)用進(jìn)程和內(nèi)核之間直接交互,內(nèi)核也可以利用信號(hào)來(lái)通知用戶空間的進(jìn)程發(fā)生了哪些系統(tǒng)事件,信號(hào)事件的來(lái)源主要有硬件來(lái)源(如鍵盤(pán) Cltr+C )和軟件來(lái)源(如 kill 命令),一旦有信號(hào)發(fā)生,進(jìn)程有三種方式響應(yīng)信號(hào) 1. 執(zhí)行默認(rèn)操作、2. 捕捉信號(hào)、3. 忽略信號(hào)。有兩個(gè)信號(hào)是應(yīng)用進(jìn)程無(wú)法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,這是為了方便我們能在任何時(shí)候結(jié)束或停止某個(gè)進(jìn)程。
前面說(shuō)到的通信機(jī)制,都是工作于同一臺(tái)主機(jī),如果要與不同主機(jī)的進(jìn)程間通信,那么就需要 Socket 通信了。Socket 實(shí)際上不僅用于不同的主機(jī)進(jìn)程間通信,還可以用于本地主機(jī)進(jìn)程間通信,可根據(jù)創(chuàng)建 Socket 的類(lèi)型不同,分為三種常見(jiàn)的通信方式,一個(gè)是基于 TCP 協(xié)議的通信方式,一個(gè)是基于 UDP 協(xié)議的通信方式,一個(gè)是本地進(jìn)程間通信方式。
以上,就是進(jìn)程間通信的主要機(jī)制了。你可能會(huì)問(wèn)了,那線程通信間的方式呢?
同個(gè)進(jìn)程下的線程之間都是共享進(jìn)程的資源,只要是共享變量都可以做到線程間通信,比如全局變量,所以對(duì)于線程間關(guān)注的不是通信方式,而是關(guān)注多線程競(jìng)爭(zhēng)共享資源的問(wèn)題,信號(hào)量也同樣可以在線程間實(shí)現(xiàn)互斥與同步:
互斥的方式,可保證任意時(shí)刻只有一個(gè)線程訪問(wèn)共享資源;
同步的方式,可保證線程 A 應(yīng)在線程 B 之前執(zhí)行;
好了,今日幫張三同學(xué)復(fù)習(xí)就到這了,希望張三同學(xué)早日收到心意的 offer,給夏天劃上充滿汗水的句號(hào)。

好文推薦
飛天茅臺(tái)超賣(mài)事故:Redis分布式鎖請(qǐng)慎用!
優(yōu)雅停止 SpringBoot 服務(wù),拒絕 kill -9 暴力停止!
每秒 570000 的寫(xiě)入,MySQL如何實(shí)現(xiàn)?
