Linux内核源代码分析-第二章 代码初识-2
2.2 代码样例了解Linux代码风格最好的方法就是实际研究一下它的部分代码。即使你不完全理解本节所讨论代码的细节也无关紧要,毕竟本节的主要目的不是理解代码,一些读者可以只对本节进行浏览。本节的主要目的是让读者对Linux代码进行初步了解,为今后的工作提供必要基础。该讨论将涉及部分广泛使用的内核代码。2.2.1 printkprintk(25836行)是内核内部消息日志记录
2.2 代码样例
了解Linux代码风格最好的方法就是实际研究一下它的部分代码。即使你不完全理解本节
所讨论代码的细节也无关紧要,毕竟本节的主要目的不是理解代码,一些读者可以只对本
节进行浏览。本节的主要目的是让读者对Linux代码进行初步了解,为今后的工作提供必
要基础。该讨论将涉及部分广泛使用的内核代码。
2.2.1 printk
printk(25836行)是内核内部消息日志记录函数。在出现诸如内核检测到其数据结构出
现不一致的事件时,内核会使用printk把相关信息打印到系统控制台上。对于printk的调
用一般分为如下几类:
* 紧急事件(emergency)—例如,panic函数(25563行)多次使用了printk。当内核检
测到发生不可恢复的内部错误时就会调用panic函数,然后尽其所能地安全关闭计算机。
这个函数中调用printk以提示用户系统将要关闭。
* 调试—从3816行开始的#ifdef块使用printk来打印SMP逻辑单元(box)中每一个处理器
的相关配置信息,但是此过程只有在使用SMP_DEBUG标志编译代码的情况下才能够被执行
。
* 普通信息—例如,当机器启动时,内核必须估计系统速度以确保设备驱动程序能够忙等
待(busy-wait)一个精确的极短周期。计算这种估计值的函数名为calibrate_delay(
19654行),它既在19661行使用printk声明马上开始计算,又在19693行报告计算结果。
另外,在第4章将详细的介绍calibrate_delay函数。
如果你已经浏览过这些参照行,你可能已经注意到printk和printf的参数十分类似:一个
格式化字符串,后跟零个或者多个参数加入字符串中。格式化字符串可能是以一组“
”开始,这里的N是从0到7的数字,包括0和7在内。数字区分了消息的日志等级(log
level),只有当日志等级高于当前控制台定义的日志等级(console_loglevel,25650行
)时,才会打印消息。root可以通过适当减小控制台的日志等级来过滤不是很紧急的消息
。如果内核在格式化字符串中检测不到日志等级序列,那么就会一直打印消息(实际上,
日志等级序列并不一定要在格式化字符串中出现,可以在格式化文本中查找到它的代码)
。
从14946行开始的#define块说明了这些特殊序列,这些定义可以帮助调用者正确区分对
printk的调用。简单地说,我称日志等级0到4为“紧急事件”,等级5到等级6为“普通信
息”,等级7自然就是我所说的“调试”(这种分类方法并不意味着其他更好的分类方法
没有用处,而只是目前我们还不关心它而已)。
在上面讨论的基础上,我们研究一下代码本身。
printk
25836:参数fmt是printf类型的格式化字符串。如果你对“...”部分的内容不熟悉,那
就 需要参阅一本好的C语言参考书(在其索引中查找“变参函数,
variadic function”)。另外,在安装的GNU/Linux中的stdarg帮助里也包含了一个有关
变参函数的简明描述,在这儿只需要敲入“man stdarg”就可以看到。
简单地说,“...”部分提示编译器fmt后面可能紧跟着数量不定的任何类型的参数。由于
这些参数在编译的时候还没有类型和名字,内核使用由三个宏va_start、va_arg和va_end
组成的特殊组及一个特殊类型—va_list对它们进行处理。
25842:msg_level记录了当前消息的日志等级。它是静态的,这看起来可能会有些奇怪—
为什么下一次对printk的调用需要记录日志等级呢?问题的答案是只有打印出新行(\n)
或者赋给一个新的日志等级序列以后,当前消息才会结束。这样,通过在包含消息结束的
新行里调用printk,就保证了在多个短期冲突的情况下,调用者只打印唯一一个长消息。
25845:在SMP逻辑单元中,内核可能试图从不同的CPU向控制台同时打印信息(有时在单
处理机(UP)逻辑单元中也会发生同样问题,但由于中断还未被覆盖掉,所以问题也并不
十分明显)。如果不进行任何协同的话,结果就将处于完全无法让人了解的杂乱无章的状
态,每个消息的各个部分都和其他消息的各个部分混杂交织在一起。
相反,内核使用旋转锁(spin-lock)来控制对控制台的访问。旋转锁将在第10章进行深
入介绍。
如果你对flags 在传送给spin_lock_irqsave之前为什么不对它初始化感到疑惑,请不要
担心:spin_lock_irqsave(对于不同的版本请分别参看12614行,12637行,12716行和
12837行)是一个宏,而不是一个函数。该宏实际上是将值写入flags中,而不是从flags
中读出值(在25895行中,存储在flags中的信息被spin_unlock_irqrestore回读,请参看
12616行,12639行,12728行和12841行)。
25846:初始化变量args,该变量代表printk参数中的“...”部分。
25848:调用内核自身的vsprintf(为节省空间而省略)实现。该函数的功能与标准
vsprintf函数非常相似,向buf中写入格式化文本(25634行)并返回写入字符串的长度(
长度不包括最后一位终止字符0字节)。很快,你将可以看到为什么这种机制会忽略buf的
前三个字符。
(正如25847行的注释中所述)我们应该注意到在这里并没有采取严格的措施来保证缓冲
器不会过载。这里系统假定1024个字符长度的buf已经足够使用(参阅25634行)。如果内
核在这里能够使用vsnprintf函数的话,情况就会好许多。然而,vsnprintf还有另外一个
参数限制了它能够写入缓冲器的字符长度。
25849:计算buf中最近使用的元素,调用va_end终止对“...”参数的处理。
25851:开始格式化消息的循环。其中存在一个内部循环能够处理更多内容(这一点随后
就能看到),因此,每次内循环开始,都开始一个新的打印行。由于通常情况下printk只
用于打印单行,所以在每次调用中,这种循环通常只执行一次。
25853:如果预先不知道消息的日志等级,printk会检查当前行是否以日志等级序列开头
。
25860:如果不是,buf中开始未使用的三个字符就能够起作用了(第一次以后的每次循环
,都会覆盖部分消息文本,但是这样并不会引起问题,因为这里的文本只是前面行中的一
部分,它们已经被打印过,而且以后也不再需要了)。这样,就可以将日志等级插入buf
中。
25866:此处有如下属性:p指向日志等级序列(消息文本紧随其后),msg指向消息文本
—请注意25852行和25865行中对msg的赋值。
由于已知p用来指示日志等级序列的开头—该日志等级序列可能是由函数自身所创建的,
日志等级可以从p中抽出并存到msg_level中。
25868:没有检测到新行,清空line_feed标志。
25869:这是前面谈到过的内循环,循环将运行到本行结束(也就是检测到新行标志)或
者缓冲器的末尾为止。
25870:除了将消息打印到控制台之外,printk还能够记录最近打印的长度为LOG_
BUF_LEN的字符组(LOG_BUF_LEN为16K,请参看25632行)。如果在控制台打开之前,内核
就已经调用printk,则显然不能在控制台上正确打印消息,但是这些消息将被尽可能地存
储到log_buf中(25656行)。当控制台打开以后,缓存在log_buf中的数据就可以转储并
在控制台上打印出来,请参看25988行。
log_buf是一个循环缓冲器,log_start和log_size变量(25657行和25646行)分别记录当
前缓冲器的开始位置和长度。本行中的按位与(AND)操作实际上是快速求模(%)运算
,它的正确性依赖于LOG_BUF_LEN的值是2的幂。
25872:保存变量跟踪记录循环日志的值。显然,日志大小会不断增长,直至达到
LOG_BUF_LEN的值为止。此后,log_size将保持不变,而插入新字符将导致log_start的增
长。
25878:请注意logged_chars(25658行)记录从机器启动之后由printk写入的所有字符的
长度,它在每次循环中都会被更新,而不是在循环结束后才改变一次。基于同样的道理,
log_start和log_size的处理方式也是一样。这实际上是一种优化的时机,本书将在结束
对函数的介绍之后再对它进行详细讨论。
25879:消息被分为若干行,这当然要使用新行标志符来进行分割。一旦内核检测到新行
标志符,就写入一个完整行,从而内循环的执行也可以提前终止。
25884:在这里我们先不考虑内部循环是否会提前退出,从msg到p的字符序列是专门提供
给控制台使用的(这种字符序列我称之为行,但是不要忘了,这里的行可能并不意味着新
行终止,因为buf也许还没有终止)。如果该行的日志等级高于系统控制台定义的日志等
级,而且当前又有控制台可供打印,那么就能够正确打印该行。(记住,printk可能在所
有控制台打开之前就已经被调用过了。)
如果在该消息块中没有发现日志等级序列,并且在前面的printk调用中也没有对
msg_level赋值,那么本行中的msg_level就是-1。由于console_loglevel总不小于1(除
非root通过sysctl接口锁定),于是总是可以打印这些行。
25886:本行应该能够被打印。printk通过遍历打开的控制台驱动链表告知每一个控制台
驱动去打印当前行设备驱动在本书的讨论范围之外,因此,控制台驱动代码则并不包含在
内)。
25888:请注意这里消息文本的开头使用的是msg而不是p,这样就在没有日志等级序列的
情况下写入消息了。然而,日志等级序列已经被存储到log_buf缓冲器中了。这样就使后
来能够访问log_buf以获取消息日志等级的代码(请参看25998行),不会再产生显示混乱
信息序列的现象。
25892:如果内层for循环发现一新行,那么buf中的剩余字符(如果有的话)将被认为是
新的消息,因此msg_level会被重置。但是无论怎样,外层循环都会持续到buf清空为止。
25895:释放在25845行获取的控制台锁(console lock)。
25896:唤醒等待被写入控制台日志的所有进程。注意即使没有文本被实际写入任何控制
台,这个过程也仍然会发生。这样处理是正确的,因为无论是否要往控制台中写入文本,
等待进程实际上都是在等待从log_buf中读出信息。在25748行,进程被转入休眠状态以等
待log_buf的活动。在休眠、唤醒和等待队列中所使用的机制将在下一节中进行讨论。
25897:返回日志中写入的字符长度。
如果对于每个字符的处理工作都能减少一点,那么从25869行开始的for循环就执行得更快
一点。当循环存在时,我们可以通过只在循环退出时将logged_chars更新一次来稍微提高
运行速度。然而我们还可以通过其他努力来提高速度。由于我们可以预知消息的长度,因
此log_size和log_start可以到最后再增长。让我们来实验一下这样能否提高速度,下面
是一段经过理想优化的代码:
请注意循环通常只需要执行一次,只有在log_buf末尾写入信息需要折行时才会多次执行
。因而log_size和log_buf只需要更新一次(或者当写入需要换行时是两次)。
这时速度的确提高了,但是有两个原因使我们并不能这样做。首先,内核可能有自己特有
的memcpy函数,我们必须确保对memcpy的调用不会再次进入对printk的调用(有一部分内
核移植版定义了自己特有的速度较快的memcpy函数版本,因此所有的移植都要在这一点上
保持一致)。如果memecpy调用printk来报告失败,那么就有可能触发无限循环。
然而在这一点上也并不是真的无药可救。使用这种解决方案的最大问题在于该内核循环的
形式中也要留意新行标志符,因此使用memcpy将整个消息拷贝到log_buf中是不正确的:
如果此处存在新行,我们将无法对其进行处理。
我们可以试验一个一箭双雕的办法。下面这种替代的尝试虽然可能比前面那种初步解决方
法速度要慢,但是它保持了内核版本的语意:
(请注意gcc的优化器十分灵敏,它足以能检测到循环内部的表达式log_buf+LOG_BUF_LEN
并没有改变,因此在上面的循环中试图手工加速计算是没有任何效果的。)
不幸的是,这种方法并不能比现在的内核版本在速度上快许多,而且那样会使得代码晦涩
难懂(如果你编写过更新log_size和log_start的代码,你就能清楚地了解这一点)。你
可以自己决定这种折衷是否值得。然而无论怎样,我们学到了一些东西,通常,不管成功
与否,改进内核代码都可以加深你对内核工作原理的理解。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)