在多核对称处理器或者是可抢占型内核中,由于进程的“并发”从而引发诸多问题。如何管理好资源,处理好进程之间的竞争,同时不影响系统的并发性能尤为重要。
处理竞争和并发问题,主要有四种手段:1锁 2信号量 3互斥体 4CRU
一并发的来源
1多任务抢占型的内核,由“宏观并行,微观并行”带来的并发。
2中断。
3多任务处理器。同一时刻有多个处理器同时运行。
二处理并发的手段
1local_irq_enable local_irq_disable
这种手段简单地关闭了中断。由于操作系统是由中断来驱动调度的,这样的话,对于单核不可抢占系统来说,这是一种有效的方式。但是前提要求是被保护的临界代码执行时间要短。才不至于会影响系统的性能。
另外这两个函数有两个变体:
local_irq_save
local_irq_restore
这两个函数会在开关中断的时候保护中断寄存器。防止标志位被破坏
2自旋锁spink_lock
处理并发的问题上,自旋锁是一个更有效的方法
自旋锁的原理就是在多个处理器之间共享全局变量V,当进程1进入临界代码段,则将V++。退出临界代码段的时候,就V--。在一个进程准备进入临界代码段之前要检查V是否为0 如果不为0则是上锁状态,该进程就进入自旋状态。
为了实现自旋锁的原子操作,自旋锁spink_locak函数在上锁之前会关闭处理器的抢占性,实际上就是关闭任务调度。
关闭处理器的抢占性的缘由可以用一下例子来说明。
假设A进程在对临界代码段code_spink进行上锁操作的时候,外部触发了一个中断,在中断退出之前,由于内核是配置为可抢占的,所以这里会出现一个调度点,如果此时任务队列内出现了新的,比原先上锁任务优先级更高的任务B,那么退出中断之后就会运行任务B,此时任务B有可能回去访问临界code_spink代码,此时临界代码还未完成,也就是说B可以访问成功,这样上锁就是失败的,也就是说,上锁的过程并不是一个原子操作。所以在自选锁上锁之前要关闭内核的可抢占性,是必须的。
3自旋锁变体
自旋锁似乎可以解决好并发问题了,但事实上还有一种情形是自旋锁无能为力的,那就是中断,倘若在执行A临界代码段的时候,发生了一个中断事件,而中断事件B有请求锁,这样的话就会发生死锁的现象。为了解决这个问题,出现了自旋锁变体
spink_lock_irqspink_unlock_irq
这两个函数在上锁之前会关闭/打开中断,然后关闭/打开抢占。显然,在知道了这个锁会被中断用到的时候应该用的是spink_lock_irq。另外,spink_lock_irq函数还有一组保护寄存器版本:
spink_lock_irqsave和spink_lock_irqstore
还有非阻塞版本:
spink_trylock_irq
spink_trylock_irqsave
为了提高效率,出现了另外一种变体:rwlock
读写者锁。这个锁主要特性就是:读进程不互斥,写进程互斥。一个读进程进入临界代码之后,可以允许多个读进程继续进入。读进程进入后,写进程不能进入。写进程进入之后,读进程或者写进程都不能进入。
自旋锁小结:自旋锁用于对临界代码的保护,其实际思想就是设置一个全局变量V,在有进程进入被保护的临界代码时都会获取锁,即检测V是否为0,如果不是0,则V++,同样的,退出时V--。另外,为了保证上锁的操作的原子性,在真正的上锁操作之前spink_lock函数会先关闭处理器的抢占性,另外,为了防止在中断代码中请求锁而引起的死锁情况,就必须使用spink_lock_irq来屏蔽处理器中断响应。为了应对读写进程的情况,自选锁还有rwlock版本,此锁作用就是允许多个读进程访问代码而写进程之间,写进程与读进程之间是互斥的。因为在请求锁失败之后,进程会进入自旋状态不会引起进程调度所以自选锁只适合保护较为短小的代码段。自选锁主要的版本有:1普通版本spink_lock2 中断版本spink_lock_irq,spink_lock_irqsave 3 无阻塞版本spink_trylockspink_trylock_irq spink_trylock_irqsave
4信号量
自旋锁在获取锁失败之后就会进入自旋状态,即空转,但是不会丧失对处理器的所有权。而信号量在获取信号量失败之后,进程就会进入休眠状态,丧失对处理器的所有权,即引起上下文切换。
所以在临界代码短小的情况下,使用自旋锁会比较好,而在临界代码处理时间比较长的情况下,应该使用信号量。
信号量的定义如下:
structsemaphore{
spinklock_tlock;
unsignedint count ;
structlist_head wait_list ;
}
其中成员lock是一个自旋锁,这个自旋锁用于实现信号量的原子操作。而count是用来表示允许进入临界区的执执行路径个数。wait_list用于管理在该信号量上面休眠的进程,一旦信号量被释放,就可以根据这个链表去唤醒进程。
信号量的初始化应该调用sema_init来完成,而不应当对成员直接赋值。
与自旋锁的lock和unlock两个操作不同,信号量的两个操作主要是dowm和 up。
lock一共有lock_irqlock_irqsave trylock_irq trylock_irqsave这四个版本的操作。dowm和up也有其他版本的操作:
1down_interruptable
这个函数首先尝试count如果大于0则获得信号量,将count--并且推出,否则,将当前进程放入sem的waiter链表,然后设置进程为休眠(详情请查看进程休眠与唤醒),并且设定休眠时间为timeout,接着启动进程调度。在休眠期间可能被唤醒,唤醒后进程判断唤醒条件,如果是超时唤醒,则返回响应的错误代码,如果是信号量释放后的唤醒,那就可以获取信号量并且返回。
2down
这个down操作与down_interrupt基本相同,唯一的区别就是down是不能被中断的,也就是说我们无法通过ctrl+d来终止进程。非必要的情况下,我们应当避免使用down。
3down_killable
这个函数导致睡眠的进程可以收到一些kill信号而被唤醒
3down_trylock
这个函数不会引起休眠,如果取得信号量则返回1否则返回0
4down_timeout
这个函数是可被中断的。并且在休眠之后,可以指定休眠的时间。如果休眠时间超出而被唤醒,则返回一个错误代码ETIMEOUT。如果获得信号量,则返回0。
5up
信号量只有一个up操作,up操作首先将count++然后查看信号量的等待队列内是否有进程在等待,如果有进程等待,则唤醒进程。
6读写者信号量
对应于读写自旋锁,信号量也有读写信号量。
读写信号量定义如下:
structre_semaphore{
__s32 activity;
spink_lock lock;
structlist_head wait;
};
与信号量基本一致,多了一个activity,含义如下:
activity= 0 没有读进程也没有写进程。
activity= -1 有一个写进程
activity= n n 为正,有n个读进程
读取者的down操作:down_readdown_read_trylock
写者的down操作:down_writedown_write_trylock
读取者的up操作:up_readup_read_trylock
写入者的up操作:up_writeup_write_trylock
7顺序锁
selock定义如下:
typedefstruct {
unsignedsequence;
spinklock_tlock;
}selock_t
顺序锁基于自旋锁实现。对于读进程不加锁,对于写进程加锁。但是在读进程开始和结束的时候会分别读取顺序值,比较顺序值,如果顺序值不同则说明读取无效。其设计思想就是,设置一个全局变量sequence,在写之前更新sequancen的值,以表明数据已经更新。
。而读的时候先获取sequancen,读完了之后要对比当前的sequence和以前的sequance是否一致,不一致则说明数据已经发生了改变,读取失败。
读取者在进入读取之前要先读取顺序值:调用函数read_seqbegin。读取完了之后可以调用read_seqretry,进行比较,如。
对于写入者,在写之前要先获取锁调用函数:
信号量或者是锁的实现目的都是解决竞争问题,只是各自实现的原理不同,导致特性不同。