kernel信号量源码分析

Linux内核中的信号量是一种经典的同步机制,用于控制对共享资源的并发访问。与完成量(Completion)不同,信号量更适用于资源计数管理,例如限制同时访问某资源的线程数量。以下是信号量的核心知识点以及与完成量的对比:

一、 结构体成员

c 复制代码
struct semaphore {
  raw_spinlock_t lock;      /* 自旋锁 */
  unsigned int count;       /* 可用资源计数 */
  struct list_head wait_list; /* 等待改信号量的任务队列 */
};

struct semaphore_waiter 用于描述一个正在等待信号量的任务,它不是信号量本体,而是挂入信号量等待队列的等待者。

c 复制代码
struct semaphore_waiter {
	struct list_head list; /* 等待队列链表节点 */
	struct task_struct *task; /* 指向当前等待的进程 */
	bool up; /* 该等待者是否被唤醒 */
};

二、 API函数

2.1 初始化信号量

动态初始化

  • sem:指向要初始化的信号量对象。
  • val:信号量初始计数值,表示初始可用的资源数量。
c 复制代码
static inline void sema_init(struct semaphore *sem, int val)
{
	static struct lock_class_key __key;
	/* 宏构造一个临时的semaphore,整体赋值给sem,该步骤会初始化其结构体的三个成员 */
	*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
	lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

静态初始化,只能定义初始值为 1 的信号量。

c 复制代码
DEFINE_SEMAPHORE(my_sem);
/*展开后等价于*/
struct semaphore name = {
	.lock = __RAW_SPIN_LOCK_UNLOCKED(name.lock),
	.count = 1,
	.wait_list = LIST_HEAD_INIT(name.wait_list),
};

2.2 获取信号量

阻塞等待,直到获取信号量:

c 复制代码
void down(struct semaphore *sem)
{
	unsigned long flags;

	might_sleep(); /* 声明该函数可能睡眠 */
	raw_spin_lock_irqsave(&sem->lock, flags); /* 获取锁、保存并关闭中断 */
	if (likely(sem->count > 0))
		sem->count--; /* 获取到资源 */
	else
		__down(sem); /* 资源不可用,进入等待队列 */
	raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);

__down函数会调用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT)

c 复制代码
static inline int __sched __down_common(struct semaphore *sem, long state,
								long timeout)
{
	struct semaphore_waiter waiter;
  /* 创建一个等待节点,挂入wait_list */
	list_add_tail(&waiter.list, &sem->wait_list);
	waiter.task = current; /* 记录当前任务 */
	waiter.up = false; /* 表明当前等待者未被up选中 */

	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;
}

可中断的等待(允许信号打断):

c 复制代码
int down_interruptible(struct semaphore *sem)
{
	unsigned long flags;
	int result = 0;

	might_sleep(); /* 声明函数可能上睡眠 */
	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--; /* 获取到资源 */
	else
		result = __down_interruptible(sem);
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return result;
}

__down_interruptible会调用__down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT) 。和阻塞等待的区别就是参数传递的是TASK_INTERRUPTIBLE ,表示可以被信号打断。

非阻塞尝试(立即返回):

c 复制代码
int down_trylock(struct semaphore *sem)
{
	unsigned long flags;
	int count;

	raw_spin_lock_irqsave(&sem->lock, flags);
	count = sem->count - 1;
	if (likely(count >= 0))
		sem->count = count;
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return (count < 0);
}

2.3 释放信号量

释放信号量并唤醒等待线程:

c 复制代码
static noinline void __sched __up(struct semaphore *sem)
{
	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); /* 唤醒对应任务 */
}
相关推荐
AlfredZhao19 小时前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80
戴为沐2 天前
Linux内存扩容指南
linux
zylyehuo2 天前
Linux 彻底且安全地删除文件
linux
用户805533698033 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
用户034095297913 天前
linux fcitx 5 雾凇拼音 设置在中文输入法下仍然输入英文标点
linux
Web3探索者5 天前
可视化服务器管理和传统命令行区别是什么?新手教程:Linux 运维到底该用图形界面还是 SSH 命令行?
linux·ssh
zylyehuo5 天前
Linux系统中网线与USB网络共享冲突
linux
Sokach10156 天前
Linux Shell 脚本从零到能用:一个新手的一天学习总结
linux
AlfredZhao7 天前
Docker 容器时区不对,`timedatectl` 不存在怎么办?
linux·timezone
zzzzzz3108 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql