linux内核重入信号量,Linux内核之自旋锁和信号量

Linux内核实现了多种同步方法,指令级支持的原子操作、自旋锁、信号量、互斥锁、完成量、大内核锁等等,我就挑比较有代表性的两个锁——自旋锁和信号量来分析。

自旋锁

Linux内核中最常用的锁就是自旋锁(spin lock),自旋锁最多只能被一个执行线程持有。如果一个执行线程试图获得一个被已经持有(即所谓争用)的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用。在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区。自旋锁的实现和体系结构密切相关,代码往往通过汇编实现。

#define __LOCK(lock) \

do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

#define __LOCK_BH(lock) \

do { local_bh_disable(); __LOCK(lock); } while (0)

#define __LOCK_IRQ(lock) \

do { local_irq_disable(); __LOCK(lock); } while (0)

#define __LOCK_IRQSAVE(lock, flags) \

do { local_irq_save(flags); __LOCK(lock); } while (0)

#define __UNLOCK(lock) \

do { preempt_enable(); __release(lock); (void)(lock); } while (0)

#define __UNLOCK_BH(lock) \

do { preempt_enable_no_resched(); local_bh_enable(); __release(lock); (void)(lock); } while (0)

#define __UNLOCK_IRQ(lock) \

do { local_irq_enable(); __UNLOCK(lock); } while (0)

#define __UNLOCK_IRQRESTORE(lock, flags) \

do { local_irq_restore(flags); __UNLOCK(lock); } while (0)

#define _spin_lock(lock)__LOCK(lock)

#define _spin_lock_nested(lock, subclass)__LOCK(lock)

#define _read_lock(lock)__LOCK(lock)

#define _write_lock(lock)__LOCK(lock)

#define _spin_lock_bh(lock)__LOCK_BH(lock)

#define _read_lock_bh(lock)__LOCK_BH(lock)

#define _write_lock_bh(lock)__LOCK_BH(lock)

#define _spin_lock_irq(lock)__LOCK_IRQ(lock)

#define _read_lock_irq(lock)__LOCK_IRQ(lock)

#define _write_lock_irq(lock)__LOCK_IRQ(lock)

#define _spin_lock_irqsave(lock, flags)__LOCK_IRQSAVE(lock, flags)

#define _read_lock_irqsave(lock, flags)__LOCK_IRQSAVE(lock, flags)

#define _write_lock_irqsave(lock, flags)__LOCK_IRQSAVE(lock, flags)

#define _spin_trylock(lock)({ __LOCK(lock); 1; })

#define _read_trylock(lock)({ __LOCK(lock); 1; })

#define _write_trylock(lock)({ __LOCK(lock); 1; })

#define _spin_trylock_bh(lock)({ __LOCK_BH(lock); 1; })

#define _spin_unlock(lock)__UNLOCK(lock)

#define _read_unlock(lock)__UNLOCK(lock)

#define _write_unlock(lock)__UNLOCK(lock)

#define _spin_unlock_bh(lock)__UNLOCK_BH(lock)

#define _write_unlock_bh(lock)__UNLOCK_BH(lock)

#define _read_unlock_bh(lock)__UNLOCK_BH(lock)

#define _spin_unlock_irq(lock)__UNLOCK_IRQ(lock)

#define _read_unlock_irq(lock)__UNLOCK_IRQ(lock)

#define _write_unlock_irq(lock)__UNLOCK_IRQ(lock)

#define _spin_unlock_irqrestore(lock, flags)__UNLOCK_IRQRESTORE(lock, flags)

#define _read_unlock_irqrestore(lock, flags)__UNLOCK_IRQRESTORE(lock, flags)

#define _write_unlock_irqrestore(lock, flags)__UNLOCK_IRQRESTORE(lock, flags)

在spinlock_api_up.h文件中定义了所有的自旋锁函数的实现,基本方式就是禁止中断->禁止抢占->获得锁,spin_lock这个函数没有禁止中断,因为它主要用于中断处理程序中保护临界区,中断处理程序本身就是禁止中断的。

因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区内,这就是为多处理机器提供了防止并发访问所需的保护机制。注意在单处理器机器上,编译的时候并不会加入自旋锁。它仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。

关于自旋锁的使用注意

1.自旋锁不可递归

Linux内核实现的自旋锁是不可递归的,这点不同于自旋锁在其他操作系统的实现,所以如果你试图得到一个你正在持有的锁,你必须自旋,等待你自己释放这个锁。但是你处于自旋忙等待中,你永远没有机会释放锁,内核就崩了。

2.中断重入会导致死锁

在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则,中断处理程序就会打断正在持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕前不可能运行,所以就造成了死锁。这里要注意一点,需要关闭的只是本地中断(当前处理器)。如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同处理器上)最终释放锁。

3.持有自旋锁时不可睡眠

持有自旋锁后如果进程睡眠了,那么就不知道何时才能重新被唤醒,此时如果还有另外的进程要获得这个自旋锁,那它就会一直忙等,非常浪费处理器资源

信号量

不同于自旋锁,Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列的那个任务将被唤醒,并获得该信号量。

接下来看看信号量是怎么实现的

void down(struct semaphore *sem)

{

unsigned long flags;

spin_lock_irqsave(&sem->lock, flags);

if (likely(sem->count > 0))

sem->count--;

else

__down(sem);

spin_unlock_irqrestore(&sem->lock, flags);

}

static noinline void __sched __down(struct semaphore *sem)

{

__down_common(sem, TASK_UNINTERRUPTIBLE, 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;

//把当前进程加入信号量的等待队列

list_add_tail(&waiter.list, &sem->wait_list);

waiter.task = task;

waiter.up = 0;

for (;;) {

//如果有信号要处理,那就当什么都没发生,直接退出

if (state == TASK_INTERRUPTIBLE && signal_pending(task))

goto interrupted;

//如果当前进程收到了SIGKILL信号,代表这个进程要被杀死了,所以也啥都不干

if (state == TASK_KILLABLE && fatal_signal_pending(task))

goto interrupted;

if (timeout <= 0)

goto timed_out;

__set_task_state(task, state);

//调度之前一定要释放自旋锁

spin_unlock_irq(&sem->lock);

//延迟调度,如果timeout=MAX_SCHEDULE_TIMEOUT,和直接调动schedule函数没区别

timeout = schedule_timeout(timeout);

//重新拿到锁

spin_lock_irq(&sem->lock);

//如果是由up函数唤醒的返回正常,如果是用户空间的信号所中断或超时信号所引起的唤醒

//那就接着执行,返回错误

if (waiter.up)

return 0;

}

timed_out:

list_del(&waiter.list);

return -ETIME;

interrupted:

list_del(&waiter.list);

return -EINTR;

}

void up(struct semaphore *sem)

{

unsigned long flags;

spin_lock_irqsave(&sem->lock, flags);

if (likely(list_empty(&sem->wait_list)))

sem->count++;

else

__up(sem);

spin_unlock_irqrestore(&sem->lock, flags);

}

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

//表明是该进程是up函数唤醒

waiter->up = 1;

//唤醒进程

wake_up_process(waiter->task);

}

从上面的代码可以看到,信号量也是依赖于自旋锁的。

信号量分为计数信号量和二值信号量(也叫互斥信号量),计数信号量不能用来进行强制互斥,因为它允许多个执行线程同时访问临界区。相反,这种信号量用来对特定代码加以限制,内核使用它的机会不多,基本用到的都是互斥信号量(计数等于1的信号量),除互斥外,信号量还能进行进程同步,把计数初始化为0就可以实现进程间的同步。

再来说说自旋锁和信号量使用场景上的区别

自旋锁适用于较短时间持有锁,信号量适用于较长时间持有锁

自旋锁是忙等,而信号量是睡眠

自旋锁可用于进程上下文和中断上下文,而信号量只能用于进程上下文

自旋锁禁止内核抢占,信号量不是

在你占用信号量的同时不能占用自旋锁。因为在你等待信号量时可能会睡眠,而在持有自旋锁时时不允许睡眠的

自旋锁不会发生上下文切换,而信号量会(我猜这就是为什么Linux内核用的最多的是自旋锁,而不是信号量的原因)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值