深刻Linux內核架構——鎖與進程間通訊

Linux做爲多任務系統,當一個進程生成的數據傳輸到另外一個進程時,或數據由多個進程共享時,或進程必須彼此等待時,或須要協調資源的使用時,應用程序必須彼此通訊。node

1、控制機制

一、競態條件

幾個進程在訪問資源時彼此干擾的狀況一般稱之爲競態條件(race condition)。在對分佈式應用編程時,這種狀況是一個主要的問題,由於競態條件沒法經過系統的試錯法檢測。只有完全研究源代碼(深刻了解各類可能發生的代碼路徑)並經過敏銳的直覺,才能找到並消除競態條件。程序員

二、臨界區

對於競態條件,其問題的本質是進程的執行在不該該的地方被中斷,從而致使進程工做得不正確。對於此問題的解決方案不必定要求臨界區不能中斷,只要沒有其餘進程進入臨界區,那麼在臨界區中執行的程序是能夠中斷的。確保幾個進程不能同時改變共享值的禁止條件稱爲互斥。shell

大多數系統採用的方案是信號量(semaphore)的使用。信號量是由E. W. Dijkstra在1965年設計的。實質上,最初的信號量是受保護的特別變量,可以表示爲正負整數,初始值爲1。它有兩個標準操做(up和down),這兩個操做分別用於控制關鍵代碼範圍的進入和退出,且假定相互競爭的進程訪問信號量機會均等。編程

在一個進程想要進入關鍵代碼時,它調用down函數。這會將信號量的值減1,即將其設置爲0,而後執行危險代碼段(此時如有其餘進程想進入該代碼段調用down操做則會等待進入關鍵代碼的進程完成操做)。在執行完操做以後,調用up函數將信號量的值加1,即重置爲初始值。數組

信號量在用戶層能夠正常工做,原則上也能夠用於解決內核內部的各類鎖問題。但事實並不是如此:性能是內核最首先的一個目標,雖然信號量初看起來容易實現,但其開銷對內核來講過大,這也是內核中提供了許多不一樣的鎖和同步機制的緣由。緩存

2、內核鎖機制

在多處理器系統上,若是幾個處理器同時處於核心態,理論上它們能夠同時訪問一個數據結構,恰好引起了競態條件。所以,在第一個SMP功能的內核版本中,對該問題的處理是每次只容許一個處理器處於核心態,但這樣效率不高。如今,內核使用了由鎖組成的細粒度網絡,用以明確保護各數據結構(若是處理器A在操做數據結構X,則處理器B能夠執行任何其餘的內核操做,但不能操做X)。網絡

內核提供了各類鎖選項,分別優化不一樣的內核數據使用模式:數據結構

原子操做:這些是最簡單的鎖操做。它們保證簡單的操做,諸如計數器加1之類,能夠不中斷地原子執行,即便操做由幾個彙編語句組成,也能夠保證;併發

自旋鎖:這些是最經常使用的鎖選項,它們用於短時間保護某段代碼,以防止其餘處理器的訪問,在內核等待自旋鎖釋放時,會重複檢查是否能獲取鎖,而不會進入睡眠狀態(忙等待),若是等待時間較長,則效率顯然不高;app

信號量:這些是用經典方法實現的,在等待信號量釋放時,內核進入睡眠狀態,直至被喚醒,喚醒後,內核才從新嘗試獲取信號量,互斥量是信號量的特例,互斥量保護的臨界區,每次只能有一個用戶進入;

讀者/寫者鎖:這些鎖會區分對數據結構的兩種不一樣類型的訪問,任意數目的處理器均可以對數據結構進行併發讀訪問,但只有一個處理器能進行寫訪問(在進行寫訪問時,讀訪問是沒法進行的)。

一、對整數的原子操做

內核定義了atomic_t數據類型,用做對整數計數器的原子操做的基礎。從內核的角度看,這些操做至關於一條彙編語句。

爲使得內核中平臺獨立的部分可以使用原子操做,用於操縱atomic_t類型變量的操做必須由特定於體系結構的代碼提供(由於內核將標準類型進行了封裝,原子變量只能藉助於ATOMIC_INIT宏初始化,不能用普通運算符處理)。

內核爲SMP系統提供了local_t數據類型。該類型容許在單個CPU上的原子操做。爲修改此類型變量,內核提供了基本上與atomic_t數據類型相同的一組函數,只是將atomic替換爲local。

二、自旋鎖

自旋鎖用於保護短的代碼段,其中只包含少許C語句,會很快執行完畢。大多數內核數據結構都有自身的自旋鎖,在處理結構中的關鍵成員時,必須得到相應的自旋鎖。

自旋鎖經過spinlock_t數據結構實現,基本上可以使用spin_lock和spin_unlock操縱。(自旋鎖的實現與體系結構相關,幾乎全是彙編語言)

自旋鎖工做狀況:

  • 若是內核中其餘地方還沒有得到lock,則由當前處理器獲取。其餘處理器不能再進入lock保護的代碼範圍;
  • 若是lock已經由另外一個處理器得到,spin_lock進入一個無限循環,重複地檢查lock是否已經由spin_unlock釋放(自旋鎖所以得名)。若是已經釋放,則得到lock,並進入臨界區。

自旋鎖使用注意:

  • 若是得到鎖以後不釋放,系統將變得不可用,全部的處理器(包括得到鎖的在內),早晚須要進入鎖對應的臨界區,它們會進入無限循環等待鎖釋放,但等不到,便產生了死鎖;
  • 自旋鎖決不該該長期持有,由於全部等待鎖釋放的處理器都處於不可用狀態,沒法用於其餘工做;
  • 內核進入到由自旋鎖保護的臨界區時,就停用內核搶佔,在啓用了內核搶佔的單處理器內核中,spin_lock(基本上)等價於preempt_disable,而spin_unlock則等價於preempt_enable。

三、信號量

內核使用的信號量定義以下(用戶空間信號量的實現有所不一樣):

1 struct semaphore {
2     atomic_t count;    //count指定了能夠同時處於信號量保護的臨界區中進程的數目
3     int sleepers;    //sleepers指定了等待容許進入臨界區的進程的數目
4     wait_queue_head_t wait;    //wait用於實現一個隊列,保存全部在該信號量上睡眠的進程的task_struct
5 };

與自旋鎖相比,信號量適合於保護更長的臨界區,以防止並行訪問。它們不該該用於保護較短的代碼範圍,由於競爭信號量時須要使進程睡眠和再次喚醒,代價很高。

大多數狀況下,不須要使用信號量的全部功能,只是將其用做互斥量,這是一種二值信號量。

信號量工做狀況:

  • 在進入臨界區時,用down對使用計數器減1,在計數器爲0時,其餘進程不能進入臨界區;
  • 在試圖用down獲取已經分配的信號量時,當前進程進入睡眠,並放置在與該信號量關聯的等待隊列上,同時,該進程被置於TASK_UNINTERRUPTIBLE狀態,在等待進入臨界區的過程當中沒法接收信號,若是信號量沒有分配,則該進程能夠當即得到信號量並進入到臨界區,而不會進入睡眠;
  • 在退出臨界區時,必須調用up,該例程負責喚醒在信號量睡眠的某個進程,該進程而後容許進入臨界區,而全部其餘等待的進程繼續睡眠。

除了只能用於內核的互斥量以外,Linux也提供了futex(快速用戶空間互斥量,fast userspacemutex),由核心態和用戶狀態組合而成,爲用戶空間進程提供了互斥量功能。

四、RCU機制

RCU(read-copy-update)是一個同步機制,該機制記錄了指向共享數據結構的指針的全部使用者。在該結構將要改變時,則首先建立一個副本(或一個新的實例),在副本中修改。在全部進行讀訪問的使用者結束對舊副本的讀取以後,指針能夠替換爲指向新的、修改後副本的指針(容許讀寫併發進行,但不對寫訪問之間的相互干擾提供保護)。使用RCU要求以下:

  • 對共享資源的訪問在大部分時間應該是隻讀的,寫訪問應該相對不多;
  • RCU保護的代碼範圍內,內核不能進入睡眠狀態;
  • 受保護資源必須經過指針訪問。

RCU能夠保護通常指針,也能夠保護雙鏈表。以通常指針爲例,假定指針ptr指向一個被RCU保護的數據結構,直接反引用指針是禁止的,首先必須調用rcu_dereference(ptr),而後反引用返回的結果,此外,反引用指針並使用其結果的代碼,須要用rcu_read_lock和rcu_read_unlock調用保護起來。對於雙向鏈表,內核也是以RCU機制爲基礎,提供了標準函數進行保護。此外由struct hlist_head和struct hlist_node組成的散列表也能夠經過RCU保護。

五、內存和優化屏障

儘管鎖足以確保原子性,但對編譯器和處理器優化過的代碼,鎖不能永遠保證時序正確。與競態條件相比,這個問題不只影響SMP系統,也影響單處理器計算機。

內核提供了下面幾個函數,可阻止處理器和編譯器進行代碼重排。

mb()、rmb()、wmb()將硬件內存屏障插入到代碼流程中。rmb()是讀訪問內存屏障。它保證在屏障以後發出的任何讀取操做執行以前,屏障以前發出的全部讀取操做都已經完成。wmb適用於寫訪問,語義與rmb相似。讀者應該能猜到,mb()合併了兩者的語義。

barrier插入一個優化屏障。該指令告知編譯器,保存在CPU寄存器中、在屏障以前有效的全部內存地址,在屏障以後都將失效。本質上,這意味着編譯器在屏障以前發出的讀寫請求完成以前,不會處理屏障以後的任何讀寫請求。

CPU仍然能夠重排時序!

smb_mb()、smp_rmb()、smp_wmb()至關於上述的硬件內存屏障,但只用於SMP系統。它們在單處理器系統上產生的是軟件屏障。

read_barrier_depends()是一種特殊形式的讀訪問屏障,它會考慮讀操做之間的依賴性。若是屏障以後的讀請求,依賴於屏障以前執行的讀請求的數據,那麼編譯器和硬件都不能重排這些請求。

六、讀者/寫者鎖

一般,任意數目的進程均可以併發讀取數據結構,而寫訪問只能限於一個進程。所以內核提供了額外的信號量和自旋鎖版本,分別稱之爲讀者/寫者信號量和讀者/寫者自旋鎖。

讀者/寫者自旋鎖定義爲rwlock_t數據類型。必須根據讀寫訪問,以不一樣的方法獲取鎖。

進程對臨界區進行讀訪問時,在進入和離開時須要分別執行read_lock和read_unlock,內核會容許任意數目的讀進程併發訪問臨界區;

write_lock和write_unlock用於寫訪問。內核保證只有一個寫進程(此時沒有讀進程)可以處於臨界區中。

/寫信號量的用法相似。所用的數據結構是struct rw_semaphore,down_read和up_read用於獲取對臨界區的讀訪問。寫訪問藉助於down_write和up_write進行。

七、大內核鎖

大內核鎖(big kernel lock)能夠鎖定整個內核,確保沒有處理器在覈心態並行運行(已通過時啦)。使用lock_kernel可鎖定整個內核,對應的解鎖使用unlock_kernel。SMP系統和啓用了內核搶佔的單處理器系統若是設置了配置選項PREEMPT_BKL,則容許搶佔大內核鎖。

八、互斥量

儘管信號量可用於實現互斥量的功能,信號量的通用性致使的開銷一般是沒必要要的。所以,內核包含了一個專用互斥量的獨立實現,它們不依賴信號量。內核包含互斥量的兩種實現:一種是經典的互斥量,另外一種是用來解決優先級反轉問題的實時互斥量。

1)經典的互斥量

經典互斥量的基本數據結構定義以下:

1 struct mutex {
2 /* 1: 未鎖定, 0: 鎖定, 負值:鎖定,可能有等待者 */
3     atomic_t count;
4     spinlock_t wait_lock;
5     struct list_head wait_list;
6 };

若是互斥量未鎖定,則count爲1。鎖定分爲兩種狀況:若是隻有一個進程在使用互斥量,則count設置爲0。若是互斥量被鎖定,並且有進程在等待互斥量解鎖(在解鎖時須要喚醒等待進程),則count爲負值。這種特殊處理有助於加快代碼的執行速度,由於在一般狀況下,不會有進程在互斥量上等待。

定義新的互斥量:

  • 靜態互斥量能夠在編譯時經過使用DEFINE_MUTEX產生(與DECLARE_MUTEX區分,後者是基於信號量的互斥量);
  • mutex_init在運行時動態初始化一個新的互斥量;
  • mutex_lock和mutex_unlock分別用於鎖定和解鎖互斥量。

(2)實時互斥量

實時互斥量是內核支持的另外一種形式的互斥量,須要在編譯時經過配置選項CONFIG_RT_MUTEX顯式啓用。與普通的互斥量相比,它們實現了優先級繼承(priority inheritance),該特性可用於解決(或在最低限度上緩解)優先級反轉的影響。

對於優先級反轉問題,能夠經過優先級繼承解決。若是高優先級進程阻塞在互斥量上,該互斥量當前由低優先級進程持有,那麼低優先級進程的優先級會臨時提升到高優先級進程的優先級。

實時互斥量的定義很是接近於普通互斥量:

1 struct rt_mutex {
2     spinlock_t wait_lock;
3     struct plist_head wait_list;
4     struct task_struct *owner;
5 };

互斥量的全部者經過owner指定,wait_lock提供實際的保護,全部等待的進程都在wait_list中排隊。與普通互斥量相比,決定性的改變是等待列表中的進程按優先級排序。在等待列表改變時,內核可相應地校訂鎖持有者的優先級。這須要到調度器的一個接口,可由函數rt_mutex_setprio提供。該函數更新動態優先級task_struct->prio,而普通優先級task_struct->normal_priority不變。

九、近似的per_CPU計數器

若是系統安裝有大量CPU,計數器可能成爲瓶頸:每次只有一個CPU能夠修改其值;全部其餘CPU都必須等待操做結束,才能再次訪問計數器。若是計數器頻繁訪問,則會嚴重影響系統性能。

對某些計數器,沒有必要時時瞭解其準確的數值。這種計數器的近似值與準確值,做用上沒什麼差異,能夠利用這種狀況,引入per-CPU計數器,加速SMP系統上計數器的操做。如圖1所示,計數器的準確值存儲在內存中某處,準確值所在內存位置以後是一個數組,每一個數組項對應於系統中的一個CPU。

 

1 近似per-CPU計數器的數據結構

若是一個處理器想要修改計數器的值(加上或減去某個值n),它不會直接修改計數器的值,由於這須要防止其餘的CPU訪問計數器(這是一個費時的操做)。相反,所需的修改將保存到與計數器相關的數組中特定於當前CPU的數組項。(舉例:,若是計數器應該加3,那麼數組中對應的數組項爲+3。若是同一個CPU在其餘時間須要從計數器減去某個值(假定是5),它也不會對計數器直接操做,而是操做數組中特定於CPU的項:將3減去5,新值爲-2。任何處理器讀取計數器值時,都不是徹底準確的。若是原值爲15,在通過前述的操做以後應該是13,但仍然是15。若是隻須要大體瞭解計數器的值,13也算得上是15的一個比較好的近似了。)

若是某個特定於CPU的數組元素修改後的絕對值超出某個閾值,則認爲這種修改有問題,將隨之修改計數器的值(這種改變不多發生)。在這種狀況下,內核須要確保經過適當的鎖機制來保護此次訪問。

只要計數器改變適度,這種方案中讀操做獲得的平均值會至關接近於計數器的準確值。

per-CPU計數器以下:

1 struct percpu_counter {
2     spinlock_t lock;
3     long count;
4     long *counters;
5 };

count是計數器的準確值,lock是一個自旋鎖,用於在須要準確值時保護計數器。counters數組中各數組項是特定於CPU的,該數組緩存了對計數器的操做。

十、鎖競爭與細粒度鎖

Linux在多CPU系統上的可伸縮性已經成爲一個很是重要的目標。在對內核代碼設計鎖規則時,特別須要考慮這個問題。鎖須要知足下面兩個目的(不過兩者一般很難同時實現):

必須防止對代碼的併發訪問,不然將致使失敗;

對性能的影響必須儘量小。

對於內核頻繁使用的數據,同時知足這兩個要求是很是複雜的,若是一整個數據結構都由一個鎖保護,那麼在內核的某個部分須要獲取鎖的時候,該鎖已經被系統的其餘部分獲取的機率很高,這種狀況下會出現較多的鎖競爭(lock contention),該鎖也會成爲內核的一個熱點。對此,將數據結構標識爲各個獨立的部分,使用多個鎖來保護,這種解決方案稱爲細粒度鎖

細粒度鎖在較大的計算機上對提升可伸縮性頗有好處,但也有兩個弊端:

獲取多個鎖會增長操做的開銷,特別是在較小的SMP計算機上;

在經過多個鎖保護一個數據結構時,很天然會出現一個操做須要同時訪問兩個受保護區域的情形,於是須要同時持有多個鎖,這要求必須遵照某種鎖定次序,必須按序獲取和釋放鎖,不然,仍然會致使死鎖。

3、System V進程間通訊

Linux使用System V(SysV)引入的機制,來支持用戶進程的進程間通訊和同步。

一、System V機制

System V UNIX的3種進程間通訊(IPC)機制(信號量、消息隊列、共享內存),都使用了全系統範圍的資源,能夠由幾個進程同時共享。

在各個獨立進程可以訪問SysV IPC對象以前,IPC對象必須在系統內惟一標識。爲此,每種IPC結構在建立時分配了一個號碼,稱爲魔數。凡知道這個魔數的各個程序,都可以訪問對應的結構。若是獨立的應用程序須要彼此通訊,則一般須要將該魔數永久地編譯到程序中。

在訪問IPC對象時,系統採用了基於文件訪問權限的一個權限系統。每一個IPC對象都有一個用戶ID和一個組ID,依賴於產生IPC對象的程序在何種UID/GID之下運行。讀寫權限在初始化時分配。相似於普通的文件,這些控制了3種不一樣用戶類別的訪問:全部者、組、其餘。

要建立一個授予全部可能訪問權限的信號量(全部者、組、其餘用戶都有讀寫權限),則必須指定標誌0666。

二、信號量

1)使用System V信號量

System V的信號量再也不看成是用於支持原子執行預約義操做的簡單類型變量,它是指一整套信號量,能夠容許幾個操做同時進行(用戶看上去是原子的)。也能夠請求只有一個信號量的信號量集合,並定義函數模擬原始信號量的簡單操做。

2)數據結構

內核使用了幾個數據結構來描述全部註冊信號量的當前狀態,並創建了一種網狀結構。它們不只負責管理信號量及其特徵(值、讀寫權限,等等),還負責經過等待列表將信號量與等待進程關聯起來。

初始的默認的IPC命名空間經過ipc_namespace的靜態實例init_ipc_ns實現。每一個命名空間都包含以下信息:

1 struct ipc_namespace {
2 ...
3     struct ipc_ids *ids[3];
4 /* 資源限制 */
5 ...
6 }

 

這裏略去了與監視資源消耗和設置資源限制相關的不少數據結構成員(好比共享內存頁的最大數目、共享內存段的最大長度、消息隊列的最大數目等)。數組ids的每一個元素對應於一種IPC機制:信號量、消息隊列、共享內存(按順序),每一個數組項指向一個struct ipc_ids的實例,用於跟蹤各種別現存的IPC對象。爲防止對每一個類別都須要查找對應的正確數組索引,內核提供了輔助函數msg_ids、shm_ids和sem_ids。

struct ipc_ids定義以下:

1 struct ipc_ids {
2     int in_use;    //保存了當前使用中IPC對象的數目
3     unsigned short seq;    //seq和seq_max用於連續產生用戶空間IPC ID(不等同於序號)
4     unsigned short seq_max;
5     struct rw_semaphore rw_mutex;    //一個內核信號量,用於實現信號量操做,避免用戶空間中的競態條件
6     struct idr ipcs_idr;
7 };

每一個IPC對象都由kern_ipc_perm的一個實例表示,每一個對象都有一個內核內部ID,ipcs_idr用於將ID關聯到指向對應的kern_ipc_perm實例的指針。使用中IPC對象的數目可能動態地增加和縮減,內核提供了一個相似於基數樹的標準數據結構用於管理該信息。

 1 struct kern_ipc_perm
 2 {
 3     int id;
 4     key_t key;    //保存了用戶程序用來標識信號量的魔數
 5     uid_t uid;        //指全部者的用戶ID
 6     gid_t gid;        //指全部者的組ID
 7     uid_t cuid;    //保存了產生信號量的進程的用戶ID
 8     gid_t cgid;    //保存了產生信號量的進程的組ID
 9     mode_t mode;    //保存了位掩碼,指定了全部者、組、其餘用戶的訪問權限
10     unsigned long seq;    //分配IPC對象時使用的序號
11 };

該結構不只可用於信號量,還能夠用於其餘的IPC機制。該結構不足以保存信號量所需的全部信息,各進程的task_struct實例中有一個與IPC相關的成員:

1 struct task_struct {
2 ...
3 #ifdef CONFIG_SYSVIPC
4 /* ipc相關 */
5     struct sysv_sem sysvsem;
6 #endif
7 ...
8 };

只有設置了配置選項CONFIG_SYSVIPC時,SysV相關代碼纔會編譯到內核中。sysv_sem數據結構封裝了一個成員struct sem_undo_list *undo_list用於撤銷信號量(用於崩潰進程修改了信號量狀態的狀況)。

sem_queue是另外一個數據結構,用於將信號量與睡眠進程關聯起來,該進程想要執行信號量操做,但目前不容許執行。

 1 struct sem_queue {
 2     struct sem_queue * next; /* 隊列中下一項 */
 3     struct sem_queue ** prev; /* 隊列中的前一項,對於第一項有*(q->prev) == q */
 4     struct task_struct* sleeper; /* 睡眠的進程 */
 5     struct sem_undo * undo; /* 用於撤銷的結構 */
 6     int pid; /* 請求信號量操做的進程ID。 */
 7     int status; /* 操做的完成狀態 */
 8     struct sem_array * sma; /* 操做的信號量數組 */
 9     int id; /* 內部信號量ID */
10     struct sembuf * sops; /* 待決操做數組 */
11     int nsops; /* 操做數目 */
12     int alter; /* 操做是否改變了數組? */
13 };

系統中每一個信號量集合,都對應於sem_array數據結構的一個實例,該實例用於管理集合中的全部信號量,sem_array結構以下:

 1 struct sem_array {
 2     struct kern_ipc_perm sem_perm; /* 權限,參見ipc.h */
 3     time_t sem_otime; /* 最後一次信號量操做的時間 */
 4     time_t sem_ctime; /* 最後一次修改的時間 */
 5     struct sem *sem_base; /* 指向數組中第一個信號量的指針 */
 6     struct sem_queue *sem_pending; /* 須要處理的待決操做 */
 7     struct sem_queue **sem_pending_last; /* 上一個待決操做 */
 8     struct sem_undo *undo; /* 該數組上的撤銷請求 */
 9     unsigned long sem_nsems; /* 數組中信號量的數目 */
10 };

2給出了所涉及的各個數據結構之間的相互關係。

 

2 信號量各數據結構之間的相互關係

kern_ipc_perm是用於管理IPC對象的數據結構的第一個成員,不只對信號量是這樣,消息隊列和共享內存對象也是如此。

3)實現系統調用

全部對信號量的操做都使用一個名爲ipc的系統調用執行。該調用不只用於信號量,也用於操做消息隊列和共享內存。其第一個參數用於將實際工做委託給其餘函數。用於信號量的函數以下所示。

  • SEMCTL執行信號量操做,並由sys_semctl實現;
  • SEMGET讀取信號量ID,相關的實現由sys_semget提供;
  • SEMOP和SEMTIMEDOP負責增長和減小信號量值,後者能夠指定超時時間限制。

4)權限檢查

IPC對象的保護機制,與普通的基於文件的對象相同。訪問權限能夠分別對對象的全部者、全部者所在組和全部其餘用戶指定(可能的權限包括讀、寫、執行)。函數ipcperms負責檢查對任意IPC對象的某種操做是否有權限進行。

三、消息隊列

進程之間通訊的另外一個方法是交換消息。這是使用消息隊列機制完成的,其實現基於System V模型。消息隊列的功能原理相對簡單,如圖3所示。

3 System V消息隊列的功能原理

產生消息並將其寫到隊列的進程一般稱之爲發送者,而一個或多個其餘進程(邏輯上稱之爲接收者)則從隊列獲取信息。各個消息包含消息正文和一個(正)數,以便在消息隊列內實現幾種類型的消息。接收者能夠根據該數字檢索消息(好比能夠指定只接受編號1的消息,或接受編號不大於5的消息)。在消息已經讀取後,內核將其從隊列刪除。即便幾個進程在同一信道上監聽,每一個消息仍然只能由一個進程讀取。

同一編號的消息按先進先出次序處理。放置在隊列開始的消息將首先讀取。但若是有選擇地讀取消息,則先進先出次序就再也不適用。

消息隊列也是使用前述信號量哪些數據結構實現,起始點是當前命名空間的適當的ipc_ids實例。內部的ID號形式上關聯到kern_ipc_perm實例,在消息隊列的實現中,須要經過類型轉換得到不一樣的數據類型(struct msg_queue)。該結構定義以下:

 1 struct msg_queue {
 2     struct kern_ipc_perm q_perm;
 3     time_t q_stime; /* 上一次調用msgsnd發送消息的時間 */
 4     time_t q_rtime; /* 上一次調用msgrcv接收消息的時間 */
 5     time_t q_ctime; /* 上一次修改的時間 */
 6     unsigned long q_cbytes; /* 隊列上當前字節數目 */
 7     unsigned long q_qnum; /* 隊列中的消息數目 */
 8     unsigned long q_qbytes; /* 隊列上最大字節數目 */
 9     pid_t q_lspid; /* 上一次調用msgsnd的pid */
10     pid_t q_lrpid; /* 上一次接收消息的pid */
11     struct list_head q_messages;
12     struct list_head q_receivers;
13     struct list_head q_senders;
14 };

3個標準的內核鏈表用於管理睡眠的發送者(q_senders)、睡眠的接收者(q_receivers)和消息自己(q_messages)。各個鏈表都使用獨立的數據結構做爲鏈表元素。

q_messages中的各個消息都封裝在一個msg_msg實例中。

1 struct msg_msg {
2     struct list_head m_list;
3     long m_type;    //指定了消息類型,用於支持前文所述消息隊列中不一樣的消息類型。
4     int m_ts; /* 消息正文長度 */
5     struct msg_msgseg* next;    //若是保存超過一個內存頁的長消息,則須要next
6 /* 接下來是實際的消息 */
7 };

結構中沒有指定存儲消息自身的字段。由於每一個消息都(至少)分配了一個內存頁,msg_msg實例則保存在該頁的起始處,剩餘的空間可用於存儲消息正文,如圖4所示。從內存頁的長度,減去msg_msg結構的長度,便可獲得msg_msg頁中可用於消息正文的最大字節數目。

4 內存中IPC消息的管理

消息正文緊接着該數據結構的實例以後存儲。使用next,可使消息分佈到任意數目的頁上。在經過消息隊列通訊時,發送進程和接收進程均可以進入睡眠:若是消息隊列已經達到最大容量,則發送者在試圖寫入消息時會進入睡眠;若是隊列中沒有消息,那麼接收者在試圖獲取消息時會進入睡眠。

睡眠的發送者放置在msg_queue的q_senders鏈表中,鏈表元素使用下列數據結構:

1 struct msg_sender {
2     struct list_head list;    //鏈表元素
3     struct task_struct* tsk;    //指向對應進程的task_struct的指針
4 };

 

 

q_receivers鏈表中用於保存接收進程的數據結構要稍長一點。

1 struct msg_receiver {
2     struct list_head r_list;
3     struct task_struct *r_tsk;
4     int r_mode;
5     long r_msgtype;
6     long r_maxsize;
7     struct msg_msg *volatile r_msg;
8 };

其中不只保存了指向對應進程的task_struct的指針,還包括了對預期消息的描述,以及指向msg_msg實例的一個指針(消息可用時,該指針指定了複製數據的目標地址)。

5是消息隊列所涉及數據結構之間的相互關係(忽略睡眠的發送進程鏈表)。

5 System V消息隊列的數據結構

四、共享內存

與信號量和消息隊列相比,共享內存沒有本質性的不一樣。

  • 應用程序請求的IPC對象,能夠經過魔數和當前命名空間的內核內部ID訪問;
  • 對內存的訪問,可能受到權限系統的限制;
  • 可使用系統調用分配與IPC對象關聯的內存,具有適當受權的全部進程,均可以訪問該內存。

內核的實現採用了與信號量和消息隊列很是相似的概念,相關數據結構關係如圖6所示。

6 System V共享內存的數據結構

smd_ids全局變量的entries數組中保存了kern_ipc_perm和shmid_kernel的組合,以便管理IPC對象的訪問權限。對每一個共享內存對象都建立一個僞文件,經過shm_file鏈接到shmid_kernel的實例。內核使用shm_file->f_mapping指針訪問地址空間對象(struct address_space),用於建立匿名映射。還須要設置所涉及各進程的頁表,使得各個進程都可以訪問與該IPC對象相關的內存區域。

4、其餘IPC機制

SysV IPC一般只對應用程序員有意義,但對shell的用戶,信號和管道更經常使用。

一、信號

SysV機制相比,信號是一種比較原始的通訊機制,其底層概念很是簡單,kill命令根據PID向進程發送信號。信號經過-s sig指定,是一個正整數,最大長度取決於處理器類型。

進程必須設置處理程序例程來處理信號。這些例程在信號發送到進程時調用(進程能夠決定阻塞某些信號,但有幾個信號的行爲沒法修改,如SIGKILL)。若是沒有顯式設置處理程序例程,內核則使用默認的處理程序實現。(init進程屬於特例,內核會忽略發送給該進程的SIGKILL信號。)

1)實現信號處理程序

sigaction系統調用用於設置新的處理程序。若是沒有爲某個信號分配用戶定義的處理程序函數,內核會自動設置預約義函數,提供合理的標準操做來處理相應的狀況。

sigaction類型中用於描述處理程序的字段,其定義是平臺相關的,但在全部體系結構上幾乎都相同。

1 struct sigaction {
2     __sighandler_t sa_handler;    //一個指向內核在信號到達時調用的處理程序函數的指針
3     unsigned long sa_flags;    //包含了額外的標誌,用於指定信號處理方式的一些約束
4 ...
5     sigset_t sa_mask; //包含了一個位掩碼,每一個比特位對應於系統中的一個信號
6 };

信號處理程序的函數原型以下:

1 typedef void __signalfn_t(int);
2 typedef __signalfn_t __user *__sighandler_t;

其參數是信號的編號,所以可使用同一個處理程序函數處理不一樣的信號。

信號處理程序使用sigaction系統調用設置,該調用將藉助用戶定義的處理程序函數替換SIGTERM的默認處理程序。

2)實現信號管理

全部信號相關的數據都是藉助於鏈式數據結構管理的,其入口是task_struct結構,其中包含了各個與信號相關的字段。

 1 struct task_struct {
 2 ...
 3 /* 信號處理程序 */
 4     struct signal_struct *signal;
 5     struct sighand_struct *sighand;
 6     sigset_t blocked;
 7     struct sigpending pending;
 8     unsigned long sas_ss_sp;
 9     size_t sas_ss_size;
10 ...
11 };

信號處理髮生在內核中,但設置的信號處理程序是在用戶狀態運行,一般,信號處理程序使用所述進程在用戶狀態下的棧。但POSIX強制要求提供一種選項,在專門用於信號處理的棧上運行信號處理程序,這個附加的棧(必須經過用戶應用程序顯式分配),其地址和長度分別保存在sas_ss_sp和sas_ss_size。

用於管理設置的信號處理程序的信息的sighand_struct以下所示:

1 struct sighand_struct {
2     atomic_t count;    //保存了共享該結構實例的進程數目
3     struct k_sigaction action[_NSIG];    //保存設置的信號處理程序,_NSIG指定了能夠處理的不一樣信號的數目
4 } ;

全部阻塞信號由task_struct的blocked成員定義,所使用的sigset_t數據類型是一個位掩碼,所包含的比特位數目必須(至少)與所支持的信號數目相同,其數據結構爲:

1 typedef struct {
2     unsigned long sig[_NSIG_WORDS];
3 } sigset_t;

pending是task_struct中與信號處理相關的最後一個成員。它創建了一個鏈表,包含全部已經引起、仍然有待內核處理的信號。它們使用了下列數據結構:

1 struct sigpending {
2     struct list_head list;    //經過雙鏈表管理全部待決信號
3     sigset_t signal;        //位掩碼,指定了仍然有待處理的全部信號的編號
4 };

7爲各結構體之間的關係。

7 信號管理結構體之間關係

3)實現信號處理

內核用於實現信號處理的最重要的系統調用有kill(向進程組的全部進程發送一個信號)、tkill(向單個進程發送一個信號)、sigpending(檢查是否有待決信號)、sigprocmask(操做阻塞信號的位掩碼)、sigsuspend(進入睡眠,直至接收某特定信號)。

對於發送信號,不論名稱如何,實際上kill和tkill基本相同,以sys_tkill爲例,其代碼流程圖如圖8所示。

8 sys_tkill代碼流程圖

find_task_by_vpid找到目標進程的task_struct以後,內核將檢查進程是否有發送該信號所需權限的工做委託給check_kill_permission,該函數檢查權限。剩餘的信號處理工做則傳遞給specific_send_sig_info進行,若是信號被阻塞(能夠用sig_ignored檢查),則當即放棄處理;不然由send_signal產生一個新的sigqueue實例(使用sigqueue_cachep緩存),其中填充了信號數據,並添加到目標進程的sigpending鏈表;若發送陳宮,則可使用signal_wake_up喚醒進程。

對於信號隊列的處理,每次由核心態切換到用戶狀態時,內核都會完成此工做。處理的發起獨立於特定的體系結構,此後,最終的效果就是調用do_signal函數(此處不詳述)。

從時序上看,信號處理的過程如圖9所示。

 

9 信號處理的執行

二、管道和套接字

管道和套接字是流行的進程間通訊機制。管道使用了虛擬文件系統對象,套接字使用了各類網絡函數以及虛擬文件系統。

管道是用於交換數據的鏈接。一個進程向管道的一端供給數據,另外一個在管道另外一端取出數據,供進一步處理。幾個進程能夠經過一系列管道鏈接起來。

管道是進程地址空間中的數據對象,在用fork或clone複製進程時一樣會被複制。使用管道通訊的程序就利用了這種特徵。在exec系統調用用另外一個程序替換子進程以後,兩個不一樣的應用程序之間就創建了一條通訊鏈路(必須把管道描述符重定向到標準輸入和輸出,或者調用dup系統調用,確保exec調用時不會關閉文件描述符)。

套接字對象在內核中初始化時也返回一個文件描述符,所以能夠像普通文件同樣處理,與管道不一樣之處在於它能夠雙向使用,還能夠用於經過網絡鏈接的遠程系統通訊。從用戶的角度來看,同一系統上兩個本地進程之間基於套接字的通訊與分別處於兩個不一樣大陸兩臺計算機上運行的應用程序之間的通訊沒有太大差異。

相關文章
相關標籤/搜索