在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í)了:淘寶店蹄皱。