<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          因為沒答好進程間通信,面試掛了...

          共 7420字,需瀏覽 15分鐘

           ·

          2020-08-04 19:48


          前言

          開場小故事

          炎炎夏日,張三騎著單車去面試花了 1 小時,一路上汗流浹背。

          結(jié)果面試過程只花了 5 分鐘就結(jié)束了,面完的時候,天還是依然是亮的,還得在烈日下奔波 1 小時回去。

          面試五分鐘,騎車兩小時。

          你看,張三因面試沒準(zhǔn)備好,吹空調(diào)的時間只有 5 分鐘,來回路上花了 2 小時曬太陽,你說慘不慘?

          所以啊,炎炎夏日,為了能延長吹空調(diào)的時間,我們應(yīng)該在面試前準(zhǔn)備得更充分些,吹空調(diào)時間是要自己爭取的。

          很明顯,在這一場面試中, 張三在進程間通信這一塊沒復(fù)習(xí)好,雖然列出了進程間通信的方式,但這只是表面功夫,應(yīng)該需要進一步了解每種通信方式的優(yōu)缺點及應(yīng)用場景。

          說真的,我們這次一起幫張三一起復(fù)習(xí)下,加深他對進程間通信的理解,好讓他下次吹空調(diào)的時間能長一點。


          正文

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

          Linux 內(nèi)核提供了不少進程間通信的機制,我們來一起瞧瞧有哪些?

          管道

          如果你學(xué)過 Linux 命令,那你肯定很熟悉「|」這個豎線。

          $?ps?auxf?|?grep?mysql

          上面命令行里的「|」豎線就是一個管道,它的功能是將前一個命令(ps auxf)的輸出,作為后一個命令(grep mysql)的輸入,從這功能描述,可以看出管道傳輸數(shù)據(jù)是單向的,如果想相互通信,我們需要創(chuàng)建兩個管道才行。

          同時,我們得知上面這種管道是沒有名字,所以「|」表示的管道稱為匿名管道,用完了就銷毀。

          管道還有另外一個類型是命名管道,也被叫做 FIFO,因為數(shù)據(jù)是先進先出的傳輸方式。

          在使用命名管道前,先需要通過 mkfifo 命令來創(chuàng)建,并且指定管道名字:

          $?mkfifo?myPipe

          myPipe 就是這個管道的名稱,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我們可以用 ls 看一下,這個文件的類型是 p,也就是 pipe(管道) 的意思:

          $?ls?-l
          prw-r--r--. 1?root????root?????????0?Jul?17?02:45?myPipe

          接下來,我們往 myPipe 這個管道寫入數(shù)據(jù):

          $?echo?"hello"?>?myPipe??//?將數(shù)據(jù)寫進管道
          ?????????????????????????//?停住了?...

          你操作了后,你會發(fā)現(xiàn)命令執(zhí)行后就停在這了,這是因為管道里的內(nèi)容沒有被讀取,只有當(dāng)管道里的數(shù)據(jù)被讀完后,命令才可以正常退出。

          于是,我們執(zhí)行另外一個命令來讀取這個管道里的數(shù)據(jù):

          $?cat?hello

          可以看到,管道里的內(nèi)容被讀取出來了,并打印在了終端上,另外一方面,echo 那個命令也正常退出了。

          我們可以看出,管道這種通信方式效率低,不適合進程間頻繁地交換數(shù)據(jù)。當(dāng)然,它的好處,自然就是簡單,同時也我們很容易得知管道里的數(shù)據(jù)已經(jīng)被另一個進程讀取了。

          那管道如何創(chuàng)建呢,背后原理是什么?

          匿名管道的創(chuàng)建,需要通過下面這個系統(tǒng)調(diào)用:

          int?pipe(int?fd[2])

          這里表示創(chuàng)建一個匿名管道,并返回了兩個描述符,一個是管道的讀取端描述符 fd[0],另一個是管道的寫入端描述符 fd[1]。注意,這個匿名管道是特殊的文件,只存在于內(nèi)存,不存于文件系統(tǒng)中。

          其實,所謂的管道,就是內(nèi)核里面的一串緩存。從管道的一段寫入的數(shù)據(jù),實際上是緩存在內(nèi)核中的,另一端讀取,也就是從內(nèi)核中讀取這段數(shù)據(jù)。另外,管道傳輸?shù)臄?shù)據(jù)是無格式的流且大小受限。

          看到這,你可能會有疑問了,這兩個描述符都是在一個進程里面,并沒有起到進程間通信的作用,怎么樣才能使得管道是跨過兩個進程的呢?

          我們可以使用 fork 創(chuàng)建子進程,創(chuàng)建的子進程會復(fù)制父進程的文件描述符,這樣就做到了兩個進程各有兩個「 fd[0]fd[1]」,兩個進程就可以通過各自的 fd 寫入和讀取同一個管道文件實現(xiàn)跨進程通信了。

          管道只能一端寫入,另一端讀出,所以上面這種模式容易造成混亂,因為父進程和子進程都可以同時寫入,也都可以讀出。那么,為了避免這種情況,通常的做法是:

          • 父進程關(guān)閉讀取的 fd[0],只保留寫入的 fd[1];

          • 子進程關(guān)閉寫入的 fd[1],只保留讀取的 fd[0];

          所以說如果需要雙向通信,則應(yīng)該創(chuàng)建兩個管道。

          到這里,我們僅僅解析了使用管道進行父進程與子進程之間的通信,但是在我們 shell 里面并不是這樣的。

          在 shell 里面執(zhí)行 A | B命令的時候,A 進程和 B 進程都是 shell 創(chuàng)建出來的子進程,A 和 B 之間不存在父子關(guān)系,它倆的父進程都是 shell。

          所以說,在 shell 里通過「|」匿名管道將多個命令連接在一起,實際上也就是創(chuàng)建了多個子進程,那么在我們編寫 shell 腳本時,能使用一個管道搞定的事情,就不要多用一個管道,這樣可以減少創(chuàng)建子進程的系統(tǒng)開銷。

          我們可以得知,對于匿名管道,它的通信范圍是存在父子關(guān)系的進程。因為管道沒有實體,也就是沒有管道文件,只能通過 fork 來復(fù)制父進程 fd 文件描述符,來達到通信的目的。

          另外,對于命名管道,它可以在不相關(guān)的進程間也能相互通信。因為命令管道,提前創(chuàng)建了一個類型為管道的設(shè)備文件,在進程里只要使用這個設(shè)備文件,就可以相互通信。

          不管是匿名管道還是命名管道,進程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個進程讀取數(shù)據(jù)時候自然也是從內(nèi)核中獲取,同時通信數(shù)據(jù)都遵循先進先出原則,不支持 lseek 之類的文件定位操作。


          消息隊列

          前面說到管道的通信方式是效率低的,因此管道不適合進程間頻繁地交換數(shù)據(jù)。

          對于這個問題,消息隊列的通信模式就可以解決。比如,A 進程要給 B 進程發(fā)送消息,A 進程把數(shù)據(jù)放在對應(yīng)的消息隊列后就可以正常返回了,B 進程需要的時候再去讀取數(shù)據(jù)就可以了。同理,B 進程要給 A 進程發(fā)送消息也是如此。

          再來,消息隊列是保存在內(nèi)核中的消息鏈表,在發(fā)送數(shù)據(jù)時,會分成一個一個獨立的數(shù)據(jù)單元,也就是消息體(數(shù)據(jù)塊),消息體是用戶自定義的數(shù)據(jù)類型,消息的發(fā)送方和接收方要約定好消息體的數(shù)據(jù)類型,所以每個消息體都是固定大小的存儲塊,不像管道是無格式的字節(jié)流數(shù)據(jù)。如果進程從消息隊列中讀取了消息體,內(nèi)核就會把這個消息體刪除。

          消息隊列生命周期隨內(nèi)核,如果沒有釋放消息隊列或者沒有關(guān)閉操作系統(tǒng),消息隊列會一直存在,而前面提到的匿名管道的生命周期,是隨進程的創(chuàng)建而建立,隨進程的結(jié)束而銷毀。

          消息這種模型,兩個進程之間的通信就像平時發(fā)郵件一樣,你來一封,我回一封,可以頻繁溝通了。

          但郵件的通信方式存在不足的地方有兩點,一是通信不及時,二是附件也有大小限制,這同樣也是消息隊列通信不足的點。

          消息隊列不適合比較大數(shù)據(jù)的傳輸,因為在內(nèi)核中每個消息體都有一個最大長度的限制,同時所有隊列所包含的全部消息體的總長度也是有上限。在 Linux 內(nèi)核中,會有兩個宏定義 MSGMAXMSGMNB,它們以字節(jié)為單位,分別定義了一條消息的最大長度和一個隊列的最大長度。

          消息隊列通信過程中,存在用戶態(tài)與內(nèi)核態(tài)之間的數(shù)據(jù)拷貝開銷,因為進程寫入數(shù)據(jù)到內(nèi)核中的消息隊列時,會發(fā)生從用戶態(tài)拷貝數(shù)據(jù)到內(nèi)核態(tài)的過程,同理另一進程讀取內(nèi)核中的消息數(shù)據(jù)時,會發(fā)生從內(nèi)核態(tài)拷貝數(shù)據(jù)到用戶態(tài)的過程。


          共享內(nèi)存

          消息隊列的讀取和寫入的過程,都會有發(fā)生用戶態(tài)與內(nèi)核態(tài)之間的消息拷貝過程。那共享內(nèi)存的方式,就很好的解決了這一問題。

          現(xiàn)代操作系統(tǒng),對于內(nèi)存管理,采用的是虛擬內(nèi)存技術(shù),也就是每個進程都有自己獨立的虛擬內(nèi)存空間,不同進程的虛擬內(nèi)存映射到不同的物理內(nèi)存中。所以,即使進程 A 和 進程 B 的虛擬地址是一樣的,其實訪問的是不同的物理內(nèi)存地址,對于數(shù)據(jù)的增刪查改互不影響。

          共享內(nèi)存的機制,就是拿出一塊虛擬地址空間來,映射到相同的物理內(nèi)存中。這樣這個進程寫入的東西,另外一個進程馬上就能看到了,都不需要拷貝來拷貝去,傳來傳去,大大提高了進程間通信的速度。


          信號量

          用了共享內(nèi)存通信方式,帶來新的問題,那就是如果多個進程同時修改同一個共享內(nèi)存,很有可能就沖突了。例如兩個進程都同時寫一個地址,那先寫的那個進程會發(fā)現(xiàn)內(nèi)容被別人覆蓋了。

          為了防止多進程競爭共享資源,而造成的數(shù)據(jù)錯亂,所以需要保護機制,使得共享的資源,在任意時刻只能被一個進程訪問。正好,信號量就實現(xiàn)了這一保護機制。

          信號量其實是一個整型的計數(shù)器,主要用于實現(xiàn)進程間的互斥與同步,而不是用于緩存進程間通信的數(shù)據(jù)

          信號量表示資源的數(shù)量,控制信號量的方式有兩種原子操作:

          • 一個是 P 操作,這個操作會把信號量減去 -1,相減后如果信號量 < 0,則表明資源已被占用,進程需阻塞等待;相減后如果信號量 >= 0,則表明還有資源可使用,進程可正常繼續(xù)執(zhí)行。

          • 另一個是 V 操作,這個操作會把信號量加上 1,相加后如果信號量 <= 0,則表明當(dāng)前有阻塞中的進程,于是會將該進程喚醒運行;相加后如果信號量 > 0,則表明當(dāng)前沒有阻塞中的進程;

          P 操作是用在進入共享資源之前,V 操作是用在離開共享資源之后,這兩個操作是必須成對出現(xiàn)的。

          接下來,舉個例子,如果要使得兩個進程互斥訪問共享內(nèi)存,我們可以初始化信號量為 1。

          具體的過程如下:

          • 進程 A 在訪問共享內(nèi)存前,先執(zhí)行了 P 操作,由于信號量的初始值為 1,故在進程 A 執(zhí)行 P 操作后信號量變?yōu)?0,表示共享資源可用,于是進程 A 就可以訪問共享內(nèi)存。

          • 若此時,進程 B 也想訪問共享內(nèi)存,執(zhí)行了 P 操作,結(jié)果信號量變?yōu)榱?-1,這就意味著臨界資源已被占用,因此進程 B 被阻塞。

          • 直到進程 A 訪問完共享內(nèi)存,才會執(zhí)行 V 操作,使得信號量恢復(fù)為 0,接著就會喚醒阻塞中的線程 B,使得進程 B 可以訪問共享內(nèi)存,最后完成共享內(nèi)存的訪問后,執(zhí)行 V 操作,使信號量恢復(fù)到初始值 1。

          可以發(fā)現(xiàn),信號初始化為 1,就代表著是互斥信號量,它可以保證共享內(nèi)存在任何時刻只有一個進程在訪問,這就很好的保護了共享內(nèi)存。

          另外,在多進程里,每個進程并不一定是順序執(zhí)行的,它們基本是以各自獨立的、不可預(yù)知的速度向前推進,但有時候我們又希望多個進程能密切合作,以實現(xiàn)一個共同的任務(wù)。

          例如,進程 A 是負責(zé)生產(chǎn)數(shù)據(jù),而進程 B 是負責(zé)讀取數(shù)據(jù),這兩個進程是相互合作、相互依賴的,進程 A 必須先生產(chǎn)了數(shù)據(jù),進程 B 才能讀取到數(shù)據(jù),所以執(zhí)行是有前后順序的。

          那么這時候,就可以用信號量來實現(xiàn)多進程同步的方式,我們可以初始化信號量為 0。

          具體過程:

          • 如果進程 B 比進程 A 先執(zhí)行了,那么執(zhí)行到 P 操作時,由于信號量初始值為 0,故信號量會變?yōu)?-1,表示進程 A 還沒生產(chǎn)數(shù)據(jù),于是進程 B 就阻塞等待;

          • 接著,當(dāng)進程 A 生產(chǎn)完數(shù)據(jù)后,執(zhí)行了 V 操作,就會使得信號量變?yōu)?0,于是就會喚醒阻塞在 P 操作的進程 B;

          • 最后,進程 B 被喚醒后,意味著進程 A 已經(jīng)生產(chǎn)了數(shù)據(jù),于是進程 B 就可以正常讀取數(shù)據(jù)了。

          可以發(fā)現(xiàn),信號初始化為 0,就代表著是同步信號量,它可以保證進程 A 應(yīng)在進程 B 之前執(zhí)行。


          信號

          上面說的進程間通信,都是常規(guī)狀態(tài)下的工作模式。對于異常情況下的工作模式,就需要用「信號」的方式來通知進程。

          信號跟信號量雖然名字相似度 66.66%,但兩者用途完全不一樣,就好像 Java 和 JavaScript 的區(qū)別。

          在 Linux 操作系統(tǒng)中, 為了響應(yīng)各種各樣的事件,提供了幾十種信號,分別代表不同的意義。我們可以通過 kill -l 命令,查看所有的信號:

          $?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

          運行在 shell 終端的進程,我們可以通過鍵盤輸入某些組合鍵的時候,給進程發(fā)送信號。例如

          • Ctrl+C 產(chǎn)生 SIGINT 信號,表示終止該進程;

          • Ctrl+Z 產(chǎn)生 SIGTSTP 信號,表示停止該進程,但還未結(jié)束;

          如果進程在后臺運行,可以通過 kill 命令的方式給進程發(fā)送信號,但前提需要知道運行中的進程 PID 號,例如:

          • kill -9 1050 ,表示給 PID 為 1050 的進程發(fā)送 SIGKILL 信號,用來立即結(jié)束該進程;

          所以,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令)。

          信號是進程間通信機制中唯一的異步通信機制,因為可以在任何時候發(fā)送信號給某一進程,一旦有信號產(chǎn)生,我們就有下面這幾種,用戶進程對信號的處理方式。

          1.執(zhí)行默認(rèn)操作。Linux 對每種信號都規(guī)定了默認(rèn)操作,例如,上面列表中的 SIGTERM 信號,就是終止進程的意思。Core 的意思是 Core Dump,也即終止進程后,通過 Core Dump 將當(dāng)前進程的運行狀態(tài)保存在文件里面,方便程序員事后進行分析問題在哪里。

          2.捕捉信號。我們可以為信號定義一個信號處理函數(shù)。當(dāng)信號發(fā)生時,我們就執(zhí)行相應(yīng)的信號處理函數(shù)。

          3.忽略信號。當(dāng)我們不希望處理某些信號的時候,就可以忽略該信號,不做任何處理。有兩個信號是應(yīng)用進程無法捕捉和忽略的,即 SIGKILLSEGSTOP,它們用于在任何時候中斷或結(jié)束某一進程。


          Socket

          前面提到的管道、消息隊列、共享內(nèi)存、信號量和信號都是在同一臺主機上進行進程間通信,那要想跨網(wǎng)絡(luò)與不同主機上的進程之間通信,就需要 Socket 通信了。

          實際上,Socket 通信不僅可以跨網(wǎng)絡(luò)與不同主機的進程間通信,還可以在同主機上進程間通信。

          我們來看看創(chuàng)建 socket 的系統(tǒng)調(diào)用:

          int?socket(int?domain,?int?type,?int?protocal)

          三個參數(shù)分別代表:

          • domain 參數(shù)用來指定協(xié)議族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本機;

          • type 參數(shù)用來指定通信特性,比如 SOCK_STREAM 表示的是字節(jié)流,對應(yīng) TCP、SOCK_DGRAM ?表示的是數(shù)據(jù)報,對應(yīng) UDP、SOCK_RAW 表示的是原始套接字;

          • protocal 參數(shù)原本是用來指定通信協(xié)議的,但現(xiàn)在基本廢棄。因為協(xié)議已經(jīng)通過前面兩個參數(shù)指定完成,protocol 目前一般寫成 0 即可;

          根據(jù)創(chuàng)建 socket 類型的不同,通信的方式也就不同:

          • 實現(xiàn) TCP 字節(jié)流通信:socket 類型是 AF_INET 和 SOCK_STREAM;

          • 實現(xiàn) UDP 數(shù)據(jù)報通信:socket 類型是 AF_INET 和 SOCK_DGRAM;

          • 實現(xiàn)本地進程間通信:「本地字節(jié)流 socket 」類型是 AF_LOCAL 和 SOCK_STREAM,「本地數(shù)據(jù)報 socket 」類型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等價的,所以 AF_UNIX 也屬于本地 socket;

          接下來,簡單說一下這三種通信的編程模式。

          針對 TCP 協(xié)議通信的 socket 編程模型

          • 服務(wù)端和客戶端初始化 socket,得到文件描述符;

          • 服務(wù)端調(diào)用 bind,將綁定在 IP 地址和端口;

          • 服務(wù)端調(diào)用 listen,進行監(jiān)聽;

          • 服務(wù)端調(diào)用 accept,等待客戶端連接;

          • 客戶端調(diào)用 connect,向服務(wù)器端的地址和端口發(fā)起連接請求;

          • 服務(wù)端 accept 返回用于傳輸?shù)?socket 的文件描述符;

          • 客戶端調(diào)用 write 寫入數(shù)據(jù);服務(wù)端調(diào)用 read 讀取數(shù)據(jù);

          • 客戶端斷開連接時,會調(diào)用 close,那么服務(wù)端 read 讀取數(shù)據(jù)的時候,就會讀取到了 EOF,待處理完數(shù)據(jù)后,服務(wù)端調(diào)用 close,表示連接關(guān)閉。

          這里需要注意的是,服務(wù)端調(diào)用 accept 時,連接成功了會返回一個已完成連接的 socket,后續(xù)用來傳輸數(shù)據(jù)。

          所以,監(jiān)聽的 socket 和真正用來傳送數(shù)據(jù)的 socket,是「兩個」 socket,一個叫作監(jiān)聽 socket,一個叫作已完成連接 socket。

          成功連接建立之后,雙方開始通過 read 和 write 函數(shù)來讀寫數(shù)據(jù),就像往一個文件流里面寫東西一樣。

          針對 UDP 協(xié)議通信的 socket 編程模型

          UDP 是沒有連接的,所以不需要三次握手,也就不需要像 TCP 調(diào)用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口號,因此也需要 bind。

          對于 UDP 來說,不需要要維護連接,那么也就沒有所謂的發(fā)送方和接收方,甚至都不存在客戶端和服務(wù)端的概念,只要有一個 socket 多臺機器就可以任意通信,因此每一個 UDP 的 socket 都需要 bind。

          另外,每次通信時,調(diào)用 sendto 和 recvfrom,都要傳入目標(biāo)主機的 IP 地址和端口。

          針對本地進程間通信的 socket 編程模型

          本地 socket ?被用于在同一臺主機上進程間通信的場景:

          • 本地 socket 的編程接口和 IPv4 、IPv6 套接字編程接口是一致的,可以支持「字節(jié)流」和「數(shù)據(jù)報」兩種協(xié)議;

          • 本地 socket 的實現(xiàn)效率大大高于 IPv4 和 IPv6 的字節(jié)流、數(shù)據(jù)報 socket 實現(xiàn);

          對于本地字節(jié)流 socket,其 socket 類型是 AF_LOCAL 和 SOCK_STREAM。

          對于本地數(shù)據(jù)報 socket,其 socket 類型是 AF_LOCAL 和 SOCK_DGRAM。

          本地字節(jié)流 socket 和 本地數(shù)據(jù)報 socket 在 bind 的時候,不像 TCP 和 UDP 要綁定 IP 地址和端口,而是綁定一個本地文件,這也就是它們之間的最大區(qū)別。


          總結(jié)

          由于每個進程的用戶空間都是獨立的,不能相互訪問,這時就需要借助內(nèi)核空間來實現(xiàn)進程間通信,原因很簡單,每個進程都是共享一個內(nèi)核空間。

          Linux 內(nèi)核提供了不少進程間通信的方式,其中最簡單的方式就是管道,管道分為「匿名管道」和「命名管道」。

          匿名管道顧名思義,它沒有名字標(biāo)識,匿名管道是特殊文件只存在于內(nèi)存,沒有存在于文件系統(tǒng)中,shell 命令中的「|」豎線就是匿名管道,通信的數(shù)據(jù)是無格式的流并且大小受限,通信的方式是單向的,數(shù)據(jù)只能在一個方向上流動,如果要雙向通信,需要創(chuàng)建兩個管道,再來匿名管道是只能用于存在父子關(guān)系的進程間通信,匿名管道的生命周期隨著進程創(chuàng)建而建立,隨著進程終止而消失。

          命名管道突破了匿名管道只能在親緣關(guān)系進程間的通信限制,因為使用命名管道的前提,需要在文件系統(tǒng)創(chuàng)建一個類型為 p 的設(shè)備文件,那么毫無關(guān)系的進程就可以通過這個設(shè)備文件進行通信。另外,不管是匿名管道還是命名管道,進程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個進程讀取數(shù)據(jù)時候自然也是從內(nèi)核中獲取,同時通信數(shù)據(jù)都遵循先進先出原則,不支持 lseek 之類的文件定位操作。

          消息隊列克服了管道通信的數(shù)據(jù)是無格式的字節(jié)流的問題,消息隊列實際上是保存在內(nèi)核的「消息鏈表」,消息隊列的消息體是可以用戶自定義的數(shù)據(jù)類型,發(fā)送數(shù)據(jù)時,會被分成一個一個獨立的消息體,當(dāng)然接收數(shù)據(jù)時,也要與發(fā)送方發(fā)送的消息體的數(shù)據(jù)類型保持一致,這樣才能保證讀取的數(shù)據(jù)是正確的。消息隊列通信的速度不是最及時的,畢竟每次數(shù)據(jù)的寫入和讀取都需要經(jīng)過用戶態(tài)與內(nèi)核態(tài)之間的拷貝過程。

          共享內(nèi)存可以解決消息隊列通信中用戶態(tài)與內(nèi)核態(tài)之間數(shù)據(jù)拷貝過程帶來的開銷,它直接分配一個共享空間,每個進程都可以直接訪問,就像訪問進程自己的空間一樣快捷方便,不需要陷入內(nèi)核態(tài)或者系統(tǒng)調(diào)用,大大提高了通信的速度,享有最快的進程間通信方式之名。但是便捷高效的共享內(nèi)存通信,帶來新的問題,多進程競爭同個共享資源會造成數(shù)據(jù)的錯亂。

          那么,就需要信號量來保護共享資源,以確保任何時刻只能有一個進程訪問共享資源,這種方式就是互斥訪問。信號量不僅可以實現(xiàn)訪問的互斥性,還可以實現(xiàn)進程間的同步,信號量其實是一個計數(shù)器,表示的是資源個數(shù),其值可以通過兩個原子操作來控制,分別是 P 操作和 V 操作。

          與信號量名字很相似的叫信號,它倆名字雖然相似,但功能一點兒都不一樣。信號是進程間通信機制中唯一的異步通信機制,信號可以在應(yīng)用進程和內(nèi)核之間直接交互,內(nèi)核也可以利用信號來通知用戶空間的進程發(fā)生了哪些系統(tǒng)事件,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令),一旦有信號發(fā)生,進程有三種方式響應(yīng)信號 1. 執(zhí)行默認(rèn)操作、2. 捕捉信號、3. 忽略信號。有兩個信號是應(yīng)用進程無法捕捉和忽略的,即 SIGKILLSEGSTOP,這是為了方便我們能在任何時候結(jié)束或停止某個進程。

          前面說到的通信機制,都是工作于同一臺主機,如果要與不同主機的進程間通信,那么就需要 Socket 通信了。Socket 實際上不僅用于不同的主機進程間通信,還可以用于本地主機進程間通信,可根據(jù)創(chuàng)建 Socket 的類型不同,分為三種常見的通信方式,一個是基于 TCP 協(xié)議的通信方式,一個是基于 UDP 協(xié)議的通信方式,一個是本地進程間通信方式。

          以上,就是進程間通信的主要機制了。你可能會問了,那線程通信間的方式呢?

          同個進程下的線程之間都是共享進程的資源,只要是共享變量都可以做到線程間通信,比如全局變量,所以對于線程間關(guān)注的不是通信方式,而是關(guān)注多線程競爭共享資源的問題,信號量也同樣可以在線程間實現(xiàn)互斥與同步:

          • 互斥的方式,可保證任意時刻只有一個線程訪問共享資源;

          • 同步的方式,可保證線程 A 應(yīng)在線程 B 之前執(zhí)行;

          好了,今日幫張三同學(xué)復(fù)習(xí)就到這了,希望張三同學(xué)早日收到心意的 offer,給夏天劃上充滿汗水的句號。


          好文推薦

          飛天茅臺超賣事故:Redis分布式鎖請慎用!

          圖解一致性哈希算法,通俗易懂!

          程序員面試指南,你離大廠Offer不遠了

          優(yōu)雅停止 SpringBoot 服務(wù),拒絕 kill -9 暴力停止!

          每秒 570000 的寫入,MySQL如何實現(xiàn)?


          瀏覽 30
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伦理片一区二区 | 欧美精品18 | 一区二区3区免费 | 成人自拍小视频 | 青青青草视频 |