跟濤哥一起學嵌入式 15:Linux進程間通信10分鐘快速入門

在Linux環境下運行程序,無論是點擊桌面上的一個圖標,還是在命令行下敲擊一個shell命令,Linux系統都會把我們的程序“包裝”成一個進程的形式,然後調度運行:每個進程輪流佔用CPU一段時間去執行,時間到了就讓給其它進程,時間片輪轉,只要輪轉得速度足夠快,就會給用戶一種錯覺:我們在電腦上一邊聽歌,一邊打字,感覺多個程序在同時運行。不同進程在運行過程中,根據業務需要,進程相互之間也會通信:比如傳輸數據、發送信號等。

Linux環境下的進程間通信(Inter-Process Communication,簡稱IPC)有多種工具可以使用,如:無名管道pipe、命名管道FIFO、消息隊列、共享內存、信號量、信號、文件鎖、socket等。這些IPC工具以系統調用或庫函數API的形式提供給用戶使用:用戶使用這些API可以在不同的進程之間傳輸數據、同步進程、或者發送信號。比如,我們可以使用ctrl+C組合鍵去終止一個進程,或者使用shell命令kill 3567去殺死一個進程pid爲3567的進程,這些其實都是給進程發送信號,進程接收信號並進行處理的過程。

不同的IPC工具,使用場合不同,各有優劣。爲了更好地使用它們,我們不僅要熟練掌握API接口的使用,還要對它們的通信機制、內核實現原理有一個大致的瞭解。只有掌握了底層的實現原理、我們才能明白每個IPC通信工具的優點和缺點、以及他們的使用場合。想要真正理解Linux進程之間到底是如何通信的,首先要搞明白Linux下的不同進程在運行過程中,在內存中是以什麼樣的形態存在的,以及與Linux內核之間是如何交互的。想要理解這點,我們還需要對Linux環境下程序的編譯、執行過程有一個大概的瞭解。



1   程序的編譯和執行

當我們在桌面上點擊一個圖標,或者在命令行下敲擊一個shell命令運行時,Linux系統會把這些可執行文件加載到內存,並封裝成一個進程,然後才能參與操作系統的調度、運行。那操作系統是如何加載的呢?



首先,我們編寫的C語言源代碼會編譯成一個可執行文件(ELF)。可執行文件分由各種不同的段(section)組成:代碼段、數據段、BSS段等。我們C程序中的不同代碼會被編譯到不同的段中:函數實現會放到代碼段;全局變量、靜態局部變量會放到數據段;未初始化的全局變量會放到BSS段中......

加載器加載程序到內存執行,一般分2步走:第一步,會首先使用fork去創建一個子進程,每個子進程有4G的虛擬地址空間。第二步,從磁盤上軟件安裝的位置,去讀取可執行文件的頭部:ELF header,獲取各個段的信息,然後分別將不同的段加載到進程空間的不同位置,如上圖所示。

在一個計算機系統中,通常會有多個進程同時運行,每一個進程差不多都是通過上面這種 fork-exec 的方式運行的。當運行的進程多了,每個進程都想霸佔CPU、獨享CPU,CPU的資源就不夠用了,這個時候操作系統就開始登場了。操作系統扮演一個調度者的角色,協調各個進程輪流佔用CPU運行。
 



如上圖所示,對於用戶運行的不同進程,在內核空間,會有一個專門的數據結構來表示:task_struct。這個結構體描述了進程的各種信息,不同的task_sruct結構體通過鏈表串起來,內核通過鏈表就可以對這些進程進行管理。操作系統會有一個叫調度器的核心組件,每隔一段時間(一般是毫秒級)會有一個定時器中斷,Linux調度器就會把正在運行的進程從CPU上趕下來,接着讓另一個進程去執行,如此反覆,週而復始。只要CPU的速度足夠快、輪流執行的頻率足夠高,對於用戶來說,就感覺多個程序同時運行。




2   進程的地址空間

每一個進程,都有一個4G大小、獨立的虛擬地址空間,然後通過頁表映射,映射到物理內存的不同位置上。CPU執行不同的進程時,根據每個進程的映射頁表,就會到其對應的物理內存上一條一條地取指令、翻譯指令、運行指令。
 



如上圖中的進程A和進程B,它們在內存中有相同的4G虛擬地址空間,但是每個進程通過各自的頁表映射,就映射到了物理內存中的不同位置。也就是說,每個進程的虛擬地址空間雖然是相同的,但是它們在物理內存空間上卻是相同隔離的、相互獨立的。在每個進程的4G虛擬地址空間中,[0,3G]這段地址空間是每個進程獨有的,而[3G,4G]這段空間是被內核佔用的,不同進程的[3G,4G]這段空間都被內核佔用。內核本身在運行時,在物理內存上也會有自己單獨的存儲空間。
 






3   Linux進程間通信的三種方法


通過上面的學習我們可以看到,用戶空間的不同進程,它們在時空上是相互隔離、相互獨立的,如同黑夜和白天,太陽和月亮,永遠不會見面,老死不相往來。但萬事沒有絕對,各個進程之間如果真想通信,還是有方法的,如下圖所示。
 



用戶空間的每個進程雖說在物理內存空間上是相互隔離、相互獨立的,但通過內核空間這一共享區域,它們還是可以相互通信的。只要內核願意、提供一些空間,不同的進程之間就可以對這塊內存空間讀寫數據,達到進程間通信的目的。磁盤也是公共存儲空間,不同進程也可以通過往磁盤上某個指定的文件讀寫數據完成進程間的通信。除此之外,不同的進程之間,如果事先商量好,也可以繞過內核,通過內存映射,在物理內存上建立一片共享內存,直接進行通信。



4   無名管道pipe通信機制


以Linux的無名管道pipe通信機制爲例:無名管道常用於有血緣關係的進程之間的通信,我們可以通過pipe系統調用去創建一個管道:
int pipe (int pipefd[2]);

該函數會創建一個管道,這個管道有兩個文件描述符,一個用來讀,一個用來寫,不同進程可以通過讀寫描述符對這個管道進行讀寫,達到進程間通信的目的。

 



無名管道在內核中的實現其實很簡單,就是Linux內核空間的一片緩衝區,通過pipefs機制把它封裝成一個文件的形式,留出文件的讀寫接口:文件描述符給用戶空間進程。用戶空間的不同進程通過這一對讀寫描述符就可以對管道進行讀寫。
 






5   更多的進程間通信工具


除了無名管道外,Linux提供了很多進程間通信的工具可以使用,比如:命名管道FIFO、信號量、消息隊列、共享內存、信號signal、socket、Dbus等。不同的IPC工具有各自的優缺點、使用場合。比如無名管道只能用於親緣關係的進程間通信,命名管道PIPE解決了這一侷限,支持任意兩進程之間的通信;消息隊列可以支持有數據格式的通信,共享內存效率最高,但是需要跟信號量、鎖等同步機制結合使用;信號主要用於進程間的異步通信,也是唯一的一種異步通信機制。

每一種IPC通信工具,都有自己的優缺點、使用場合和侷限,我們只有全面瞭解和掌握各個IPC工具的使用,知曉其優缺點,才能在實際的工作中根據需要,選擇合適的通信機制。除了這些POSIX/system V標準接口定義的IPC工具外,Linux系統還擴展了一些自己獨特的API,如signalfd、timerfd等,解決了信號通信機制的一些缺陷。想要進一步瞭解這些IPC工具接口的使用和實現機制,可以關注教程:《Linux系統編程》第05期:進程間通信,目前已經錄製完畢,已在CSDN學院發佈,詳情請點擊:Linux系統編程第05期:進程間通信

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章