自旋锁&信号量

1. 自旋锁

    Linux内核中最常见的锁是自旋锁。一个自旋锁就是一个互斥设备,它只能有两个值:"锁定"和"解锁"。如果锁可用,则"锁定"位被设置,而代码继续进入临界区;相反,如果锁被其他进程争用,则代码进入忙循环并重复检查这个锁,直到锁可用为止。这个循环就是自旋锁的"自旋"。自旋锁最多只能被一个可执行的线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用。注意,同一个锁可以用在多个位置。缺点:一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间)。所以,自旋锁不应该被长时间持有。当然,可以采用另外的方式处理对锁的争用:让请求线程睡眠,直到锁重新可用时在唤醒它。但是,这里有两次明显的上下文切换,被阻塞的线程要换入或换出。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时。

    注意:

    1) 如果禁止内核被抢占,那么在编译时自旋锁会被完成剔除出内核。

    2) Linux内核实现的自旋锁是不可递归的。小心自加锁。

    3) 调试自旋锁, 加上配置选项CONFIG_DEBUG_SPINLOCK。

    4)自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠),在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则中断处理程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。

    5) 加锁是对数据不是对代码。

    6) 所有自旋锁的等待在本质上都是不可中断的。

    1.1. 自旋锁API介绍自旋锁实现与体系结构密切相关,定义在<linux/spinlock.h>中。在编译时对自旋锁的初始化:

    spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

    或者在运行时:

    void spin_lock_init(spinlock_t *lock);

    进入临界区:

    spin_lock(&my_lock);

    /*访问数据*/

    spin_unlock(&my_lock);

    内核提供禁止中断同时请求锁的接口:

    spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

    unsigned long flags;

    spin_lock_irqsave(&my_lock, flags);

    /*访问数据*/

    spin_unlock_irqrestore(&my_lock, flags);函数spin_lock_irqsave保存了中断的当前状态,并禁止了本地中断,然后在获取指定的锁;函数spin_unlock_irqrestore对指定的锁解锁,然后让中断恢复到加锁前的状态。

    内核提供的自旋锁的接口:

    void spin_lock_irq (spinlock_t *lock);

    void spin_unlock_irq (spinlock_t *lock);

    void spin_lock_bh (spinlock_t *lock);

    void spin_unlock_bh (spinlock_t *lock);

    如果能够确保没有任何其他代码禁止本地处理器的中断,也就是说,能够确保在释放自旋锁时应该启用中断,这可以使用spin_lock_irq函数,而无需跟踪标志。函数spin_lock_bh在获得锁之前禁止软件中断,但是会让硬件中断保持打开。

    int spin_trylock (spinlock_t *lock);

    int spin_trylock_bh (spinlock_t *lock);

    这两个函数是非阻塞性的,成功获取自旋锁返回非零值,否则返回零。

因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区,这就 为多处理器提供了防止并发访问所需的保护机制,但是在单处理器上,编译的时候不会加入自旋锁。它仅仅被当作一个设置内核抢占机制是否被启用的开关。注意, Linux内核实现的自旋锁是不可递归的,这一点不同于自旋锁在其他操作系统中的实现,如果你想得到一个你正持有的锁,你必须自旋,等待你自己释放这个 锁,但是你处于自旋忙等待中,所以永远没有机会释放锁,于是你就被自己锁死了,一定要注意!

自旋锁可以用在中断处理程序中,但是在使用时一定要在获取锁之前,首先禁止本地 中断(当前处理器上的中断),否则中断处理程序就可能打断正持有锁的内核代码,有可能会试图支争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自 旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕之前不可能运行,这就会造成双重请求死锁。

自旋锁与下半部:由于下半部(中断程序下半部)可以抢占进程上下文中的代码,所 以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半 部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。对于软中断,无论是否同种类型,如果数据被软中断共享,那么它 必须得到锁的保护,因为同种类型的两个软中断也可以同进运行在一个系统的多个处理器上。但是,同一个处理器上的一个软中断绝不会抢占另一个软中断,因此, 根本不需要禁止下半部。

 

1.3. 读-写自旋锁当对某个数据结构的操作可以清晰化为读/写两种类型时,读/写自旋锁就派上了用场。一个或多个读任务可以并发的持有读锁;相反,写锁只能被一个写任务所持有,而且此时不能有并发的读操作。多个读任务可以安全地获得同一个读锁,事实上,即使一个线程递归地获取同一读锁也是安全的。

    初始化读/写锁:

    rwlock_t my_rwlock=RW_LOCK_UNLOCKED;

    在读任务的代码分支上:

    read_lock(&my_rwlock);

    /*读数据*/

    read_unlock(&my_rwlock);

    在写任务的代码分支上:

    write_lock(&my_rwlock);

    /*读数据*/

    write_unlock(&my_rwlock);

    注意:不能把一个读锁升级为一个写锁:

    read_lock(&my_rwlock);

    write_lock(&my_rwlock);

    这将会带来死锁,因为写锁会不断自旋,等待所有的读锁被释放,其中包括自己。

    内核实现的读锁和写锁接口在<linux/spinlock.h>中有定义。

    在使用Linux读-写自旋锁时,需要考虑一点事这种机制照顾读比照顾写要多一点。当读锁被持有时,写操作为了互斥访问只能等待,但是读任务却可以继续成功的占用读锁,而且自旋等待的写任务在所有读任务释放锁之前是无法获得写锁的。

2. 信号量

    Linux中的信号量是一种睡眠锁,如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。这就比自旋锁提供更好的处理器利用率,因为把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销。

    以下是信号量的睡眠特性:

    1) 由于争用信号量的进程在等待锁重新变得可用时会睡眠,所以信号量适合用于锁被长时间持有的情况

    2)因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间还有长

    3) 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,在中断上下文中是不能进行调度的4) 持有信号量的进程可以睡眠,因为其他进程试图获取同一信号量锁时不会因此而死锁,而持有信号量的进程最终会继续执行的5) 持有信号量的进程不能占用自旋锁。因为等待信号量时会睡眠,而在持有自旋锁时是不允许睡眠的往往在需要和用户空间同步时,你的代码会需要睡眠,此时使用信号量是唯一的选择。信号量不同于自旋锁,它不会禁止内核抢占,所以持有信号量的代码可以被抢占。这就意味着信号量不会对调度的等待时间带来负面影响。

    信号量的一个重要特性,它可以同时允许任意数量的所持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量实在声明信号量时指定,这个值称为使用者数量。通常情况下,信号量和自旋锁一样,在一个时刻只允许一个持有者。这时计数等于1,这样的信号量称为二值信号量或互斥信号量。

    信号量支持两个原子操作P()和V(),这两个名字来之荷兰语Proberen(探查)和Vershogen(增加)。后来系统把这两种操作称为down()和up()。down操作是对信号量计数减一来请求获取一个信号量。如果结果是0或大于0,获得信号量锁,任务就进入临界区。如果是负数,任务就会被放入等待队列,处理器执行其它任务。up操作是释放信号量,增加信号量的计数值。

    2.1. 创建和初始化信号量信号量的实现与体系有关,具体实现 (在<asm/semaphore.h>中定义)。

    静态声明信号量:

    static DECLARE_SEMAPHORE(name,count);

    互斥信号量的静态声明:

    static DECLARE_MUTEX(name);

    运行时动态初始化信号量:

    sema_init(sem,count);

    init_MUTEX(sem);

    2.2. 使用信号量函数down_interruptible()试图获取指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态进入睡眠,也就是说,该任务可以被信号唤醒。如果进程在等待获取信号量的时候接受到了信号,那么该进程就会被唤醒。而函数down_ interruptible()会返回-EINTR。

    函数down()会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,进程在等待信号量的时候就不再响应信号了。

    函数down_trylock()尝试以阻塞方式来获取指定的信号量。在信号量已被占用时,他立刻返回非0值,否则返回0。

    要释放指定的信号量,需要调用up()函数。

    实例如下:

    static DECLARE_MUTEX(my_sem);

    if(down_interruptible(&my_sem))

    {

    /* 信号被接受,信号量还未获取 */

    }

    /*访问共享数据*/

    up(my_sem);

    2.3. 读-写信号量读-写信号量在内核中是由rw_semaphore结构表示的,定义在<linux/rwsem.h>中。

    静态声明读-写信号量:

    static DECLARE_RWSEM(name);

    运行时动态初始化读-写信号量:

    init_rwsem(struct rw_semaphore *sem);

    所有的读-写信号量都是互斥信号量。只要没有写者,并发持有读锁的读者数不限。只有唯一的写者可以获取写锁。所有读-写锁的睡眠都不会被信号打断,所以他只有一个版本down操作。

    实例如下:

    static DECLARE_ RWSEM (my_rwsem);

    down_read(&my_rwsem);

    /*访问共享数据*/

    up_read(my_sem);

    down_write(&my_rwsem);

    /*访问共享数据*/

    up_write(my_sem);

    与标准信号量一样,读-写信号量耶提供了down_read_trylock()和down_write_trylock()方法。读-写信号量比读-写自旋锁多一种特有的操作:downgrade_writer()。这个函数可以动态地将获取的写锁转为读锁。

    2.4. 自旋锁与信号量

   在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用信号量。比较如下:

建议的加锁方法

低开销加锁

优先使用自旋锁

短期锁定

优先使用自旋锁

长期锁定

优先使用信号量

中断上下文中加锁

使用自旋锁

持有锁需要睡眠

使用信号量

posted on 2014-03-13 11:31  程良  阅读(1128)  评论(0编辑  收藏  举报

导航