信号量
Linux Kernel 除了提供了自旋锁,还提供了睡眠锁,信号量就是一种睡眠锁。信号量的特点是,如果一个任务试图获取一个已经被占用的信号量,他会被推入等待队列,让其进入睡眠。此刻处理器重获自由,去执行其他的代码。当持有的信号量被释放,处于等待队列的任务将被唤醒,并获取到该信号量。
从信号量的睡眠特性得出一些结论:
- 由于竞争信号量的时候,未能拿到信号的进程会进入睡眠,所以信号量可以适用于长时间持有。
- 而且信号量不适合短时间的持有,因为会导致睡眠的原因,维护队列,唤醒,等各种开销,在短时间的锁定某对象,反而比忙等锁的效率低
- 由于睡眠的特性,只能在进程上下文进行调用,无法再中断上下文中使用信号量。
- 一个进程可以在持有信号量的情况下去睡眠(可能并不需要,这里只是假如),另外的进程尝试获取该信号量时候,不会死锁。
- 期望去占用一个信号量的同时,不允许持有自旋锁,因为企图去获取信号量的时候,可能导致睡眠,而自旋锁不允许睡眠
在有一些特定的场景,自旋锁和信号量没得选,比如中断上下文,只能用自旋锁,比如需要要和用户空间做同步的时候,代码需要睡眠,信号量是唯一选择。如果有的地方,既可以选择信号量,又可以选自旋锁,则需要根据持有锁的时间长短来进行选择。理想情况下是,越短的时间持有,选择自旋锁,长时间的适合信号量。与此同时,信号量不会关闭调度,他不会对调度造成影响
信号量允许多个锁持有者,而自旋锁在一个时刻,最多允许一个任务持有。信号量同时允许的持有者数量可以在声明信号的时候指定。绝大多数情况下,信号量允许一个锁的持有者,这种类型的信号量称之为二值信号量,也就是互斥信号量。
一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。
信号量的操作
信号量相关的东西放置到了: include/linux/semaphore.h 文件
初始化一个信号量有两种方式:
struct semaphore sem;
sema_init(&sem, val);
DEFINE_SEMAPHORE(sem)
内核针对信号量提供了一组操作接口:
注意:down_interruptible 接口,在获取不到信号量的时候,该任务会进入 INTERRUPTABLE 的睡眠,但是 down() 接口会导致进入 UNINTERRUPTABLE 的睡眠,down 用的较少
信号量的实现
1.信号量的结构:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量用结构semaphore描述,它在自旋锁的基础上改进而成,它包括一个自旋锁、信号量计数器和一个等待队列。用户程序只能调用信号量API函数,而不能直接访问信号量结构。
2. 初始化函数sema_init
#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
}
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
初始化了信号量中的 spinlock 结构,count 计数器和初始化链表。
3. 可中断获取信号量函数down_interruptible
static noinline int __sched __down_interruptible(struct semaphore *sem)
{
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
down_interruptible 进入后,获取信号量获取成功,进入临界区,否则进入 __down_interruptible->__down_common
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = current;
waiter.up = false;
for (;;) {
if (signal_pending_state(state, current))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_current_state(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;
}
加入到等待队列,将状态设置成为 TASK_INTERRUPTIBLE , 并设置了调度的 Timeout : MAX_SCHEDULE_TIMEOUT
在调用了 schedule_timeout,使得进程进入了睡眠状态
4. 释放信号量函数 up
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);
}
如果等待队列为空,即,没有睡眠的进程期望获取这个信号量,则直接 count++,否则调用 __up:
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);
}
取出队列中的元素,进行唤醒操作。