各种spinlock形式及使用条件

要澄清的是,互斥手段的选择,不是根据临界区的大小,而是根据临界区的性质,以及
有哪些部分的代码,即哪些内核执行路径来争夺。

从严格意义上说,semaphore和spinlock_XXX属于不同层次的互斥手段,前者的
实现有赖于后者,这有点象HTTP和TCP的关系,都是协议,但层次是不同的。

先说semaphore,它是进程级的,用于多个进程之间对资源的互斥,虽然也是在
内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果
竞争不上,会有context switch,进程可以去sleep,但CPU不会停,会接着运行
其他的执行路径。从概念上说,这和单CPU或多CPU没有直接的关系,只是在
semaphore本身的实现上,为了保证semaphore结构存取的原子性,在多CPU中需要
spinlock来互斥。

在内核中,更多的是要保持内核各个执行路径之间的数据访问互斥,这是最基本的
互斥问题,即保持数据修改的原子性。semaphore的实现,也要依赖这个。在单CPU
中,主要是中断和bottom_half的问题,因此,开关中断就可以了。在多CPU中,
又加上了其他CPU的干扰,因此需要spinlock来帮助。这两个部分结合起来,
就形成了spinlock_XXX。它的特点是,一旦CPU进入了spinlock_XXX,它就不会
干别的,而是一直空转,直到锁定成功为止。因此,这就决定了被
spinlock_XXX锁住的临界区不能停,更不能context switch,要存取完数据后赶快
出来,以便其他的在空转的执行路径能够获得spinlock。这也是spinlock的原则
所在。如果当前执行路径一定要进行context switch,那就要在schedule()之前
释放spinlock,否则,容易死锁。因为在中断和bh中,没有context,无法进行
context switch,只能空转等待spinlock,你context switch走了,谁知道猴年
马月才能回来。

因为spinlock的原意和目的就是保证数据修改的原子性,因此也没有理由在spinlock
锁住的临界区中停留。

spinlock_XXX有很多形式,有


  spin_lock()/spin_unlock(),
  spin_lock_irq()/spin_unlock_irq(),
  spin_lock_irqsave/spin_unlock_irqrestore()
  spin_lock_bh()/spin_unlock_bh()

  local_irq_disable/local_irq_enable
  local_bh_disable/local_bh_enable




那么,在什么情况下具体用哪个呢?这要看是在什么内核执行路径中,以及要与哪些内核
执行路径相互斥。我们知道,内核中的执行路径主要有:

用户进程的内核态,此时有进程context,主要是代表进程在执行系统调用
    等。
中断或者异常或者自陷等,从概念上说,此时没有进程context,不能进行
    context switch。
bottom_half,从概念上说,此时也没有进程context。
同时,相同的执行路径还可能在其他的CPU上运行。



这样,考虑这四个方面的因素,通过判断我们要互斥的数据会被这四个因素中
的哪几个来存取,就可以决定具体使用哪种形式的spinlock。

如果只要和其他CPU互斥,就要用spin_lock/spin_unlock,

如果要和irq及其他CPU互斥,就要用spin_lock_irq/spin_unlock_irq,

如果既要和irq及其他CPU互斥,又要保存EFLAG的状态,就要用spin_lock_irqsave/spin_unlock_irqrestore,

如果要和bh及其他CPU互斥,就要用spin_lock_bh/spin_unlock_bh,

如果不需要和其他CPU互斥,只要和irq互斥,则用local_irq_disable/local_irq_enable,
如果不需要和其他CPU互斥,只要和bh互斥,则用local_bh_disable/local_bh_enable,
等等。

值得指出的是,对同一个数据的互斥,在不同的内核执行路径中,
所用的形式有可能不同(见下面的例子)。

举一个例子。在中断部分中有一个irq_desc_t类型的结构数组变量irq_desc[],
该数组每个成员对应一个irq的描述结构,里面有该irq的响应函数等。
在irq_desc_t结构中有一个spinlock,用来保证存取(修改)的互斥。

对于具体一个irq成员,irq_desc[irq],对其存取的内核执行路径有两个,一是
在设置该irq的响应函数时(setup_irq),这通常发生在module的初始化阶段,或
系统的初始化阶段;二是在中断响应函数中(do_IRQ)。代码如下:


int setup_irq(unsigned int irq, struct irqaction * new)
{
        int shared = 0;
        unsigned long flags;
        struct irqaction *old, **p;
        irq_desc_t *desc = irq_desc + irq;

       
        if (new->flags & SA_SAMPLE_RANDOM) {
               
                rand_initialize_irq(irq);
        }

       
[1]     spin_lock_irqsave(&desc->lock,flags);
        p = &desc->action;
        if ((old = *p) != NULL) {
               
                if (!(old->flags & new->flags & SA_SHIRQ)) {
[2]                     spin_unlock_irqrestore(&desc->lock,flags);
                        return -EBUSY;
                }

               
                do {
                        p = &old->next;
                        old = *p;
                } while (old);
                shared = 1;
        }

        *p = new;

        if (!shared) {
                desc->depth = 0;
                desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT |IRQ_WAITING);
                desc->handler->startup(irq);
        }
[3]     spin_unlock_irqrestore(&desc->lock,flags);

        register_irq_proc(irq);
        return 0;
}

asmlinkage unsigned int do_IRQ(struct pt_regs regs)
      
       
        int irq = regs.orig_eax & 0xff;
        int cpu = smp_processor_id();
        irq_desc_t *desc = irq_desc + irq;
        struct irqaction * action;
        unsigned int status;

        kstat.irqs[cpu][irq]++;
[4]     spin_lock(&desc->lock);
        desc->handler->ack(irq);
       
        status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
        status |= IRQ_PENDING;

       
        action = NULL;
        if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
                action = desc->action;
                status &= ~IRQ_PENDING;
                status |= IRQ_INPROGRESS;
        }
        desc->status = status;

       
        if (!action)
                goto out;

       
        for (;;) {
[5]             spin_unlock(&desc->lock);
                handle_IRQ_event(irq, &regs, action);
[6]             spin_lock(&desc->lock);
               
                if (!(desc->status & IRQ_PENDING))
                        break;
                desc->status &= ~IRQ_PENDING;
        }
        desc->status &= ~IRQ_INPROGRESS;
out:
       
        desc->handler->end(irq);
[7]     spin_unlock(&desc->lock);

        if (softirq_pending(cpu))
                do_softirq();
        return 1;
}



在setup_irq()中,因为其他CPU可能同时在运行setup_irq(),或者在运行setup_irq()时,
本地irq中断来了,要执行do_IRQ()以修改desc->status。为了同时防止来自其他CPU和
本地irq中断的干扰,如[1][2][3]处所示,使用了spin_lock_irqsave/spin_unlock_irqrestore()


而在do_IRQ()中,因为do_IRQ()本身是在中断中,而且此时还没有开中断,本CPU中没有
什么可以中断其运行,其他CPU则有可能在运行setup_irq(),或者也在中断中,但这二者
对本地do_IRQ()的影响没有区别,都是来自其他CPU的干扰,因此只需要用spin_lock/spin_unlock

如[4][5][6][7]处所示。值得注意的是[5]处,先释放该spinlock,再调用具体的响应函数。

再举个例子:


static void tasklet_hi_action(struct softirq_action *a)
{
        int cpu = smp_processor_id();
        struct tasklet_struct *list;

[8]     local_irq_disable();
        list = tasklet_hi_vec[cpu].list;
        tasklet_hi_vec[cpu].list = NULL;
[9]     local_irq_enable();

        while (list) {
                struct tasklet_struct *t = list;

                list = list->next;

                if (tasklet_trylock(t)) {
                        if (!atomic_read(&t->count)) {
                                if (!test_and_clear_bit(TASKLET_STATE_SCHED,&t->state))
                                        BUG();
                                t->func(t->data);
                                tasklet_unlock(t);
                                continue;
                        }
                        tasklet_unlock(t);
                }

[10]            local_irq_disable();
                t->next = tasklet_hi_vec[cpu].list;
                tasklet_hi_vec[cpu].list = t;
                __cpu_raise_softirq(cpu, HI_SOFTIRQ);
[11]            local_irq_enable();
        }
}



这里,对tasklet_hi_vec[cpu]的修改,不存在CPU之间的竞争,因为每个CPU有各自独立的数据,
所以只要防止irq的干扰,用local_irq_disable/local_irq_enable即可,如[8][9][10][11]处
所示。

Q:
大侠,你好。
在文章里你写到“如果不需要和其他CPU互斥,只要和bh互斥,则用local_bh_disable/local_bh_enable,”。
不知道如果要系统调用和bh互斥,在系统调用中用local_bh_disable/local_bh_enable,那在bh中用什么呢?

A:

如果你确信数据只会被系统调用和BH修改,那么,在系统调用
中应该用spin_lock_bh/spin_unlock_bh,
在BH中用spin_lock/
spin_unlock。原因如下:

1.在系统调用中,因为同时在其他CPU中可能也在执行系统调用
或BH,因此要用spin_lock_ 前缀;在本CPU中,由于随时可能
有中断,而中断结束时会运行BH,所以要用_bh后缀。合在一起
就是spin_lock_bh/spin_unlock_bh。

2.在BH中,同样要防止外CPU的系统调用和BH,因此,
spin_lock_ 前缀是一定要的,对于本CPU,只有中断可以打断
BH的运行,而你又确信中断处理不形成竞争关系,所以,他强
任他强,可以不管他。又因为在一个CPU上,BH是不会重入的,
所以,不需要后缀;合起来,就是spin_lock/spin_unlock。


总结一下,说白了,就是回答两个问题,一,你是谁?即你
当前在哪个内核执行路径中?二,你要防谁?即你要防止哪
几个内核执行路径的干扰?对号入座可以矣。

zhrank says:
>>需要澄清的是,互斥手段的选择,不是根据临界区的大小,而是根据临界区的性质,以及
>>有哪些部分的代码,即哪些内核执行路径来争夺。

我觉得不完全正确,互斥手段的选择, 应该是根据临界区的大小, 临界区的性质以及竞争临界区
的执行路径的数量这三个因数来同时决定.

首先, 如果竞争临界区的执行路径中存在interrupt handler的话, 那么只能选用spinlock
且本地关中断的方法.

其次, 因为semaphore会导致进程上下文切换, 因此如果临界区也就一两百条指令, 也即属于
短期互斥, 且竞争临界区的执行路径数量不多, 那么选用spinlock反而会比用semaphore的
性能要好. 因为上下文切换本身就是一个很大的开销, 另外, 上下文切换后会使得cpu cache
出险大量的cache miss. 从而使系统吞吐量下降.

 

 

最近在内核频繁使用了自旋锁,自旋锁如果使用不当,极易引起死锁,在此总结一下。

自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。

自旋锁最初是为了在多处理器系统(SMP)使用而设计的,但是只要考虑到并发问题,单处理器在运行可抢占内核时其行为就类似于SMP。因此,自旋锁对于SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。

自旋锁有几个重要的特性:1、被自旋锁保护的临界区代码执行时不能进入休眠。2、被自旋锁保护的临界区代码执行时是不能被被其他中断中断。3、被自旋锁保护的临界区代码执行时,内核不能被抢占。从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器

考虑上面第一种情况,想象你的内核代码请求到一个自旋锁并且在它的临界区里做它的事情,在中间某处,你的代码失去了处理器。或许它已调用了一个函数(copy_from_user,假设)使进程进入睡眠。也或许,内核抢占发威,一个更高优先级的进程将你的代码推到了一边。此时,正好某个别的线程想获取同一个锁,如果这个线程运行在和你的内核代码不同的处理器上(幸运的情况),那么它可能要自旋等待一段时间(可能很长),当你的代码从休眠中唤醒或者重新得到处理器并释放锁,它就能得到锁。而最坏的情况是,那个想获取锁得线程刚好和你的代码运行在同一个处理器上,这时它将一直持有CPU进行自旋操作,而你的代码是永远不可能有任何机会来获得CPU释放这个锁了,这就是悲催的死锁

考虑上面第二种情况,和上面第一种情况类似。假设我们的驱动程序正在运行,并且已经获取了一个自旋锁,这个锁控制着对设备的访问。在拥有这个锁得时候,设备产生了一个中断,它导致中断处理例程被调用,而中断处理例程在访问设备之前,也要获得这个锁。当中断处理例程和我们的驱动程序代码在同一个处理器上运行时,由于中断处理例程持有CPU不断自旋,我们的代码将得不到机会释放锁,这也将导致死锁。

因此,如果我们有一个自旋锁,它可以被运行在(硬件或软件)中断上下文中的代码获得,则必须使用某个禁用中断的spin_lock形式的锁来禁用本地中断(注意,只是禁用本地CPU的中断,不能禁用别的处理器的中断),使用其他的锁定函数迟早会导致系统死锁(导致死锁的时间可能不定,但是发生上述死锁情况的概率肯定是有的,看处理器怎么调度了)。如果我们不会在硬中断处理例程中访问自旋锁,但可能在软中断(例如,以tasklet的形式运行的代码)中访问,则应该使用spin_lock_bh,以便在安全避免死锁的同时还能服务硬件中断。

补充:

锁定一个自旋锁的函数有四个:

void spin_lock(spinlock_t *lock);      

最基本得自旋锁函数,它不失效本地中断。

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

在获得自旋锁之前禁用硬中断(只在本地处理器上),而先前的中断状态保存在flags中

void spin_lockirq(spinlock_t *lock);

在获得自旋锁之前禁用硬中断(只在本地处理器上),不保存中断状态

void spin_lock_bh(spinlock_t *lock);

在获得锁前禁用软中断,保持硬中断打开状态

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值