當多核CPU同時執行一段代碼的時候,就容易發生搶佔,這段代碼可以叫做臨界區,其他內核控制路徑能夠進入臨界區前,進入臨界區前的內核控制路徑必須全部執行完這段代碼,爲了避免這種共享數據發生競爭,就需要採用同步技術,本文就簡單介紹linux內核當中的一些同步原語;
(一) per-cpu變量
最簡單的同步技術就是把內核變量申明爲per-cpu變量,這個變量只會在本地CPU操作時調用,就不用考慮其他CPU搶佔的情況;
將一個共享memory變成Per-CPU memory本質上是一個耗費更多memory來解決performance的方法。當一個在多個CPU之間共享的變量變成每個CPU都有屬於自己的一個私有的變量的時候,我們就不必考慮來自多個CPU上的併發,僅僅考慮本CPU上的併發就可以了;
注意:
per-cpu變量爲來自不同的CPU的併發訪問提供保護,但對來自異步函數(中斷函數和可延遲函數)的訪問不提供保護;
內核搶佔可能使CPU變量產生競爭條件,內核控制路徑應該在禁用搶佔的情況下訪問per-cpu變量;
per-cpu API:
聲明和定義Per-CPU變量的API | 描述 |
---|---|
DECLARE_PER_CPU(type, name) DEFINE_PER_CPU(type, name) | 普通的、沒有特殊要求的per cpu變量定義接口函數 |
DECLARE_PER_CPU_FIRST(type, name) DEFINE_PER_CPU_FIRST(type, name) | 通過該API定義的per cpu變量位於整個per cpu相關section的最前面 |
DECLARE_PER_CPU_SHARED_ALIGNED(type, name) DEFINE_PER_CPU_SHARED_ALIGNED(type, name) | 通過該API定義的per cpu變量在SMP的情況下會對齊到L1 cache line ,對於UP,不需要對齊到cachine line |
DECLARE_PER_CPU_ALIGNED(type, name) DEFINE_PER_CPU_ALIGNED(type, name) | 無論SMP或者UP,都是需要對齊到L1 cache line |
DECLARE_PER_CPU_PAGE_ALIGNED(type, name) DEFINE_PER_CPU_PAGE_ALIGNED(type, name) | 爲定義page aligned per cpu變量而設定的API接口 |
DECLARE_PER_CPU_READ_MOSTLY(type, name) DEFINE_PER_CPU_READ_MOSTLY(type, name) | 通過該API定義的per cpu變量是read mostly的 |
靜態定義的per cpu變量不能象普通變量那樣進行訪問,需要使用特定的接口函數,具體如下:
233 #define this_cpu_ptr(ptr) \
234 ({ \
235 __verify_pcpu_ptr(ptr); \
236 SHIFT_PERCPU_PTR(ptr, my_cpu_offset); \
//SHIFT_PERCPU_PTR: 原始的per cpu變量的地址,經過shift轉成實際的percpu 副本的地址;
237 })
260 /*
261 * Must be an lvalue. Since @var must be a simple identifier,
262 * we force a syntax error here if it isn't.
263 */
264 #define get_cpu_var(var) \
265 (*({ \
266 preempt_disable(); \
267 this_cpu_ptr(&var); \
268 }))
269
270 /*
271 * The weird & is necessary because sparse considers (void)(var) to be
272 * a direct dereference of percpu variable (var).
273 */
274 #define put_cpu_var(var) \
275 do { \
276 (void)&(var); \
277 preempt_enable(); \
278 } while (0)
上面這兩個接口函數已經內嵌了鎖的機制(preempt disable),用戶可以直接調用該接口進行本CPU上該變量副本的訪問。如果用戶確認當前的執行環境已經是preempt disable(或者是更厲害的鎖,例如關閉了CPU中斷),那麼可以使用lock-free版本的Per-CPU變量的API:__get_cpu_var :
258 #define __get_cpu_var(var) (*this_cpu_ptr(&(var)))
只有Per-CPU變量的原始變量還是不夠的,必須爲每一個CPU建立一個副本,怎麼建?直接靜態定義一個NR_CPUS的數組?NR_CPUS定義了系統支持的最大的processor的個數,並不是實際中系統processor的數目,這樣的定義非常浪費內存對於NUMA系統,每個CPU上的Per-CPU變量的副本應該位於它訪問最快的那段memory上,也就是說Per-CPU變量的各個CPU副本可能是散佈在整個內存地址空間的,而這些空間之間是有空洞的。
(二)原子操作
那些有多個內核控制路徑進行read-modify-write的變量,內核提供了一個特殊的類型atomic_t來避免竟態,申明的變量就叫原子變量,這樣的行爲我們可以叫做原子操作;
定義:atomic_t val_name = ATOMIC_INIT(val);
typedef struct {
int counter;
} atomic_t;
原子操作API
接口函數 | 描述 |
---|---|
static inline void atomic_add(int i, atomic_t *v) | 給一個原子變量v增加i |
static inline int atomic_add_return(int i, atomic_t *v) | 同上,只不過將變量v的最新值返回 |
static inline void atomic_sub(int i, atomic_t *v) | 給一個原子變量v減去i |
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new) | 比較old和原子變量ptr中的值,如果相等,那麼就把new值賦給原子變量。返回舊的原子變量ptr中的值 |
atomic_read | 獲取原子變量的值 |
atomic_set | 設定原子變量的值 |
atomic_inc(v) | 原子變量的值加1 |
atomic_dec(v) | 原子變量的值減去1 |
atomic_sub_and_test(i, v) | 給一個原子變量v減去i,並判斷變量v的最新值是否等於0 |
atomic_add_negative(i,v) | 給一個原子變量v增加i,並判斷變量v的最新值是否是負數 |
static inline int atomic_add_unless(atomic_t *v, int a, int u) | 只要原子變量v不等於u,那麼就執行原子變量v加a的操作,如果v不等於u,返回非0值,否則返回0值 |
代碼分析:
TODO
(三) memory barrier
編譯器可以在將c翻譯成彙編的時候進行優化(例如內存訪問指令的重新排序),讓產出的彙編指令在CPU上運行的時候更快;然而,這種優化產出的結果未必符合程序員原始的邏輯;
程序員需通過內嵌在c代碼中的memory barrier來指導編譯器的優化行爲(這種memory barrier又叫做優化屏障,Optimization barrier),讓編譯器產出即高效,又邏輯正確的代碼;
Memory barrier 包括兩類:
1.編譯器 barrier
2.CPU Memory barrier
Memory barrier 能夠讓 CPU 或編譯器在內存訪問上有序。一個 Memory barrier 之前的內存訪問操作必定先於其之後的完成。
Linux 內核提供函數 barrier() 用於讓編譯器保證其之前的內存訪問先於其之後的完成;
memory barrier相關的API列表:
接口名稱 | 描述 |
---|---|
barrier() | 優化屏障,阻止編譯器爲了進行性能優化而進行的memory access smp_wmb()reorder; |
mb() | 內存屏障(包括讀和寫),用於SMP和UP; |
rmb() | 讀內存屏障,用於SMP和UP; |
wmb() | 寫內存屏障,用於SMP和UP; |
smp_mb() | 用於SMP場合的內存屏障,對於UP不存在memory order的問題(對彙編指令),因此,在UP上就是一個優化屏障,確保彙編和c代碼的memory order是一致的; |
smp_rmb() | 用於SMP場合的讀內存屏障; |
smp_wmb() | 用於SMP場合的寫內存屏障; |
13 /* Optimization barrier */
14 /* The "volatile" is due to gcc bugs */
15 #define barrier() __asm__ __volatile__("": : :"memory")
53 #define smp_mb() barrier()
54 #define smp_rmb() barrier()
55 #define smp_wmb() barrier()
這裏的memory就是告知gcc,在彙編代碼中,我修改了memory中的內容,嵌入式彙編之前的c代碼塊和嵌入式彙編之後的c代碼塊看到的memory是不一樣的,對memory的訪問不能依賴於嵌入式彙編之前的c代碼塊中寄存器的內容,需要重新加載;
而之所以會有memory barrier這個“邪惡的東西”是由於CPU的速度要快於(數量級上的差異)memory以及他們之間的互連器件;
對於memory需要更深入的理解可以查看:https://www.kernel.org/doc/Documentation/memory-barriers.txt
(四) 自旋鎖spin_lock
如果共享數據被中斷上下文和進程上下文訪問,該如何保護呢?如果只有進程上下文的訪問,那麼可以考慮使用semaphore或者mutex的鎖機制,但是現在中斷上下文也參和進來,那些可以導致睡眠的lock就不能使用了,這時候,可以考慮使用spin lock;
特點:
spin lock是一種死等的鎖機制。當前的執行thread會不斷的重新嘗試直到獲取鎖進入臨界區。
只允許一個thread進入。semaphore可以允許多個thread進入,spin lock不行,一次只能有一個thread獲取鎖並進入臨界區,其他的thread都是在門口不斷的嘗試。
執行時間短。由於spin lock死等這種特性,因此它使用在那些代碼不是非常複雜的臨界區(當然也不能太簡單,否則使用原子操作或者其他適用簡單場景的同步機制就OK了),如果臨界區執行時間太長,那麼不斷在臨界區門口“死等”的那些thread是多麼的浪費CPU;
可以在中斷上下文執行。由於不睡眠,因此spin lock可以在中斷上下文中適用。
當兩個進程執行在同一個cpu,執行在進程上下文的時候,其中一個進程獲取spin_lock的時候禁止本cpu上的搶佔;如果兩個進程在不同的cpu上的時候,某個進程會等待已經獲取鎖的進程釋放鎖;
接口API的類型 | spinlock中的定義 | raw_spinlock的定義 |
---|---|---|
定義spin lock並初始化 : | DEFINE_SPINLOCK | DEFINE_RAW_SPINLOCK |
動態初始化spin lock : | spin_lock_init | raw_spin_lock_init |
獲取指定的spin lock : | spin_lock | raw_spin_lock |
獲取指定的spin lock同時disable本CPU中斷 : | spin_lock_irq | raw_spin_lock_irq |
保存本CPU當前的irq狀態,disable本CPU中斷並獲取指定的spin lock : | spin_lock_irqsave | raw_spin_lock_irqsave |
獲取指定的spin lock同時disable本CPU的bottom half : | spin_lock_bh | raw_spin_lock_bh |
釋放指定的spin lock : | spin_unlock | raw_spin_unlock |
釋放指定的spin lock同時enable本CPU中斷 : | spin_unlock_irq | raw_spin_unock_irq |
釋放指定的spin lock同時恢復本CPU的中斷狀態: | spin_unlock_irqstore | raw_spin_unlock_irqstore |
釋放指定的spin lock同時enable本CPU的bottom half: | spin_unlock_bh | raw_spin_unlock_bh |
嘗試去獲取spin lock,如果失敗,不會spin,而是返回非零值 : | spin_trylock | raw_spin_trylock |
判斷spin lock是否是locked,如果其他的thread已經獲取了該lock,那麼返回非零值,否則返回0 : | spin_is_locked | raw_spin_is_locked |
這裏爲什麼有raw_spin_lock:
要解答這個問題,我們要回到2004年,MontaVista Software, Inc的開發人員在郵件列表中提出來一個Real-Time Linux Kernel的模型,旨在提升Linux的實時性,之後Ingo Molnar很快在他的一個項目中實現了這個模型,並最終產生了一個Real-Time preemption的patch。
該模型允許在臨界區中被搶佔,而且申請臨界區的操作可以導致進程休眠等待,這將導致自旋鎖的機制被修改,由原來的整數原子操作變更爲信號量操作。當時內核中已經有大約10000處使用了自旋鎖的代碼,直接修改spin_lock將會導致這個patch過於龐大,於是,他們決定只修改哪些真正不允許搶佔和休眠的地方,而這些地方只有100多處,這些地方改爲使用raw_spin_lock,但是,因爲原來的內核中已經有raw_spin_lock這一名字空間,用於代表體系相關的原子操作的實現,於是linus本人建議:
把原來的raw_spin_lock改爲arch_spin_lock;
把原來的spin_lock改爲raw_spin_lock;
實現一個新的spin_lock;
對於2.6.33和之後的版本,我的理解是:
儘可能使用spin_lock;
絕對不允許被搶佔和休眠的地方,使用raw_spin_lock,否則使用spin_lock;
如果你的臨界區足夠小,使用raw_spin_lock;
總結就是:spinlock,在rt linux(配置了PREEMPT_RT)的時候可能會被搶佔(實際底層可能是使用支持PI(優先級翻轉)的mutext)。而raw_spinlock,即便是配置了PREEMPT_RT也要頑強的spin;arch_spinlock是architecture相關的實現 ;
64 typedef struct spinlock {
65 union {
66 struct raw_spinlock rlock;
67
68 #ifdef CONFIG_DEBUG_LOCK_ALLOC
69 # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
70 struct {
71 u8 __padding[LOCK_PADSIZE];
72 struct lockdep_map dep_map;
73 };
74 #endif
75 };
76 } spinlock_t;
20 typedef struct raw_spinlock {
21 arch_spinlock_t raw_lock;
22 #ifdef CONFIG_GENERIC_LOCKBREAK
23 unsigned int break_lock;
24 #endif
25 #ifdef CONFIG_DEBUG_SPINLOCK
26 unsigned int magic, owner_cpu;
27 void *owner;
28 #endif
29 #ifdef CONFIG_DEBUG_LOCK_ALLOC
30 struct lockdep_map dep_map;
31 #endif
32 } raw_spinlock_t;
首先定義一個spinlock_t的數據類型,其本質上是一個整數值(對該數值的操作需要保證原子性),該數值表示spin lock是否可用。初始化的時候被設定爲1。當thread想要持有鎖的時候調用spin_lock函數,該函數將spin lock那個整數值減去1,然後進行判斷,如果等於0,表示可以獲取spin lock,如果是負數,則說明其他thread的持有該鎖,本thread需要spin。
補充spin_lock,spin_lock_irq,spin_lock_irqsave的區別:
- 普通的spin_lock是preempt_disable();而spin_lock_irq和spin_lock_irqsave是preempt_disable()同時local_irq_disable();在任何情況下使用spin_lock_irq都是安全的。因爲它既禁止本地中斷,又禁止內核搶佔;
- spin_lock比spin_lock_irq速度快,但是它並不是任何情況下都是安全的。
- spin_lock_irqsave保留當前中斷狀態,進入退出臨界區中斷狀態不變;
舉個例子:進程A中調用了spin_lock(&lock)然後進入臨界區,此時來了一箇中斷(interrupt),該中斷也運行在和進程A相同的CPU上,並且在該中斷處理程序中恰巧也會spin_lock(&lock)試圖獲取同一個鎖。由於是在同一個CPU上被中斷,進程A會被設置爲TASK_INTERRUPT狀態,中斷處理程序無法獲得鎖,會不停的忙等,由於進程A被設置爲中斷狀態,schedule()進程調度就無法再調度進程A運行,這樣就導致了死鎖!
但是如果該中斷處理程序運行在不同的CPU上就不會觸發死鎖。 因爲在不同的CPU上出現中斷不會導致進程A的狀態被設爲TASK_INTERRUPT,只是換出。當中斷處理程序忙等被換出後,進程A還是有機會獲得CPU,執行並退出臨界區。所以在使用spin_lock時要明確知道該鎖不會在中斷處理程序中使用;
(五) 讀/寫spin_lock
spin lock嚴格的限制只有一個thread可以進入臨界區,但是實際中,有些對共享資源的訪問可以嚴格區分讀和寫的,這時候,其實多個讀的thread進入臨界區是OK的,使用spin lock則限制一個讀thread進入,從而導致性能的下降。
讀寫鎖工作原理:
(1)假設臨界區內沒有任何的thread,這時候任何read thread或者write thread可以進入,但是隻能是其一。
(2)假設臨界區內有一個read thread,這時候新來的read thread可以任意進入,但是write thread不可以進入
(3)假設臨界區內有一個write thread,這時候任何的read thread或者write thread都不可以進入
(4)假設臨界區內有一個或者多個read thread,write thread當然不可以進入臨界區,但是該write thread也無法阻止後續read thread的進入,他要一直等到臨界區一個read thread也沒有的時候,纔可以進入,多麼可憐的write thread。
讀/寫spin_lock API:
接口API描述 | rw spinlock API |
---|---|
定義rw spin lock並初始化 | DEFINE_RWLOCK |
動態初始化rw spin lock | rwlock_init |
獲取指定的rw spin lock | read_lock write_lock |
獲取指定的rw spin lock同時disable本CPU中斷 | read_lock_irq write_lock_irq |
保存本CPU當前的irq狀態,disable本CPU中斷並獲取指定的rw spin lock | read_lock_irqsave write_lock_irqsave |
獲取指定的rw spin lock同時disable本CPU的bottom half | read_lock_bh write_lock_bh |
釋放指定的spin lock | read_unlock write_unlock |
釋放指定的rw spin lock同時enable本CPU中斷 | read_unlock_irq write_unlock_irq |
釋放指定的rw spin lock同時恢復本CPU的中斷狀態 | read_unlock_irqrestore write_unlock_irqrestore |
獲取指定的rw spin lock同時enable本CPU的bottom half | read_unlock_bh write_unlock_bh |
嘗試去獲取rw spin lock,如果失敗,不會spin,而是返回非零值 | read_trylock write_trylock |
(六) RCU(Read-Copy Update)
RCU(Read-Copy Update)是Linux內核比較成熟的新型讀寫鎖,具有較高的讀寫併發性能,常常用在需要互斥的性能關鍵路徑。
RCU允許多個讀者和寫者併發執行,而且RCU是不用鎖的,它不使用被所有CPU共享的鎖或者計數器,在這一點上與讀/寫自旋鎖和順序鎖,由於高速緩存行竊用和失效而有很高的的開銷,RCU具有更大的優勢;
RCU的核心理念是讀者訪問的同時,寫者可以更新訪問對象的副本,但寫者需要等待所有讀者完成訪問之後,才能刪除老對象。這個過程實現的關鍵和難點就在於如何判斷所有的讀者已經完成訪問。通常把寫者開始更新,到所有讀者完成訪問這段時間叫做寬限期(Grace Period)。
RCU的使用場景比較受限,主要適用於下面的場景:
(1)RCU只能保護動態分配的數據結構,並且必須是通過指針訪問該數據結構
(2)受RCU保護的臨界區內不能sleep(SRCU不是本文的內容)
(3)讀寫不對稱,對writer的性能沒有特別要求,但是reader性能要求極高。
(4)reader端對新舊數據不敏感。
對於reader,RCU的操作包括:
(1)rcu_read_lock,用來標識RCU read side臨界區的開始。
(2)rcu_dereference,該接口用來獲取RCU protected pointer。reader要訪問RCU保護的共享數據,當然要獲取RCU protected pointer,然後通過該指針進行dereference的操作。
(3)rcu_read_unlock,用來標識reader離開RCU read side臨界區
對於writer,RCU的操作包括:
(1)rcu_assign_pointer。該接口被writer用來進行removal的操作,在witer完成新版本數據分配和更新之後,調用這個接口可以讓RCU protected pointer指向RCU protected data。
(2)synchronize_rcu。writer端的操作可以是同步的,也就是說,完成更新操作之後,可以調用該接口函數等待所有在舊版本數據上的reader線程離開臨界區,一旦從該函數返回,說明舊的共享數據沒有任何引用了,可以直接進行reclaimation的操作。
(3)call_rcu。當然,某些情況下(例如在softirq context中),writer無法阻塞,這時候可以調用call_rcu接口函數,該函數僅僅是註冊了callback就直接返回了,在適當的時機會調用callback函數,完成reclaimation的操作。這樣的場景其實是分開removal和reclaimation的操作在兩個不同的線程中:updater和reclaimer。
//讀鎖;
237 static inline void __rcu_read_lock(void)
238 {
239 preempt_disable();
240 }
241
242 static inline void __rcu_read_unlock(void)
243 {
244 preempt_enable();
245 }
//讀者在讀完RCU保護的數據結構之前,是不能睡眠的;
call_rcu:內核每經過一個時鐘tick就週期性的檢測CPU是否經過了一個靜止狀態;如果所有cpu都經過了靜止狀態;本地tasklet就執行鏈表當中所有的回調函數;
(七) 信號量
內核信號量類似於自旋鎖,因爲當鎖關閉的時侯,不允許內核控制路徑繼續運行,當一個內核任務試圖獲取內核信號量所保護的臨界區資源時,相應的進程被掛起到一個等待隊列中,然後讓其睡眠,當前cpu就可以去執行其他的代碼,資源被釋放的時候,進程在再次變爲可運行的;因此只有可以睡眠的函數纔可以獲取內核信號量,中斷處理程序和可延遲函數都不能使用內核信號量;
16 struct semaphore {
17 raw_spinlock_t lock;
18 unsigned int count;
19 struct list_head wait_list;
20 };
信號量不同於自旋鎖,它不會禁止內核搶佔,所以持有信號量的代碼可以被搶佔;同時自旋鎖在一個時刻只允許一個任務持有它,信號量同時允許的持有數量可以在申明信號量時指定;
信號量初始化:
32 static inline void sema_init(struct semaphore *sem, int val)
33 {
34 static struct lock_class_key __key;
35 *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
36 lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
37 }
38
獲取信號量:
75 int down_interruptible(struct semaphore *sem)
76 {
77 unsigned long flags;
78 int result = 0;
79
80 raw_spin_lock_irqsave(&sem->lock, flags);
81 if (likely(sem->count > 0))
82 sem->count--;
83 else
84 result = __down_interruptible(sem);
85 raw_spin_unlock_irqrestore(&sem->lock, flags);
86
87 return result;
88 }
89 EXPORT_SYMBOL(down_interruptible);
241 static noinline int __sched __down_interruptible(struct semaphore *sem)
242 {
243 return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
244 }
204 static inline int __sched __down_common(struct semaphore *sem, long state,
205 long timeout)
206 {
207 struct task_struct *task = current;
208 struct semaphore_waiter waiter;
209 //先添加到wait列表;
210 list_add_tail(&waiter.list, &sem->wait_list);
211 waiter.task = task;
212 waiter.up = false;
213
214 for (;;) {
215 if (signal_pending_state(state, task))
216 goto interrupted;
217 if (unlikely(timeout <= 0))
218 goto timed_out;
219 __set_task_state(task, state);//設置爲可interruptible;
220 raw_spin_unlock_irq(&sem->lock);
221 timeout = schedule_timeout(timeout);
222 raw_spin_lock_irq(&sem->lock);
223 if (waiter.up)
224 return 0;
225 }
226
227 timed_out:
228 list_del(&waiter.list);
229 return -ETIME;//timeout之後就停止等待了;
230
231 interrupted:
232 list_del(&waiter.list);
233 return -EINTR;
234 }
常用接口說明:
39 extern void down(struct semaphore *sem); //該函數用於獲得信號量sem,他會導致睡眠,睡眠狀態不可喚醒,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數。該函數將把sem的值減1,如果信號量sem的值非負,就直接返回,否則調用者將被掛起,直到別的任務釋放該信號量才能繼續運行。
40 extern int __must_check down_interruptible(struct semaphore *sem);//該函數功能和down類似,不同之處爲,down不會被信號(signal)打斷,但down_interruptible能被信號打斷,因此該函數有返回值來區分是正常返回還是被信號中斷,如果返回0,表示獲得信號量正常返回,如果被信號打斷,返回-EINTR。
42 extern int __must_check down_trylock(struct semaphore *sem);該函數試着獲得信號量sem,如果能夠即時獲得,他就獲得該信號量並返回0,否則,表示不能獲得信號量sem,返回值爲非0值。因此,他不會導致調用者睡眠,能在中斷上下文使用。
44 extern void up(struct semaphore *sem);//釋放信號量;並喚醒等待該資源進程隊列的第一個進程
釋放信號量:
178 void up(struct semaphore *sem)
179 {
180 unsigned long flags;
181
182 raw_spin_lock_irqsave(&sem->lock, flags);
183 if (likely(list_empty(&sem->wait_list)))
184 sem->count++;
185 else
186 __up(sem);
187 raw_spin_unlock_irqrestore(&sem->lock, flags);
188 }
189 EXPORT_SYMBOL(up);
256 static noinline void __sched __up(struct semaphore *sem)
257 {
258 struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
259 struct semaphore_waiter, list);
260 list_del(&waiter->list);
261 waiter->up = true;
262 wake_up_process(waiter->task);
263 }
首先獲得sem所在的wait_list爲頭部的鏈表的第一個有效節點,然後從鏈表中將其刪除,然後喚醒該節點上睡眠的進程。
由此可見,對於sem上的每次down_interruptible調用,都會在sem的wait_list鏈表尾部加入一新的節點。對於sem上的每次up調用,都會刪除掉wait_list鏈表中的第一個有效節點,並喚醒睡眠在該節點上的進程。
(八) 互斥量
前面說過信號量可以設置進入臨界去的task的數量,而互斥量mutex就是允許使用計數爲1的信號量;
50 struct mutex {
51 /* 1: unlocked, 0: locked, negative: locked, possible waiters */
52 atomic_t count;
53 spinlock_t wait_lock;
54 struct list_head wait_list;
55 #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER)
56 struct task_struct *owner;
57 #endif
58 #ifdef CONFIG_MUTEX_SPIN_ON_OWNER
59 struct optimistic_spin_queue osq; /* Spinner MCS lock */
60 #endif
61 #ifdef CONFIG_DEBUG_MUTEXES
62 const char *name;
63 void *magic;
64 #endif
65 #ifdef CONFIG_DEBUG_LOCK_ALLOC
66 struct lockdep_map dep_map;
67 #endif
68 };
常用接口如下:
void init_MUTEX (struct semaphore *sem);
該函數用於初始化一個互斥鎖,即他把信號量sem的值設置爲1。
void init_MUTEX_LOCKED (struct semaphore *sem);
該函數也用於初始化一個互斥鎖,但他把信號量sem的值設置爲0,即一開始就處在已鎖狀態。
DECLARE_MUTEX(name)
該宏聲明一個信號量name並初始化他的值爲1,即聲明一個互斥鎖。
DECLARE_MUTEX_LOCKED(name)
該宏聲明一個互斥鎖name,但把他的初始值設置爲0,即鎖在創建時就處在已鎖狀態。因此對於這種鎖,一般是先釋放後獲得
int fastcall __sched mutex_lock_interruptible(struct mutex *lock);和mutex_lock()一樣,也是獲取互斥鎖。在獲得了互斥鎖或進入睡眠直到獲得互斥鎖之後會返回0。如果在等待獲取鎖的時候進入睡眠狀態收到一個信號(被信號打斷睡眠),則返回_EINIR。
int fastcall __sched mutex_trylock(struct mutex *lock);試圖獲取互斥鎖,如果成功獲取則返回1,否則返回0,不等待。
void fastcall mutex_unlock(struct mutex *lock);//釋放被當前進程獲取的互斥鎖。該函數不能用在中斷上下文中,而且不允許去釋放一個沒有上鎖的互斥鎖。
互斥量和信號量:它們的標準使用方式都有簡單的規範:除非mutex的某個約束妨礙你使用,負責相比信號量要優先使用mutex;
(九) 完成變量
如果內核中一個任務需要發出信號通知另一個任務發生了某個特定的事件,可以用完成變量來實現這種同步:
include/linux/completion.h:
申明和定義:
25 struct completion {
26 unsigned int done;
27 wait_queue_head_t wait;
28 };
44 #define DECLARE_COMPLETION(work) \
45 struct completion work = COMPLETION_INITIALIZER(work)
完成變量API:
方法 | 描述 |
---|---|
wait_for_completion | 等待指定的完成變量,接受信號 |
complete | 發送完成並喚醒信號給等待的任務 |
wait_for_completion() -> do_wait_for_common()
61 do_wait_for_common(struct completion *x,
62 long (*action)(long), long timeout, int state)
63 {
64 if (!x->done) {
65 DECLARE_WAITQUEUE(wait, current);
66
67 __add_wait_queue_tail_exclusive(&x->wait, &wait);
68 do {
69 if (signal_pending_state(state, current)) {
70 timeout = -ERESTARTSYS;
71 break;
72 }
73 __set_current_state(state);
74 spin_unlock_irq(&x->wait.lock);
75 timeout = action(timeout);
76 spin_lock_irq(&x->wait.lock);
77 } while (!x->done && timeout);
78 __remove_wait_queue(&x->wait, &wait);
79 if (!x->done)
80 return timeout;
81 }
82 x->done--;
83 return timeout ?: 1;
84 }