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

相关推荐
Android系统攻城狮2 小时前
Linux PipeWire深度解析之pw_factory_create_object调用流程与实战(七十二)
linux·音频进阶·pipewire音频进阶
lsh曙光2 小时前
playbook剧本(2)--Ansible变量
linux·运维·网络
lengjingzju2 小时前
编译与调试完全指南—第14章 系统配置与调优
linux
大鹏的NLP博客3 小时前
通用 Linux 嵌入式板端 C/C++ ABI 与 Glibc 依赖治理规范
linux·c++·交叉编译
tedcloud1234 小时前
book-to-skill 怎么部署?把技术书和项目文档变成 AI Agent 可复用知识
linux·运维·服务器·人工智能·开源
瀚高PG实验室4 小时前
SQL优化案例:使用分区表优化SQL查询性能
linux·服务器·数据库·sql·microsoft·postgresql
青禾8375 小时前
Linux 系统信息、权限管理与 Python 并发编程(协程与线程)完全指南
linux·python
2301_800954995 小时前
Linux 常用系统信息与进程管理命令速查
linux·运维·服务器
T型码农要学习6 小时前
eMMC_命令整理
linux·驱动开发·emmc
刚入门的大一新生7 小时前
Linux-进程4
linux·运维·服务器