先看一段Linux信号量down的实现:
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
raw_spin_lock_irqsave(&sem->lock, flags);//获取spinlock并关本地中断来保护count数据。
if (likely(sem->count > 0))//如果大于0则表明当前进程可以成功获取信号量。
sem->count--;
else
result = __down_interruptible(sem);//获取失败,等待。
raw_spin_unlock_irqrestore(&sem->lock, flags);//恢复中断寄存器,打开本地中断,并释放spinlock。
return result;
}
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 task_struct *task = current; //得到当前进程结构
struct semaphore_waiter waiter;//struct semaphore_waiter数据结构用于描述获取信号量失败的进程,每个进程会有一个semaphore_waiter数据结构,并把当前进程放到信号量sem的成员变量wait_list链表中。
list_add_tail(&waiter.list, &sem->wait_list); //将waiter加入到信号量sem->waiter_list尾部
waiter.task = task;//waiter.task指向当前正在运行的进程。
waiter.up = false;
for (;;) {
if (signal_pending_state(state, task))//根据不同state和当前信号pending情况,决定是否进入interrupted处理。
goto interrupted;
if (unlikely(timeout <= 0)) //timeout设置错误
goto timed_out;
__set_task_state(task, state); //设置当前进程task->state。
raw_spin_unlock_irq(&sem->lock); //下面即将睡眠,这里释放了spinlock锁,和down()中的获取spinlock锁对应。
timeout = schedule_timeout(timeout); //主动让出CPU,相当于当前进程睡眠。
raw_spin_lock_irq(&sem->lock); //重新获取spinlock锁,在down()会重新释放锁。这里保证了schedule_timeout()不在spinlock环境中。
if (waiter.up) //waiter.up为true时,说明睡眠在waiter_list队列中的进程被该信号量的up操作唤醒。
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
这个书上也能查到。大意是进程A想要获取信号量,假如没有其他进程已经占有信号量,那么顺理成章地获取sem->count并且减一表示现在资源数减一了。假如已经有其他进程占有信号量,于是sem->count<=0,进入else分支走的函数调用链为:
__down_interruptible
__down_common
在__down_common内部,当前进程A首先释放自选锁raw_spin_unlock_irq,然后被加入到等待队列后让出cpu进入睡眠。此后其他进程B让出了信号量,执行了sem->count加一,waiter.up被设置为true并且唤醒了进程A。于是进程A从schedule_timeout返回,又再次对sem->lock加自选锁raw_spin_lock_irq,并且因为waiter.up为true而从__down_common返回到__down_interruptible,继而返回到down_interruptible。此时问题来了!
你觉得代码是否从result = __down_interruptible(sem)后,就直接从down_interruptible退出了是嘛?错了!虽然down_interruptible没有显式循环在,但实际上因为__down_common内重新对sem->lock加自选锁,现代码会重新执行if (likely(sem->count > 0)),进而对信号量进程减一!
这与我们的常识代码是顺序执行的不符。我也是将此问题反复反刍给deepSeek后才了解原理:
#define raw_spin_lock_irq(lock) \
do { \
local_irq_disable(); \ // 关本地中断
preempt_disable(); \ // 关闭抢占
arch_spin_lock(&(lock)->raw_lock); \ // 架构相关的自旋锁获取
__acquire(lock); \ // 插入Acquire语义内存屏障
} while (0)
关键在于这个Acquire屏障。__acquire(lock)
宏通过barrier()
或编译器指令(如asm volatile("" ::: "memory")
)插入Acquire屏障,确保临界区内的内存访问(如sem->count
)不会被重排到锁获取之前。于是只要加自旋锁,一个代码的隐式循环就出现了!
这样就能保证任何进程看到的sem->count总是最新的,总会先判断一下其是否>0而后执行后续逻辑。