文章目录
- [1. 前言](#1. 前言)
- [2. semaphore 的实现](#2. semaphore 的实现)
- [3. 小结](#3. 小结)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. semaphore 的实现
c
// include/linux/semaphore.h
/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count; /* 当前 还允许进入临界区进程数 */
struct list_head wait_list; /* 睡眠等待的进程列表 */
};
/* semaphore 初始化宏 */
#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
}
/* 静态 semaphore 定义 */
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
/* 动态定义(位于堆、栈空间)的 semaphore 初始化接口 */
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);
}
/* semaphore 锁 P/V 操作接口 */
extern void down(struct semaphore *sem);
extern int __must_check down_interruptible(struct semaphore *sem);
extern int __must_check down_killable(struct semaphore *sem);
extern int __must_check down_trylock(struct semaphore *sem);
extern int __must_check down_timeout(struct semaphore *sem, long jiffies);
extern void up(struct semaphore *sem);
简要的看下 down_interruptible() / up() 接口,其它的变种接口不做展开。
先看 down_interruptible():
c
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0)) /* 持锁: 锁的用户数 +1 */
sem->count--;
else /* 已经达到锁的最大并发上限, 进入睡眠等待, 直到有用户调用 up() 释放锁被唤醒 */
result = __down_interruptible(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result; /* 返回 0 表示拿锁成功, 否则拿锁失败 */
}
static noinline int __sched __down_interruptible(struct semaphore *sem)
{
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
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; /* 等锁过程中, 被信号中断 */
}
再看 up() 接口:
c
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); /* 有睡眠等待进程, 释放锁的时候, 按等待的 FIFO 顺序唤醒等待进程 */
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
static noinline void __sched __up(struct semaphore *sem)
{
/* 按等待的 FIFO 顺序唤醒进程, 一次唤醒一个进程 */
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); /* 唤醒最先进入等待的一个等锁进程 */
}
3. 小结
semophore 典型用法如下:
c
struct semaphore sem;
int count = 1;
sema_init(&sem, count);
if (down_interruptible(&sem) == 0) { // 拿锁
// 临界区代码操作
...
up(&sem); // 释放锁
}
结合前面分析的代码,这很好理解,同一时间只有一个持锁进程。非典型的情形是初始 count 为 0,或者初始 count 为大于 1 的场景。
先来看看初始 count 为 0 的场景,这是调用 down_interruptible() 将直接进入睡眠,然后在 up() 是被唤醒。
再看看 初始 count 为大于 1 的场景,这结合代码,也不难理解。可能唯一让人不解的是:允许多个进程同时进入临界区,这和大多只允许一个进程进入临界区的锁有着很大不同。允许多个进程同时进入临界区,临界区访问的数据必定是允许多进程同时安全访问的:要么有其它同步机制保护,要么多进程访问不会引起数据不一致的情形。
最后,我们小结 semaphore 的特点:
- 允许 1 到多个进程同时进入临界区,这和其它大多同一时间只允许一个进程访问临界区的锁有很大不同;
- 当大于 1 个进程同时拿锁进入临界区时(count > 1 的情形),如果这些拿锁进程间访问临界区数据会造成不一致的情况,应该使用其它同步机制进行保护,或者将 count 限制为 1,让大家彼此互斥访问;
- 会进入睡眠,无法用于原子上下文,如中断处理场景
- 释放锁时一次唤醒一个进程,且严格按照等锁的先后顺序依次唤醒,一次只唤醒一个进程。
按理来说,我们代码里面的 down*()/up() 操作应该成对出现,但从代码分析你会发现,当没有进程持有 sempaphore 时,多调用几次 up() 也不会有问题,它上调了允许并发进入临界区进程的数量,因为 sempaphore 的实现无法让你直到用户初始允许的并发进程数,也无法让你知道锁的持有进程是谁。