linux内核中的互斥操作

内核中的很多操作在进行的过程中都不容许受到打扰,最典型的例子就是队列操作。如果两个进程都要将一个数据结构链入到同一个队列的尾部,要是在第一个进程完成了一半的时候发生了调度,让第二个进程插了进来,结果很可能就乱了。类似的干扰也有可能来自某个中断服务程序或bh函数。在多处理器SMP结构的系统中,这种干扰还有可能来自另个处理器。

不过,除了一个进程主动调用schedule染出CPU的情况(显然不会发生在不容许收到干扰的过程中途)之外,只有在系统空间返回到用户空间的前夕才有可能发生调度。这样的安排使得上述两个进程间的干扰实际上不会发生在内核中。这一点在进程的调度与切换博客中已经讨论过了。所以上述两个进程在内核中互相干扰的情况实际上只会发生在多处理器的系统中,在单处理器的系统中时不会发生的。但是,在另一种情况下,则仍有个发生进程间的干扰。系统中有些资源是共享的,但是在具体使用期间却需要独占,而且对这些自愿的访问可能会受阻而需要睡眠等待。进程在访问此类资源的时候就可能受到其他进程的干扰。至于来自中断服务程序(包括bh函数)的干扰,则总是有可能的。并且,在多处理器系统中,不但要防止来自统一处理器上的中断服务程序的干扰,还要防止来自其他处理器的中断服务程序的干扰。

在多处理器SMP系统结构系列博客中,我们将讨论有关多处理器结构的种种问题。但是,如上所述,如果不加防止,单处理器系统中在一定条件下也会发生进程间的互相干扰。另一方面,这种措施也被借用在系统调用vfork中,用作父进程与子进程之间对共享虚存空间的互斥保护手段。

进程间对共享资源的互斥访问,或者说对进程间干扰的防范,是通过信号量(semaphore)这种机制来实现的。内核中为此提供了down和up两个函数,分别对应于操作系统理论中的P和V两种操作。至于信号量,则是一种数据结构类型semaphore。

先看数据结构,semaphore的定义如下:

struct semaphore {
	atomic_t count;
	int sleepers;
	wait_queue_head_t wait;
#if WAITQUEUE_DEBUG
	long __magic;
#endif
};

计数器count所计的就是信号量中的那个量,它代表可使用资源的数量。没有学过操作系统理论的读者不妨把这个数据结构想象成一个院子的大门,而count表示一共有几张门票。当一个进程想要进入这个院子的围墙里面干些什么的时候,先要在大门口领取门票。所以count的数值即表示还有几个进程可以进门。在典型的情况下,一共就只有一张票,所以只有一个进程可以进去。

当一个进程来到门口要领票,却发现门票已经发完的时候,就只好到大门旁边的休息室去睡觉、等候。这个休息室就是这里的队列wait,而计数器sleepers则表示有几个进程正在等候。进入了院子的进程,完成了它要做的时请以后,还是从同一个大门出去,并将门票交还。如果在交还门票之前,门票的数量已经是0,那就可能有进程正在休息室中等候,所以还要向这些正在等候的进程打个招呼,说现在有门票了,或者说,将这些进程唤醒,让它们去竞争那张门票。可见,原理其实很简单。下面通过一段实例,看看这段过程具体是怎么实现的,这段实例取自系统调用umount,我们在这里并不关心怎样拆卸(umount)一个已经安装的文件系统,而是关心怎样把一部分关键性的操作保护起来。

下面的代码取自文件super.c:

static DECLARE_MUTEX(mount_sem);

/*
 * Now umount can handle mount points as well as block devices.
 * This is important for filesystems which use unnamed block devices.
 *
 * We now support a flag for forced unmount like the other 'big iron'
 * unixes. Our API is identical to OSF/1 to avoid making a mess of AMD
 */

asmlinkage long sys_umount(char * name, int flags)
{
......
	/* puts nd.mnt */
	down(&mount_sem);
	retval = do_umount(nd.mnt, 0, flags);
	up(&mount_sem);
......
	return retval;
}

这里的目的是要把do_umount保护起来,因为在同一时间里整个系统中只允许有一个进程在安装或拆卸文件系统,而安装或拆卸文件系统的过程又是可能(实际上是必定)受阻,因而中途会发生调度的。为达到这个目的,首先在第50行建立起一个独门的院子,或者说信号量mount_sem,并且把要加以保护的操作放在进门(down)和出门(up)两个操作之间。要进入这个院子时必须要先执行down以得到一张门票,而当完成了操作从里面出来时则要执行up以归还门票并唤醒可能正在等待的其它进程。操作系统理论里把这段需要独家关起门来干的操作称为临界区(critical section)。顺便说一下,把critical section翻译成临界区似乎有点学究气,critical其实就是非常重要,搞不好的话后果可能很严重的意思。

有关DECLARE_MUTEX的定义如下:

#if WAITQUEUE_DEBUG
# define __SEM_DEBUG_INIT(name) \
		, (int)&(name).__magic
#else
# define __SEM_DEBUG_INIT(name)
#endif

#define __SEMAPHORE_INITIALIZER(name,count) \
{ ATOMIC_INIT(count), 0, __WAIT_QUEUE_HEAD_INITIALIZER((name).wait) \
	__SEM_DEBUG_INIT(name) }

#define __MUTEX_INITIALIZER(name) \
	__SEMAPHORE_INITIALIZER(name,1)

#define __DECLARE_SEMAPHORE_GENERIC(name,count) \
	struct semaphore name = __SEMAPHORE_INITIALIZER(name,count)

#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)
#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)

宏定义ATOMIC_INIT和__WAIT_QUEUE_HEAD_INITIALIZER分别在include/asm-i386/atomic.h和include/linux/wait.h中,读者可以自行阅读。总之,经过gcc的预处理以后,前面的第50行就变成类似于这样的语句:

static struct semaphore mount_sem={{(1)},0,...}

也就是说,通过DECLARE_MUTEX家里的信号量只有一张门票,所以只有一个进程可以进入临界区。另一种通过DECLARE_MUTEX_LOCKED建立的信号量则一张门票也没有,一定要等到某个进程通过up操作送来一张才能把它发给一个进程而允许其进入大门。读者已经在系统调用fork博客中看到过此种信号量的运用。两种信号量各有各的用处,而DECLARE_MUTEX_LOCKED正反映了它们各自的用途。此外,信号量既可以作为全局变量存在,也可以作为某个函数的局部变量存在。

对于信号量的操作只有down和up两种,这是两个inline函数。先看down:


/*
 * 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"
		"1:\n"
		".section .text.lock,\"ax\"\n"
		"2:\tcall __down_failed\n\t"
		"jmp 1b\n"
		".previous"
		:"=m" (sem->count)
		:"c" (sem)
		:"memory");
}

这段嵌入汇编代码的输出部为空,说明执行后并不改变寄存器的内容;而输入部则使指针sem与寄存器ECX结合。由于count是semaphore数据结构中的第一个部分,所以指向该数据结构的指针sem即为指向sem->count的指针,从而第122行的decl指令所递减的实际上是sem->count。减了以后的结果若为0或者大于0,或者说如果成功地拿到了一张门票,那么就在标号1处结束了。注意这里在指令decl前面有个前缀LOCK,表示在执行这条指令时要把总线锁住,以防可能来自同一系统中其他CPU的干扰。

如果减了以后的结果为负数,那么表示拿不到门票,就转到标号2处调用__down_failed。实际上,进程在__down_failed中会进入睡眠,一直到要被唤醒并成功地拿到门票才会从那里返回,然后转到标号1而结束down操作,即进入了临界区。

down=>__down_failed

/*
 * The semaphore operations have a special calling sequence that
 * allow us to do a simpler in-line version of them. These routines
 * need to convert that sequence back into the C sequence when
 * there is contention on the semaphore.
 *
 * %ecx contains the semaphore pointer on entry. Save the C-clobbered
 * registers (%eax, %edx and %ecx) except %eax when used as a return
 * value..
 */
asm(
".align 4\n"
".globl __down_failed\n"
"__down_failed:\n\t"
	"pushl %eax\n\t"
	"pushl %edx\n\t"
	"pushl %ecx\n\t"
	"call __down\n\t"
	"popl %ecx\n\t"
	"popl %edx\n\t"
	"popl %eax\n\t"
	"ret"
);

显然,这里的目的只在于调用__down。代码的作者在这个文件的开头处加了段注释,或可帮助读者更好地理解:

/*
 * Semaphores are implemented using a two-way counter:
 * The "count" variable is decremented for each process
 * that tries to acquire the semaphore, while the "sleeping"
 * variable is a count of such acquires.
 *
 * Notably, the inline "up()" and "down()" functions can
 * efficiently test if they need to do any extra work (up
 * needs to do something only if count was negative before
 * the increment operation.
 *
 * "sleeping" and the contention routine ordering is
 * protected by the semaphore spinlock.
 *
 * Note that these functions are only called when there is
 * contention on the lock, and as such all this is the
 * "non-critical" part of the whole semaphore business. The
 * critical part is the inline stuff in <asm/semaphore.h>
 * where we want to avoid any extra jumps and calls.
 */

/*
 * Logic:
 *  - only on a boundary condition do we need to care. When we go
 *    from a negative count to a non-negative, we wake people up.
 *  - when we go from a non-negative count to a negative do we
 *    (a) synchronize with the "sleeper" count and (b) make sure
 *    that we're on the wakeup list before we synchronize so that
 *    we cannot lose wakeup events.
 */

再看__down的代码:

down=>__down_failed=>__down


void __down(struct semaphore * sem)
{
	struct task_struct *tsk = current;
	DECLARE_WAITQUEUE(wait, tsk);
	tsk->state = TASK_UNINTERRUPTIBLE;
	add_wait_queue_exclusive(&sem->wait, &wait);

	spin_lock_irq(&semaphore_lock);
	sem->sleepers++;
	for (;;) {
		int sleepers = sem->sleepers;

		/*
		 * Add "everybody else" into it. They aren't
		 * playing, because we own the spinlock.
		 */
		if (!atomic_add_negative(sleepers - 1, &sem->count)) {
			sem->sleepers = 0;
			break;
		}
		sem->sleepers = 1;	/* us - see -1 above */
		spin_unlock_irq(&semaphore_lock);

		schedule();
		tsk->state = TASK_UNINTERRUPTIBLE;
		spin_lock_irq(&semaphore_lock);
	}
	spin_unlock_irq(&semaphore_lock);
	remove_wait_queue(&sem->wait, &wait);
	tsk->state = TASK_RUNNING;
	wake_up(&sem->wait);
}

有关等待队列中各元素的数据结构wait_queue_head_t以及宏定义DECLARE_WAITQUEUE,读者已在前一篇博客中看到过,此处不再赘述。而add_wait_queue_exclusive则把代表当前进程的等待队列元素wait链入到由队列头sem->wait代表的等待队列的尾部。当CPU执行到达for(;;)循环时,sem->sleeper表明(连当前进程在内)一共有几个进程正在等着要进入临界区。另一方面,虽然当前进程是因为拿不到门票,进不了临界区才到了__down中,但是由于在这里的spin_lock_irq之前没有加锁,说不定已经由某个进程(当然是在另一个处理器上)在此期间已经执行了一次up操作,因而这个时候实际上已经有门票了。如果不再做一次检查,那就会无谓地进入睡眠而等待已经存在的门票。更糟的是,可能再也没有进程来唤醒它了。所以,在for循环中通过atomic_add_negative所作的检查是很关键的。而且,它所做的还不仅仅是检查,它将(sleepers - 1)加到sem->count上去,使得它的值不会小于-1。举例来说,如果在当前进程执行down之前sem->count为0,并且从那时候以来并无进程在此信号量上执行up,那么(sleepers - 1)为0,而sem->count为-1,相加的结果仍是-1,此时atomic_add_negative返回非0,表示当前进程仍需等待。而若在65行之前已经有个进程在此信号量上执行了up操作,那么sleepers - 1仍为0,但是sem->count变成了0,相加的结果为0而不是负数,此时atomic_add_negative返回0,表示当前今后才能不需要等待了,可以进入临界区了,就好像本次操作在down里面将sem->count从1变成了0一样。当sem->count的值为正数或0时表示还有多少资源,或者可以理解为还剩下几张门片;而sem->count为负的时候则表明已经没有资源并且有进程正在等待,却并不需要表明到底有几个进程正在等待。这样,在up操作中可以用一条指令将sem->count加1,然后根据结果是否为0判定是否有进程需要唤醒。

如果当前进程发现不再需要等待了,它就通过这里的break语句跳出for循环,并在返回之前唤醒等待队列中的其他进程。不过,如果有其他进程正在等待的话,被唤醒之后多半通不过第74行的测试,这是因为那时候sem->sleepers已经设成了0(见第75行),所以(sleepers-1)为-1,已经是个负数;除非那时sem->count已经变成1,否则atomic_add_negative必然会返回非0。这一点等下我们还要讨论。

当atomic_add_negative返回非0时,当前进程就真的要进入睡眠状态等待了,所以在第81行调用schedule。由于入睡的状态为TASK_UNINTERRUPTIBLE,所以不会因接收到信号而被唤醒。同时,由于标志位TASK_EXCLUSIVE(2.3.38)为1,所以只有排在队列中的第一个进程才会唤醒。还要指出的是,当睡眠中的进程被唤醒而从schedule中返回,并回到循环体的前部时,由于sem->sleepers在78行被设成1,所以此时(sleepers-1)必然为0,所以能否进入临界区的条件取决于当时的sem->count是否为负数(-1)。在典型的情况下,被up操作所唤醒的进程会碰上sem->count为0,从而能跳出for循环从__down返回而进入临界区。在从__down返回之前它还要再从队列中唤醒一个进程,而那个进程就往往要继续等待了。

从代码中可以看出,当有多个进程在等待进入一个临界区时,当前进程略有些优势,然后就是先来先进,而进程的优先级别并没有起作用。在有实时要求的系统中这未必不是一个缺陷,将来的版本中也许会考虑这个问题。

还有个问题也与优先级和临界区相联系,称为优先级倒转。试想这么一种情景:一个优先级很高的进程在某一临界区门外等待,而正在临界区里面的进程偏偏优先级很低,而且一旦操作受阻进入睡眠,然后被唤醒时便一时得不到机会运行,于是便急惊风遇上慢中郎。在这样的情况下,优先级高的进程因临界区内的进程优先级太低而受了连累。解决的办法是,当有优先级高的进程在临界区外等待的时候,就暂时把它的高优先级借给临界区内的进程,提高其竞争力。目前在此版本的linux内核中尚未实现此种机制,这也是一个可以改进的地方。不过,问题也并不像想象中那么严重,因为内核中需要在临界区内进行的操作一般都是很短促的,不至于受阻;反之。必须在临界区内进行、而又有可能中途受阻而需要睡眠的操作,则一般不宜由优先级很高的进程来进行。

再来看up就比较简单了,这也是在semaphore.h中:

/*
 * Note! This is subtle. We jump to wake people up only if
 * the semaphore was negative (== somebody was waiting on it).
 * The default case (no contention) will result in NO
 * jumps for both down() and up().
 */
static inline void up(struct semaphore * sem)
{
#if WAITQUEUE_DEBUG
	CHECK_MAGIC(sem->__magic);
#endif
	__asm__ __volatile__(
		"# atomic up operation\n\t"
		LOCK "incl %0\n\t"     /* ++sem->count */
		"jle 2f\n"
		"1:\n"
		".section .text.lock,\"ax\"\n"
		"2:\tcall __up_wakeup\n\t"
		"jmp 1b\n"
		".previous"
		:"=m" (sem->count)
		:"c" (sem)
		:"memory");
}

显然,与down的代码是相似的,不同之处仅在于这是递增,而不是递减sem->count,并且在递增以后结果为0或负数时就调用__up_wakeup,那也是在semaphore.c中:

up=>__up_wakeup

asm(
".align 4\n"
".globl __up_wakeup\n"
"__up_wakeup:\n\t"
	"pushl %eax\n\t"
	"pushl %edx\n\t"
	"pushl %ecx\n\t"
	"call __up\n\t"
	"popl %ecx\n\t"
	"popl %edx\n\t"
	"popl %eax\n\t"
	"ret"
);

up=>__up_wakeup=>__up

/*
 * Logic:
 *  - only on a boundary condition do we need to care. When we go
 *    from a negative count to a non-negative, we wake people up.
 *  - when we go from a non-negative count to a negative do we
 *    (a) synchronize with the "sleeper" count and (b) make sure
 *    that we're on the wakeup list before we synchronize so that
 *    we cannot lose wakeup events.
 */

void __up(struct semaphore *sem)
{
	wake_up(&sem->wait);
}

这里的wake_up和一些有关的宏定义都是在sched.h中定义的:

#define wake_up(x)			__wake_up((x),TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE,WQ_FLAG_EXCLUSIVE)
#define wake_up_all(x)			__wake_up((x),TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE,0)
#define wake_up_sync(x)			__wake_up_sync((x),TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE,WQ_FLAG_EXCLUSIVE)
#define wake_up_interruptible(x)	__wake_up((x),TASK_INTERRUPTIBLE,WQ_FLAG_EXCLUSIVE)
#define wake_up_interruptible_all(x)	__wake_up((x),TASK_INTERRUPTIBLE,0)
#define wake_up_interruptible_sync(x)	__wake_up_sync((x),TASK_INTERRUPTIBLE,WQ_FLAG_EXCLUSIVE)

而__wake_up则在sched.c中。读者可以看到这个函数依次唤醒一个队列中的所有符合条件的进程。但是,如果一个被唤醒进程的TASK_EXCLUSIVE标志为1就不再继续唤醒队列中其余的进程了(sched.c)。

up=>__up_wakeup=>__up=>wake_up=>__wake_up

void __wake_up(wait_queue_head_t *q, unsigned int mode, unsigned int wq_mode)
{
	__wake_up_common(q, mode, wq_mode, 0);
}

up=>__up_wakeup=>__up=>wake_up=>__wake_up=>__wake_up_common


static inline void __wake_up_common (wait_queue_head_t *q, unsigned int mode,
				     unsigned int wq_mode, const int sync)
{
	struct list_head *tmp, *head;
	struct task_struct *p, *best_exclusive;
	unsigned long flags;
	int best_cpu, irq;

	if (!q)
		goto out;

	best_cpu = smp_processor_id();
	irq = in_interrupt();
	best_exclusive = NULL;
	wq_write_lock_irqsave(&q->lock, flags);

#if WAITQUEUE_DEBUG
	CHECK_MAGIC_WQHEAD(q);
#endif

	head = &q->task_list;
#if WAITQUEUE_DEBUG
        if (!head->next || !head->prev)
                WQ_BUG();
#endif
	tmp = head->next;
	while (tmp != head) {
		unsigned int state;
                wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);

		tmp = tmp->next;

#if WAITQUEUE_DEBUG
		CHECK_MAGIC(curr->__magic);
#endif
		p = curr->task;
		state = p->state;
		if (state & mode) {
#if WAITQUEUE_DEBUG
			curr->__waker = (long)__builtin_return_address(0);
#endif
			/*
			 * If waking up from an interrupt context then
			 * prefer processes which are affine to this
			 * CPU.
			 */
			if (irq && (curr->flags & wq_mode & WQ_FLAG_EXCLUSIVE)) {
				if (!best_exclusive)
					best_exclusive = p;
				if (p->processor == best_cpu) {
					best_exclusive = p;
					break;
				}
			} else {
				if (sync)
					wake_up_process_synchronous(p);
				else
					wake_up_process(p);
				if (curr->flags & wq_mode & WQ_FLAG_EXCLUSIVE)
					break;
			}
		}
	}
	if (best_exclusive) {
		if (sync)
			wake_up_process_synchronous(best_exclusive);
		else
			wake_up_process(best_exclusive);
	}
	wq_write_unlock_irqrestore(&q->lock, flags);
out:
	return;
}

可以看出,当一个进程正在等待进入一个临界区时,它所等待的是独占资源(在使用期间需要独占的资源)的释放。而进入了一个临界区的进程则占用了一项独占资源。如果一个进程进入了一个临界区A;而又企图进入另外一个临界区B的话,那就可能会因为进入不了那个临界区,也就是得不到所需的资源,而只好在B的队列中等待。那么,所等待的资源又在谁的手里呢?如果已经占有了那项资源的进程恰好也正在A的队列中等待,那就发生了所谓的死锁,因为此时两个今后才能都无法向前推进而到达可以释放资源的那一步。显然,对共享资源(在使用期间也允许共享的资源)的使用是不会导致死锁的。在linux系统中,多数的资源都是可共享的,而独占资源的使用则置于临界区中。只要保证不在一个临界区中企图进入另一个临界区,那就不会发生死锁,而这也是防止死锁的最简单的办法。进一步,即使在一个临界区中企图进入另一个临界区,但是如果为所有的临界区排好一个次序,所有的进程在进入临界区时都遵守相同的次序(例如,只能先进A后进B,而不允许先进B后进A),则也不会发生上述因循环等待而引起的死锁。在目前的内核中尚无防止和化解死锁的措施,也没有防止进程在一个临界区中不按次序进入另一个临界区的措施。所以,这也是将来可加以改进的一个方面。

在内核中,需要互斥的不仅仅是进程与进程之间,干扰也可能发生于进程与中断服务程序(或bh函数)之间。同时,信号量也并非防止进程间,特别是在不同处理器上运行的进程之间互相干扰的唯一手段。例如,关中断无疑是保证同一处理器中进程与中断服务程序间互斥的一种手段,但是它不能繁殖来自另一个处理器上的中断服务程序或进程的干扰。

另一种有效的手段就是加锁。读者在前面__down的代码中看到的spin_lock_irq和spin_unlock_irq就是其中之一。特别是在多处理器SMP结构的系统中,由软件实现的各种锁尤其起着无可替代的作用。

/*
 * These are the generic versions of the spinlocks and read-write
 * locks..
 */
#define spin_lock_irqsave(lock, flags)		do { local_irq_save(flags);       spin_lock(lock); } while (0)
#define spin_lock_irq(lock)			do { local_irq_disable();         spin_lock(lock); } while (0)
#define spin_lock_bh(lock)			do { local_bh_disable();          spin_lock(lock); } while (0)

#define read_lock_irqsave(lock, flags)		do { local_irq_save(flags);       read_lock(lock); } while (0)
#define read_lock_irq(lock)			do { local_irq_disable();         read_lock(lock); } while (0)
#define read_lock_bh(lock)			do { local_bh_disable();          read_lock(lock); } while (0)

#define write_lock_irqsave(lock, flags)		do { local_irq_save(flags);      write_lock(lock); } while (0)
#define write_lock_irq(lock)			do { local_irq_disable();        write_lock(lock); } while (0)
#define write_lock_bh(lock)			do { local_bh_disable();         write_lock(lock); } while (0)

#define spin_unlock_irqrestore(lock, flags)	do { spin_unlock(lock);  local_irq_restore(flags); } while (0)
#define spin_unlock_irq(lock)			do { spin_unlock(lock);  local_irq_enable();       } while (0)
#define spin_unlock_bh(lock)			do { spin_unlock(lock);  local_bh_enable();        } while (0)

#define read_unlock_irqrestore(lock, flags)	do { read_unlock(lock);  local_irq_restore(flags); } while (0)
#define read_unlock_irq(lock)			do { read_unlock(lock);  local_irq_enable();       } while (0)
#define read_unlock_bh(lock)			do { read_unlock(lock);  local_bh_enable();        } while (0)

#define write_unlock_irqrestore(lock, flags)	do { write_unlock(lock); local_irq_restore(flags); } while (0)
#define write_unlock_irq(lock)			do { write_unlock(lock); local_irq_enable();       } while (0)
#define write_unlock_bh(lock)			do { write_unlock(lock); local_bh_enable();        } while (0)

首先来看看同一组加锁操作之间的不同。例如,spin_lock_irqsave与spin_lock_irq之间的区别仅在于前者调用local_irq_save而后者调用local_irq_disable。相应的解锁操作spin_unlock_irqrestore与spin_unlock_irq之间的区别也就因此而不同,前者调用local_irq_restore而后者调用local_irq_enable。

再来看看不同组的加锁操作有什么不同。例如,spin_lock_irq和read_lock_irq之间的区别仅在于前者调用spin_lock而后者调用read_lock。

每一个操作都包含了两部分。一部分是操作明以local开头的,其作用是关闭或开启本处理器上的中断响应。另一部分是操作名以_lock结尾的,其作用是反之来自其他处理器的干扰。我们先来看处理开中断、关中断的操作,这定义如下:

/* For spinlocks etc */
#define local_irq_save(x)	__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x): /* no input */ :"memory")
#define local_irq_restore(x)	__restore_flags(x)
#define local_irq_disable()	__cli()
#define local_irq_enable()	__sti()

可见,local_irq_save和local_irq_disable都通过cli指令来关闭中断,但是前者先把当前的处理器状态标志寄存器的内容保存起来,因为其中的IF标志就反映当前的中断时开着还是关着(指令cli就是把IF标志位清0),以便在去锁时加以恢复。由于状态标志寄存器并非通用寄存器,所以要用push和pop指令经过堆栈将其内容保存到参数x中。相应地,local_irq_restore与local_irq_enable的区别也在于此。

再来看spin_lock,其定义如下:

static inline void spin_lock(spinlock_t *lock)
{
#if SPINLOCK_DEBUG
	__label__ here;
here:
	if (lock->magic != SPINLOCK_MAGIC) {
printk("eip: %p\n", &&here);
		BUG();
	}
#endif
	__asm__ __volatile__(
		spin_lock_string
		:"=m" (lock->lock) : : "memory");
}

参数lock的类型为,定如下:
 


/*
 * Your basic SMP spinlocks, allowing only a single CPU anywhere
 */

typedef struct {
	volatile unsigned int lock;
#if SPINLOCK_DEBUG
	unsigned magic;
#endif
} spinlock_t;

如果不考虑调试,这实际上就是一个无符号整数,但是这样有利于防止gcc在编译过程以及有害的优化。代码中引用的spin_lock_string又是一个宏定义:

#define spin_lock_string \
	"\n1:\t" \
	"lock ; decb %0\n\t" \
	"js 2f\n" \
	".section .text.lock,\"ax\"\n" \
	"2:\t" \
	"cmpb $0,%0\n\t" \
	"rep;nop\n\t" \
	"jle 2b\n\t" \
	"jmp 1b\n" \
	".previous"

这里的%0与参数lock->lock相结合。这里的指令decb将操作数,即lock->lock减1,而后缀b则表示操作数为0位。这条指令带有前缀lock,表示在执行时要将总线锁住,不让其他处理器访问,以此来保证该条指令执行的原子性。减1以后,要是结果非负(符号位为0)则加锁成功,所以就反悔了。如果发现减1以后的结果成了负数,那就表示已经有其他操作先加了锁,因此被锁在了门外,这时就转移到标号2处循环测试,等待加锁者去锁后将lock->lock设置成大于0,然后又试着加锁。

从代码中可以看出,如果lock->lock的值原来就已经是0或负数,则处理器不断地循环测试它的值,直至其变成大于0为止,所以才有spin_lock这个名字。所谓spin就是连轴转的意思。处理器不断地这么连轴转,当然是在做无用功。那么为什么不像在对信号量的down操作那样进入睡眠,把CPU让给其他进程来做些有用功呢?这是因为想要加锁的这段程序未必是在一个进程的上下文中调用的,它可能调用自一段中断服务程序或者bh函数,根本就不是可调度的。这也说明,加锁的时间不能太长,否则就可能太浪费了。

至于spin_unlock的代码那就很简单了:

static inline void spin_unlock(spinlock_t *lock)
{
#if SPINLOCK_DEBUG
	if (lock->magic != SPINLOCK_MAGIC)
		BUG();
	if (!spin_is_locked(lock))
		BUG();
#endif
	__asm__ __volatile__(
		spin_unlock_string
		:"=m" (lock->lock) : : "memory");
}

同样,spin_unlock_string也是个宏定义:

/*
 * This works. Despite all the confusion.
 */
#define spin_unlock_string \
	"movb $1,%0"

代码中的指令movb将lock->lock设置成1,如此而已。这了指令不带有前缀lock,因为指令movb的操作本身就是原子性的。相比之下,前面的指令decb因为涉及读-改-写周期,所以从总线角度看不是原子性的。

读者也许会问,既然被锁在门外的处理器只是在做不用功,那为何不干脆就把总线锁了,一直到要做的事情完成以后才开放呢?这还是不同的,正是连轴转做无用功的处理器仍能响应中断。而如果干脆把总线锁了那就连中断也不能响应了。再说,系统中也还可能有其他处理器,只要不想进入同一段代码或受同一把锁保护的代码,就可以继续运行。

再来看read_lock和write_lock,其实现也是大同小异,我们把这些代码留给读者自己阅读:

/*
 * On x86, we implement read-write locks as a 32-bit counter
 * with the high bit (sign) being the "contended" bit.
 *
 * The inline assembly is non-obvious. Think about it.
 *
 * Changed to use the same technique as rw semaphores.  See
 * semaphore.h for details.  -ben
 */
/* the spinlock helpers are in arch/i386/kernel/semaphore.c */

static inline void read_lock(rwlock_t *rw)
{
#if SPINLOCK_DEBUG
	if (rw->magic != RWLOCK_MAGIC)
		BUG();
#endif
	__build_read_lock(rw, "__read_lock_failed");
}

static inline void write_lock(rwlock_t *rw)
{
#if SPINLOCK_DEBUG
	if (rw->magic != RWLOCK_MAGIC)
		BUG();
#endif
	__build_write_lock(rw, "__write_lock_failed");
}

#define read_unlock(rw)		asm volatile("lock ; incl %0" :"=m" ((rw)->lock) : : "memory")
#define write_unlock(rw)	asm volatile("lock ; addl $" RW_LOCK_BIAS_STR ",%0":"=m" ((rw)->lock) : : "memory")

代码中引用的一些宏操作和宏定义为:

#define RW_LOCK_BIAS		 0x01000000
#define RW_LOCK_BIAS_STR	"0x01000000"

#define __build_read_lock_ptr(rw, helper)   \
	asm volatile(LOCK "subl $1,(%0)\n\t" \
		     "js 2f\n" \
		     "1:\n" \
		     ".section .text.lock,\"ax\"\n" \
		     "2:\tcall " helper "\n\t" \
		     "jmp 1b\n" \
		     ".previous" \
		     ::"a" (rw) : "memory")

#define __build_read_lock_const(rw, helper)   \
	asm volatile(LOCK "subl $1,%0\n\t" \
		     "js 2f\n" \
		     "1:\n" \
		     ".section .text.lock,\"ax\"\n" \
		     "2:\tpushl %%eax\n\t" \
		     "leal %0,%%eax\n\t" \
		     "call " helper "\n\t" \
		     "popl %%eax\n\t" \
		     "jmp 1b\n" \
		     ".previous" \
		     :"=m" (*(volatile int *)rw) : : "memory")

#define __build_read_lock(rw, helper)	do { \
						if (__builtin_constant_p(rw)) \
							__build_read_lock_const(rw, helper); \
						else \
							__build_read_lock_ptr(rw, helper); \
					} while (0)

#define __build_write_lock_ptr(rw, helper) \
	asm volatile(LOCK "subl $" RW_LOCK_BIAS_STR ",(%0)\n\t" \
		     "jnz 2f\n" \
		     "1:\n" \
		     ".section .text.lock,\"ax\"\n" \
		     "2:\tcall " helper "\n\t" \
		     "jmp 1b\n" \
		     ".previous" \
		     ::"a" (rw) : "memory")

#define __build_write_lock_const(rw, helper) \
	asm volatile(LOCK "subl $" RW_LOCK_BIAS_STR ",(%0)\n\t" \
		     "jnz 2f\n" \
		     "1:\n" \
		     ".section .text.lock,\"ax\"\n" \
		     "2:\tpushl %%eax\n\t" \
		     "leal %0,%%eax\n\t" \
		     "call " helper "\n\t" \
		     "popl %%eax\n\t" \
		     "jmp 1b\n" \
		     ".previous" \
		     :"=m" (*(volatile int *)rw) : : "memory")

#define __build_write_lock(rw, helper)	do { \
						if (__builtin_constant_p(rw)) \
							__build_write_lock_const(rw, helper); \
						else \
							__build_write_lock_ptr(rw, helper); \
					} while (0)

调用__build_write_lock和__build_read_lock时的第二个参数都是函数指针,分别为__write_lock_failed和__read_lock_failed,其代码如下:

#if defined(CONFIG_SMP)
asm(
"
.align	4
.globl	__write_lock_failed
__write_lock_failed:
	" LOCK "addl	$" RW_LOCK_BIAS_STR ",(%eax)
1:	cmpl	$" RW_LOCK_BIAS_STR ",(%eax)
	jne	1b

	" LOCK "subl	$" RW_LOCK_BIAS_STR ",(%eax)
	jnz	__write_lock_failed
	ret


.align	4
.globl	__read_lock_failed
__read_lock_failed:
	lock ; incl	(%eax)
1:	cmpl	$1,(%eax)
	js	1b

	lock ; decl	(%eax)
	js	__read_lock_failed
	ret
"
);
#endif

值得指出的是,如果CPU进入了一段加锁的代码A以后又企图进入另一段加锁的代码B,那就有可能在那里被关在门外,而如果已经在B中的处理器恰好又企图进入A而也被关在了门外,因此两个处理器都陷入可连轴转,那就形成了死锁。这跟在信号量上通过down操作嵌套地进入了临界区时可能会形成的死锁是一致的。防止此种死锁的手段主要有:

  1. 不允许在进入加时的代码以后再进入其他加锁的代码,这是最简单的。
  2. 如果允许这样做的话,就要为所有加锁的代码段建立一个统一的次序,例如必须是先进入A后进入B。

目前的linux内核中尚未采取措施来防止这种死锁,这也有待于将来进一步的改进。不过,并非每个程序员都需要编写在内核中运行的程序,负责开发内核程序(如设备驱动)的程序通常总是比较有经验、水平比较高的人,他们自会注意这个问题。另一方面,linux系统中的多数资源都是可共享的,需要放在临界区中或者加锁的代码时很少、很短的。所以,在实际使用中死锁并不是一个很现实的问题。

还有,临界区和加锁在概念上是类似的,但对系统影响的额程序却不同,所以在使用时要加以区分。如果不问青红皂白,动不动就加锁,奶好像为了抓小偷而全城戒严一样,属于防范过当了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值