在Linux環(huán)境下運(yùn)行程序,無論是點(diǎn)擊桌面上的一個(gè)圖標(biāo),還是在命令行下敲擊一個(gè)shell命令,Linux系統(tǒng)都會(huì)把我們的程序“包裝”成一個(gè)進(jìn)程的形式,然后調(diào)度運(yùn)行:每個(gè)進(jìn)程輪流占用CPU一段時(shí)間去執(zhí)行,時(shí)間到了就讓給其它進(jìn)程,時(shí)間片輪轉(zhuǎn),只要輪轉(zhuǎn)得速度足夠快,就會(huì)給用戶一種錯(cuò)覺:我們?cè)陔娔X上一邊聽歌,一邊打字,感覺多個(gè)程序在同時(shí)運(yùn)行。不同進(jìn)程在運(yùn)行過程中,根據(jù)業(yè)務(wù)需要,進(jìn)程相互之間也會(huì)通信:比如傳輸數(shù)據(jù)、發(fā)送信號(hào)等。
Linux環(huán)境下的進(jìn)程間通信(Inter-Process Communication,簡(jiǎn)稱IPC)有多種工具可以使用,如:無名管道pipe、命名管道FIFO、消息隊(duì)列、共享內(nèi)存、信號(hào)量、信號(hào)、文件鎖、socket等。這些IPC工具以系統(tǒng)調(diào)用或庫函數(shù)API的形式提供給用戶使用:用戶使用這些API可以在不同的進(jìn)程之間傳輸數(shù)據(jù)、同步進(jìn)程、或者發(fā)送信號(hào)。比如,我們可以使用ctrl+C組合鍵去終止一個(gè)進(jìn)程,或者使用shell命令kill 3567去殺死一個(gè)進(jìn)程pid為3567的進(jìn)程,這些其實(shí)都是給進(jìn)程發(fā)送信號(hào),進(jìn)程接收信號(hào)并進(jìn)行處理的過程。
不同的IPC工具,使用場(chǎng)合不同,各有優(yōu)劣。為了更好地使用它們,我們不僅要熟練掌握API接口的使用,還要對(duì)它們的通信機(jī)制、內(nèi)核實(shí)現(xiàn)原理有一個(gè)大致的了解。只有掌握了底層的實(shí)現(xiàn)原理、我們才能明白每個(gè)IPC通信工具的優(yōu)點(diǎn)和缺點(diǎn)、以及他們的使用場(chǎng)合。想要真正理解Linux進(jìn)程之間到底是如何通信的,首先要搞明白Linux下的不同進(jìn)程在運(yùn)行過程中,在內(nèi)存中是以什么樣的形態(tài)存在的,以及與Linux內(nèi)核之間是如何交互的。想要理解這點(diǎn),我們還需要對(duì)Linux環(huán)境下程序的編譯、執(zhí)行過程有一個(gè)大概的了解。
1 程序的編譯和執(zhí)行
當(dāng)我們?cè)谧烂嫔宵c(diǎn)擊一個(gè)圖標(biāo),或者在命令行下敲擊一個(gè)shell命令運(yùn)行時(shí),Linux系統(tǒng)會(huì)把這些可執(zhí)行文件加載到內(nèi)存,并封裝成一個(gè)進(jìn)程,然后才能參與操作系統(tǒng)的調(diào)度、運(yùn)行。那操作系統(tǒng)是如何加載的呢?

首先,我們編寫的C語言源代碼會(huì)編譯成一個(gè)可執(zhí)行文件(ELF)。可執(zhí)行文件分由各種不同的段(section)組成:代碼段、數(shù)據(jù)段、BSS段等。我們C程序中的不同代碼會(huì)被編譯到不同的段中:函數(shù)實(shí)現(xiàn)會(huì)放到代碼段;全局變量、靜態(tài)局部變量會(huì)放到數(shù)據(jù)段;未初始化的全局變量會(huì)放到BSS段中......
加載器加載程序到內(nèi)存執(zhí)行,一般分2步走:第一步,會(huì)首先使用fork去創(chuàng)建一個(gè)子進(jìn)程,每個(gè)子進(jìn)程有4G的虛擬地址空間。第二步,從磁盤上軟件安裝的位置,去讀取可執(zhí)行文件的頭部:ELF header,獲取各個(gè)段的信息,然后分別將不同的段加載到進(jìn)程空間的不同位置,如上圖所示。
在一個(gè)計(jì)算機(jī)系統(tǒng)中,通常會(huì)有多個(gè)進(jìn)程同時(shí)運(yùn)行,每一個(gè)進(jìn)程差不多都是通過上面這種 fork-exec 的方式運(yùn)行的。當(dāng)運(yùn)行的進(jìn)程多了,每個(gè)進(jìn)程都想霸占CPU、獨(dú)享CPU,CPU的資源就不夠用了,這個(gè)時(shí)候操作系統(tǒng)就開始登場(chǎng)了。操作系統(tǒng)扮演一個(gè)調(diào)度者的角色,協(xié)調(diào)各個(gè)進(jìn)程輪流占用CPU運(yùn)行。

如上圖所示,對(duì)于用戶運(yùn)行的不同進(jìn)程,在內(nèi)核空間,會(huì)有一個(gè)專門的數(shù)據(jù)結(jié)構(gòu)來表示:task_struct。這個(gè)結(jié)構(gòu)體描述了進(jìn)程的各種信息,不同的task_sruct結(jié)構(gòu)體通過鏈表串起來,內(nèi)核通過鏈表就可以對(duì)這些進(jìn)程進(jìn)行管理。操作系統(tǒng)會(huì)有一個(gè)叫調(diào)度器的核心組件,每隔一段時(shí)間(一般是毫秒級(jí))會(huì)有一個(gè)定時(shí)器中斷,Linux調(diào)度器就會(huì)把正在運(yùn)行的進(jìn)程從CPU上趕下來,接著讓另一個(gè)進(jìn)程去執(zhí)行,如此反復(fù),周而復(fù)始。只要CPU的速度足夠快、輪流執(zhí)行的頻率足夠高,對(duì)于用戶來說,就感覺多個(gè)程序同時(shí)運(yùn)行。
2 進(jìn)程的地址空間
每一個(gè)進(jìn)程,都有一個(gè)4G大小、獨(dú)立的虛擬地址空間,然后通過頁表映射,映射到物理內(nèi)存的不同位置上。CPU執(zhí)行不同的進(jìn)程時(shí),根據(jù)每個(gè)進(jìn)程的映射頁表,就會(huì)到其對(duì)應(yīng)的物理內(nèi)存上一條一條地取指令、翻譯指令、運(yùn)行指令。

如上圖中的進(jìn)程A和進(jìn)程B,它們?cè)趦?nèi)存中有相同的4G虛擬地址空間,但是每個(gè)進(jìn)程通過各自的頁表映射,就映射到了物理內(nèi)存中的不同位置。也就是說,每個(gè)進(jìn)程的虛擬地址空間雖然是相同的,但是它們?cè)谖锢韮?nèi)存空間上卻是相同隔離的、相互獨(dú)立的。在每個(gè)進(jìn)程的4G虛擬地址空間中,[0,3G]這段地址空間是每個(gè)進(jìn)程獨(dú)有的,而[3G,4G]這段空間是被內(nèi)核占用的,不同進(jìn)程的[3G,4G]這段空間都被內(nèi)核占用。內(nèi)核本身在運(yùn)行時(shí),在物理內(nèi)存上也會(huì)有自己?jiǎn)为?dú)的存儲(chǔ)空間。

3 Linux進(jìn)程間通信的三種方法
通過上面的學(xué)習(xí)我們可以看到,用戶空間的不同進(jìn)程,它們?cè)跁r(shí)空上是相互隔離、相互獨(dú)立的,如同黑夜和白天,太陽和月亮,永遠(yuǎn)不會(huì)見面,老死不相往來。但萬事沒有絕對(duì),各個(gè)進(jìn)程之間如果真想通信,還是有方法的,如下圖所示。

用戶空間的每個(gè)進(jìn)程雖說在物理內(nèi)存空間上是相互隔離、相互獨(dú)立的,但通過內(nèi)核空間這一共享區(qū)域,它們還是可以相互通信的。只要內(nèi)核愿意、提供一些空間,不同的進(jìn)程之間就可以對(duì)這塊內(nèi)存空間讀寫數(shù)據(jù),達(dá)到進(jìn)程間通信的目的。磁盤也是公共存儲(chǔ)空間,不同進(jìn)程也可以通過往磁盤上某個(gè)指定的文件讀寫數(shù)據(jù)完成進(jìn)程間的通信。除此之外,不同的進(jìn)程之間,如果事先商量好,也可以繞過內(nèi)核,通過內(nèi)存映射,在物理內(nèi)存上建立一片共享內(nèi)存,直接進(jìn)行通信。
4 無名管道pipe通信機(jī)制
以Linux的無名管道pipe通信機(jī)制為例:無名管道常用于有血緣關(guān)系的進(jìn)程之間的通信,我們可以通過pipe系統(tǒng)調(diào)用去創(chuàng)建一個(gè)管道:
int pipe (int pipefd[2]);
該函數(shù)會(huì)創(chuàng)建一個(gè)管道,這個(gè)管道有兩個(gè)文件描述符,一個(gè)用來讀,一個(gè)用來寫,不同進(jìn)程可以通過讀寫描述符對(duì)這個(gè)管道進(jìn)行讀寫,達(dá)到進(jìn)程間通信的目的。

無名管道在內(nèi)核中的實(shí)現(xiàn)其實(shí)很簡(jiǎn)單,就是Linux內(nèi)核空間的一片緩沖區(qū),通過pipefs機(jī)制把它封裝成一個(gè)文件的形式,留出文件的讀寫接口:文件描述符給用戶空間進(jìn)程。用戶空間的不同進(jìn)程通過這一對(duì)讀寫描述符就可以對(duì)管道進(jìn)行讀寫。

5 更多的進(jìn)程間通信工具
除了無名管道外,Linux提供了很多進(jìn)程間通信的工具可以使用,比如:命名管道FIFO、信號(hào)量、消息隊(duì)列、共享內(nèi)存、信號(hào)signal、socket、Dbus等。不同的IPC工具有各自的優(yōu)缺點(diǎn)、使用場(chǎng)合。比如無名管道只能用于親緣關(guān)系的進(jìn)程間通信,命名管道PIPE解決了這一局限,支持任意兩進(jìn)程之間的通信;消息隊(duì)列可以支持有數(shù)據(jù)格式的通信,共享內(nèi)存效率最高,但是需要跟信號(hào)量、鎖等同步機(jī)制結(jié)合使用;信號(hào)主要用于進(jìn)程間的異步通信,也是唯一的一種異步通信機(jī)制。
每一種IPC通信工具,都有自己的優(yōu)缺點(diǎn)、使用場(chǎng)合和局限,我們只有全面了解和掌握各個(gè)IPC工具的使用,知曉其優(yōu)缺點(diǎn),才能在實(shí)際的工作中根據(jù)需要,選擇合適的通信機(jī)制。除了這些POSIX/system V標(biāo)準(zhǔn)接口定義的IPC工具外,Linux系統(tǒng)還擴(kuò)展了一些自己獨(dú)特的API,如signalfd、timerfd等,解決了信號(hào)通信機(jī)制的一些缺陷。想要進(jìn)一步了解這些IPC工具接口的使用和實(shí)現(xiàn)機(jī)制,可以關(guān)注教程:《Linux系統(tǒng)編程》第05期:進(jìn)程間通信,目前已經(jīng)錄制完畢,已在各大平臺(tái)陸續(xù)上傳,已經(jīng)通過淘寶預(yù)售購買的同學(xué)可以直接下載學(xué)習(xí)了:淘寶店。