原子操作和信号量

原子操作和信号量
2008年10月25日 星期六 13:53


 

POSIX 有信号量,SysV IPC有信号量,核内也有信号量,接口很简单,一个down(),一个up(),分别对应P操作和V操作,down()调用可能引起线程挂起,因此和 sleep_on类似,也有interruptible系列接口。down意味着信号量减1,up意味着信号量加1,这两个操作显然需要互斥。在 Linux 2.4中,并没有如想象中的用锁实现,而是使用了原子操作。

在include/asm/atomic.h中定义了一系列原子操作,包括原子读、原子写、原子加等等,大多是直接用汇编语句来实现的,这里就不详细解释。

我们从信号量数据结构开始,它定义在include/asm/semaphore.h中:

struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
}

down() 操作可以理解为申请资源,up()操作可以理解为释放资源,因此,信号量实际表示的是资源的数量以及是否有进程正在等待。在semaphore结构 中,count相当于资源计数,为正数或0时表示可用资源数,-1则表示没有空闲资源且有等待进程。而等待进程的数量并不关心。这种设计主要是考虑与信号 量的原语相一致,当某个进程执行up()函数释放资源,点亮信号灯时,如果count恢复到0,则表示尚有进程在等待该资源,因此执行唤醒操作。一个典型 的down()-up()流程是这样的:

down()-->count做原子减1操作,如果结果不小于0则表示成功申请,从down()中返回;
-->如果结果为负(实际上只可能是-1),则表示需要等待,则调用__down_fail();
__down_fail()调用__down(),__down()用C代码实现,要求已不如down()和__down_fail()严格,在此作实际的等待(arch/i386/kernel/semaphore.c):

void __down(struct semaphore * sem)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk);
tsk->state = TASK_UNINTERRUPTIBLE;
add_wait_queue_exclusive(&sem->wait, &wait);
spin_lock_irq(&semaphore_lock);
sem->sleepers++;
for (;;) {
int sleepers = sem->sleepers;
/*
* Add "everybody else" into it. They aren't
* playing, because we own the spinlock.
*/
if (!atomic_add_negative(sleepers - 1, &sem->count)) {
sem->sleepers = 0;
break;
}
sem->sleepers = 1; /* us - see -1 above */
spin_unlock_irq(&semaphore_lock);
schedule();
tsk->state = TASK_UNINTERRUPTIBLE;
spin_lock_irq(&semaphore_lock);
}
spin_unlock_irq(&semaphore_lock);
remove_wait_queue(&sem->wait, &wait);
tsk->state = TASK_RUNNING;
wake_up(&sem->wait);
}

__down()-->当前进程进入wait等待队列,状态为不可中断的挂起,sleepers++,如果这是第一次申请失败,则sleepers值为1,否则为2--这个设置纯粹是为了下面这句原子加而安排的。

在 真正进入休眠以前,__down()还是需要判断一下是不是确实没有资源可用,因为在spin_lock之前什么都可能发生。 atomic_add_negative()将sleepers-1(只可能是0或者1,分别表示仅有一个等待进程或是多个)加到count(如果有多个 进程申请资源失败进入__down(),count可能为-2、-3等)之上,这个加法完成后,结果为0只可能是在sleepers等于1的时候发生(因 为如果sleepers等于2,表示有多个进程执行了down(),则count必然小于-1,因此sleepers-1+count必然小于0),表示 count在此之前已经变为0了,也就是说已经有进程释放了资源,因此本进程不用休眠而是获得资源退出__down(),从而也从down()中返回;如 果没有进程释放资源,那么在所有等待进程的这一加法完成后,count将等于-1。因此,从down()调用外面看(无论是在down()中休眠还是获得 资源离开down()),count为负时只可能为-1(此时sleepers等于1),这么设计使得up()操作只需要对count加1,判断是否为0 就可以知道是否有必要执行唤醒操作__up_wakeup()了。

获得了资源的进程将把sleepers设为0,并 唤醒所有其他等待进程,这个操作实际上只是起到恢复count为-1,并使它们再次进入休眠的作用,因为第一个被唤醒的等待进程执行 atomic_add_negative()操作后会将count恢复为-1,然后将sleepers置为1;以后的等待进程则会像往常一样重新休眠。

将down()操作设计得如此复杂的原因和结果就是up操作相当简单。up()利用汇编原子地将count加1,如果小于等于0表示有等待进程,则调用__up_wakeup()-->__up()唤醒wait;否则直接返回。

在down()中竞争获得资源的进程并不是按照优先级排序的,只是在up()操作完成后第一个被唤醒或者正在__down()中运行而暂未进入休眠的进程成功的可能性稍高一些。

尽 管可以将信号量的count初始化为1从而实现一种互斥锁(mutex),但Linux并不保证这个count不会超过1,因为up操作并不考虑 count的初值,所以只能依靠程序员自己来控制不要无谓的执行up()从而破坏mutex的语义。相关的初始化接口定义在include/asm /semaphore.h中,但一般程序员可以通过sema_init()接口来初始化信号量:

#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)
#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)
static inline void sema_init (struct semaphore *sem, int val)
static inline void init_MUTEX (struct semaphore *sem)
static inline void init_MUTEX_LOCKED (struct semaphore *sem)

除 了down()以外,Linux还提供了一个down_interruptible(),操作序列与down()基本相同,仅在休眠状态为可中断和信号处 理上有所不同。在标准的信号量以外,还有一套读写信号量,用于将资源的读写区分开来进行同步以提高效率,采用读写锁来实现,有兴趣的可以参阅文后列出的参 考资料。

http://www.ibm.com/developerworks/cn/linux/kernel/sync/
-------------------
信号量和自旋锁
内核同步措施

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


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


自旋锁对信号量
------------------------------------------------------
需求                     建议的加锁方法

低开销加锁               优先使用自旋锁
短期锁定                 优先使用自旋锁
长期加锁                 优先使用信号量
中断上下文中加锁          使用自旋锁
持有锁是需要睡眠、调度     使用信号量
http://blog.chinaunix.net/u1/38576/showart_367985.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值