大内核锁 BKL

参考:http://blog.csdn.net/universus/article/details/5623971                                                             
          http://blog.csdn.net/chenyu105/article/details/7726492

         《Linux Kernel Development》 3ed_CN p159

           2.6.34

  锁保护的是数据,在程序路径中访问数据,确保数据被访问的惟一性,就必须保证只有一个线程正在位于相应的程序路径。       
大内核锁也是一种锁,问题在于其锁的粒度太粗,当有大量不同的数据在一个程序路径中被访问时,若能保证只有一个线程位于该程序路径,则可以保证数据正在被访问的惟一性。大内核锁的意义在于,线程要想进入某个程序路径来访问数据,就必须获得该锁,表面上看就像是大内核锁保护了程序路径,其它线程无法进入程序路径。
     

内核中哪一处锁住了大量的数据?如下:

//此处以2.6.34为准,3.10.5中在内核初始化时没有使用这种机制
void
start_kernel(void) | 此处只关注BKL |---->local_irq_disable(); |---->lock_kernel(); |---->local_irq_enable(); |---->rest_init() |---->unlock_kernel();

#define lock_kernel() do {                  \                                                              
    _lock_kernel(__func__, __FILE__, __LINE__);     \
    }while(0)

 

 

#define unlock_kernel() do {                    \
    _unlock_kernel(__func__, __FILE__, __LINE__);       \                                                          
} while (0)

_lock_kernel的实现:

void __lockfunc _lock_kernel(const char *func, const char *file, int line)                                         
    |---->int depth = current->lock_depth + 1;
    |     注意:新建进程时在copy_process中将新建的进程的lock_depth设置成-1
    |
    |如果上首次进行lock_kernel,则尝试获取这把锁
    |if (likely(!depth)) 
    |{
    |   might_sleep();
    |   __lock_kernel();
    |}       
    |
    |---->current->lock_depth = depth;

__lock_kernel()
static inline void __lock_kernel(void)
    |---->preempt_disable()禁止抢占
    |---->do_raw_spin_lock(&kernel_flag);

此以上可以看出,对于lock_kernel实际上就是将current->lock_depth值加1,为了防止死锁,只在首次进行lock_kernel时preempt_disable(),同时do_raw_spin_lock(&kernel_flag),相当于spin_lock,只是由于数据类型不同而分成两部完成。

 

  持有BKL的进程可以睡眠,BKL支持嵌套加锁解锁,为了支持该特性,在task_struct中专门为BKL设置了加锁计数器lock_depth域。 问题在于,既然我们有新的机制以充分发挥SMP的性能,为何不把BKL剔除?
  原因:因为持有BKL的执行路径可以睡眠,可以嵌套对BKL加锁解锁,而spin_lock不支持这样的机制,如果将BKL彻底踢出而换成BKL机制,由于无法完整考察在相应的程序路径中是否调用了schedule、是否嵌套获取锁,因此很容易造成内核死锁。

在schedule中进行进程切换时,会检查当前进程是否持有大内核锁,以及在进程重新得到CPU使用权时会检查自身原来是否持有自旋锁。
release_kernel_lock会判断如果当前进程持有大内核锁,则preempt_enable()并释放锁。
reacquire_kernel_lock在进程再次被调度回来后,检查当前进程在切换之前是否持有大内核锁。如果持有的话,说明在进程切换时,当前进程的大内核锁被强行释放了,需要再次获取:获取锁、preempt_disable();但是我们也应注意此处与spin_lock的区别,spin_lock时若不能或的锁则一直自旋,而reacquire_kernel_lock中会不断地尝试获取自旋锁,每次没有获取成功时都需要检查是否需要放弃CPU使用权。


void
schedule(void) |详细的请参考笔记:http://www.cnblogs.com/openix/p/3272406.html |此处重点说明对大内核锁的处理 | |---->release_kernel_lock(prev); |---->if (unlikely((tsk)->lock_depth >= 0)) | __release_kernel_lock(); | |---->do_raw_spin_unlock(&kernel_flag); | |---->preempt_enable_no_resched(); |---->context_swith(); |---->reacquire_kernel_lock(current); |---->if (unlikely(task->lock_depth >= 0)) | return __reacquire_kernel_lock(); | |----while (!do_raw_spin_trylock(&kernel_flag)) { | |---- if (need_resched()) | |---- return -EAGAIN; //重新调度 | |----} | |----preempt_disable(); | |---->return 0;

 

小结:

用等同于spinlock_t的数据结构来实现大内核锁(实际是raw_spinlock_t);                                                 
__lock_kernel()等同于spin_lock()
__unlock_kernel()等同于spin_unlock()
__release_kernel_lock()近似于同于spin_unlock()  (没有检查自身是否需要放弃CPU使用权的检查)
___reacquire_kernel_lock()近似于spin_lock()  (但是在调用过程中检查是否需要放弃CPU使用权)
同时也相当于用spin_lock与spin_unlock的方式来获取锁(尽管实际上并非如此,但是对于2.6.34而言,这是等同的)。
持有大内核锁的用户代码可以睡眠:进程切换时会检查当前进程是否持有大内核锁,而采取释放核重获的操作。

 

附:引入大内核锁的原因。  请参考两处链接中的内容

早期引入大内核锁的原因:                                                                                           
    引入大内核锁以支持SMP处理器,在内核入口设置BKL,一旦一个处理器进入内核态就立刻上锁。由于只有一个处理器在运行内核态运行,内核的执行本质上和单处理器没有什么区别。进入该机制的缺点:多处理器的性能只能体现在用户态的并行处理上,而在内核态还是单线执行,不能挖掘SMP的性能。由于内核大部分代码是多处理器安全的,只有少数全局资源具有互斥性,所以所有处理器都可以随时进入内核态执行,关键在于需要把具有排它性的资源找出,并在访问这些资源时加以保护。这样,大内核锁从保护整个内核态缩小为零散地保护内核态的某些关键数据

posted on 2013-09-07 21:28  阿加  阅读(1546)  评论(0编辑  收藏  举报

导航