原文地址:http://hi.baidu.com/_kouu/blog/item/c7f1bcd864bb76f939012f9f.html
Big Kernel Lock(BKL)(大内核锁),是linux内核中使用到的一种锁,它跟普通的锁原理上的一样的:
lock_kernel();
/* 临界区 */
unlock_kernel();
但是它又有一些非常诡异的地方。从表面上看:
1、BKL是一个全局的锁(注意,是“一个”而不是“一种”),它保护所有使用它来同步的临界区。一旦一个进程获得BKL,进入被它保护的临界区,不但该临界区被上锁,所有被它保护的临界区都一起被锁住。
这看起来非常之武断:进程A在CPU_1上操作链表list_a,而进程B在CPU_2上操作全局变量var_b,这两者本身毫无瓜葛。但如果你使用了BKL,它们就要“被同步”。
2、BKL是递归锁。同一进程中可以对BKL嵌套的上锁和解锁,当解锁次数等于上锁次数时,锁才真正被释放。
这一点虽然跟内核中的其他锁不大一样,但倒也不算神奇,用户态的pthread_mutex也支持这样的递归锁。
3、BKL有自动释放的特性。在CPU_N上,如果当前进程A持有BKL,则当CPU_N上面发生调度时,进程A持有的BKL将被自动释放。而当进程A再次被调度执行时,它又自动获得BKL(如果BKL正在被其他进程持有,则进程A不会被调度执行)。
这个特性对于普通的用户态程序来说实在是不可思议:进程A进入了临界区,要准备修改某全局链表list_a,但是由于某种原因而进入睡眠(比如系统内存紧缺时,等待分配内存),结果锁就被自动释放了。而另一个进程B就可以堂而皇之的获得锁而进入临界区,并且把全局链表list_a给改了。等进程A从睡眠中被唤醒的时候,就发现这个世界全变了……而锁呢?竟然完全不起作用。
BKL的前两个特性还好理解,第三个特性实在是匪夷所思。这么诡异的锁是怎么来的呢?
百度一下“大内核锁”可以了解到:据说在linux 2.0时代,内核是不支持SMP(对称多处理器)的。在迈入linux 2.2时代的时候,SMP逐渐流行起来,内核需要对其进行支持了。但是发现,内核中的很多代码在多个CPU上同时运行的时候会存在问题。怎么办呢?最完美的解决办法当然是把所有存在问题的地方都找出来,然后给它们分别安排一个锁。但是这样做的话工作量会很大,为了快速支持SMP,linux内核出了狠招,这就是BKL:CPU进入内核态时就上BKL、退出内核态时释放。于是,系统中同一时刻就只有一个CPU会处于内核态,内核代码就没有了“在多个CPU上同时运行”的问题。
这一招很有效,但是显然很拙劣,内核代码不能在多个CPU上同时运行,SMP的优势大打折扣。于是后来的内核版本又逐步逐步的在削减被BKL所保护的临界区,以期把它们都消灭干净。
类似上面的描述在网络上比比皆是,但是似乎网络上对于BKL的描述也仅限于此。看完这些,你对BKL是否感觉云里雾里的呢?反正我之前看到BKL的时候实在是想不通,感觉这些描述没讲到点子上。还有人说,BKL保护的是代码,而不是数据(资源)。这个说法实在太抽象,抽象到说了等于没说。(有什么代码是需要保护的?如果不是因为代码跟资源打交道,它还有被保护的必要?)
为什么要设计这样一种锁呢?对于一种特殊的、非传统的机制,要理解为什么,要找到它存在的意义,最好的办法莫过于:找出一个场景,在这个场景下使用这种特殊的机制比使用其他传统的机制更有优势。比如,为什么要设计读写锁?因为在读写分明的场景下,使用读写锁可以避免读读冲突,减少程序的阻塞。再比如,为什么要设计seqlock(见《linux seqlock & rcu 浅析》)?因为在读写分明、且读多写少写优先的场景下,使用seqlock可以避免写操作被阻塞。
那么BKL呢?它在什么场景下能带来什么样的优势呢?网络上找不到这样的描述。我试图在内核代码中寻找这样的场景,但是完全找不到。似乎BKL的存在毫无道理,怎么看,把现有的BKL换成spinlock或者信号量,都是更好的选择(这些锁至少不是全局的)。
要想解释BKL为什么会设计成这样子,或许只能从它的诞生时需要解决的问题入手……
想一想,一个进程上了一个锁,保护了一个临界区。那么在锁被释放之前,其他进程都应该遵守规则,而不进入这个临界区。这里的“其他进程”有两层含义:
1、由于调度,在同一CPU上交错运行的其他进程;
2、由于SMP,在不同CPU上同时运行的其他进程;
把“其他进程”划分成这两类,似乎有点畸形,但这也正是BKL畸形之所在。因为我们一般所说的同步、所用到的锁都是针对所有“其他进程”的,不管是“在同一CPU上交错运行的其他进程”,还是“在不同CPU上同时运行的其他进程”,都应该在被同步的范围之内。
在SMP和BKL被引入内核之前,内核代码是能够正常运行的,这时候系统中只有一个CPU。内核中也有一些同步措施,它们的作用是同步“在同一CPU上交叉运行的其他进程”。
随着SMP的引入,“在不同CPU上同时运行的其他进程”出现了,而BKL的作用就是(且仅仅是)解决它们的同步问题。于是,当发生进程调度的时候,内核自动把上一个进程持有的BKL释放。因为BKL不关心“在同一CPU上交叉运行的其他进程”的同步问题,这些问题是由原有的那些机制去保证的。
前面说过,我们一般看到的锁都是同时支持上面两类“其他进程”的。而BKL只支持其中之一,这就是它不健全的地方。正是因为这种不健全,所以很难想象有什么应用场景是我们应该去使用BKL的。它并不是一个可以独立使用的东西,它只是一个补丁,在它的背后必须有另一种同步机制的支持,这种机制必须足以应付“在同一CPU上交叉运行的其他进程”带来的同步问题。
那么BKL为什么不能设计成跟普通的锁一样健全呢?因为,正是由于BKL的不健全,一个持有BKL的进程在进入睡眠之后,其它进程还可以持有BKL,还可以进入临界区。如果它健全的话,其他进程都只有傻傻等待的份,SMP的优势将再打折扣。
再具体一点看看,当SMP引入内核之后,产生了两个问题:
1、原先那些针对“在同一CPU上交叉运行的其他进程”的锁需要升级,要支持“在不同CPU上同时运行的其他进程”。
这一点是显而易见的,上面已经讨论过了,不能同时支持两类“其他进程”的锁是不健全的锁。并且这个问题也很好解决,把锁的实现改一改就好了,CPU必定会提供SMP环境下的相应的指令支持。
2、解决了第一个问题还不够。有这样一些地方,原先并不存在“在同一CPU上交叉运行的其他进程”的可能性,因此被认为不需要同步,或者说已经隐含了这样的同步关系。
在内核中,没有内核抢占(暂时禁用或不支持)且与中断处理程序没有关系的代码,在单CPU情况下,如果它不主动让出CPU,那么就可以认为不会有其他进程与它交叉运行。
有了这样的隐含的同步关系,也就不需要进行显式的同步。但是SMP的出现打破了这一论断,因为出现了“在不同CPU上同时运行的其他进程”,原本隐含的同步关系已经不能代表同步关系的全部,所以,其中的一些地方可能还是需要显式的进行同步。BKL就是针对这些地方而产生的。
总的说来,BKL就是为了在那些原本已经隐含了单CPU下的同步关系的地方打一个补丁,以确保这些地方在SMP环境下也不会出现问题。反过来,不健全的BKL之所以能够工作,是因为背后隐含了单CPU下的同步关系。
然而正如文章开头说到的,这个问题是有完美解决方案的,把这些因为SMP而需要同步的地方都找出来,然后一一安排合适的同步机制(这些同步机制能同时解决单CPU和SMP下的同步问题)。BKL只不过是一种临时方案。
由于BKL的不健全,逻辑上有很诡异的地方,内核代码越来越复杂,牵涉到BKL的地方更是越来越难以维护。再加上BKL对SMP的限制较大。所以一直以来内核开发者们对BKL深恶痛绝,并且一再努力削减。(当然,请神容易送神难。大内核锁的全局性和递归性使得调用它的代码很难被理清。)
终于,BKL据说要在2.6.37版本的内核代码中被完全消灭了。