信号量
Linux 内核中应用最广泛的同步原语除了自旋锁就是信号量(Semaphore)。可以说自旋锁和信号量在很大程度上是一种互补的关系,它们有各自适用的场景,两者的场景加起来基本上可以覆盖一切内核中的所有场景。
信号量可以是多值的(多值信号量),当其为二值信号量(只有两个值)时,类似于锁:一个值代表未锁,另一个值代表已锁。其工作原理与自旋锁最大的不同在于:获取锁的进程(内核的一个执行路径),若不能立即得到锁,就会发生调度转入睡眠;另外的进程释放锁时,唤醒等待该锁的进程。
前面已经说过自旋锁的使用的限制主要有两点:
(1)持有自旋锁的临界区不允许调度和睡眠
(2)其次就是竞争激烈时整体性能不好
而信号量恰好解决这两个问题:因为锁的竞争者 不是忙等,信号量的临界区允许调度和睡眠而不会导致死锁;因为锁的竞争者会转入睡眠, 从而让出 CPU 资源给别的进程,因此对锁的竞争不会影响整体性能。有优点就有缺点,中断上下文要求整体运行时间可预测(不能太长),而信号量临界区可能发生调度, 因此不能用于中断上下文(只能用于进程上下文)。另外,如果临界区的代码很短,那么用信号量并不合算,因为进程睡眠-唤醒的代价太大,消耗的 CPU 资源可能远远大于短时间的忙等。和自旋锁一样,信号量也分为普通信号量和读写信号量两种。
(1)普通信号量
在 Linux-2.6.26 以前,普通信号量的实现是体系结构相关的。考虑到信号量的原语并不像自旋锁那样用在对性能要求很高的场景,Linux-2.6.26 开始从可读性出发实现了通用的普通信号量。通用普通信号量的数据类型定义如下:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
这里面,count 是最重要的计数器字段,它标识了信号量的状态:0 表示忙已 锁,值为正代表自由未锁,允许竞争者进入临界区。因此 count 的初值就是最大允许进入临界区的进程数目,初值为 2 的信号量就是二值信号量。二值信号量类似于一个普通的锁,而多值信号量类似于一个允许一定并发性的锁。wait_list 字段是当信号量为忙时, 所有等待信号量的进程列表,而 lock 则是保护 wait_list 的自旋锁。
普通信号量的主要 API 有:
/*静态定义一个名为 sem 信号量*/
DEFINE_SEMAPHORE(sem):
/*初始化一个信号量 sem,计数器初值为 val,也就是count的值初始化为val*/
void sema_init(struct semaphore *sem, int val):
/*减少信号量 sem 的计数器(类似于获取锁)。如果失败(计数器已经是 0),那么转入睡眠(状态为 TASK_UNINTERRUPTIBLE,不会被任何信号唤醒)并把当前进程挂到 wait_list;被唤醒后继续尝试获取锁*/
void down(struct semaphore *sem)
/*增加信号量 sem 的计数器(类似于释放锁),然后唤醒 wait_list 里面的第一个进程(如果里面有的话)*/
void up(struct semaphore *sem)
/*尝试减少信号量 sem 的计数器(类似于获取锁),如果成功就返回 0,如果失败(计数器已经是 0)就返回 1,进程不会睡眠*/
int down_trylock(struct semaphore *sem)
/*减少信号量 sem 的计数器(类似于获取锁)。如果失败(计数器已经是 0),那么转入 睡眠(状态为TASK_KILLABLE,会被致命信号唤醒)并把当前进程挂到wait_list;被唤 醒后继续尝试获取锁。正常返回 0,被信号唤醒则返回-EINTR*/
int down_killable(struct semaphore *sem)
/*减少信号量 sem 的计数器(类似于获取锁)。如果失败(计数器已经是 0),那么转入睡眠(状态为 TASK_INTERRUPTIBLE,会被任意信号唤醒)并把当其进程挂到 wait_list; 被唤醒后继续尝试获取锁。正常返回 0,被信号唤醒则返回-EINTR*/
int down_interruptible(struct semaphore *sem)
/*减少信号量 sem 的计数器(类似于获取锁)。如果失败(计数器已经是 0),那么转入 睡眠(状态为 TASK_ UNINTERRUPTIBLE,但睡眠时间达到超时值 jiffies 后会被唤醒)并 把当其进程挂到 wait_list;被唤醒后继续尝试获取锁。正常返回 0,被超时唤醒则返回 -ETIME*/
int down_timeout(struct semaphore *sem, long jiffies)
(2)读写信号量
读写信号量的引入原因类似于读写自旋锁,是为了区分不同的竞争者(读者和写者), 以便允许读者共享而写者互斥。读写信号量既有通用版本,也有各种体系结构自己实现的版本。龙芯使用的是通用版本。
struct rw_semaphore {
long count;
struct list_head wait_list;
raw_spinlock_t wait_lock;
#ifdef CONFIG_RWSEM_SPIN_ON_OWNER
struct optimistic_spin_queue osq;
struct task_struct *owner;
#endif
」
主要字段 count、wait_list 和 wait_lock 的含义与普通信号量的含义基本相同,用法也相同。而 CONFIG_ RWSEM_SPIN_ON_OWNER 是 Linux-3.16 开始引入的,通过 MCS 锁来优化读写信号量的性能,其原理类似于 Queued Spinlock。
读写信号量的主要 API 如下:
/*静态声明一个名为 sem 信号量*/
DECLARE_RWSEM(sem)
/*初始化一个读写信号量 sem*/
init_rwsem(sem)
/*读者减少信号量 sem 的计数器(类似于获取锁)*/
down_read(struct rw_semaphore *sem)
/*读者增加信号量 sem 的计数器(类似于释放锁)*/
up_read(struct rw_semaphore *sem)
/*写者减少信号量 sem 的计数器(类似于获取锁)*/
down_write(struct rw_semaphore *sem)
/*写者增加信号量 sem 的计数器(类似于释放锁)*/
up_write(struct rw_semaphore *sem)
/*读者尝试减少信号量 sem 的计数器(类似于获取锁),如果count的值已经为0,立即返回不睡眠*/
down_read_trylock(struct rw_semaphore *sem)
/*写者尝试减少信号量 sem 的计数器(类似于获取锁),如果count的值已经为0,立即返回不睡眠*/
down_write_trylock(struct rw_semaphore *sem):
/*写者锁降级,即将写锁转换成读锁*/
downgrade_write(struct rw_semaphore *sem)
具体的代码实现请看include/linux/semaphore.h中的实现,这里不做详细的介绍。