竞争_LOCK

 

 

自旋锁spin_lock和raw_spin_lock

本文不打算详细探究spin_lock的详细实现机制,只是最近对raw_spin_lock的出现比较困扰,搞不清楚什么时候用spin_lock,什么时候用raw_spin_lock,因此有了这篇文章。


/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  临界区(Critical Section)

我们知道,临界区是指某个代码区间,在该区间中需要访问某些共享的数据对象,又或者是总线,硬件寄存器等,通常这段代码区间的范围要控制在尽可能小的范围内。临界区内需要对这些数据对象和硬件对象的访问进行保护,保证在退出临界区前不会被临界区外的代码对这些对象进行修改。出现以下几种情形时,我们需要使用临界区进行保护:
  • (1)  在可以抢占(preemption)的系统中,两个线程同时访问同一个对象;
  • (2)  线程和中断同时访问同一个对象;
  • (3)  在多核系统中(SMP),可能两个CPU可能同时访问同一个对象;

2.  自旋锁(spin_lock)

针对单处理器系统,对第一种情况,只要临时关闭系统抢占即可,我们可以使用以下方法:
[cpp]  view plain copy
  1. preempt_disable();  
  2. .....  
  3. // 访问共享对象的代码  
  4. ......  
  5. preempt_enable();  

同样地,针对单处理器系统,第二种情况,只要临时关闭中断即可,我们可以使用以下方法:

[cpp]  view plain copy
  1. local_irq_disable();  
  2. ......  
  3. // 访问共享对象的代码  
  4. ......  
  5. local_irq_enable();  

那么,针对多处理器的系统,以上的方法还成立么?答案很显然:不成立。

对于第一种情况,虽然抢占被禁止了,可是另一个CPU上还有线程在运行,如果这个线程也正好要访问该共享对象,上面的代码段显然是无能为力了。

对于第二种情况,虽然本地CPU的中断被禁止了,可是另一个CPU依然可以产生中断,如果他的中断服务程序也正好要访问该共享对象,上面的代码段也一样无法对共享对象进行保护。

实际上,在linux中,上面所说的三种情况都可以用自旋锁(spin_lock)解决。基本的自旋锁函数对是:
  • spin_lock(spinlock_t *lock);
  • spin_unlock(spinlock_t *lock);
对于单处理器系统,在不打开调试选项时,spinlock_t实际上是一个空结构,把上面两个函数展开后,实际上就只是调用preempt_disable()和preempt_enable(),对于单处理器系统,关掉抢占后,其它线程不能被调度运行,所以并不需要做额外的工作,除非中断的到来,不过内核提供了另外的变种函数来处理中断的问题。

对于多处理器系统,spinlock_t实际上等效于内存单元中的一个整数,内核保证spin_lock系列函数对该整数进行原子操作,除了调用preempt_disable()和preempt_enable()防止线程被抢占外,还必须对spinlock_t上锁,这样,如果另一个CPU的代码要使用该临界区对象,就必须进行自旋等待。

对于中断和普通线程都要访问的对象,内核提供了另外两套变种函数:
  • spin_lock_irq(spinlock_t *lock);
  • spin_unlock_irq(spinlock_t *lock);
和:
  • spin_lock_irqsave(lock, flags);
  • spin_lock_irqrestore(lock, flags);
我们可以按以下原则使用上面的三对变种函数(宏):
  • 如果只是在普通线程之间同时访问共享对象,使用spin_lock()/spin_unlock();
  • 如果是在中断和普通线程之间同时访问共享对象,并且确信退出临界区后要打开中断,使用spin_lock_irq()/spin_unlock_irq();
  • 如果是在中断和普通线程之间同时访问共享对象,并且退出临界区后要保持中断的状态,使用spin_lock_irqsave()/spin_unlock_irqrestore();
其实变种还不止这几个,还有read_lock_xxx/write_lock_xxx、spin_lock_bh/spin_unlock_bh、spin_trylock_xxx等等。但常用的就上面几种。

3.  raw_spin_lock

在2.6.33之后的版本,内核加入了raw_spin_lock系列,使用方法和spin_lock系列一模一样,只是参数有spinlock_t变为了raw_spinlock_t。而且在内核的主线版本中,spin_lock系列只是简单地调用了raw_spin_lock系列的函数,但内核的代码却是有的地方使用spin_lock,有的地方使用raw_spin_lock。是不是很奇怪?要解答这个问题,我们要回到2004年,MontaVista Software, Inc的开发人员在邮件列表中提出来一个Real-Time Linux Kernel的模型,旨在提升Linux的实时性,之后Ingo Molnar很快在他的一个项目中实现了这个模型,并最终产生了一个Real-Time preemption的patch。
该模型允许在临界区中被抢占,而且申请临界区的操作可以导致进程休眠等待,这将导致自旋锁的机制被修改,由原来的整数原子操作变更为信号量操作。当时内核中已经有大约10000处使用了自旋锁的代码,直接修改spin_lock将会导致这个patch过于庞大,于是,他们决定只修改哪些真正不允许抢占和休眠的地方,而这些地方只有100多处,这些地方改为使用raw_spin_lock,但是,因为原来的内核中已经有raw_spin_lock这一名字空间,用于代表体系相关的原子操作的实现,于是linus本人建议:
  • 把原来的raw_spin_lock改为arch_spin_lock;
  • 把原来的spin_lock改为raw_spin_lock;
  • 实现一个新的spin_lock;
写到这里不知大家明白了没?对于2.6.33和之后的版本,我的理解是:
  • 尽可能使用spin_lock;
  • 绝对不允许被抢占和休眠的地方,使用raw_spin_lock,否则使用spin_lock;
  • 如果你的临界区足够小,使用raw_spin_lock;
对于没有打上Linux-RT(实时Linux)的patch的系统,spin_lock只是简单地调用raw_spin_lock,实际上他们是完全一样的,如果打上这个patch之后,spin_lock会使用信号量完成临界区的保护工作,带来的好处是同一个CPU可以有多个临界区同时工作,而原有的体系因为禁止抢占的原因,一旦进入临界区,其他临界区就无法运行,新的体系在允许使用同一个临界区的其他进程进行休眠等待,而不是强占着CPU进行自旋操作。写这篇文章的时候,内核的版本已经是3.3了,主线版本还没有合并Linux-RT的内容,说不定哪天就会合并进来,也为了你的代码可以兼容Linux-RT,最好坚持上面三个原则。



spin_lock VS spin_lock_irqsave

 

他们两者只有一个差别:是否调用local_irq_disable()函数, 即是否禁止本地中断。在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。举个例子:进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。 因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。 spin_lock_irq这个宏会关中断,关掉的是单个cpu的中断,这个宏preempt_disable()禁掉的是当前cpu的抢占。spin_lock可重入和FIFO。不具备任务优先级。在多核竞争负担重。test_and_settest_and_set指令取出内存某一单元(位)的值,然后再给该单元(位)赋一个新值,关于为何这两条指令能实现互斥我们不在赘述,读者可以了解其算法) 这些指令涉及对同一存储单元的两次或两次以上操作,这些操作将在几个指令周期内完成,但由于中断只能发生在两条机器指令之间,而同一指令内的多个指令周期不可中断,从而保证swap指令或test_and_set指令的执行不会交叉进行.

 

 MUTEX VS SPINLOCK

 

无论是mutex还是spinlock,如果一个thread去给一个已经被其他thread占用的锁上锁,那么从此刻起到其他thread对此锁解锁的时间长短将会导致mutex和spinlock出现下面的问题。mutex的问题是,它一旦上锁失败就会进入sleep,让其他thread运行,这就需要内核将thread切换到sleep状态,如果mutex又在很短的时间内被释放掉了,那么又需要将此thread再次唤醒,这需要消耗许多CPU指令和时间,这种消耗还不如让thread去轮讯。也就是说,其他thread解锁时间很短的话会导致CPU的资源浪费。

spinlock的问题是,和上面正好相反,如果其他thread解锁的时间很长的话,这种spinlock进行轮讯的方式将会浪费很多CPU资源。

 

PER-CPU

per-CPU 变量是一个有趣的 2.6 内核特性,定义在 <linux/percpu.h> 中。当创建一个per-CPU变量,系统中每个处理器都会获得该变量的副本。其优点是对per-CPU变量的访问(几乎)不需要加锁,因为每个处理器都使 用自己的副本。per-CPU 变量也可存在于它们各自的处理器缓存中,这就在频繁更新时带来了更好性能。

在编译时间创建一个per-CPU变量使用如下宏定义:

DEFINE_PER_CPU(type, name);
/*若变量( name)是一个数组,则必须包含类型的维数信息,例如一个有 3 个整数的per-CPU 数组创建如下: */
DEFINE_PER_CPU(int[3], my_percpu_array);

 
虽然操作per-CPU变量几乎不必使用锁定机制。 但是必须记住 2.6 内核是可抢占的,所以在修改一个per-CPU变量的临界区中可能被抢占。并且还要避免进程在对一个per-CPU变量访问时被移动到另一个处理器上运 行。所以必须显式使用 get_cpu_var 宏来访问当前处理器的变量副本, 并在结束后调用 put_cpu_var。 对 get_cpu_var 的调用返回一个当前处理器变量版本的 lvalue ,并且禁止抢占。又因为返回的是lvalue,所以可被直接赋值或操作。例如:

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);


当要访问另一个处理器的变量副本时, 使用:

per_cpu(variable,int cpu_id);

 
当代码涉及到多处理器的per-CPU变量,就必须实现一个加锁机制来保证访问安全。
 
动态分配per-CPU变量方法如下:

void*alloc_percpu(type);
void *__alloc_percpu(size_t size,size_t align);/*需要一个特定对齐的情况下调用*/
void free_percpu(void*per_cpu_var);/* 将per-CPU 变量返回给系统*/

/*访问动态分配的per-CPU变量通过 per_cpu_ptr 来完成,这个宏返回一个指向给定 cpu_id 版本的per_cpu_var变量的指针。若操作当前处理器版本的per-CPU变量,必须保证不能被切换出那个处理器:*/
per_cpu_ptr(void*per_cpu_var,int cpu_id);

/*通常使用 get_cpu 来阻止在使用per-CPU变量时被抢占,典型代码如下:*/

int cpu;
cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);
/* work with ptr */
put_cpu();

/*当使用编译时的per-CPU 变量, get_cpu_var 和 put_cpu_var 宏将处理这些细节。动态per-CPU变量需要更明确的保护*/


per-CPU变量可以导出给模块, 但必须使用一个特殊的宏版本:

EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);


/*要在模块中访问这样一个变量,声明如下:*/
DECLARE_PER_CPU(type, name);

 
注意:在某些体系架构上,per-CPU变量的使用是受地址空间有限的。若在代码中创建per-CPU变量, 应当尽量保持变量较小.
 

资源竞争中的一些问题。

           高速缓存一致性问题,每一个CPU有一个本地的CACHE拷贝。通过snoop机制,对系统总线进行监控,如果内存有写的动作,并且cache中有操作目标,就放弃原cache line.memory barrier 机制,对于逻辑上已经完成,物理上没有完成的操作,一次性的完成。锁总线的操作都会引起memory barrier。MP和多线程安全问题。中断和线程并发问题。对硬件的同时访问。访问关键区时来中断。重入的实现,不能更新全局资源的值,不能返回全局资源的值,只能使用调用者的数据。不能调用不能重入的函数。不上锁只初始化一次的实例。死锁条件(1) 互斥条件:一个资源每次只能被一个进程使用。(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

 
一些解决办法
L1执行write_through L2 执行copy_back避免CACHE一致性问题。两个导致SMP不安全问题,锁同一把锁两次,有先后执行顺序的驱动线程在两个CPU上分别同时执行,导致初始化的数据还没有准备好,就已经被你一个CPU上的线程使用。要考虑驱动代码在那种环境里面执行,在进程还是在中断处理函数中。考虑在执行环境中的并发问题,代码要可重入,关键区的保护和硬件的同时访问。死锁,比如spin_lock 被递归调用,在调用spin_lock时使用disable_irq.
 
down_interruptible()
 

void down(struct semaphore *sem);//down减少信号量的值,并在必要时一直等待

int down_interruptible(struct semaphore *sem);//down_interruptible完成相同工作,它允许等待在某个信号量上的用户空间进程可被用户中断

int down_trylock(struct semaphore *sem);//down_trylock不会休眠,如果信号量在调用时不可获得,会立即返回一个非零值

down_interruptible()是处理信号量的函数。

      他的返回值有三种  1. “0”          2. “-ETIME”              3.“-EINTR”

      0 代表正常返回

      -ETIME 等待超时

      -EINTR 中断

被信号打断就是说有另外的内核控制路径给这个因竞争信号量而睡眠的进程发送了信号,也就是在这个进程的进程描述符的保存signal的字段设置了值,在down_interruptible竞争信号量的循环过程中会查看自己是否收到了信号,如果收到了就立即返回,放弃继续竞争信号量,同时返回值-EINTR,表示被信号中断而返回,而正常获取信号量返回值是0.
 66/**
  67 * down_interruptible - acquire the semaphore unless interrupted
  68 * @sem: the semaphore to be acquired
  69 *
  70 * Attempts to acquire the semaphore.  If no more tasks are allowed to
  71 * acquire the semaphore, calling this function will put the task to sleep.
  72 * If the sleep is interrupted by a signal, this function will return -EINTR.
  73 * If the semaphore is successfully acquired, this function returns 0.
  74 */
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值