信号量

来源:互联网 发布:美菱冰箱 知乎 编辑:程序博客网 时间:2024/06/09 20:02

内核同步措施

为了避免并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。 我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别。

Linux 使用的同步机制可以说从2.0到2.6以来不断发展完善。从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随 Linux从单处理器到对称多处理器的过度;伴随着从非抢占内核到抢占内核的过度。锁机制越来越有效,也越来越复杂。

目前来说内核中原子操作多用来做计数使用,其它情况最常用的是两种锁以及它们的变种:一个是自旋锁,另一个是信号量。我们下面就来着重介绍一下这两种锁机制。

自旋锁

自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。

自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。

自旋锁的基本形式如下:
    spin_lock(&mr_lock);
    //临界区
    spin_unlock(&mr_lock);

    因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
    简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。
    死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了。


信号量
    Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
    信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。

信号量基本使用形式为:
static DECLARE_MUTEX(mr_sem);//声明互斥信号量
if(down_interruptible(&mr_sem))
    //可被中断的睡眠,当信号来到,睡眠的任务被唤醒
    //临界区
up(&mr_sem);


信号量和自旋锁区别
    虽然听起来两者之间的使用条件复杂,其实在实际使用中信号量和自旋锁并不易混淆。注意以下原则:
    如果代码需要睡眠——这往往是发生在和用户空间同步时——使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更加简单一些。如果需要在自旋锁和信号量中作选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选择。另外,信号量不同于自旋锁,它不会关闭内核抢占,所以持有信号量的代码可以被抢占。这意味者信号量不会对影响调度反应时间带来负面影响。

自旋锁对信号量

需求                     建议的加锁方法

低开销加锁               优先使用自旋锁
短期锁定                 优先使用自旋锁
长期加锁                 优先使用信号量
中断上下文中加锁          使用自旋锁
持有锁是需要睡眠、调度     使用信号量



------------------

信号量(semaphore)是用于保护临界区的一种常用方法。只有得到信号量的进程才能执行临界区代码,而没有得到信号量的进程进入休眠等待状态。

Linux系统中与信号量相关的操作主要有如下4种。

1 定义信号量

下面代码定义名为sem的信号量。

struct semaphore sem;

struct semaohore结构体在内核中定义如下:

/include/linux/semaphore.h目录下:

struct semaphore{

       spinlock_t              lock;

       unsigned int           count;

       struct list_head       wait_list;

};

2初始化信号量

/include/linux/semaphore.h目录下,void sema_init(struct semaphore*

sem, int val) 函数用于初始化信号量,并设置信号量sem的值为val。尽管信号量可以被初始化为大于1的值从而成为一个计数信号量,但是它通常不被这样使用。

内核定义了两个宏来把sem的值设置为1或者0

#define init_MUTEX(sem)                  sema_init(sem, 1)

#define init_MUTEX_LOCKED(sem)         sema_init(sem, 0)

使用init_MUTEX(sem)初始化信号量时,表示信号量最初是可以被获取的。而使用init_MUTEX_LOCKED(sem)初始化信号量时,此信号量只有先被释放才可以获取。

3获取信号量

void down(struct semaphore *sem);

该函数用于获取信号量sem,它会导致睡眠,因此不能在中断上下文使用。

在内核里该函数的源代码如下:

kernel/semaphore.c文件里:

53 void down(struct semaphore *sem)

 54 {

 55         unsigned long flags;

 56

 57         spin_lock_irqsave(&sem->lock, flags);

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

 59                 sem->count--;

 60         else

 61                 __down(sem);

 62         spin_unlock_irqrestore(&sem->lock, flags);

 63 }

这里重点看58行:if (likely(sem->count > 0)),这句话表示当获取信号量成功时,就执行sem->count—;即对信号量的值减一。else表示获取信号量失败,此时调用__down函数进入睡眠状态,并将此进程插入到等待队列尾部。

内核定义了信号量的等待队列结构体:

193 struct semaphore_waiter {

194         struct list_head list;

195         struct task_struct *task;

196         int up;

197 };

此结构体是一个双向循环链表。

int down_interruptible(struct semaphore *sem);

该函数功能与down()类似,不同之处是,down()在获取信号量失败进入睡眠状态时的进程是不能被打断的,而down_interruptible()在进入睡眠状态时的进程能被信号打断,信号也会导致函数返回。下面我们也来看一看这个函数的源码:

kernel/semaphore.c文件里:

75 int down_interruptible(struct semaphore *sem)

 76 {

 77         unsigned long flags;

 78         int result = 0;

 79

 80         spin_lock_irqsave(&sem->lock, flags);

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

 82                 sem->count--;

 83         else

 84                 result = __down_interruptible(sem);

 85         spin_unlock_irqrestore(&sem->lock, flags);

 86

 87         return result;

 88 }

这里我们可以看到,当获取信号量成功时,返回0,而获取信号量失败时,返回一个非0的值。在使用down_interruptible()函数获取信号量时,对返回值一般会进行检查,如果非0,通常立即返回-ERESTARTSYS。如:

if ( down_interruptible(&sem) )

                     return -ERESTARTSYS;

这里还有一个问题:在获取信号量失败后,为什么down不能被中断,而down_interruptible却可以被中断呢?我们从downdown_interruptible的源代码可以得知,在获取信号量失败后,down函数运行了__down函数,而down_interruptible函数运行了__down_interruptible。那么让我们来看一下这两个函数的源码:

kernel/semaphore.c文件里:

236 static noinline void __sched __down(struct semaphore *sem)

237 {

238   __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);

239 }

240

241 static noinline int __sched __down_interruptible(struct semaphore *sem)

242 {

243 return __down_common(sem,TASK_INTERRUPTIBLE,                     MAX_SCHEDULE_TIMEOUT);

244 }

__down函数里,是把进程的状态设置为TASK_UNINTERRUPTIBLE,即不可中断状态。

而在__down_interruptible里,是把进程的状态设置为TASK_INTERRUPTIBLE,即可中断状态。这就解释了以上提出的问题。

4释放信号量

void up(struct semaphore *sem);

该函数用于释放信号量sem,唤醒等待者。

它的源代码如下:

178 void up(struct semaphore *sem)

179 {

180         unsigned long flags;

181

182         spin_lock_irqsave(&sem->lock, flags);

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

184                 sem->count++;

185         else

186                 __up(sem);

187         spin_unlock_irqrestore(&sem->lock, flags);

183行的语句中,up函数首先判断等待队列是否为空,如果是空的话,就执行sem->count++;否则,执行__up()函数,释放掉等待队列尾部的信号量。

信号量用于同步举例:

 

 

   


如果信号量被初始化为0,则它可以用于同步,同步意味着一个执行单元的继续执行需要等待另一个执行单元完成某事,保证执行的先后顺序。

如上图所示,执行单元A执行代码区域a之前,必须等待执行单元B执行完代码区域b后释放信号量给它。

以下模块很好地使用了信号量:

#include <linux/init.h>

#include <linux/module.h>

#include <linux/sched.h>

#include <linux/sem.h>

 

struct semaphore sem1;

struct semaphore sem2;

 

int num[2][5] = {

       {0,2,4,6,8},

       {1,3,5,7,9}

};

int thread_one(void *p);

int thread_two(void *p);

int thread_one(void *p)

{

       int *num = (int *)p;

       int i;

       for(i = 0; i < 5; i++){

              down(&sem1);      //获取信号量1

              printk("%d ", num[i]);

              up(&sem2);    //释放信号量2

       }

       return 0;

}

int thread_two(void *p)

{

       int *num = (int *)p;

       int i;

       for(i = 0; i < 5; i++){

              down(&sem2);             //获取信号量2

              printk("%d ", num[i]);

              up(&sem1);           //释放信号量1

       }

       return 0;

}

static int lan_init(void)

{

       printk("lan is coming\n");

       init_MUTEX(&sem1);  //初始化信号量1,使信号量1最初可被获取

       init_MUTEX_LOCKED(&sem2);  //初始化信号量2,使信号量2只有被释放后才可被获取

       kernel_thread(thread_one, num[0], CLONE_KERNEL);

       kernel_thread(thread_two, num[1], CLONE_KERNEL);

       return 0;

}

static void lan_exit(void)

{

       printk("\nlan exit\n");

}

module_init(lan_init);

module_exit(lan_exit);