详谈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.ltesting.net