查看原文
其他

Linux 内核设计模式(一)

2016-09-13 赵菁菁 轩语轩缘 Gad-腾讯游戏开发者平台

在内核社区中,讨论热度持续高涨的话题之一就是保持质量。非常明显的是:我们需要保持、甚至提高质量,不太明显的是:如何最佳实现这一目标。一个广泛使用的方法已经真正成功实现了这个目标,该方法增加了内核各种方面的可见性,这使得那些方面的质量更加明显,所以这往往会带来质量的提升。

这种可见性的增加可以有许多形式:

checkpatch.pl脚本强调了许多来自公认代码格式化样式的偏差,这鼓励大家(记得使用此脚本的人)修正那些样式错误。因此,通过增加样式的可见性,我们提升了外观的统一性,那么也就在一定程度上提升了质量。

lockdep”系统(启用时)在锁(以及相关状态,如:是否启用了中断)之间动态测量依赖性,之后“lockdep”会报告任何看起来奇怪的内容。这些怪事不总是意味着可能是死锁或者类似问题,在许多情况下,死锁的可能性可以忽略。所以,通过增加锁依赖图的可见性,质量可以得到提升。


内核包括各种其他的可见性增强功能,比如:内存未使用区域的“定位”,这样一来不合法访问就会更加明显;或者简单地使用符号名称,而不是使用堆栈中普通的十六进制地址,这样一来bug报告可用性就更高了。


在更高层次上,“git”修订追踪软件用于跟踪内核变化,让我们很容易看到谁在什么时候做了什么。事实是该软件鼓励将注释附到每个补丁上,这样我们回答“为什么代码是那样的”问题就容易得多了。这种可见性可以加深开发者的理解,随着更多的开发者更好地了解,可见性也很可能提高质量。


在提升可见性“会”或者“可以”提高质量的地方,有许多其他区域,在这些区域中,我们将探讨一个特殊区域,在这里你的作者感觉可见性可以按照一种方法提升,这种方法将会带来质的改进。那个区域阐释了内核特定设计模式。


设计模式


“设计模式”是一个概念,最初是在建筑领域的名词,之后被引入到计算机工程中,特别是面向对象编程的领域,参考1994年出版物《设计模式︰ 可重用面向对象软件元素》。维基百科有关于本主题进一步的

简单地说,设计模式描述了设计问题的特定分类,并详细阐述了解决这类问题的已经在过去被证明有效方法。设计模式的一个特别的好处是它结合了问题说明和解决办法描述,并且给了它们一个名字。一种模式拥有一个简单且令人难忘的名字是特别有价值的,如果开发者和审阅者知道相同模式的相同名字,那么重要的设计决策就可以用一两句话传达,这样一来可以让决策可见性更高。

在 Linux 内核代码库中有许多被认定为有效的设计模式,然而大部分的设计模式都没有被文档化,因此,把这些设计模式提供给其他开发人员就不那么容易了。我的希望是通过明确地记录这些模式,让这些设计模式得到更广泛的使用,因此,开发人员将能够更快地找到共同问题的有效解决方案。

在本系列的其余部分中,我们将会着眼于三个问题领域,并寻找各种较大变化范围和重要性的设计模式。我们这样做的目标不仅是要阐明这些模式,也是为了展现这种模式的价值和适用范围,以便其他人也可能会努力解释他们见过的模式。

在整个系列中,我给出了来自Linux 内核的大量实例,因为这些例子是阐释任何模式的重要组成部分,除非另有说明它们都是来自2.6.30-rc4。


引用计数器

使用引用计数器管理对象生命期的想法是相当普遍的。核心理念就是采用一个计数器,当得到一个新的引用时计数器增加;当引用释放时计数器减少。当此计数器达到零时,对象使用的任何资源(如用于存储它的内存)都可以被释放。

管理引用计数机制似乎很简单,但是也有一些细微之处,让人很容易误会该机制。部分由于这个原因,Linux 内核有一个(2004 年)称为"kref"的数据类型,带有相关的支持例程 (见Documentation/kref.txt, <linux/kref.h>,和 lib/kref.c)。这些例程封装了一些微妙之处,特别是这些例程表明了给定的计数器以一种特殊方式被用作引用计数器。如上所述,名字对于设计模式是非常有价值,而且对于审阅者来说,仅仅是为内核开发者提供那些名称来使用就已经有重要意义了。

AndrewMorton说:

我更在乎能够说“啊,它使用 kref。我明白那种惯用语,我知道它调试功能很好,我知道它能找出常见的错误”。这是比“噢,废话,这件事实现它自己的引用计数-我需要为其审阅的常见错误”更好。

在 Linux 内核中,依据对于设计模式的明确支持,kref的内容给内核提供了tick和cross。当kref清晰地体现了一种重要设计模式时,就应该能得到tick,并且详尽描述tick,在代码中对于tick的使用清晰可见。但是tick需要一个cross,因为kref 只封装了关于引用计数故事的一部分。有一些引用计数的用法,这些用法不是很适合kref模型,我们很快就会看到这一点。为引用计数设定一种"祝福"机制,这种机制不提供所需的功能,但是因为人们可能在kref本不属于的地方使用kref,所以实际上这种机制可能引发错误,所以我们认为它应该只是工作,其实它不会如人所愿。

理解引用计数复杂性的一个有用的步骤是:了解通常有对于一个对象的两种明显不同的引用,事实上,还可以是三种甚至更多,但是那非常罕见,通常可以从两种的案例中派生出来。我们将两种引用类型叫做“外部”和“内部”,虽然在一些情况下“强”和“弱”可能更合适。

“外部”引用可能是我们最熟悉的那种引用。它们通过“get”和“put”方法计数,保有该计数器的子系统可能与管理对象的子系统相距甚远。计数外部引用的存在有一个强大而简单的意义:此对象在使用中。

相比之下,"内部"引用往往不计数,只会被内部地带到管理对象(或者一些关系近的对象)的系统中。不同的内部引用可以有非常不同的含义,因此对于实现也有非常不同的影响。

可能内部引用最常见的例子就是缓存,缓存提供“按名查找”服务。如果你知道一个对象的名称,如果对象确实在缓存中存在的话,你可以应用缓存来获取外部引用。这种缓存会持有一个对象列表,或者对象是在大量列表的其中一个里,这些列表可能是在哈希表下。在这样列表上的对象的存在就是对象的引用。但是,它很可能不是一个计数的引用,它不意味着“此对象在使用中”,只意味着“此对象在徘徊以防有人需要它”。直到所有外部引用都已经被舍弃,对象才会从列表中移除,甚至对象有可能这时候也不会立即移除。很明显内部引用的存在和性质对于引用计数如何实现有重要影响

一个分类不同引用计数样式的有用方式是:通过必需的“put”操作实现。“get”操作永远都是相同的,它取得外部引用,并生成另一个外部引用,它的实现类似下方代码:

assert(obj->refcount > 0) ; increment(obj->refcount);

或者,在Linux内核C:

BUG_ON(atomic_read(&obj->refcnt)) ; atomic_inc(&obj->refcnt);

注意那个“get”不能在一个未引用的对象上使用,那里还需要一些其他内容。

put”操作有三个变体。虽然在用例中可以有一些重叠,但是为了代码清晰,还是让它们分开比较好。Linux-C中的三个书写方式是:

1. atomic_dec(&obj->refcnt);

2. if (atomic_dec_and_test(&obj->refcnt)) { ... do stuff ... }

 3. if (atomic_dec_and_lock(&obj->refcnt, &subsystem_lock)) {

                ..... do stuff ....

                         spin_unlock(&subsystem_lock);

             }


“kref"样式

从中间开始,第二种样式是用于 kref样式。当对象不会比其最后外部引用生命长时,这种样式很合适。当引用计数变为0时,对象需要被释放或以其它方式处理,因此需要对零条件进行atomic_dec_and_test() 测试。

通常适合这种样式的对象不需要担忧的内部引用,这与sysfs中的大多数对象情况一样, 这是kref的“重要用户”。如果相反,使用 kref 样式的对象没有内部引用,就不能允许从内部引用创建外部引用,除非那里有已知的其他外部引用。如果有需要,一个初始量是可用的︰

atomic_inc_not_zero(&obj->refcnt);

假设它的增量不是0,之后返回一个结果表示成功。atomic_inc_not_zero() Linux内核中是一个相对新的创造,它作为无锁页面缓存工作的一部分在2005年晚期出现。由于这个原因,这个初始量没有被广泛使用,一些本可以从中得到好处的代码取而代之用了自旋锁。难过的是,kref包也没有使用这个初始量。

这种引用的样式的一个有趣的例子没有使用kref,甚至没有使用atomic_inc_not_zero()(尽管它可以并且可以说是应该使用),这个例子在struct super中使用两个引用计数器:s_count 和 s_active。

s_active 完全符合引用计数器的 kref 样式。超级块开始工作, s_active 值为1 (在 alloc_super()中设置),并且,当s_active 变为零,更远的外部引用不能被获取到。此规则在grab_super()中编码,尽管这不是你们能马上明白的。只要s_active不为零,(由于历史的原因)当前代码就添加一个非常大的值(S_BIAS), 并且grab_super()会测试s_count超过S_BIAS的值,而不是测试s_active为零。如果要测试后者,使用 atomic_inc_not_zero()也很容易实现,而且还避免使用自旋锁了。

s_count 提供了一种不同的同时带有"内部"和"外部"方面的引用。因为它的语义比s_active计数引用的语义要弱得多,所以它是内部的。s_count计数的引用只是意味着“这个超级块只是现在无法释放”,而没有断言它确实是在活跃中。因为它很像一个生命从1开始的kref(好吧,实际上是1 *S_BIAS),所以它是外部的,当它变为零 (在 __put_super()中), 超级块被销毁。

所以这两种引用计数器可以用两个kref代替,假如:

S_BIAS 设为1

grab_super() 使用atomic_inc_not_zero()而不是对S_BIAS进行测试

一些自旋锁调用消失了,细节省略了,作为读者的练习。


“kcref”样式

Linux 内核没有"kcref"的对象,但是这个名字似乎很适合提出下一个引用计数器样式。"C"代表"缓存",因为这种风格在高速缓存中很常用。所以它是一个内核缓存的引用。

kcref 使用 atomic_dec_and_lock() ,如上面第三种所述。这样做是因为,它被释放或检查来看看是否需要任何其他特殊处理。这需要在下一个锁下完成,以确保在计算当前状态期间没有获取新的引用。

这里提到一个简单例子:i_count 引用在struct inode 中,iput()的重要部分如下:

 if(atomic_dec_and_lock(&inode->i_count, &inode_lock))

            iput_final(inode);

其中 iput_final()检查inode的状态,决定是否可以销毁inode,或者只是把它放在缓存中以防之后会被重用。

除此之外还有一些事情,inode_lock可以防止从inode哈希表的内部引用创建新的外部引用。由于这个原因,只有在拥有inode_lock时才允许将内部引用转换成外部引用,支持这个的功能被称为iget_locked()(或iget5_locked())不是偶然。

稍微复杂一点的例子在struct dentry中,其中d_count管理一个像kcref这样的内容,这个例子更复杂,因为在我们确定没有新引用可以被获取之前,需要先获取两个锁——dcache_lock  de->d_lock。这需要我们要么掌握一个锁,之后tomic_dec_and_lock()另一个(如prune_one_dentry()中),要么我们atomic_dec_and_lock()第一个,之后声明第二个并重新测试引用计数器——如dput()中。事实上这是一个很好的例子,你永远不能假定你已经封装了所有可能的引用计数样式,几乎不可能预见需要使用两个锁。

更加复杂的kcref样式引用计数器是中的mnt_count,这里的复杂性是这种结构拥有的两种引用计数器的相互作用;mnt_count是外部引用的相当简单的计数器,mnt_pinned为来自流程审计模块的内部引用技术。特别的是,它计算在文件系统(就这样吧,其实可以起一个更有意义的名字的)中打开的审计文件的数量。复杂性来源于这样一个事实:当只剩几个内部引用时,这些内部引用全都被转换成外部引用。在此探索这些细节留作感兴趣读者的练习。


"plain"样式

引用计数最后的一个样式只包括递减引用计数器(atomic_dec()),而不做其他任何事。这种样式在内核中相对比较少见是有充足理由的,随意放置未引用的对象不是个好主意。

在structbuffer_head中是应用此样式的一个实例,fs/buffer.c和<linux/buffer_head.h>管理该结构体,put_bh()函数简单来说如下:

   static inline void put_bh(struct buffer_head *bh)

    {

       smp_mb__before_atomic_dec();

       atomic_dec(&bh->b_count);

    }

这样是可以的,因为buffer_heads有生命期规则,这些规则与页紧密相关。一个或多个buffer_heads获取页分配后,可以把页分为一个个较小部分(缓冲区),这些缓冲区往往留在那里,直到页被释放,此时将清除所有buffer_heads(try_to_free_buffers()调用drop_buffers()清除)。

一般情况下,如果已知总存在一个内部引用的话,“plain”样式是适合的,这样对象就不会丢失,而如果还有一些处理需要借由这个内部引用,最终也会找到并且释放对象。


反面模式

作为设计模式的简介,需要对引用计数器的回顾做一个总结,我们会讨论一个相关的反面模式概念。设计模式是已经展现到工作中并且应该被鼓励的方法,反面模式就是历史告诉我们效果不好并且不应鼓励的方法。

你的作者想要建议:在引用计数中使用"bias"是一个反模式的例子。在这个语境中,bias就是一个很大的值,它加上或者减去引用计数,用于有效地存储一位信息。我们已经看见了s_count为超级块管理bias的想法。在这种情况下bias的存在表明,s_active 值非零,这是很容易直接测试的。所以这里bias没有加任何值,只是掩盖了代码的真正目的。

另一个bias的例子在fs/sysfs/sysfs.h和fs/sysfs/dir.c中,受struct sysfs_dirent管理。有趣的是, sysfs_dirent像超级块一样有两个引用计数器,也叫s_count 和 s_active。在这种情况下,当入口失效时,s_active的负bias值很大。相同的那一位信息也会在标识词s_flags中有效地存储,而且更加清楚。在标识中存储单个位信息很容易理解,将它们存储为计数器中的bias,这种做法应作为首选。

一般情况下,使用bias不会增添任何清晰性,因为它不是一种常见模式,它不能比单个标识位提供更多的功能,内存太紧密了以至于不能找到一位来记录的情况是极其少见的,记录的内容包括任何bias存在另外使用的东西。由于这些原因,引用计数器中的bias应该被认作是反面模式,而且应该尽可能避免使用。


结论

本篇文章带来了围绕引用计数器的各种设计模式的探讨。只有像“krefvskcref”和“外部”vs“内部”引用这样的术语对于提高不同引用和计数器的行为可见性是非常有帮助的,当我们处理krefkcref时有代码体现这一点,每次都使用这些代码对于开发者和审阅者来说都非常有帮助,开发者可能第一次发现选择正确的模型很简单,审阅者可以更清晰地看到究竟要实现什么。

我们在这篇文章中提到的设计模式有:

kref: 当一个对象的生命期只能维持到最后的外部引用消失时,kref是合适的。如果有任何对于对象的内部引用,这些内部引用只能用atomic_inc_not_zero()提升为外部引用。例子:struct super_block中的s_active和s_count。


kcref:当最后的外部引用消失对象依然存在时,带有atomic_dec_and_lock()的kcref是合适的。一个内部引用只能在保有子系统所的地方被转换成一个外部引用。例子:struct inode中的i_count。


plain:当一个对象的生命期隶属于其他对象时,plain引用模式是合适的。对象上的非零引用计数器必须被看做是父对象的内部引用,而且把内部引用转换成外部引用必须和父对象遵守相同规则。例子:struct buffer_head 中的b_count。


bias的引用: 当你感觉有必要在引用计数器中向值中添加大的bias来表示一些特别状态时,不要这么做!在别的地方使用标识位。这是一种反面模式。


下周我们会继续进行另一领域的探讨:Linux内核已经证明了一些成功的设计模式,并且探讨了复杂数据结构的较丰富部分(本系列的第二部分和第三部分已经上线)。


练习

在准备这个系列时,你的作者已经被提醒:没有什么代码的直接研究成果可以用来澄清对于这些问题的理解。考虑到这一点,这里有一些为感兴趣读者准备的练习:


  • 在struct super中用kref替代 s_active 和 s_count ,在过程中舍弃S_BIAS。将结果与最初使用正确性、可维护性和性能“三剑客”产生的结果进行比较。


  • 为mnt_pinned和操作它的相关函数选择一个更有意义的名字。


  • 向kref库添加一个利用atomic_inc_not_zero()的函数,之后利用这个函数,在net/sunrpc/svcauth.c——一种违反了kref抽象的使用方式中(或)移除kref上的atomic_dec_and_lock()。


  • 在struct 页(例如见mm_types.h)检验_count引用计数器,确定它的行为是否非常像kref或者kcref(提升:它不是“plain”)。这应该涉及到确定任何和所有内部引用以及相关的上锁规则。确定页面缓存(struct address_space.page_tree)是否拥有一个计数的引用,或者解释它为什么不应该拥有,这会包括理解page_freeze_refs()和理解它在__remove_mapping()和page_cache_{get,add}_speculative()中的用法。


  • 奖励积分:提供一系列最小自包含补丁,用于实现任何已经被上述调查证明有效的改变。

【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权;

GAD小妹时间


GAD小妹

【今晚直播】点击阅读原文观看直播

萤火:手把手教你做有意思的设计



近期热热文


技术无罪?这真的是程序员们的免死金牌吗?

一个小技巧,解决DirectX 9半像素偏移问题



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存