詳談Linux 2_4_x內核信號量機制

發表于:2007-07-04來源:作者:點擊數: 標簽:
信號量作為一種同步機制,在每個成熟的現代操作系統的實現過程中,起著不可替代的作用,對于 Linux 也不例外,在Linux2_4_x下,實現內核信號量機制的代碼雖然不長,但由于涉及到多個進程間的相互干擾,并且在Linux發展過程中,不斷進行優化,所以非常難于理解
信號量作為一種同步機制,在每個成熟的現代操作系統的實現過程中,起著不可替代的作用,對于Linux也不例外,在Linux2_4_x下,實現內核信號量機制的代碼雖然不長,但由于涉及到多個進程間的相互干擾,并且在Linux發展過程中,不斷進行優化,所以非常難于理解,在講解Linux源代碼的各類文章中,也大都對此語焉不詳,本人通過認真閱讀,對這部分代碼有了一定的了解,這里介紹出來,希望對大家有所幫助。

信號量作為一種同步機制,在每個成熟的現代操作系統的實現過程中,起著不可替代的作用,對于Linux也不例外,在Linux2_4_x下,實現內核信號量機制的代碼雖然不長,但由于涉及到多個進程間的相互干擾,并且在Linux發展過程中,不斷進行優化,所以非常難于理解,在講解Linux源代碼的各類文章中,也大都對此語焉不詳,本人通過認真閱讀,對這部分代碼有了一定的了解,這里介紹出來,希望對大家有所幫助。

首先看看信號量的概念:
1.信號量的類型定義:
每個信號量至少須記錄兩個信息:信號量的值和等待該信號量的進程隊列。它的類型定義如下:(用類PASCAL語言表述)
semaphore = record
value: integer;
queue: ^PCB;
end;
其中PCB是進程控制塊,是操作系統為每個進程建立的數據結構。
s.value>=0時,s.queue為空;
s.value<0時,s.value的絕對值為s.queue中等待進程的個數;

2.PV原語:
對一個信號量變量可以進行兩種原語操作:p操作和v操作,定義如下:
procedure p(var s:samephore);
{
s.value=s.value-1;
if (s.value<0) asleep(s.queue);
}
procedure v(var s:samephore);
{
s.value=s.value+1;
if (s.value<=0) wakeup(s.queue);
}
其中用到兩個標準過程:
asleep(s.queue);執行此操作的進程的PCB進入s.queue尾部,進程變成等待狀態
wakeup(s.queue);將s.queue頭進程喚醒插入就緒隊列。s.value初值為1時,可以用來實現進程的互斥。p操作和v操作是不可中斷的程序段,稱為原語。如果將信號量看作共享變量,則pv操作為其臨界區,多個進程不能同時執行,一般用硬件方法保證。一個信號量只能置一次初值,以后只能對之進行p操作或v操作。

再來看看在對應的Linux2.4.x具體實現中,信號量的數據結構:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
可以看到相對于操作系統原理中的定義,數據結構多了一個sleepers成員,這個變量是為使主要分支代碼執行速度更快,所作的優化。
其中,count相當于資源計數,為正數或0時表示可用資源數,-1則表示沒有空閑資源且有等待進程,而其他的負數僅僅一個中間過程,當所有進程都穩定下來后,將最終變為-1。
sleepers相當于一個狀態標志,它僅有0和1兩個值,有時會出現2,但也只是中間狀態,并沒有實際意義。當sleepers為0時,表示沒有進程在等待,或等待中的這些進程正在被喚醒過程中。當sleepers為1時,表示至少有一個進程在等待中
wait是等待隊列。
在這里可以看到,等待進程的數量并不被關心。

下面我們來看函數,Linux2.4.x具體實現中,down()函數相當于p操作,up()函數相當于v操作。
在down()函數中,count做原子減1操作,如果結果不小于0[第4行],則表示成功申請,從down()中返回,如果結果為負(大多數情況是-1,注意判斷“結果不小于0”的原因,是因為結果有可能是其他負數),表示需要等待,則調用__down_fail()[第7行],
(include/asm-i386/semaphore.h):
static inline void down(struct semaphore * sem)
{
1__asm__ __volatile__(
2"# atomic down operation\n\t"
3LOCK "decl %0\n\t" /* --sem->count */
4"js 2f\n"
5"1:\n"
6".section .text.lock,\"ax\"\n"
7"2:\tcall __down_failed\n\t"
8"jmp 1b\n"
9".previous"
10:"=m" (sem->count)
11:"c" (sem)
12:"memory");
}

__down_fail()調用__down(),
(arch/i386/kernel/semaphore.c):
asm(
".text\n"
".align 4\n"
".globl __down_failed\n"
"__down_failed:\n\t"
13"pushl %eax\n\t"
14"pushl %edx\n\t"
15"pushl %ecx\n\t"
16"call __down\n\t"
17"popl %ecx\n\t"
18"popl %edx\n\t"
19"popl %eax\n\t"
20"ret"
);

__down()用C代碼實現。
(arch/i386/kernel/semaphore.c):
void __down(struct semaphore * sem)
{
21 struct task_struct *tsk = current;
22 DECLARE_WAITQUEUE(wait, tsk);
23 tsk->state = TASK_UNINTERRUPTIBLE;
24 add_wait_queue_exclusive(&sem->wait, &wait);
25
26 spin_lock_irq(&semaphore_lock);
27 sem->sleepers++;
28 for (;;) {
29 int sleepers = sem->sleepers;
30
31 /*
32 * Add "everybody else" into it. They aren't
33 * playing, because we own the spinlock.
34 */
35 if (!atomic_add_negative(sleepers - 1, &sem->count)) {
36 sem->sleepers = 0;
37 break;
38 }
39 sem->sleepers = 1; /* us - see -1 above */
40 spin_unlock_irq(&semaphore_lock);
41
42 schedule();
43 tsk->state = TASK_UNINTERRUPTIBLE;
44 spin_lock_irq(&semaphore_lock);
45 }
46 spin_unlock_irq(&semaphore_lock);
47 remove_wait_queue(&sem->wait, &wait);
48 tsk->state = TASK_RUNNING;
49 wake_up(&sem->wait);
}
代碼中關于聲明一個等待隊列,將本進程加入等待隊列,然后睡眠等等這些操作,是在Linux內核中,進行主動進程調度的標準方法,這里就不加以特殊說明了。
但需要提醒大家注意的是,調用schedule()函數的位置[第42行]是在加鎖范圍之外,也就是spin_unlock_irq(&semaphore_lock)[第40行]之后,spin_lock_irq(&semaphore_lock)[第44行]之前。這么做的原因是spin_lock_irq操作如果加鎖失敗,那么它會不停重試,直到成功為止,如果上一次加鎖成功的那個操作占用鎖時間過長,比如在加鎖范圍內調用了schedule()函數(schedule函數可能會導致當前進程陷入睡眠),就會導致大量CPU時間的浪費。
同樣是由于調用了schedule()函數,就意味著這種信號量機制,只能用在進程與進程之間的互斥中。如果你在中斷處理函數中,或softirq操作中使用down()函數,schedule()函數包含的如下語句:
(kernel/sched.c):
50 if (in_interrupt()) {
51 printk("Scheduling in interrupt\n");
52 BUG();
53 }
就會產生錯誤。

如上所訴,下面的場景都是建立在兩個或多個進程間的,還需要說明的一點是,這些場景都是在SMP,也就是在多CPU情況下的。
為什么要強調這點呢,因為當進程運行在內核態時,是不會發生強制性的進程切換的,就是說在代碼中沒有明確調用schedule()函數,就不會發生進程切換,如果進程在內核態發現需要進行進程切換,而自己又不想進入睡眠狀態,一般是設置進程數據結構中的標志need_resched,表示本進程在返回用戶態前夕,需要進行一次進程切換[第55行]:
(arch/i386/kernel/entry.S):
ENTRY(ret_from_sys_call)
54 cli# need_resched and signals atomic test
55 cmpl ,need_resched(%ebx)
56 jne reschedule
57 cmpl ,sigpending(%ebx)
58 jne signal_return

還有一個問題,如果代碼在內核態運行過程中發生中斷,會不會發生進程切換呢?的確,當代碼在用戶態運行時發生中斷,是有可能發生進程切換的,這也是為什么當我們在用戶態編寫一個死循環程序時,不會把CPU時間耗盡,因為有時鐘中斷,當該進程的時間片到了時,它不想讓也得讓了。那么在內核態會不會發生這種情況嗎?不會,因為在從中斷返回時,會首先判斷是在什么運行態下發生中斷的,只有在用戶態發生的中斷,才有可能在這里進行進程切換,換句話,如果你在驅動程序中編了個死循環,那這個CPU恐怕就真的死循環了[第63行]:
(arch/i386/kernel/entry.S):
ENTRY(ret_from_intr)
59 GET_CURRENT(%ebx)
60 ret_from_exception:
61 movl EFLAGS(%esp),%eax# mix EFLAGS and CS
62 movb CS(%esp),%al
63 testl $(VM_MASK | 3),%eax# return to VM86 mode or non-supervisor?
64 jne ret_from_sys_call
65 jmp restore_all

回到我們原來的話題,在下面場景描述中,不斷會用到"進程B從這一點開始運行","進程C從那一點繼續"等等說明,這在單CPU情況下顯然是不可能的,在說明過程中就不具備典型意義。所以我們的場景是建立在多CPU情況下的。

我們繼續來看代碼,除去有關等待隊列操作和進程切換外,down函數代碼中僅剩下與sleepers變量有關的部分,和最后一句wake_up(&sem->wait)[第49行]。請注意,函數開始部分,加入等待隊列時調用的是add_wait_queue_exclusive()函數[第24行],這意味著這里的wake_up(也包括下面將介紹的up函數中的wake_up)僅會喚醒在隊列頭的那個等待進程。說到這里需要提一句,在Linux2.2的代碼中,wake_up函數是會喚醒所有的等待進程的,但這樣會導致CPU資源的浪費,比如說,在這個信號量上有100個進程正在睡眠,當信號量被up時,只可能有一個進程被喚醒,但實際上另外99進程也都被喚醒了,只不過他們發現自己晚到了一步,只好又去睡眠,2.4上用僅喚醒在隊列頭的那個等待進程的方法,來修正了這個問題,但又引入了另一個問題:睡眠隊列是先入先出的,與進程優先級沒有關系,這就有可能使進程優先級高的進程不會被優先喚醒。這個問題在Linux 2.2中顯然是沒有的,有關這個功能的代碼不知是否會在以后的版本中被引入。

現在來看與sleepers變量有關的部分,關于sleepers的所有操作,都在down函數之內,而且還都是在加鎖范圍以內的,也就是說對sleepers變量的操作是原子的,不會發生這個進程對sleepers變量的操作運行到一半,就被另一個進程所打斷的現象。sleepers的初始值是0,而退出加鎖范圍前,sleepers不是被設置為0[第36行],就是被設置為1[第39行]。這就可以認為sleepers變量實際上僅有0和1兩個值

下面我們來構造一些場景進行說明:
場景一:
count值為1,有一個進程企圖獲得該信號量。
程序會在第3行對count進行減1操作,第4行判斷發現count值大于等于0,所以在第5行直接返回。這個過程最簡單。

場景二:
count值為0,有一個進程企圖獲得該信號量。
程序會在第3行對count進行減1操作,結果為-1,第4行判斷發現count值小于0,所以跳至第7行執行__down_failed,進而在第16行執行__down函數。在__down函數中,首先聲明睡眠隊列,然后加鎖,這時sleepers的值為初始值0,表示在這之前沒有進程睡眠在這個信號量上,通過對這個值加1[第27行],然后又減1[第35行]的操作后,將這個值加回到count中[第35行],結果count當然仍為-1,所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,sleepers這個值本身也是為下一次將它加回到count中做好準備。最后解鎖,睡眠,等待被喚醒。

場景三:
count值為-1,有一個進程企圖獲得該信號量。
程序會在第3行對count進行減1操作,結果為-2,第4行判斷發現count值小于0,所以跳至第7行執行__down_failed,進而在第16行執行__down函數。在__down函數中,首先聲明睡眠隊列,然后加鎖,這時sleepers的值肯定為1(因為count值為-1,表示有進程睡眠在這個信號量上),通過對這個值加1[第27行],然后又減1[第35行]的操作后,將這個值加回到count中[第35行],結果為-1,這里等于將上個進程欠著count的,在sleepers中的那個值還給了count,但這個進程還得欠著,所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,并為下一次將這個值加回到count中做好準備。最后解鎖,睡眠,等待被喚醒。

場景四:
count值為1,有兩個進程同時企圖獲得該信號量。
當進程A在第3行對count進行減1操作后,進程B又執行到第3行,并也對count進行減1操作,使其值為-1,但進程A在第4行仍舊會判斷發現count值大于等于0,所以在第5行直接返回。這是因為第5行只是根據當前CPU的標志寄存器進行判斷,而進程B所在的CPU執行的結果,并不會影響到進程A所在的CPU的標志寄存器。進程B以后的執行過程就相當于場景二。

場景五:
count值為0,有兩個進程同時企圖獲得該信號量。
當進程A在第3行對count進行減1操作后,進程B又執行到第3行,并也對count進行減1操作,使其值為-2,進程A在第4行判斷發現count值小于0,所以跳至第7行執行__down_failed,進而在第16行執行__down函數。在__down函數中,首先聲明睡眠隊列,然后加鎖。這時進程B再運行,將被阻止在加鎖語句上[第26行]。進程A發現sleepers的值為初始值0,表示在這之前沒有進程睡眠在這個信號量上,通過對這個值加1[第27行],然后又減1[第35行]的操作后,將這個值加回到count中[第35行],結果仍為-2,所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,并為下一次將這個值加回到count中做好準備。最后解鎖,睡眠,等待被喚醒。由于進程A解鎖,所以進程B得以繼續運行,進程B發現sleepers的值為1,表示有進程睡眠在這個信號量上,通過對這個值加1[第27行],然后又減1[第35行]的操作后,將這個值加回到count中[第35行],結果為-1,仍不為0,所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,并為下一次將這個值加回到count中做好準備。最后解鎖,睡眠,等待被喚醒。

場景六:
count值為-1,有兩個進程同時企圖獲得該信號量。
這個場景分析過程同場景五,結果依然是count值為-1,sleepers為1,這兩個進程進入睡眠狀態,等待被喚醒。

對于count值其它負值的情況,其實是上述的某個場景未進行到穩定狀態的結果,分析過程可以歸并到上述的某個場景中。通過以上場景分析,我們可以總結出,穩定狀態的結果實際上只有兩種情況:
1.count大于等于0,sleepers為0,沒有進程睡眠在等待隊列中。
2.count為-1,sleepers為1,有進程睡眠在等待隊列中。

下面我們將up函數的執行,也插入到場景中來。
首先分析一下up()函數,它的實現就簡單了,up()利用匯編原子地將count加1,如果小于等于0表示有等待進程(大多數情況結果是0或正數,判斷“小于等于0”而不是“等于0”的原因,是因為結果有可能是負數),則調用__up_wakeup(),進而調用__up()喚醒等待進程,否則直接返回。
(include/asm-i386/semaphore.h):
static inline void up(struct semaphore * sem)
{
66__asm__ __volatile__(
67"# atomic up operation\n\t"
68LOCK "incl %0\n\t" /* ++sem->count */
69"jle 2f\n"
70"1:\n"
71".section .text.lock,\"ax\"\n"
72"2:\tcall __up_wakeup\n\t"
73"jmp 1b\n"
74".previous"
75:"=m" (sem->count)
76:"c" (sem)
77:"memory");
}

(arch/i386/kernel/semaphore.c):
asm(
".text\n"
".align 4\n"
".globl __up_wakeup\n"
"__up_wakeup:\n\t"
78"pushl %eax\n\t"
79"pushl %edx\n\t"
80"pushl %ecx\n\t"
81"call __up\n\t"
82"popl %ecx\n\t"
83"popl %edx\n\t"
84"popl %eax\n\t"
85"ret"
);

(arch/i386/kernel/semaphore.c):
void __up(struct semaphore *sem)
{
86wake_up(&sem->wait);
}

場景七:
count值為0,有一個進程釋放了該信號量。
程序會在第68行對count進行加1操作,第69行判斷發現count值大于0,所以在第70行直接返回。

場景八:
count值為-1,有一個進程釋放了該信號量,有一個進程在等待隊列中睡眠。
進程A會在第68行對count進行加1操作,結果為0,第69行判斷發現count值小于等于0,所以跳至第72行執行__up_wakeup,進而在第81行執行__up函數,最后在第86行執行wake_up函數,喚醒在等待隊列隊列頭的進程B。進程B被喚醒后,將繼續執行第43,44行,繼而執行到第35行,在這里,sleepers的值為1,經過減1[第35行]操作后,將這個值加回到count中[第35行],結果count仍為0,所以執行第36行,將sleepers的值設為0,表示狀態為喚醒進程的過程中,然后跳出循環,做一些解瑣和清除隊列的操作后,調用wake_up函數[第49行],由于這個場景中只有一個進程在等待隊列中睡眠,所以這里的wake_up函數將什么也不做。這時的狀態為count為0,sleepers為0,沒有其他進程在等待隊列中睡眠。進程B最終會調用一個up函數,而進入場景七。

場景九:
count值為-1,有一個進程釋放了該信號量,有兩個進程在等待隊列中睡眠。
這個場景的前一部分,和場景八相同,在進程B調用wake_up函數[第49行]后,在等待隊列中睡眠的進程C被喚醒,進程C將繼續執行第43,44行,繼而執行到第35行,在這里,count值為0,sleepers的值也為0,經過減1[第35行]操作后,將這個值加回到count中[第35行],結果為-1。所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,并為下一次將這個值加回到count中做好準備。最后解鎖,而又進入睡眠,等待再次被喚醒。被喚醒的進程B最終會調用一個up函數,來喚醒又進入睡眠狀態的進程C,而進入場景八。
這里我們可以看到一個up操作實際上連續的喚醒了兩個進程,只不過進程C判斷發現"好像其實并沒人打算叫醒我,所以我還是接著睡吧",但進程C被喚醒不是沒有意義的,因為count,以及sleepers這兩個標志值,是由它來恢復為睡眠狀態的值的,即count為-1,sleepers為1。

我們來看一個復雜的場景。
場景十:
count值為-1,有兩個進程同時釋放了該信號量,有兩個進程在等待隊列中睡眠。
進程A會在第68行對count進行加1操作,結果為0,第69行判斷發現count值小于等于0,所以跳至第72行執行__up_wakeup,進而在第81行執行__up函數,最后在第86行執行wake_up函數,喚醒在等待隊列隊列頭的那個進程。這時進程B執行,它也會在第68行對count進行加1操作,此時結果為1,第69行判斷發現count值大于0,所以在第70行直接返回。注意,進程B沒有執行wake_up函數,現在我們來看進程A是如何將兩個進程都喚醒的,首先等待隊列中隊列頭的進程C被喚醒后,將繼續執行第43,44行,繼而執行到第35行,在這里,sleepers的值為1,經過減1[第35行]操作后,將這個值加回到count中[第35行],結果仍為1,atomic_add_negative這個函數是判斷結果是否為負數,所以這里會執行第36行,將sleepers的值設為0,表示狀態為喚醒進程的過程中,然后跳出循環,做一些解瑣和清除隊列的操作后,調用wake_up函數[第49行]喚醒進程D,進程D被喚醒后,將繼續執行第43,44行,繼而執行到第35行,在這里,sleepers的值為0,經過減1[第35行]操作后,將這個值加回到count中[第35行],結果為0,所以也會執行第36行,將sleepers的值設為0,表示狀態為喚醒進程的過程中,然后跳出循環,做一些解瑣和清除隊列的操作后,調用wake_up函數[第49行],由于現在已經沒有進程在等待隊列中睡眠,所以這里的wake_up函數將什么也不做。
這個場景可以看到第49行的wake_up函數,也會實際的承擔喚醒進程的工作。當然如果這里還有一個進程E在等待隊列中睡眠,那它就會像場景九中的進程C一樣,被喚醒,然后又去睡眠。

下面看一個,down函數運行未完成,而up函數就開始運行的場景。
場景十一:
count值為0,有一個進程企圖獲得該信號量,同時一個進程釋放了該信號量。
當進程A在第3行對count進行減1操作后,進程B在第68行對count進行加1操作,使其值恢復為0,然后在第69行判斷發現count值小于等于0,所以跳至第72行執行__up_wakeup,進而在第81行執行__up函數,最后在第86行執行wake_up函數,而此時實際上并沒有進程在等待隊列中睡眠,所以這句wake_up[第86行]將什么也不做。這時進程A開始執行,因為它還不知道進程B已經執行了一次up操作,所以它在第4行判斷發現count值小于0,然后跳至第7行執行__down_failed,進而在第16行執行__down函數。在__down函數中,首先聲明睡眠隊列,然后加鎖。進程A發現sleepers的值為初始值0,表示在這之前沒有進程睡眠在這個信號量上,通過對這個值加1[第27行],然后又減1[第35行]的操作后,將這個值加回到count中[第35行],由于之前的up操作,所以結果為0,然后當然是執行第36行,將sleepers的值設為0,表示狀態為喚醒進程的過程中,然后跳出循環,做一些解瑣和清除隊列的操作后,調用wake_up函數[第49行],由于已經沒有進程在等待隊列中睡眠,所以這里的wake_up函數將什么也不做。
請注意,up操作過程中是沒有鎖機制的,就是說,在down操作運行的任何階段,即使是在加鎖范圍以內,up都是有可能發生的。如果它在add_wait_queue_exclusive[第24行]以后,atomic_add_negative(sleepers - 1,&sem->count)[第35行]以前發生,那么up操作的wake_up函數[第86行]就會把剛剛加入到睡眠隊列,但還沒有真正睡眠的這個進程又加回到運行隊列。然后進程A在第36行退出循環。另外atomic_add_negative(sleepers - 1,&sem->count)[第35行]是一個原子操作,add_wait_queue_exclusive[第24行]操作過程中有專門的隊列鎖,所以在這兩句運行中,是不會被打斷的。
這里我們看到了down函數進入循環,而又發現"其實不需要睡眠"的例子。

最后,我們看看count值為其它負數時,即不穩定狀態下,up函數被執行的情況。
場景十二:
count值為0,有兩個進程同時企圖獲得該信號量,同時一個進程釋放了該信號量。
當進程A在第3行對count進行減1操作后,進程B又執行到第3行,并也對count進行減1操作,使其值為-2,此時進程C運行,并在第68行對count進行加1操作,使其值變為-1,然后第69行判斷發現count值小于等于0,所以跳至第72行執行__up_wakeup,進而在第81行執行__up函數,最后在第86行執行wake_up函數,而此時實際上并沒有進程在等待隊列中睡眠,所以這句wake_up將什么也不做。然后進程A在第4行判斷發現count值小于0,所以跳至第7行執行__down_failed,進而在第16行執行__down函數。在__down函數中,首先聲明睡眠隊列,然后加鎖。這時進程B再運行,將被阻止在加鎖語句上[第26行]。進程A發現sleepers的值為初始值0,表示在這之前沒有進程睡眠在這個信號量上,通過對這個值加1[第27行],然后又減1[第35行]的操作后,將這個值加回到count中[第35行],結果為-1,所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,并為下一次將這個值加回到count中做好準備。最后解鎖,睡眠,等待被喚醒。由于進程A解鎖,所以進程B得以繼續運行,進程B發現sleepers的值為1,表示有進程睡眠在這個信號量上,通過對這個值加1[第27行],然后又減一[第35行]的操作后,將這個值加回到count中[第35行],結果為0,所以執行第36行,將sleepers的值設為0,表示狀態為喚醒進程的過程中,然后跳出循環,做一些解瑣和清除隊列的操作后,調用wake_up函數[第49行],喚醒在等待隊列中睡眠的進程A,進程A將繼續執行第43,44行,繼而執行到第35行,在這里,count值為0,sleepers的值也為0,經過減1[第35行]操作后,將這個值加回到count中[第35行],結果為-1。所以跳至第39行將sleepers的值設為1,表示有進程在睡眠隊列,并為下一次將這個值加回到count中做好準備。最后解鎖,而又進入睡眠,等待再次被喚醒。被喚醒的進程B最終會調用一個up函數,來喚醒又進入睡眠狀態的進程A,而進入場景八。
這個場景中,我們發現一個有意思的事情,就是進程A先于進程B進入加鎖范圍,但反而是進程B首先被喚醒,但由于進程A和進程B分別運行于不同的CPU,且企圖獲得該信號量的時間基本相同,所以出現這種情況也沒什么大的關系。

也許有人會問,在實際情況中,上面說的這些場景真的有可能發生嗎?考慮到當某個進程運行到上述的一個特定點,此時在這個CPU上發生中斷,該CPU轉而去處理這個中斷(當然它處理完中斷還是會從這個特定點繼續向下運行的,另外這個特定點也不會在加鎖范圍之內,因為spin_lock_irq函數會執行一句cli來關掉中斷),這時另一個CPU的進程沒有中斷打斷,而繼續運行,上面描述的場景就發生了。雖然這種幾率是非常小的,但操作系統的實現過程中,卻不得不考慮它們。

通過上面十二個場景,我們可以看到有關內核信號量的代碼,雖然不長,但需要考慮的情況卻要如此的復雜而全面。我希望通過這篇文章,可以對正在閱讀Linux源代碼的朋友們,多少提供一些幫助。

原文轉自:http://www.anti-gravitydesign.com

国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97