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); /* 唤醒对应任务 */
}
相关推荐
Lonely 净土4 小时前
Linux 运维文件写入操作
linux·运维·服务器·文件管理
吠品7 小时前
Wine 在 Linux 上运行 Windows 软件完整指南
java·linux·服务器
深念Y9 小时前
从 Windows 迁移到 Linux 开发环境的记录
linux·windows·链接·bun·ram·imdisk
知无不研10 小时前
Linux I/O复用之epoll
linux·服务器·epoll·socket编程
fb_1234511 小时前
Linux运维封神|SELinux详解+CentOS7启动原理+Root密码重置+开机故障排查实战
linux·运维·服务器
苏宸啊11 小时前
linux网络编程udp服务器和客户端代码编写(echo版本和字典版本)
linux·网络
~|Bernard|11 小时前
Linux 定时任务(Cron)完整教程
linux·运维·服务器
小雪崩12 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
深念Y12 小时前
linux桌面快捷方式开关脚本编写中的几个常见坑
linux·运维·服务器·快捷方式·桌面·kde
奇树谦13 小时前
HDD 为什么适合顺序大文件读写:从 RAID 5 到 RAID 50 的原理与性能分析
linux·运维·网络