神奇的大内核锁

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版本的内核代码中被完全消灭了。


<think>好的,我现在需要分析用户提供的关于代码注释的观点,并理解其来源和含义。首先,用户提到函数实现通常不需要添加注释,尤其是不要描述显而易见的现象或翻译代码。这一点可能来自于软件工程的最佳实践,比如《代码整洁之道》或者《程序员修炼之道》这类书籍,强调代码可读性本身的重要性。 接下来,用户指出在使用了巧妙方法时,应在函数定义处添加解释性注释,比如编程技巧、步骤或理由。这可能参考了某些编码标准,例如Google的代码风格指南,其中建议解释复杂的逻辑或非直观的决策。另外,像开源项目如Linux内核的编码规范也可能有这样的要求,因为它们需要多人协作,清晰的注释有助于维护。 关于Doxygen处理源文件和头文件的规则一致,但源文件通常不需要生成文档,因此使用/* ... */或//注释。这应该来源于Doxygen的官方文档,说明如何配置注释以避免自动生成冗余的文档内容。例如,Doxygen默认处理头文件中的注释作为接口文档,而源文件中的实现细节可能不需要公开,因此注释格式的选择会影响文档生成的结果。 用户还提到不要使用奇怪的表述方式,比如使用don’t而不是dont,这可能涉及到代码注释的可读性和专业性,符合一般的编码规范,如PEP 8或其他语言特定的风格指南,强调注释应使用标准英语,避免缩写和俚语。 需要验证这些观点是否确实来自权威资料。例如,检查《代码整洁之道》是否有类似建议,或者Google的代码风格指南中是否提到注释应解释为什么而非怎么做。同时,查阅Doxygen文档确认其处理源文件和头文件的规则。此外,确认常见的编码标准是否反对自然语言翻译代码作为注释,以及是否推荐在巧妙实现处添加解释。 可能存在混淆的地方是,源文件是否需要生成文档。某些项目可能确实在源文件中使用Doxygen注释,但用户提到避免生成不符合预期的文档,因此可能推荐在头文件中进行文档注释,而源文件中的注释仅用于实现细节,不使用Doxygen的特殊格式(如/** ... */),而是普通注释。这需要进一步确认Doxygen的配置细节,比如哪些注释块会被解析。 总结,用户的观点综合了多个最佳实践和工具规范,主要目的是提高代码的可维护性,减少冗余注释,同时在必要时提供关键解释,确保代码的意图清晰,便于团队协作和后期维护。</think>以下是对该观点的分析及解读,分步说明其来源和核心思想: 一、观点来源分析 1. 出自代码规范最佳实践 - 参考《代码整洁之道》《代码大全》等经典著作,强调代码即文档 - 继承UNIX编程哲学中的"Show, don't tell"思想 - 符合Linux内核编码风格指南等开源项目规范 2. 基于Doxygen工具特性 - 遵循Doxygen官方文档规则(v1.9.7+) - 区分接口文档实现注释的定位差异 - 符合工具对注释格式的解析逻辑 二、核心原则解读 1. 注释的本质目的 - 不是翻译代码(Bad:// 给变量i加1 → i++) - 应解释代码无法表达的信息: * 设计决策的理由(为何选择特定算法) * 非直观的实现技巧(位操作优化) * 并发控制逻辑(锁的作用域划分) * 外部约束条件(兼容旧系统的workaround) 2. 实现注释的层次要求 ```c // 坏注释(冗余描述) /* 计算两个数的和 */ int add(int a, int b) { return a + b; // 返回a加b的结果 } // 好注释(解释必要性) /* * 使用快速傅里叶变换实现多项式乘法 * 选择Cooley-Tukey算法原因: * 1. 输入规模n常为2的幂次 * 2. 相比直接计算O(n^2)提升至O(n log n) * 注意:调用前需确保系数数组已补零对齐 */ void fft_multiply(...) { // 蝴蝶操作优化缓存局部性 BUTTERFLY_OPERATION(...); } ``` 3. Doxygen实践规范 | 文件类型 | 注释内容 | 格式建议 | |----------|-------------------------|------------------| | 头文件 | 接口文档(功能/参数/返回值)| /** Doxygen注释 */ | | 源文件 | 实现细节说明 | /* 普通注释 */ | 三、典型反例说明 1. 无意义翻译 ```c // 错误示例 pthread_mutex_lock(&mutex); // 给互斥锁加锁 ``` 2. 正确示范 ```c /* * 此处加锁仅保护共享计数器,文件操作不涉及竞态 * 故在写操作前释放锁以降低锁粒度 */ pthread_mutex_lock(&counter_mutex); global_counter++; pthread_mutex_unlock(&counter_mutex); write_log(...); // 无锁写操作(线程安全IO接口) ``` 四、应用建议 1. 编写时自检三问: - 这个注释是否重复代码表达的内容? - 删除注释是否影响理解关键设计? - 能否通过重构代码使注释变得多余? 2. 维护策略: - 将注释视为代码逻辑的"变更日志" - 修改代码时必须同步更新关联注释 - 使用静态分析工具(如clang-tidy)验证注释一致性 此观点体现了软件工程中"意图导向注释"的核心思想,强调通过注释传递代码无法承载的设计知识,而非重复语法层面的机械描述。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值