Linux作為多任務系統,當一個行程生成的資料傳輸到另一個行程時,或資料由多個行程共享時,或行程必須彼此等待時,或需要協調資源的使用時,應用程式必須彼此通信,
一、控制機制
1、競態條件
幾個行程在訪問資源時彼此干擾的情況通常稱之為競態條件(race condition),在對分布式應用編程時,這種情況是一個主要的問題,因為競態條件無法通過系統的試錯法檢測,只有徹底研究源代碼(深入了解各種可能發生的代碼路徑)并通過敏銳的直覺,才能找到并消除競態條件,
2、臨界區
對于競態條件,其問題的本質是行程的執行在不應該的地方被中斷,從而導致行程作業得不正確,對于此問題的解決方案不一定要求臨界區不能中斷,只要沒有其他行程進入臨界區,那么在臨界區中執行的程式是可以中斷的,確保幾個行程不能同時改變共享值的禁止條件稱為互斥,
大多數系統采用的方案是信號量(semaphore)的使用,信號量是由E. W. Dijkstra在1965年設計的,實質上,最初的信號量是受保護的特別變數,能夠表示為正負整數,初始值為1,它有兩個標準操作(up和down),這兩個操作分別用于控制關鍵代碼范圍的進入和退出,且假定相互競爭的行程訪問信號量機會均等,
在一個行程想要進入關鍵代碼時,它呼叫down函式,這會將信號量的值減1,即將其設定為0,然后執行危險代碼段(此時若有其他行程想進入該代碼段呼叫down操作則會等待進入關鍵代碼的行程完成操作),在執行完操作之后,呼叫up函式將信號量的值加1,即重置為初始值,
信號量在用戶層可以正常作業,原則上也可以用于解決內核內部的各種鎖問題,但事實并非如此:性能是內核最首先的一個目標,雖然信號量初看起來容易實作,但其開銷對內核來說過大,這也是內核中提供了許多不同的鎖和同步機制的原因,
二、內核鎖機制
在多處理器系統上,如果幾個處理器同時處于核心態,理論上它們可以同時訪問一個資料結構,剛好引發了競態條件,因此,在第一個SMP功能的內核版本中,對該問題的處理是每次只允許一個處理器處于核心態,但這樣效率不高,現在,內核使用了由鎖組成的細粒度網路,用以明確保護各資料結構(如果處理器A在操作資料結構X,則處理器B可以執行任何其他的內核操作,但不能操作X),
內核提供了各種鎖選項,分別優化不同的內核資料使用模式:
原子操作:這些是最簡單的鎖操作,它們保證簡單的操作,諸如計數器加1之類,可以不中斷地原子執行,即使操作由幾個匯編陳述句組成,也可以保證;
自旋鎖:這些是最常用的鎖選項,它們用于短期保護某段代碼,以防止其他處理器的訪問,在內核等待自旋鎖釋放時,會重復檢查是否能獲取鎖,而不會進入睡眠狀態(忙等待),如果等待時間較長,則效率顯然不高;
信號量:這些是用經典方法實作的,在等待信號量釋放時,內核進入睡眠狀態,直至被喚醒,喚醒后,內核才重新嘗試獲取信號量,互斥量是信號量的特例,互斥量保護的臨界區,每次只能有一個用戶進入;
讀者/寫者鎖:這些鎖會區分對資料結構的兩種不同型別的訪問,任意數目的處理器都可以對資料結構進行并發讀訪問,但只有一個處理器能進行寫訪問(在進行寫訪問時,讀訪問是無法進行的),
1、對整數的原子操作
內核定義了atomic_t資料型別,用作對整數計數器的原子操作的基礎,從內核的角度看,這些操作相當于一潭訓編陳述句,
為使得內核中平臺獨立的部分能夠使用原子操作,用于操縱atomic_t型別變數的操作必須由特定于體系結構的代碼提供(因為內核將標準型別進行了封裝,原子變數只能借助于ATOMIC_INIT宏初始化,不能用普通運算子處理),
內核為SMP系統提供了local_t資料型別,該型別允許在單個CPU上的原子操作,為修改此型別變數,內核提供了基本上與atomic_t資料型別相同的一組函式,只是將atomic替換為local,
2、自旋鎖
自旋鎖用于保護短的代碼段,其中只包含少量C陳述句,會很快執行完畢,大多數內核資料結構都有自身的自旋鎖,在處理結構中的關鍵成員時,必須獲得相應的自旋鎖,
自旋鎖通過spinlock_t資料結構實作,基本上可使用spin_lock和spin_unlock操縱,(自旋鎖的實作與體系結構相關,幾乎全是匯編語言)
自旋鎖作業情況:
如果內核中其他地方尚未獲得lock,則由當前處理器獲取,其他處理器不能再進入lock保護的代碼范圍;
如果lock已經由另一個處理器獲得,spin_lock進入一個無限回圈,重復地檢查lock是否已經由spin_unlock釋放(自旋鎖因此得名),如果已經釋放,則獲得lock,并進入臨界區,
自旋鎖使用注意:
如果獲得鎖之后不釋放,系統將變得不可用,所有的處理器(包括獲得鎖的在內),遲早需要進入鎖對應的臨界區,它們會進入無限回圈等待鎖釋放,但等不到,便產生了死鎖;
自旋鎖決不應該長期持有,因為所有等待鎖釋放的處理器都處于不可用狀態,無法用于其他作業;
內核進入到由自旋鎖保護的臨界區時,就停用內核搶占,在啟用了內核搶占的單處理器內核中,spin_lock(基本上)等價于preempt_disable,而spin_unlock則等價于preempt_enable,
3、信號量
內核使用的信號量定義如下(用戶空間信號量的實作有所不同):
struct semaphore {
atomic_t count; //count指定了可以同時處于信號量保護的臨界區中行程的數目
int sleepers; //sleepers指定了等待允許進入臨界區的行程的數目
wait_queue_head_t wait; //wait用于實作一個佇列,保存所有在該信號量上睡眠的行程的task_struct
};
與自旋鎖相比,信號量適合于保護更長的臨界區,以防止并行訪問,它們不應該用于保護較短的代碼范圍,因為競爭信號量時需要使行程睡眠和再次喚醒,代價很高,
大多數情況下,不需要使用信號量的所有功能,只是將其用作互斥量,這是一種二值信號量,
信號量作業情況:
- 在進入臨界區時,用down對使用計數器減1,在計數器為0時,其他行程不能進入臨界區;
- 在試圖用down獲取已經分配的信號量時,當前行程進入睡眠,并放置在與該信號量關聯的等待佇列上,同時,該行程被置于TASK_UNINTERRUPTIBLE狀態,在等待進入臨界區的程序中無法接收信號,如果信號量沒有分配,則該行程可以立即獲得信號量并進入到臨界區,而不會進入睡眠;
- 在退出臨界區時,必須呼叫up,該例程負責喚醒在信號量睡眠的某個行程,該行程然后允許進入臨界區,而所有其他等待的行程繼續睡眠,
除了只能用于內核的互斥量之外,Linux也提供了futex(快速用戶空間互斥量,fastuserspacemutex),由核心態和用戶狀態組合而成,為用戶空間行程提供了互斥量功能,
4、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保護,
5、記憶體和優化屏障
盡管鎖足以確保原子性,但對編譯器和處理器優化過的代碼,鎖不能永遠保證時序正確,與競態條件相比,這個問題不僅影響SMP系統,也影響單處理器計算機,
內核提供了下面幾個函式,可阻止處理器和編譯器進行代碼重排,
mb()、rmb()、wmb()將硬體記憶體屏障插入到代碼流程中,rmb()是讀訪問記憶體屏障,它保證在屏障之后發出的任何讀取操作執行之前,屏障之前發出的所有讀取操作都已經完成,wmb適用于寫訪問,語意與rmb類似,讀者應該能猜到,mb()合并了二者的語意,
barrier插入一個優化屏障,該指令告知編譯器,保存在CPU暫存器中、在屏障之前有效的所有記憶體地址,在屏障之后都將失效,本質上,這意味著編譯器在屏障之前發出的讀寫請求完成之前,不會處理屏障之后的任何讀寫請求,
但CPU仍然可以重排時序!
smb_mb()、smp_rmb()、smp_wmb()相當于上述的硬體記憶體屏障,但只用于SMP系統,它們在單處理器系統上產生的是軟體屏障,
read_barrier_depends()是一種特殊形式的讀訪問屏障,它會考慮讀操作之間的依賴性,如果屏障之后的讀請求,依賴于屏障之前執行的讀請求的資料,那么編譯器和硬體都不能重排這些請求,
6、讀者/寫者鎖
通常,任意數目的行程都可以并發讀取資料結構,而寫訪問只能限于一個行程,因此內核提供了額外的信號量和自旋鎖版本,分別稱之為讀者/寫者信號量和讀者/寫者自旋鎖,
讀者/寫者自旋鎖定義為rwlock_t資料型別,必須根據讀寫訪問,以不同的方法獲取鎖,
行程對臨界區進行讀訪問時,在進入和離開時需要分別執行read_lock和read_unlock,內核會允許任意數目的讀行程并發訪問臨界區;
write_lock和write_unlock用于寫訪問,內核保證只有一個寫行程(此時沒有讀行程)能夠處于臨界區中,
讀/寫信號量的用法類似,所用的資料結構是struct rw_semaphore,down_read和up_read用于獲取對臨界區的讀訪問,寫訪問借助于down_write和up_write進行,
7、大內核鎖
大內核鎖(big kernel lock)可以鎖定整個內核,確保沒有處理器在核心態并行運行(已經過時啦),使用lock_kernel可鎖定整個內核,對應的解鎖使用unlock_kernel,SMP系統和啟用了內核搶占的單處理器系統如果設定了配置選項PREEMPT_BKL,則允許搶占大內核鎖,
8、互斥量
盡管信號量可用于實作互斥量的功能,信號量的通用性導致的開銷通常是不必要的,因此,內核包含了一個專用互斥量的獨立實作,它們不依賴信號量,內核包含互斥量的兩種實作:一種是經典的互斥量,另一種是用來解決優先級反轉問題的實時互斥量,
(1)經典的互斥量
經典互斥量的基本資料結構定義如下:
struct mutex {
/* 1: 未鎖定, 0: 鎖定, 負值:鎖定,可能有等待者 */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
};
如果互斥量未鎖定,則count為1,鎖定分為兩種情況:如果只有一個行程在使用互斥量,則count設定為0,如果互斥量被鎖定,而且有行程在等待互斥量解鎖(在解鎖時需要喚醒等待行程),則count為負值,這種特殊處理有助于加快代碼的執行速度,因為在通常情況下,不會有行程在互斥量上等待,
定義新的互斥量:
- 靜態互斥量可以在編譯時通過使用DEFINE_MUTEX產生(與DECLARE_MUTEX區分,后者是基于信號量的互斥量);
- mutex_init在運行時動態初始化一個新的互斥量;
- mutex_lock和mutex_unlock分別用于鎖定和解鎖互斥量,
(2)實時互斥量
實時互斥量是內核支持的另一種形式的互斥量,需要在編譯時通過配置選項CONFIG_RT_MUTEX顯式啟用,與普通的互斥量相比,它們實作了優先級繼承(priority inheritance),該特性可用于解決(或在最低限度上緩解)優先級反轉的影響,
對于優先級反轉問題,可以通過優先級繼承解決,如果高優先級行程阻塞在互斥量上,該互斥量當前由低優先級行程持有,那么低優先級行程的優先級會臨時提高到高優先級行程的優先級,
實時互斥量的定義非常接近于普通互斥量:
struct rt_mutex {
spinlock_t wait_lock;
struct plist_head wait_list;
struct task_struct *owner;
};
互斥量的所有者通過owner指定,wait_lock提供實際的保護,所有等待的行程都在wait_list中排隊,與普通互斥量相比,決定性的改變是等待串列中的行程按優先級排序,在等待串列改變時,內核可相應地校正鎖持有者的優先級,這需要到調度器的一個介面,可由函式rt_mutex_setprio提供,該函式更新動態優先級task_struct->prio,而普通優先級task_struct->normal_priority不變,
9、近似的per_CPU計數器
如果系統安裝有大量CPU,計數器可能成為瓶頸:每次只有一個CPU可以修改其值;所有其他CPU都必須等待操作結束,才能再次訪問計數器,如果計數器頻繁訪問,則會嚴重影響系統性能,
對某些計數器,沒有必要時時了解其準確的數值,這種計數器的近似值與準確值,作用上沒什么差別,可以利用這種情況,引入per-CPU計數器,加速SMP系統上計數器的操作,如圖所示,計數器的準確值存盤在記憶體中某處,準確值所在記憶體位置之后是一個陣列,每個陣列項對應于系統中的一個CPU,

如果一個處理器想要修改計數器的值(加上或減去某個值n),它不會直接修改計數器的值,因為這需要防止其他的CPU訪問計數器(這是一個費時的操作),相反,所需的修改將保存到與計數器相關的陣列中特定于當前CPU的陣列項,(舉例:,如果計數器應該加3,那么陣列中對應的陣列項為+3,如果同一個CPU在其他時間需要從計數器減去某個值(假定是5),它也不會對計數器直接操作,而是操作陣列中特定于CPU的項:將3減去5,新值為-2,任何處理器讀取計數器值時,都不是完全準確的,如果原值為15,在經過前述的操作之后應該是13,但仍然是15,如果只需要大致了解計數器的值,13也算得上是15的一個比較好的近似了,)
如果某個特定于CPU的陣列元素修改后的絕對值超出某個閾值,則認為這種修改有問題,將隨之修改計數器的值(這種改變很少發生),在這種情況下,內核需要確保通過適當的鎖機制來保護這次訪問,
只要計數器改變適度,這種方案中讀操作得到的平均值會相當接近于計數器的準確值,
per-CPU計數器如下:
struct percpu_counter {
spinlock_t lock;
long count;
long *counters;
};
count是計數器的準確值,lock是一個自旋鎖,用于在需要準確值時保護計數器,counters陣列中各陣列項是特定于CPU的,該陣列快取了對計數器的操作,
10、鎖競爭與細粒度鎖
Linux在多CPU系統上的可伸縮性已經成為一個非常重要的目標,在對內核代碼設計鎖規則時,特別需要考慮這個問題,鎖需要滿足下面兩個目的(不過二者通常很難同時實作):
必須防止對代碼的并發訪問,否則將導致失敗;
對性能的影響必須盡可能小,
對于內核頻繁使用的資料,同時滿足這兩個要求是非常復雜的,如果一整個資料結構都由一個鎖保護,那么在內核的某個部分需要獲取鎖的時候,該鎖已經被系統的其他部分獲取的概率很高,這種情況下會出現較多的鎖競爭(lock contention),該鎖也會成為內核的一個熱點,對此,將資料結構標識為各個獨立的部分,使用多個鎖來保護,這種解決方案稱為細粒度鎖,
細粒度鎖在較大的計算機上對提高可伸縮性很有好處,但也有兩個弊端:
獲取多個鎖會增加操作的開銷,特別是在較小的SMP計算機上;
在通過多個鎖保護一個資料結構時,很自然會出現一個操作需要同時訪問兩個受保護區域的情形,因而需要同時持有多個鎖,這要求必須遵守某種鎖定次序,必須按序獲取和釋放鎖,否則,仍然會導致死鎖,
推薦自己的Linux、C/C++技術交流群:【960994558】整理了一些個人覺得比較好的學習書籍、視頻資料共享在里面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等.),有需要的可以自行添加哦!~

三、System V行程間通信
Linux使用System V(SysV)引入的機制,來支持用戶行程的行程間通信和同步,
1、System V機制
System V UNIX的3種行程間通信(IPC)機制(信號量、訊息佇列、共享記憶體),都使用了全系統范圍的資源,可以由幾個行程同時共享,
在各個獨立行程能夠訪問SysV IPC物件之前,IPC物件必須在系統內唯一標識,為此,每種IPC結構在創建時分配了一個號碼,稱為魔數,凡知道這個魔數的各個程式,都能夠訪問對應的結構,如果獨立的應用程式需要彼此通信,則通常需要將該魔數永久地編譯到程式中,
在訪問IPC物件時,系統采用了基于檔案訪問權限的一個權限系統,每個IPC物件都有一個用戶ID和一個組ID,依賴于產生IPC物件的程式在何種UID/GID之下運行,讀寫權限在初始化時分配,類似于普通的檔案,這些控制了3種不同用戶類別的訪問:所有者、組、其他,
要創建一個授予所有可能訪問權限的信號量(所有者、組、其他用戶都有讀寫權限),則必須指定標志0666,
2、信號量
(1)使用System V信號量
System V的信號量不再當作是用于支持原子執行預定義操作的簡單型別變數,它是指一整套信號量,可以允許幾個操作同時進行(用戶看上去是原子的),也可以請求只有一個信號量的信號量集合,并定義函式模擬原始信號量的簡單操作,
(2)資料結構
內核使用了幾個資料結構來描述所有注冊信號量的當前狀態,并建立了一種網狀結構,它們不僅負責管理信號量及其特征(值、讀寫權限,等等),還負責通過等待串列將信號量與等待行程關聯起來,
初始的默認的IPC命名空間通過ipc_namespace的靜態實體init_ipc_ns實作,每個命名空間都包含如下資訊:
struct ipc_namespace {
...
struct ipc_ids *ids[3];
/* 資源限制 */
...
}
這里略去了與監視資源消耗和設定資源限制相關的很多資料結構成員(比如共享記憶體頁的最大數目、共享記憶體段的最大長度、訊息佇列的最大數目等),陣列ids的每個元素對應于一種IPC機制:信號量、訊息佇列、共享記憶體(按順序),每個陣列項指向一個struct ipc_ids的實體,用于跟蹤各類別現存的IPC物件,為防止對每個類別都需要查找對應的正確陣列索引,內核提供了輔助函式msg_ids、shm_ids和sem_ids,
struct ipc_ids定義如下:
struct ipc_ids {
int in_use; //保存了當前使用中IPC物件的數目
unsigned short seq; //seq和seq_max用于連續產生用戶空間IPC ID(不等同于序號)
unsigned short seq_max;
struct rw_semaphore rw_mutex; //一個內核信號量,用于實作信號量操作,避免用戶空間中的競態條件
struct idr ipcs_idr;
};
每個IPC物件都由kern_ipc_perm的一個實體表示,每個物件都有一個內核內部ID,ipcs_idr用于將ID關聯到指向對應的kern_ipc_perm實體的指標,使用中IPC物件的數目可能動態地增長和縮減,內核提供了一個類似于基數樹的標準資料結構用于管理該資訊,
struct kern_ipc_perm
{
int id;
key_t key; //保存了用戶程式用來標識信號量的魔數
uid_t uid; //指所有者的用戶ID
gid_t gid; //指所有者的組ID
uid_t cuid; //保存了產生信號量的行程的用戶ID
gid_t cgid; //保存了產生信號量的行程的組ID
mode_t mode; //保存了位掩碼,指定了所有者、組、其他用戶的訪問權限
unsigned long seq; //分配IPC物件時使用的序號
};
該結構不僅可用于信號量,還可以用于其他的IPC機制,該結構不足以保存信號量所需的所有資訊,各行程的task_struct實體中有一個與IPC相關的成員:
struct task_struct {
...
#ifdef CONFIG_SYSVIPC
/* ipc相關 */
struct sysv_sem sysvsem;
#endif
...
};
只有設定了配置選項CONFIG_SYSVIPC時,SysV相關代碼才會編譯到內核中,sysv_sem資料結構封裝了一個成員struct sem_undo_list *undo_list用于撤銷信號量(用于崩潰行程修改了信號量狀態的情況),
sem_queue是另一個資料結構,用于將信號量與睡眠行程關聯起來,該行程想要執行信號量操作,但目前不允許執行,
struct sem_queue {
struct sem_queue * next; /* 佇列中下一項 */
struct sem_queue ** prev; /* 佇列中的前一項,對于第一項有*(q->prev) == q */
struct task_struct* sleeper; /* 睡眠的行程 */
struct sem_undo * undo; /* 用于撤銷的結構 */
int pid; /* 請求信號量操作的行程ID, */
int status; /* 操作的完成狀態 */
struct sem_array * sma; /* 操作的信號量陣列 */
int id; /* 內部信號量ID */
struct sembuf * sops; /* 待決操作陣列 */
int nsops; /* 運算元目 */
int alter; /* 操作是否改變了陣列? */
};
系統中每個信號量集合,都對應于sem_array資料結構的一個實體,該實體用于管理集合中的所有信號量,sem_array結構如下:
struct sem_array {
struct kern_ipc_perm sem_perm; /* 權限,參見ipc.h */
time_t sem_otime; /* 最后一次信號量操作的時間 */
time_t sem_ctime; /* 最后一次修改的時間 */
struct sem *sem_base; /* 指向陣列中第一個信號量的指標 */
struct sem_queue *sem_pending; /* 需要處理的待決操作 */
struct sem_queue **sem_pending_last; /* 上一個待決操作 */
struct sem_undo *undo; /* 該陣列上的撤銷請求 */
unsigned long sem_nsems; /* 陣列中信號量的數目 */
};
圖給出了所涉及的各個資料結構之間的相互關系,

kern_ipc_perm是用于管理IPC物件的資料結構的第一個成員,不僅對信號量是這樣,訊息佇列和共享記憶體物件也是如此,
(3)實作系統呼叫
所有對信號量的操作都使用一個名為ipc的系統呼叫執行,該呼叫不僅用于信號量,也用于操作訊息佇列和共享記憶體,其第一個引數用于將實際作業委托給其他函式,用于信號量的函式如下所示,
SEMCTL執行信號量操作,并由sys_semctl實作;
SEMGET讀取信號量ID,相關的實作由sys_semget提供;
SEMOP和SEMTIMEDOP負責增加和減少信號量值,后者可以指定超時時間限制,
(4)權限檢查
IPC物件的保護機制,與普通的基于檔案的物件相同,訪問權限可以分別對物件的所有者、所有者所在組和所有其他用戶指定(可能的權限包括讀、寫、執行),函式ipcperms負責檢查對任意IPC物件的某種操作是否有權限進行,
3、訊息佇列
行程之間通信的另一個方法是交換訊息,這是使用訊息佇列機制完成的,其實作基于System V模型,訊息佇列的功能原理相對簡單,如圖所示,

產生訊息并將其寫到佇列的行程通常稱之為發送者,而一個或多個其他行程(邏輯上稱之為接收者)則從佇列獲取資訊,各個訊息包含訊息正文和一個(正)數,以便在訊息佇列內實作幾種型別的訊息,接收者可以根據該數字檢索訊息(比如可以指定只接受編號1的訊息,或接受編號不大于5的訊息),在訊息已經讀取后,內核將其從佇列洗掉,即使幾個行程在同一信道上監聽,每個訊息仍然只能由一個行程讀取,
同一編號的訊息按先進先出次序處理,放置在佇列開始的訊息將首先讀取,但如果有選擇地讀取訊息,則先進先出次序就不再適用,
訊息佇列也是使用前述信號量哪些資料結構實作,起始點是當前命名空間的適當的ipc_ids實體,內部的ID號形式上關聯到kern_ipc_perm實體,在訊息佇列的實作中,需要通過型別轉換獲得不同的資料型別(struct msg_queue),該結構定義如下:
struct msg_queue {
struct kern_ipc_perm q_perm;
time_t q_stime; /* 上一次呼叫msgsnd發送訊息的時間 */
time_t q_rtime; /* 上一次呼叫msgrcv接收訊息的時間 */
time_t q_ctime; /* 上一次修改的時間 */
unsigned long q_cbytes; /* 佇列上當前位元組數目 */
unsigned long q_qnum; /* 佇列中的訊息數目 */
unsigned long q_qbytes; /* 佇列上最大位元組數目 */
pid_t q_lspid; /* 上一次呼叫msgsnd的pid */
pid_t q_lrpid; /* 上一次接收訊息的pid */
struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
};
3個標準的內核鏈表用于管理睡眠的發送者(q_senders)、睡眠的接收者(q_receivers)和訊息本身(q_messages),各個鏈表都使用獨立的資料結構作為鏈表元素,
q_messages中的各個訊息都封裝在一個msg_msg實體中,
struct msg_msg {
struct list_head m_list;
long m_type; //指定了訊息型別,用于支持前文所述訊息佇列中不同的訊息型別,
int m_ts; /* 訊息正文長度 */
struct msg_msgseg* next; //如果保存超過一個記憶體頁的長訊息,則需要next
/* 接下來是實際的訊息 */
};
結構中沒有指定存盤訊息自身的欄位,因為每個訊息都(至少)分配了一個記憶體頁,msg_msg實體則保存在該頁的起始處,剩余的空間可用于存盤訊息正文,如圖所示,從記憶體頁的長度,減去msg_msg結構的長度,即可得到msg_msg頁中可用于訊息正文的最大位元組數目,

訊息正文緊接著該資料結構的實體之后存盤,使用next,可以使訊息分布到任意數目的頁上,在通過訊息佇列通信時,發送行程和接收行程都可以進入睡眠:如果訊息佇列已經達到最大容量,則發送者在試圖寫入訊息時會進入睡眠;如果佇列中沒有訊息,那么接收者在試圖獲取訊息時會進入睡眠,
睡眠的發送者放置在msg_queue的q_senders鏈表中,鏈表元素使用下列資料結構:
struct msg_sender {
struct list_head list; //鏈表元素
struct task_struct* tsk; //指向對應行程的task_struct的指標
};
q_receivers鏈表中用于保存接收行程的資料結構要稍長一點,
struct msg_receiver {
struct list_head r_list;
struct task_struct *r_tsk;
int r_mode;
long r_msgtype;
long r_maxsize;
struct msg_msg *volatile r_msg;
};
其中不僅保存了指向對應行程的task_struct的指標,還包括了對預期訊息的描述,以及指向msg_msg實體的一個指標(訊息可用時,該指標指定了復制資料的目標地址),
圖是訊息佇列所涉及資料結構之間的相互關系(忽略睡眠的發送行程鏈表),

4、共享記憶體
與信號量和訊息佇列相比,共享記憶體沒有本質性的不同,
應用程式請求的IPC物件,可以通過魔數和當前命名空間的內核內部ID訪問;
對記憶體的訪問,可能受到權限系統的限制;
可以使用系統呼叫分配與IPC物件關聯的記憶體,具備適當授權的所有行程,都可以訪問該記憶體,
內核的實作采用了與信號量和訊息佇列非常類似的概念,相關資料結構關系如圖所示,

在smd_ids全域變數的entries陣列中保存了kern_ipc_perm和shmid_kernel的組合,以便管理IPC物件的訪問權限,對每個共享記憶體物件都創建一個偽檔案,通過shm_file連接到shmid_kernel的實體,內核使用shm_file->f_mapping指標訪問地址空間物件(struct address_space),用于創建匿名映射,還需要設定所涉及各行程的頁表,使得各個行程都能夠訪問與該IPC物件相關的記憶體區域,
四、其他IPC機制
SysV IPC通常只對應用程式員有意義,但對shell的用戶,信號和管道更常用,
1、信號
與SysV機制相比,信號是一種比較原始的通信機制,其底層概念非常簡單,kill命令根據PID向行程發送信號,信號通過-s sig指定,是一個正整數,最大長度取決于處理器型別,
行程必須設定處理程式例程來處理信號,這些例程在信號發送到行程時呼叫(行程可以決定阻塞某些信號,但有幾個信號的行為無法修改,如SIGKILL),如果沒有顯式設定處理程式例程,內核則使用默認的處理程式實作,(init行程屬于特例,內核會忽略發送給該行程的SIGKILL信號,)
(1)實作信號處理程式
sigaction系統呼叫用于設定新的處理程式,如果沒有為某個信號分配用戶定義的處理程式函式,內核會自動設定預定義函式,提供合理的標準操作來處理相應的情況,
sigaction型別中用于描述處理程式的欄位,其定義是平臺相關的,但在所有體系結構上幾乎都相同,
struct sigaction {
__sighandler_t sa_handler; //一個指向內核在信號到達時呼叫的處理程式函式的指標
unsigned long sa_flags; //包含了額外的標志,用于指定信號處理方式的一些約束
...
sigset_t sa_mask; //包含了一個位掩碼,每個位元位對應于系統中的一個信號
};
信號處理程式的函式原型如下:
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;
其引數是信號的編號,因此可以使用同一個處理程式函式處理不同的信號,
信號處理程式使用sigaction系統呼叫設定,該呼叫將借助用戶定義的處理程式函式替換SIGTERM的默認處理程式,
(2)實作信號管理
所有信號相關的資料都是借助于鏈式資料結構管理的,其入口是task_struct結構,其中包含了各個與信號相關的欄位,
struct task_struct {
...
/* 信號處理程式 */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
...
};
信號處理發生在內核中,但設定的信號處理程式是在用戶狀態運行,通常,信號處理程式使用所述行程在用戶狀態下的堆疊,但POSIX強制要求提供一種選項,在專門用于信號處理的堆疊上運行信號處理程式,這個附加的堆疊(必須通過用戶應用程式顯式分配),其地址和長度分別保存在sas_ss_sp和sas_ss_size,
用于管理設定的信號處理程式的資訊的sighand_struct如下所示:
struct sighand_struct {
atomic_t count; //保存了共享該結構實體的行程數目
struct k_sigaction action[_NSIG]; //保存設定的信號處理程式,_NSIG指定了可以處理的不同信號的數目
} ;
所有阻塞信號由task_struct的blocked成員定義,所使用的sigset_t資料型別是一個位掩碼,所包含的位元位數目必須(至少)與所支持的信號數目相同,其資料結構為:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
pending是task_struct中與信號處理相關的最后一個成員,它建立了一個鏈表,包含所有已經引發、仍然有待內核處理的信號,它們使用了下列資料結構:
struct sigpending {
struct list_head list; //通過雙鏈表管理所有待決信號
sigset_t signal; //位掩碼,指定了仍然有待處理的所有信號的編號
};
圖為各結構體之間的關系,

(3)實作信號處理
內核用于實作信號處理的最重要的系統呼叫有kill(向行程組的所有行程發送一個信號)、tkill(向單個行程發送一個信號)、sigpending(檢查是否有待決信號)、sigprocmask(操作阻塞信號的位掩碼)、sigsuspend(進入睡眠,直至接收某特定信號),
對于發送信號,不論名稱如何,實際上kill和tkill基本相同,以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函式(此處不詳述),
從時序上看,信號處理的程序如圖所示,

2、管道和套接字
管道和套接字是流行的行程間通信機制,管道使用了虛擬檔案系統物件,套接字使用了各種網路函式以及虛擬檔案系統,
管道是用于交換資料的連接,一個行程向管道的一端供給資料,另一個在管道另一端取出資料,供進一步處理,幾個行程可以通過一系列管道連接起來,
管道是行程地址空間中的資料物件,在用fork或clone復制行程時同樣會被復制,使用管道通信的程式就利用了這種特征,在exec系統呼叫用另一個程式替換子行程之后,兩個不同的應用程式之間就建立了一條通信鏈路(必須把管道描述符重定向到標準輸入和輸出,或者呼叫dup系統呼叫,確保exec呼叫時不會關閉檔案描述符),
套接字物件在內核中初始化時也回傳一個檔案描述符,因此可以像普通檔案一樣處理,與管道不同之處在于它可以雙向使用,還可以用于通過網路連接的遠程系統通信,從用戶的角度來看,同一系統上兩個本地行程之間基于套接字的通信與分別處于兩個不同大陸兩臺計算機上運行的應用程式之間的通信沒有太大差別,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/232551.html
標籤:其他
上一篇:cgb2008-京淘day12
下一篇:如何用C語言完成水仙花數的搜索
