linux訊號量
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 world\n", 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:用於刪除一個已經無需繼續使用的訊號量識別符號。
轉載:https://www.linuxidc.com/Linux/2016-10/136542.htm