Linux信号量、自旋锁与内存屏障

先看一段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而后执行后续逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值