很多编程的书里在介绍mutex和semaphore的时候都会说,mutex是一种特殊的semaphore.
当semaphore的N=1时,就变成了binary semaphore,也就等同与mutex了。
但是实际上,在linux中,他们的实现什有区别的,导致最后应用的行为也是有区别的。
先看下面这个例子,这是一段linux kernel的代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/delay.h>
static DEFINE_MUTEX(g_mutex);
static DEFINE_SEMAPHORE(g_semaphore);
static int fun1(void *p)
{
while (true) {
mutex_lock(&g_mutex);
msleep(1000);
printk("1\n");
mutex_unlock(&g_mutex);
}
return 0;
}
static int fun2(void *p)
{
while (true) {
mutex_lock(&g_mutex);
msleep(1000);
printk("2\n");
mutex_unlock(&g_mutex);
}
return 0;
}
static int fun3(void *p)
{
while (true) {
down(&g_semaphore);
msleep(1000);
printk("3\n");
up(&g_semaphore);
}
return 0;
}
static int fun4(void *p)
{
while (true) {
down(&g_semaphore);
msleep(1000);
printk("4\n");
up(&g_semaphore);
}
return 0;
}
static int hello_init(void)
{
kernel_thread(fun1, NULL, 0);
kernel_thread(fun2, NULL, 0);
kernel_thread(fun3, NULL, 0);
kernel_thread(fun4, NULL, 0);
return 0;
}
module_init(hello_init);
这段代码很简单,4个线程,2个去获取mutex,2个去获取semaphore。
我们可以先只enable thread1和thread2。
我开始预期的结果什输出1212121212...
但是实际的结果是111111111...22222222222...11111111111...
假设enable thread3和thread4,输出则变成了34343434343434...
显然,mutex和semaphore使用的结果不一样。
为什么会造成这样的结果哪?
我们分析一下kernel里mutex和semaphore的实现就可以明白了
kernel/mutex.c
__mutex_unlock_common_slowpath(atomic_t *lock_count, int nested)在解锁的时候,会先把lock->count设置成1,
{
struct mutex *lock = container_of(lock_count, struct mutex, count);
unsigned long flags;
spin_lock_mutex(&lock->wait_lock, flags);
mutex_release(&lock->dep_map, nested, _RET_IP_);
debug_mutex_unlock(lock);
/*
* some architectures leave the lock unlocked in the fastpath failure
* case, others need to leave it locked. In the later case we have to
* unlock it here
*/
if (__mutex_slowpath_needs_to_unlock())
atomic_set(&lock->count, 1);
if (!list_empty(&lock->wait_list)) {
/* get the first entry from the wait-list: */
struct mutex_waiter *waiter =
list_entry(lock->wait_list.next,
struct mutex_waiter, list);
debug_mutex_wake_waiter(lock, waiter);
wake_up_process(waiter->task);
}
spin_unlock_mutex(&lock->wait_lock, flags);
}
然后从等待这个mutex的队列里取出第一个任务,并wake_up这个任务。
这里要注意,wake_up_process只是把这个任务设置成可调度,并不是直接就进行调度了。
所以当一个线程unlock mutex之后,只要在自己还没有被调度出去之前再次很快的lock mutex的话,他依旧会成功。
于是,这就出现了一开始那个程序的结果。
在代码本身没有死锁的情况下,不合适得使用mutex,会造成饥饿的发生。
那semaphore到底是如何避免这样的情况发生的哪?
kernel/semaphore.c
void down(struct semaphore *sem)首先在down的时候,回去查看sem->count的值,假如大于0,就进行--操作。
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
__down(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct task_struct *task = current;
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = task;
waiter.up = false;
for (;;) {
if (signal_pending_state(state, task))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_task_state(task, state);
raw_spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout);
raw_spin_lock_irq(&sem->lock);
if (waiter.up)
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
static noinline void __sched __up(struct semaphore *sem)
{
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
struct semaphore_waiter, list);
list_del(&waiter->list);
waiter->up = true;
wake_up_process(waiter->task);
}
这就好比第一个线程去down semaphore。
等到第二个线程再去down的时候,count为0了,就进入了__down_common函数。
这个函数里面会一直检查waiter.up,知道为true了才会退出。
至此,第二个线程就被block在了down函数里。
等到第一个线程up semaphore,假如这个semaphore的等待队列里还有任务,则设置waiter->up为true并唤醒任务。
这里用的也是wake_up_process,貌似和mutex的实现什一样的,但是接下来就不一样了。
假设第一个线程之后又很快的去down semaphore,会发生什么哪?
由于sem->count还是为0,这个线程在down的时候就会被block住而发生调度。
第二个线程此时就可以获得semaphore而继续执行代码了。
一直直到没有任何线程在等待队列里了,sem->count才会被++。
所以,semaphore就变成了这样的行为。
总结:mutex在使用时没有任何顺序的保证,它仅仅是保护了资源,但是效率会比较高。
而semaphore则有顺序的保证,使得每个使用者都能依次获得他,但是相应的会损失一点效率。