c++ fork 进程时 共享内存_因为没答好进程间通信,面试挂了...
前言
開場小故事
炎炎夏日,張三騎著單車去面試花了 1 小時,一路上汗流浹背。
結(jié)果面試過程只花了 5 分鐘就結(jié)束了,面完的時候,天還是依然是亮的,還得在烈日下奔波 1 小時回去。
面試五分鐘,騎車兩小時。
你看,張三因面試沒準備好,吹空調(diào)的時間只有 5 分鐘,來回路上花了 2 小時曬太陽,你說慘不慘?
所以啊,炎炎夏日,為了能延長吹空調(diào)的時間,我們應該在面試前準備得更充分些,吹空調(diào)時間是要自己爭取的。
很明顯,在這一場面試中, 張三在進程間通信這一塊沒復習好,雖然列出了進程間通信的方式,但這只是表面功夫,應該需要進一步了解每種通信方式的優(yōu)缺點及應用場景。
說真的,我們這次一起幫張三一起復習下,加深他對進程間通信的理解,好讓他下次吹空調(diào)的時間能長一點。
正文
每個進程的用戶地址空間都是獨立的,一般而言是不能互相訪問的,但內(nèi)核空間是每個進程都共享的,所以進程之間要通信必須通過內(nèi)核。
Linux 內(nèi)核提供了不少進程間通信的機制,我們來一起瞧瞧有哪些?
管道
如果你學過 Linux 命令,那你肯定很熟悉「|」這個豎線。
$?ps?auxf?|?grep?mysql上面命令行里的「|」豎線就是一個管道,它的功能是將前一個命令(ps auxf)的輸出,作為后一個命令(grep mysql)的輸入,從這功能描述,可以看出管道傳輸數(shù)據(jù)是單向的,如果想相互通信,我們需要創(chuàng)建兩個管道才行。
同時,我們得知上面這種管道是沒有名字,所以「|」表示的管道稱為匿名管道,用完了就銷毀。
管道還有另外一個類型是命名管道,也被叫做 FIFO,因為數(shù)據(jù)是先進先出的傳輸方式。
在使用命名管道前,先需要通過 mkfifo 命令來創(chuàng)建,并且指定管道名字:
$?mkfifo?myPipemyPipe 就是這個管道的名稱,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我們可以用 ls 看一下,這個文件的類型是 p,也就是 pipe(管道) 的意思:
$?ls?-lprw-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)容沒有被讀取,只有當管道里的數(shù)據(jù)被讀完后,命令才可以正常退出。
于是,我們執(zhí)行另外一個命令來讀取這個管道里的數(shù)據(jù):
$?cat?hello可以看到,管道里的內(nèi)容被讀取出來了,并打印在了終端上,另外一方面,echo 那個命令也正常退出了。
我們可以看出,管道這種通信方式效率低,不適合進程間頻繁地交換數(shù)據(jù)。當然,它的好處,自然就是簡單,同時也我們很容易得知管道里的數(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)建的子進程會復制父進程的文件描述符,這樣就做到了兩個進程各有兩個「 fd[0] 與 fd[1]」,兩個進程就可以通過各自的 fd 寫入和讀取同一個管道文件實現(xiàn)跨進程通信了。
管道只能一端寫入,另一端讀出,所以上面這種模式容易造成混亂,因為父進程和子進程都可以同時寫入,也都可以讀出。那么,為了避免這種情況,通常的做法是:
父進程關閉讀取的 fd[0],只保留寫入的 fd[1];
子進程關閉寫入的 fd[1],只保留讀取的 fd[0];
所以說如果需要雙向通信,則應該創(chuàng)建兩個管道。
到這里,我們僅僅解析了使用管道進行父進程與子進程之間的通信,但是在我們 shell 里面并不是這樣的。
在 shell 里面執(zhí)行 A | B命令的時候,A 進程和 B 進程都是 shell 創(chuàng)建出來的子進程,A 和 B 之間不存在父子關系,它倆的父進程都是 shell。
所以說,在 shell 里通過「|」匿名管道將多個命令連接在一起,實際上也就是創(chuàng)建了多個子進程,那么在我們編寫 shell 腳本時,能使用一個管道搞定的事情,就不要多用一個管道,這樣可以減少創(chuàng)建子進程的系統(tǒng)開銷。
我們可以得知,對于匿名管道,它的通信范圍是存在父子關系的進程。因為管道沒有實體,也就是沒有管道文件,只能通過 fork 來復制父進程 fd 文件描述符,來達到通信的目的。
另外,對于命名管道,它可以在不相關的進程間也能相互通信。因為命令管道,提前創(chuàng)建了一個類型為管道的設備文件,在進程里只要使用這個設備文件,就可以相互通信。
不管是匿名管道還是命名管道,進程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個進程讀取數(shù)據(jù)時候自然也是從內(nèi)核中獲取,同時通信數(shù)據(jù)都遵循先進先出原則,不支持 lseek 之類的文件定位操作。
消息隊列
前面說到管道的通信方式是效率低的,因此管道不適合進程間頻繁地交換數(shù)據(jù)。
對于這個問題,消息隊列的通信模式就可以解決。比如,A 進程要給 B 進程發(fā)送消息,A 進程把數(shù)據(jù)放在對應的消息隊列后就可以正常返回了,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)核,如果沒有釋放消息隊列或者沒有關閉操作系統(tǒng),消息隊列會一直存在,而前面提到的匿名管道的生命周期,是隨進程的創(chuàng)建而建立,隨進程的結(jié)束而銷毀。
消息這種模型,兩個進程之間的通信就像平時發(fā)郵件一樣,你來一封,我回一封,可以頻繁溝通了。
但郵件的通信方式存在不足的地方有兩點,一是通信不及時,二是附件也有大小限制,這同樣也是消息隊列通信不足的點。
消息隊列不適合比較大數(shù)據(jù)的傳輸,因為在內(nèi)核中每個消息體都有一個最大長度的限制,同時所有隊列所包含的全部消息體的總長度也是有上限。在 Linux 內(nèi)核中,會有兩個宏定義 MSGMAX 和 MSGMNB,它們以字節(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)存技術,也就是每個進程都有自己獨立的虛擬內(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,則表明當前有阻塞中的進程,于是會將該進程喚醒運行;相加后如果信號量 > 0,則表明當前沒有阻塞中的進程;
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 操作,使得信號量恢復為 0,接著就會喚醒阻塞中的線程 B,使得進程 B 可以訪問共享內(nèi)存,最后完成共享內(nèi)存的訪問后,執(zhí)行 V 操作,使信號量恢復到初始值 1。
可以發(fā)現(xiàn),信號初始化為 1,就代表著是互斥信號量,它可以保證共享內(nèi)存在任何時刻只有一個進程在訪問,這就很好的保護了共享內(nèi)存。
另外,在多進程里,每個進程并不一定是順序執(zhí)行的,它們基本是以各自獨立的、不可預知的速度向前推進,但有時候我們又希望多個進程能密切合作,以實現(xiàn)一個共同的任務。
例如,進程 A 是負責生產(chǎn)數(shù)據(jù),而進程 B 是負責讀取數(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 就阻塞等待;
接著,當進程 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 應在進程 B 之前執(zhí)行。
信號
上面說的進程間通信,都是常規(guī)狀態(tài)下的工作模式。對于異常情況下的工作模式,就需要用「信號」的方式來通知進程。
信號跟信號量雖然名字相似度 66.66%,但兩者用途完全不一樣,就好像 Java 和 JavaScript 的區(qū)別。
在 Linux 操作系統(tǒ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í)行默認操作。Linux 對每種信號都規(guī)定了默認操作,例如,上面列表中的 SIGTERM 信號,就是終止進程的意思。Core 的意思是 Core Dump,也即終止進程后,通過 Core Dump 將當前進程的運行狀態(tài)保存在文件里面,方便程序員事后進行分析問題在哪里。
2.捕捉信號。我們可以為信號定義一個信號處理函數(shù)。當信號發(fā)生時,我們就執(zhí)行相應的信號處理函數(shù)。
3.忽略信號。當我們不希望處理某些信號的時候,就可以忽略該信號,不做任何處理。有兩個信號是應用進程無法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它們用于在任何時候中斷或結(jié)束某一進程。
Socket
前面提到的管道、消息隊列、共享內(nèi)存、信號量和信號都是在同一臺主機上進行進程間通信,那要想跨網(wǎng)絡與不同主機上的進程之間通信,就需要 Socket 通信了。
實際上,Socket 通信不僅可以跨網(wǎng)絡與不同主機的進程間通信,還可以在同主機上進程間通信。
我們來看看創(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é)流,對應 TCP、SOCK_DGRAM ?表示的是數(shù)據(jù)報,對應 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 編程模型
服務端和客戶端初始化 socket,得到文件描述符;
服務端調(diào)用 bind,將綁定在 IP 地址和端口;
服務端調(diào)用 listen,進行監(jiān)聽;
服務端調(diào)用 accept,等待客戶端連接;
客戶端調(diào)用 connect,向服務器端的地址和端口發(fā)起連接請求;
服務端 accept 返回用于傳輸?shù)?socket 的文件描述符;
客戶端調(diào)用 write 寫入數(shù)據(jù);服務端調(diào)用 read 讀取數(shù)據(jù);
客戶端斷開連接時,會調(diào)用 close,那么服務端 read 讀取數(shù)據(jù)的時候,就會讀取到了 EOF,待處理完數(shù)據(jù)后,服務端調(diào)用 close,表示連接關閉。
這里需要注意的是,服務端調(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ā)送方和接收方,甚至都不存在客戶端和服務端的概念,只要有一個 socket 多臺機器就可以任意通信,因此每一個 UDP 的 socket 都需要 bind。
另外,每次通信時,調(diào)用 sendto 和 recvfrom,都要傳入目標主機的 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)核提供了不少進程間通信的方式,其中最簡單的方式就是管道,管道分為「匿名管道」和「命名管道」。
匿名管道顧名思義,它沒有名字標識,匿名管道是特殊文件只存在于內(nèi)存,沒有存在于文件系統(tǒng)中,shell 命令中的「|」豎線就是匿名管道,通信的數(shù)據(jù)是無格式的流并且大小受限,通信的方式是單向的,數(shù)據(jù)只能在一個方向上流動,如果要雙向通信,需要創(chuàng)建兩個管道,再來匿名管道是只能用于存在父子關系的進程間通信,匿名管道的生命周期隨著進程創(chuàng)建而建立,隨著進程終止而消失。
命名管道突破了匿名管道只能在親緣關系進程間的通信限制,因為使用命名管道的前提,需要在文件系統(tǒng)創(chuàng)建一個類型為 p 的設備文件,那么毫無關系的進程就可以通過這個設備文件進行通信。另外,不管是匿名管道還是命名管道,進程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個進程讀取數(shù)據(jù)時候自然也是從內(nèi)核中獲取,同時通信數(shù)據(jù)都遵循先進先出原則,不支持 lseek 之類的文件定位操作。
消息隊列克服了管道通信的數(shù)據(jù)是無格式的字節(jié)流的問題,消息隊列實際上是保存在內(nèi)核的「消息鏈表」,消息隊列的消息體是可以用戶自定義的數(shù)據(jù)類型,發(fā)送數(shù)據(jù)時,會被分成一個一個獨立的消息體,當然接收數(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 操作。
與信號量名字很相似的叫信號,它倆名字雖然相似,但功能一點兒都不一樣。信號是進程間通信機制中唯一的異步通信機制,信號可以在應用進程和內(nèi)核之間直接交互,內(nèi)核也可以利用信號來通知用戶空間的進程發(fā)生了哪些系統(tǒng)事件,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令),一旦有信號發(fā)生,進程有三種方式響應信號 1. 執(zhí)行默認操作、2. 捕捉信號、3. 忽略信號。有兩個信號是應用進程無法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,這是為了方便我們能在任何時候結(jié)束或停止某個進程。
前面說到的通信機制,都是工作于同一臺主機,如果要與不同主機的進程間通信,那么就需要 Socket 通信了。Socket 實際上不僅用于不同的主機進程間通信,還可以用于本地主機進程間通信,可根據(jù)創(chuàng)建 Socket 的類型不同,分為三種常見的通信方式,一個是基于 TCP 協(xié)議的通信方式,一個是基于 UDP 協(xié)議的通信方式,一個是本地進程間通信方式。
以上,就是進程間通信的主要機制了。你可能會問了,那線程通信間的方式呢?
同個進程下的線程之間都是共享進程的資源,只要是共享變量都可以做到線程間通信,比如全局變量,所以對于線程間關注的不是通信方式,而是關注多線程競爭共享資源的問題,信號量也同樣可以在線程間實現(xiàn)互斥與同步:
互斥的方式,可保證任意時刻只有一個線程訪問共享資源;
同步的方式,可保證線程 A 應在線程 B 之前執(zhí)行;
好了,今日幫張三同學復習就到這了,希望張三同學早日收到心意的 offer,給夏天劃上充滿汗水的句號。
好文推薦
飛天茅臺超賣事故:Redis分布式鎖請慎用!
圖解一致性哈希算法,通俗易懂!
程序員面試指南,你離大廠Offer不遠了
優(yōu)雅停止 SpringBoot 服務,拒絕 kill -9 暴力停止!
每秒 570000 的寫入,MySQL如何實現(xiàn)?
總結(jié)
以上是生活随笔為你收集整理的c++ fork 进程时 共享内存_因为没答好进程间通信,面试挂了...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql跨表查询_跨表查询经常有,何为跨表
- 下一篇: python脚本编写_如何用Python