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

相关推荐
顶点多余2 小时前
Linux中进程间通信 ---管道篇
linux·运维·服务器
zzzsde2 小时前
【Linux】进程控制(2):进程等待&&进程替换
linux·服务器·网络
longxibo2 小时前
【Ubuntu datasophon1.2.1 二开之八:验证实时数据入湖】
大数据·linux·clickhouse·ubuntu·linq
恋红尘2 小时前
K8S 服务发现-叩丁狼
linux·docker·kubernetes
IMPYLH2 小时前
Linux 的 dd 命令
linux·运维·服务器
minji...2 小时前
Linux 进程间通信(一)进程间通信与匿名管道
linux·运维·服务器·数据结构·数据库·c++
feng_you_ying_li2 小时前
linux的指令终章与权限之用户权限(3)
linux·运维·服务器
s6516654962 小时前
Linux内核学习-汇编笔记
linux
IMPYLH2 小时前
Linux 的 csplit 命令
linux·运维·服务器·数据库