提示:本分析基于linux2.4.20内核代码
double_down()函数用在linux中VFS的一些函数中,例如vfs_rmdir()、vfs_rename_dir()等函数中,用来对两个文件系统对象(指dentry、inode等对象)进行锁定操作,其代码如下:
/*
* Whee.. Deadlock country. Happily there are only two VFS
* operations that does this..
*/
static inline void double_down(struct semaphore *s1, struct semaphore *s2)
{
if (s1 != s2) {
if ((unsigned long) s1 < (unsigned long) s2) {
struct semaphore *tmp = s2;
s2 = s1; s1 = tmp;
}
down(s1);
}
down(s2);
}
两个信号量,该先获得哪个呢?先看看一些相关结构的定义。
semaphore相关结构的定义如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
#if WAITQUEUE_DEBUG
long __magic;
#endif
};
typedef struct { volatile int counter; } atomic_t;
根据以上两个结构的定义,可以知道double_down()函数中 “if ((unsigned long) s1 < (unsigned long) s2)”语句对两个信号量的counter值进行了无符号比较,选择两个counter中无符号值大的那个信号量先来获得(若得不到,就等待)。
再根据down()的代码(如下所示),如果一个信号量被初始化为1,那么这个信号量在运行中的可能值就是1、0、-1、-2等等。
/*
* This is ugly, but we want the default case to fall through.
* "__down_failed" is a special asm handler that calls the C
* routine that actually waits. See arch/i386/kernel/semaphore.c
*/
static inline void down(struct semaphore * sem)
{
#if WAITQUEUE_DEBUG
CHECK_MAGIC(sem->__magic);
#endif
__asm__ __volatile__(
"# atomic down operation\n\t"
LOCK "decl %0\n\t" /* --sem->count */
"js 2f\n" /* JS 符号位为 “1″ 时转移. 负数的符号位为1 */
"1:\n"
LOCK_SECTION_START("")
"2:\tcall __down_failed\n\t"
"jmp 1b\n"
LOCK_SECTION_END
:"=m" (sem->count)
:"c" (sem)
:"memory");
}
设想有下面这样的场景:
1)两个进程a和b
2)两个信号量A和B,并且都初始化为1
a()
{
double_down(A, B);
/* do something */
double_up(A, B);
}
b()
{
double_down(A, B);
/* do something */
double_up(A, B);
}
3)必然死锁的执行线路
【线程a得到信号量A(A减为0)】->【系统调度发生,切换到线程b】->【线程b执行double_down,检测到信号量B的值(为1)较大,于是得到B】->【... ...】
之后无论系统怎么调度,都会出现经典的死锁情景:线程a等待信号量B,而线程b等待信号量A,从而发生死锁。
4)不会死锁的执行线路
【系统的运行使信号量A的值为-1,B的值为0】->【此时,线程a执行double_down,由于-1的无符号值大于0的无符号值,所以线程a试图得到信号量A而不是B,这样得到信号量B的down操作永远不会被执行,从而不会死锁】
5)在3)中,如果在原来的行线路的第三步【线程b执行double_down,检测到信号量B的值(为1)较大,于是得到B】中,把double_down中得到信号量的检测条件改为得到较小的信号量并获取它,则线程b会等待信号量A,而不是把先占有信号量B,就可以避免死锁。
分析以上情景可以得到一个简单的结论:在一组锁上进行多重所操作时,应该尽量先试图去得到锁值(信号量值)最小的那个锁,并以此类推。
根据上面的结论,linux中的double_down()函数代码改为下面所示或许更合理(即去掉if语句中的那两个unsigned)。
static inline void double_down(struct semaphore *s1, struct semaphore *s2)
{
if (s1 != s2) {
if ((long) s1 < (long) s2) {
struct semaphore *tmp = s2;
s2 = s1; s1 = tmp;
}
down(s1);
}
down(s2);
}