互斥与同步

在多核对称处理器或者是可抢占型内核中,由于进程的并发从而引发诸多问题。如何管理好资源,处理好进程之间的竞争,同时不影响系统的并发性能尤为重要。


处理竞争和并发问题,主要有四种手段: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_irqsavespink_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来完成,而不应当对成员直接赋值。


与自旋锁的lockunlock两个操作不同,信号量的两个操作主要是dowm up

lock一共有lock_irqlock_irqsave trylock_irq trylock_irqsave这四个版本的操作。dowmup也有其他版本的操作:

1down_interruptable


这个函数首先尝试count如果大于0则获得信号量,将count--并且推出,否则,将当前进程放入semwaiter链表,然后设置进程为休眠(详情请查看进程休眠与唤醒),并且设定休眠时间为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,进行比较,如。


对于写入者,在写之前要先获取锁调用函数:



信号量或者是锁的实现目的都是解决竞争问题,只是各自实现的原理不同,导致特性不同。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值