2021-05-12 14:32:11
Linux進程間的通訊方式和原理
進程的概念
- 進程是作業系統的概念,每當我們執行一個程式時,對於作業系統來講就建立了一個進程,在這個過程中,伴隨著資源的分配和釋放。可以認為進程是一個程式的一次執行過程。
進程通訊的概念
- 進程使用者空間是相互獨立的,一般而言是不能相互存取的。但很多情況下進程間需要互相通訊,來完成系統的某項功能。進程通過與核心及其它進程之間的互相通訊來協調它們的行為。
進程通訊的應用場景
-
資料傳輸:一個進程需要將它的資料傳送給另一個進程,傳送的資料量在一個位元組到幾兆位元組之間。
-
共用資料:多個進程想要操作共用資料,一個進程對共用資料的修改,別的進程應該立刻看到。
-
通知事件:一個進程需要向另一個或一組進程傳送訊息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
-
資源共用:多個進程之間共用同樣的資源。為了作到這一點,需要核心提供鎖和同步機制。
-
進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。
進程通訊的方式
管道( pipe ):
管道包括三種:
- 普通管道PIPE: 通常有兩種限制,一是單工,只能單向傳輸;二是只能在父子或者兄弟進程間使用.
- 流管道s_pipe: 去除了第一種限制,為半雙工,只能在父子或兄弟進程間使用,可以雙向傳輸.
- 命名管道:name_pipe:去除了第二種限制,可以在許多並不相關的進程之間進行通訊.
號誌( semophore ) :
- 號誌是一個計數器,可以用來控制多個進程對共用資源的存取。它常作為一種鎖機制,防止某進程正在存取共用資源時,其他進程也存取該資源。因此,主要作為進程間以及同一進程內不同執行緒之間的同步手段。
訊息佇列( message queue ) :
- 訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。訊息佇列克服了信號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
信號 ( sinal ) :
- 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。
共用記憶體( shared memory ) :
- 共用記憶體就是對映一段能被其他進程所存取的記憶體,這段共用記憶體由一個進程建立,但多個進程都可以存取。共用記憶體是最快的 IPC 方式,它是針對其他進程間通訊方式執行效率低而專門設計的。它往往與其他通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。
通訊端( socket ) :
- 套解口也是一種進程間通訊機制,與其他通訊機制不同的是,它可用於不同機器間的進程通訊。
各進程間通訊的原理及實現
管道
管道是如何通訊的
管道是由核心管理的一個緩衝區,相當於我們放入記憶體中的一個紙條。管道的一端連線一個進程的輸出。這個進程會向管道中放入資訊。管道的另一端連線一個進程的輸入,這個進程取出被放入管道的資訊。一個緩衝區不需要很大,它被設計成為環形的資料結構,以便管道可以被迴圈利用。當管道中沒有資訊的話,從管道中讀取的進程會等待,直到另一端的進程放入資訊。當管道被放滿資訊的時候,嘗試放入資訊的進程會等待,直到另一端的進程取出資訊。當兩個進程都終結的時候,管道也自動消失。
管道是如何建立的
從原理上,管道利用fork機制建立,從而讓兩個進程可以連線到同一個PIPE上。最開始的時候,上面的兩個箭頭都連線在同一個進程Process 1上(連線在Process 1上的兩個箭頭)。當fork複製進程的時候,會將這兩個連線也複製到新的進程(Process 2)。隨後,每個進程關閉自己不需要的一個連線 (兩個黑色的箭頭被關閉; Process 1關閉從PIPE來的輸入連線,Process 2關閉輸出到PIPE的連線),這樣,剩下的紅色連線就構成了如上圖的PIPE。
- 管道通訊的實現細節
在 Linux 中,管道的實現並沒有使用專門的資料結構,而是藉助了檔案系統的file結構和VFS的索引節點inode。通過將兩個 file 結構指向同一個臨時的 VFS 索引節點,而這個 VFS 索引節點又指向一個物理頁面而實現的。如下圖
有兩個 file 資料結構,但它們定義檔案操作例程地址是不同的,其中一個是向管道中寫入資料的例程地址,而另一個是從管道中讀出資料的例程地址。這樣,使用者程式的系統呼叫仍然是通常的檔案操作,而核心卻利用這種抽象機制實現了管道這一特殊操作。
關於管道的讀寫
管道實現的原始碼在fs/pipe.c中,在pipe.c中有很多函數,其中有兩個函數比較重要,即管道讀函數pipe_read()和管道寫函數pipe_wrtie()。管道寫函數通過將位元組複製到 VFS 索引節點指向的實體記憶體而寫入資料,而管道讀函數則通過複製實體記憶體中的位元組而讀出資料。當然,核心必須利用一定的機制同步對管道的存取,為此,核心使用了鎖、等待佇列和信號。
當寫進程向管道中寫入時,它利用標準的庫函數write(),系統根據庫函數傳遞的檔案描述符,可找到該檔案的 file 結構。file 結構中指定了用來進行寫操作的函數(即寫入函數)地址,於是,核心呼叫該函數完成寫操作。寫入函數在向記憶體中寫入資料之前,必須首先檢查 VFS 索引節點中的資訊,同時滿足如下條件時,才能進行實際的記憶體複製工作:
- 記憶體中有足夠的空間可容納所有要寫入的資料;
- 記憶體沒有被讀程式鎖定。
如果同時滿足上述條件,寫入函數首先鎖定記憶體,然後從寫進程的地址空間中複製資料到記憶體。否則,寫入進程就休眠在 VFS 索引節點的等待佇列中,接下來,核心將呼叫排程程式,而排程程式會選擇其他進程執行。寫入進程實際處於可中斷的等待狀態,當記憶體中有足夠的空間可以容納寫入資料,或記憶體被解鎖時,讀取進程會喚醒寫入進程,這時,寫入進程將接收到信號。當資料寫入記憶體之後,記憶體被解鎖,而所有休眠在索引節點的讀取進程會被喚醒。
管道的讀取過程和寫入過程類似。但是,進程可以在沒有資料或記憶體被鎖定時立即返回錯誤資訊,而不是阻塞該進程,這依賴於檔案或管道的開啟模式。反之,進程可以休眠在索引節點的等待佇列中等待寫入進程寫入資料。當所有的進程完成了管道操作之後,管道的索引節點被丟棄,而共用資料頁也被釋放。
Linux函數原型
#include <unistd.h>
int pipe(int filedes[2]);
filedes[0]用於讀出資料,讀取時必須關閉寫入端,即close(filedes[1]);
filedes[1]用於寫入資料,寫入時必須關閉讀取端,即close(filedes[0])。
程式範例:
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if(pipe(fd) 0){ /* 先建立管道得到一對檔案描述符 */
exit(0);
}
if((pid = fork()) 0) /* 父進程把檔案描述符複製給子進程 */
exit(1);
else if(pid > 0){ /* 父進程寫 */
close(fd[0]); /* 關閉讀描述符 */
write(fd[1], "nhello worldn", 14);
}
else{ /* 子進程讀 */
close(fd[1]); /* 關閉寫端 */
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
命名管道
由於基於fork機制,所以管道只能用於父進程和子進程之間,或者擁有相同祖先的兩個子進程之間 (有親緣關係的進程之間)。為了解決這一問題,Linux提供了FIFO方式連線進程。FIFO又叫做命名管道(named PIPE)。
實現原理
FIFO (First in, First out)為一種特殊的檔案型別,它在檔案系統中有對應的路徑。當一個進程以讀(r)的方式開啟該檔案,而另一個進程以寫(w)的方式開啟該檔案,那麼核心就會在這兩個進程之間建立管道,所以FIFO實際上也由核心管理,不與硬碟打交道。之所以叫FIFO,是因為管道本質上是一個先進先出的佇列資料結構,最早放入的資料被最先讀出來,從而保證資訊交流的順序。FIFO只是借用了檔案系統(file system,命名管道是一種特殊型別的文??,因為Linux中所有事物都是檔案,它在檔案系統中以檔名的形式存在。)來為管道命名。寫模式的進程向FIFO檔案中寫入,而讀模式的進程從FIFO檔案中讀出。當刪除FIFO檔案時,管道連線也隨之消失。FIFO的好處在於我們可以通過檔案的路徑來識別管道,從而讓沒有親緣關係的進程之間建立連線
函數原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );
其中filename是被建立的檔名稱,mode表示將在該檔案上設定的許可權位和將被建立的檔案型別(在此情況下為S_IFIFO),dev是當建立裝置特殊檔案時使用的一個值。因此,對於先進先出檔案它的值為0。
程式範例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int res = mkfifo("/tmp/my_fifo", 0777);
if (res == 0)
{
printf("FIFO created/n");
}
exit(EXIT_SUCCESS);
}
參考文獻
Linux進程間通訊之管道(pipe)、命名管道(FIFO)與信號(Signal)
號誌
什麼是號誌
為了防止出現因多個程式同時存取一個共用資源而引發的一系列問題,我們需要一種方法。比如在任一時刻只能有一個執行執行緒存取程式碼的臨界區域。臨界區域是指執行資料更新的程式碼需要獨佔式地執行。而號誌就可以提供這樣的一種存取機制,讓一個臨界區同一時間只有一個執行緒在存取它,也就是說號誌是用來調協進程對共用資源的存取的。
號誌是一個特殊的變數,程式對其存取都是原子操作,且只允許對它進行等待(即P(信號變數))和傳送(即V(信號變數))資訊操作。最簡單的號誌是只能取0和1的變數,這也是號誌最常見的一種形式,叫做二進位制號誌。而可以取多個正整數的號誌被稱為通用號誌。
號誌的工作原理
由於號誌只能進行兩種操作等待和傳送信號,即P(sv)和V(sv),他們的行為是這樣的:
- P(sv):如果sv的值大於零,就給它減1;如果它的值為零,就掛起該進程的執行
- V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復執行,如果沒有進程因等待sv而掛起,就給它加1.
舉個例子,就是兩個進程共用號誌sv,一旦其中一個進程執行了P(sv)操作,它將得到號誌,並可以進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放號誌,這時第二個進程就可以恢復執行。
Linux的號誌機制
Linux提供了一組精心設計的號誌介面來對信號進行操作,它們不只是針對二進位制號誌,下面將會對這些函數進行介紹,但請注意,這些函數都是用來對成組的號誌值進行操作的。它們宣告在標頭檔案sys/sem.h中。
semget函數
它的作用是建立一個新號誌或取得一個已有號誌,原型為:
int semget(key_t key, int num_sems, int sem_flags);
-
第一個引數key是整數值(唯一非零),不相關的進程可以通過它存取一個號誌,它代表程式可能要使用的某個資源,程式對所有號誌的存取都是間接的,程式先通過呼叫semget函數並提供一個鍵,再由系統生成一個相應的信號識別符號(semget函數的返回值),只有semget函數才直接使用號誌鍵,所有其他的號誌函數使用由semget函數返回的號誌識別符號。如果多個程式使用相同的key值,key將負責協調工作。
-
第二個引數num_sems指定需要的號誌數目,它的值幾乎總是1。
-
第三個引數sem_flags是一組標誌,當想要當號誌不存在時建立一個新的號誌,可以和值IPC_CREAT做按位元或操作。設定了IPC_CREAT標誌後,即使給出的鍵是一個已有號誌的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則可以建立一個新的,唯一的號誌,如果號誌已存在,返回一個錯誤。
semget函數成功返回一個相應信號識別符號(非零),失敗返回-1.
semop函數
它的作用是改變號誌的值,原型為:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的號誌識別符號,sembuf結構的定義如下:
struct sembuf{
short sem_num;//除非使用一組號誌,否則它為0
short sem_op;//號誌在一次操作中需要改變的資料,通常是兩個數,一個是-1,即P(等待)操作,
//一個是+1,即V(傳送信號)操作。
short sem_flg;//通常為SEM_UNDO,使作業系統跟蹤信號,
//並在進程沒有釋放該號誌而終止時,作業系統釋放號誌
};
semctl函數
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四個引數,它通常是一個union semum結構,定義如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
前兩個引數與前面一個函數中的一樣,command通常是下面兩個值中的其中一個
SETVAL:用來把號誌初始化為一個已知的值。p 這個值通過union semun中的val成員設定,其作用是在號誌第一次使用前對它進行設定。
IPC_RMID:用於刪除一個已經無需繼續使用的號誌識別符號。
本文永久更新連結地址:http://www.linuxidc.com/Linux/2016-10/136542.htm
相關文章