Linux 锁 (3) - semaphore

文章目录

  • [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 的实现无法让你直到用户初始允许的并发进程数,也无法让你知道锁的持有进程是谁。

相关推荐
AlfredZhao18 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫2 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++