進程間通信機制(管道、信號、共享內存/信號量/消息隊列)、線程間通信機制(互斥鎖、條件變量、posix匿名信號量)

進程間通信機制(管道、信號、共享內存/信號量/消息隊列)、線程間通信機制(互斥鎖、條件變量、posix匿名信號量)

分類: linux內核基礎 1246人閱讀 評論(0) 收藏 舉報

注:本分類下文章大多整理自《深入分析linux內核源代碼》一書,另有參考其他一些資料如《linux內核完全剖析》、《linux c 編程一站式學習》等,只是爲了更好地理清系統編程和網絡編程中的一些概念性問題,並沒有深入地閱讀分析源碼,我也是草草翻過這本書,請有興趣的朋友自己參考相關資料。此書出版較早,分析的版本爲2.4.16,故出現的一些概念可能跟最新版本內核不同。

此書已經開源,閱讀地址 http://www.kerneltravel.net


一、管道

在Linux 中,管道是一種使用非常頻繁的通信機制。從本質上說,管道也是一種文件,但它又和一般的文件有所不同,管道可以克服使用文件進行通信的兩個問題,具體表現如下所述。

• 限制管道的大小。實際上,管道是一個固定大小的緩衝區。在Linux 中,該緩衝區的大小爲1 頁,即4KB,使得它的大小不像文件那樣不加檢驗地增長。使用單個固定緩衝區也會帶來問題,比如在寫管道時可能變滿,當這種情況發生時,隨後對管道的write()調用將默認地被阻塞,等待某些數據被讀取,以便騰出足夠的空間供write()調用寫。

• 讀取進程也可能工作得比寫進程快。當所有當前進程數據已被讀取時,管道變空。當這種情況發生時,一個隨後的read()調用將默認地被阻塞,等待某些數據被寫入,這解決了read()調用返回文件結束的問題。

注意,從管道讀數據是一次性操作,數據一旦被讀,它就從管道中被拋棄,釋放空間以便寫更多的數據。

(一)、管道的結構

在Linux 中,管道的實現並沒有使用專門的數據結構,而是藉助了文件系統的file 結構和VFS 的索引節點inode。通過將兩個 file 結構指向同一個臨時的 VFS 索引節點,而這個 VFS 索引節點又指向一個物理頁面而實現的。如圖7.1 所示。


圖7.1 中有兩個 file 數據結構,但它們定義文件操作例程地址是不同的,其中一個是向管道中寫入數據的例程地址,而另一個是從管道中讀出數據的例程地址。這樣,用戶程序的系統調用仍然是通常的文件操作,而內核卻利用這種抽象機制實現了管道這一特殊操作。

一個普通的管道僅可供具有共同祖先的兩個進程之間共享,並且這個祖先必須已經建立了供它們使用的管道。
注意,在管道中的數據始終以和寫數據相同的次序來進行讀,這表示lseek()系統調用對管道不起作用。

二、信號

(一)、信號在內核中的表示


中斷的響應和處理都發生在內核空間,而信號的響應發生在內核空間,信號處理程序的執行卻發生在用戶空間。
那麼,什麼時候檢測和響應信號呢?通常發生在以下兩種情況下:
(1)當前進程由於系統調用、中斷或異常而進入內核空間以後,從內核空間返回到用戶空間前夕;
(2)當前進程在內核中進入睡眠以後剛被喚醒的時候,由於檢測到信號的存在而提前返回到用戶空間。

當有信號要響應時,處理器執行路線的示意圖如圖33.2 所示。


當用戶進程通過系統調用剛進入內核的時候,CPU會自動在該進程的內核棧上壓入下圖所示的內容:



在處理完系統調用以後,就要調用do_signal()函數進行設置frame等工作。這時內核堆棧的狀態應該跟下圖左半部分類似(系統調用將一些信息壓入棧了):

在找到了信號處理函數之後,do_signal() 函數首先把內核堆棧中存放返回執行點的eip保存爲old_eip,然後將eip替換爲信號處理函數的地址,然後將內核中保存的ESP(即用戶態棧地址)減去一定的值,目的是擴大用戶態的棧,然後將內核棧上的內容保存到用戶棧上,這個過程就是設置frame.值得注意的是下面兩點:

1、之所以把EIP的值設置成信號處理函數的地址,是因爲一旦進程返回用戶態,就要去執行信號處理程序,所以EIP要指向信號處理程序而不是原來應該執行的地址。
2、之所以要把frame從內核棧拷貝到用戶棧,是因爲進程從內核態返回用戶態會清理這次調用所用到的內核棧(類似函數調用),內核棧又太小,不能單純的在棧上保存另一個frame(想象一下嵌套信號處理),而我們需要EAX(系統調用返回值)、EIP這些信息以便執行完信號處理函數後能繼續執行程序,所以把它們拷貝到用戶態棧以保存起來。

這時進程返回用戶空間,就會根據內核棧中的EIP值執行信號處理函數。那麼,信號處理程序執行完後,怎麼返回程序繼續執行呢?
信號處理程序執行完畢之後,進程會主動調用sigreturn()系統調用再次回到內核,查看有沒有其他信號需要處理,如果沒有,這時內核就會做一些善後工作,將之前保存的frame恢復到內核棧,恢復eip的值爲old_eip,然後返回用戶空間,程序就能夠繼續執行。至此,內核遍完成了一次(或幾次)信號處理工作。



 C++ Code 
1
2
 
(By default,  the  signal  handler  is invoked on the normal process stack.  It is possible to arrange that the signal handler
 uses an alternate stack; see sigaltstack(2for a discussion of how to do this and when it might be useful.)



三、System V 的IPC 機制

爲了提供與其他系統的兼容性,Linux 也支持3 種system Ⅴ的進程間通信機制:消息、信號量(semaphores)和共享內存,Linux 對這些機制的實施大同小異。我們把信號量、消息和共享內存統稱System V IPC 的對象,每一個對象都具有同樣類型的接口,即系統調用。就像每個文件都有一個打開文件號一樣,每個對象也都有唯一的識別號,進程可以通過系統調用傳遞的識別號來存取這些對象,與文件的存取一樣,對這些對象的存取也要驗證存取權限,System V IPC 可以通過系統調用對對象的創建者設置這些對象的存取權限。在Linux 內核中,System V IPC 的所有對象有一個公共的數據結構pc_perm 結構,它是IPC 對象的權限描述,在linux/ipc.h 中定義如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
 
struct ipc_perm
{
    key_t key; /* 鍵 */
    ushort uid; /* 對象擁有者對應進程的有效用戶識別號和有效組識別號 */
    ushort gid;
    ushort cuid; /* 對象創建者對應進程的有效用戶識別號和有效組識別號 */
    ushort cgid;
    ushort mode; /* 存取模式 */
    ushort seq; /* 序列號 */
};

在這個結構中,要進一步說明的是鍵(key)。鍵和識別號指的是不同的東西。系統支持兩種鍵:公有和私有。如果鍵是公有的,則系統中所有的進程通過權限檢查後,均可以找到System V IPC 對象的識別號。如果鍵是私有的,則鍵值爲0,說明每個進程都可以用鍵值0 建立一個專供其私用的對象。注意,對System V IPC 對象的引用是通過識別號而不是通過鍵。

(一)、信號量

Linux 中信號量是通過內核提供的一系列數據結構實現的,這些數據結構存在於內核空間,對它們的分析是充分理解信號量及利用信號量實現進程間通信的基礎,下面先給出信號量的數據結構(存在於include/linux/sem.h 中)

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
(1)系統中每個信號量的數據結構(sem)
struct sem
{
    int semval; /* 信號量的當前值 */
    unsigned short  semzcnt;  /* # waiting for zero */
    unsigned short  semncnt;  /* # waiting for increase */
    int sempid; /*在信號量上最後一次操作的進程識別號*/
};

(2)系統中表示信號量集合(set)的數據結構(semid_ds)
struct semid_ds
{
    struct ipc_perm sem_perm; /* IPC 權限 */
    long sem_otime; /* 最後一次對信號量操作(semop)的時間 */
    long sem_ctime; /* 對這個結構最後一次修改的時間 */
    struct sem *sem_base; /* 在信號量數組中指向第一個信號量的指針 */
    struct sem_queue *sem_pending; /* 待處理的掛起操作*/
    struct sem_queue **sem_pending_last; /* 最後一個掛起操作 */
    struct sem_undo *undo; /* 在這個數組上的undo 請求 */
    ushort sem_nsems; /* 在信號量數組上的信號量號 */
};

(3)系統中每一信號量集合的隊列結構(sem_queue)
struct sem_queue
{
    struct sem_queue *next;  /* 隊列中下一個節點 */
    struct sem_queue **prev;  /* 隊列中前一個節點, *(q->prev) == q */
    struct wait_queue *sleeper;  /* 正在睡眠的進程 */
    struct sem_undo *undo;  /* undo 結構*/
    int pid; /* 請求進程的進程識別號 */
    int status; /* 操作的完成狀態 */
    struct semid_ds *sma;  /*有操作的信號量集合數組 */
    struct sembuf *sops;  /* 掛起操作的數組 */
    int nsops; /* 操作的個數 */
};

 C++ Code 
1
2
3
4
5
6
 
struct sembuf
{
    ushort sem_num; /* 在數組中信號量的索引值 */
    short sem_op; /* 信號量操作值(正數、負數或0) */
    short sem_flg; /* 操作標誌,爲IPC_NOWAIT 或SEM_UNDO*/
};



如果進程被掛起,Linux 必須保存信號量的操作狀態並將當前進程放入等待隊列。爲此,Linux 內核在堆棧中建立一個 sem_queue 結構並填充該結構。新的 sem_queue 結構添加到集合的等待隊列中(利用 sem_pending 和 sem_pending_last 指針)。當前進程放入sem_queue 結構的等待隊列中(sleeper)後調用調度程序選擇其他的進程運行。

當某個進程修改了信號量而進入臨界區之後,卻因爲崩潰或被“殺死(kill)”而沒有退出臨界區,這時,其他被掛起在信號量上的進程永遠得不到運行機會,這就是所謂的死鎖。Linux 通過維護一個信號量數組的調整列表(semadj)來避免這一問題。其基本思想是,當應用這些“調整”時,讓信號量的狀態退回到操作實施前的狀態。

(二)、消息隊列

Linux 中的消息可以被描述成在內核地址空間的一個內部鏈表,每一個消息隊列由一個IPC 的標識號唯一地標識。Linux 爲系統中所有的消息隊列維護一個 msgque 鏈表,該鏈表中的每個指針指向一個 msgid_ds 結構,該結構完整描述一個消息隊列。



 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 
(1)消息緩衝區(msgbuf)
/* msgsnd 和msgrcv 系統調用使用的消息緩衝區*/
struct msgbuf
{
    long mtype; /* 消息的類型,必須爲正數 */
    char mtext[1]; /* 消息正文 */
};

(2)消息結構(msg)
struct msg
{
    struct msg *msg_next; /* 隊列上的下一條消息 */
    long msg_type; /*消息類型*/
    char *msg_spot; /* 消息正文的地址 */
    short msg_ts; /* 消息正文的大小 */
};

(3)消息隊列結構(msgid_ds)
/* 在系統中的每一個消息隊列對應一個msgid_ds 結構 */
struct msgid_ds
{
    struct ipc_perm msg_perm;
    struct msg *msg_first; /* 隊列上第一條消息,即鏈表頭*/
    struct msg *msg_last; /* 隊列中的最後一條消息,即鏈表尾 */
    time_t msg_stime; /* 發送給隊列的最後一條消息的時間 */
    time_t msg_rtime; /* 從消息隊列接收到的最後一條消息的時間 */
    time_t msg_ctime; /* 最後修改隊列的時間*/
    ushort msg_cbytes; /*隊列上所有消息總的字節數 */
    ushort msg_qnum; /*在當前隊列上消息的個數 */
    ushort msg_qbytes; /* 隊列最大的字節數 */
    ushort msg_lspid; /* 發送最後一條消息的進程的pid */
    ushort msg_lrpid; /* 接收最後一條消息的進程的pid */
};

(三)、共享內存

與消息隊列和信號量集合類似,內核爲每一個共享內存段(存在於它的地址空間)維護着一個特殊的數據結構shmid_ds,這個結構在include/linux/shm.h 中定義如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
/* 在系統中 每一個共享內存段都有一個shmid_ds 數據結構. */
struct shmid_ds
{
    struct ipc_perm shm_perm; /* 操作權限 */
    int shm_segsz; /* 段的大小(以字節爲單位) */
    time_t shm_atime; /* 最後一個進程附加到該段的時間 */
    time_t shm_dtime; /* 最後一個進程離開該段的時間 */
    time_t shm_ctime; /* 最後一次修改這個結構的時間 */
    unsigned short shm_cpid; /*創建該段進程的 pid */
    unsigned short shm_lpid; /* 在該段上操作的最後一個進程的pid */
    short shm_nattch; /*當前附加到該段的進程的個數 */
    /* 下面是私有的 */
    unsigned short shm_npages; /*段的大小(以頁爲單位) */
    unsigned long *shm_pages; /* 指向frames -> SHMMAX 的指針數組 */
    struct vm_area_struct *attaches; /* 對共享段的描述 */
};

我們用圖 7.4 來表示共享內存的數據結構shmid_ds 與其他相關數據結構的關係。


某個進程第1 次訪問共享虛擬內存時將產生缺頁異常。這時,Linux 找出描述該內存的vm_area_struct 結構,該結構中包含用來處理這種共享虛擬內存段的處理函數地址。共享內存缺頁異常處理代碼對shmid_ds 的頁表項表進行搜索,以便查看是否存在該共享虛擬內存的頁表項。如果沒有,系統將分配一個物理頁並建立頁表項,該頁表項加入 shmid_ds 結構的同時也添加到進程的頁表中。這就意味着當下一個進程試圖訪問這頁內存時出現缺頁異常,共享內存的缺頁異常處理代碼則把新創建的物理頁給這個進程。因此說,第1 個進程對共享內存的存取引起創建新的物理頁面,而其他進程對共享內存的存取引起把那個頁添加到它們的地址空間。

當某個進程不再共享其虛擬內存時,利用系統調用將共享段從自己的虛擬地址區域中移去,並更新進程頁表。當最後一個進程釋放了共享段之後,系統將釋放給共享段所分配的物理頁。

當共享的虛擬內存沒有被鎖定到物理內存時,共享內存也可能會被交換到交換區中。

四、Posix 的IPC 機制

信號量:分爲命名和匿名信號量。命名信號量通常用於不共享內存的進程之間(內核實現);匿名信號量可以用於線程通信(存放於線程共享的內存,如全局變量),或者用於進程間通信(存放於進程共享的內存,如System V/ Posix 共享內存)。

消息隊列、共享內存:與System V 類似。

互斥鎖mutex + 匿名信號量:線程通信
互斥鎖mutex + 條件變量condition :線程通信



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