分类: LINUX
2010-03-18 15:56:25
这是翻译版,英文原址:
========
设计模式最早来源于建筑学,后被计算机科学引用。简单来说,一个设计模式描述了某类设计问题,并且针对此类问题给出了一个被实践证明有效的解决方案。
Linux内核的开发中也遇到过很多设计问题,并且针对这些问题,内核开发者给出了很好的通用解决方案,只不过长久以来没有人很好的归纳和文档化这些设计模式,导致不是每个开发者都能对这些设计模式心知肚明。
以下介绍了几种Linux内核的常用设计模式
使用RC来管理一个对象的生命周期是很普遍的。RC的主要思想就是,使用一个计数器,当对象被引用时递增计数器,当引用被解除时递减计数器。当计数器的值为零时,对象所使用的各种资源可以被释放。
管理RC的机制看起来是很直接的。然而,有些微妙之处会使得这个机制产生问题。由于这个原因(当然还有其他的一些原因),Linux内核中出现了一个数据类型”kref”以及围绕它的一些辅助函数(参考Documentation/kref.txt,
Andrew Morton说:
我很希望看到一个patch时,我可以说”啊哈,它用了kref。我了解这个玩意儿,我知道它很安全而且考虑了各种出错处理。”我不希望说:”哦,该死的,这个patch居然实现了它自己的RC方式,我需要review这个实现是否有问题。”
从内核显式地支持设计模式这方面来看,引入kref,可以说即成功又不足。说成功是由于,kref非常清楚地把一种重要的设计模式具体化,并且被很好地文档化,其使用也是高可视的。说到不足,由于kref只是包装了RC的部分情形。内核中还有一些用到RC的地方无法用kref模式来很好的解决。一个不能提供完全功能的RC机制实际上在鼓励错误,因为大家可能会在不适用的地方使用kref并且还以为kref可以搞定。
为了理解RC的复杂性,需要先理解:一般来说,有两种截然不同的引用对象的方式。我们称之为外部引用和内部引用,在某些场合也会被称为强引用和弱引用。
我们通常想到的是外部引用。一般通过get,put来改变计数器。并且在这种模式下,一个子系统的对象可以被其他子系统的对象引用。存在一个外部引用表示:对象正在被使用。
相反的,一个内部引用一般是不会被计数的,并且只被对象所属的子系统内部持有。不同的内部引用可以有完全不同的意义以及不同的实现。
也许最常见的内部引用实例是:提供”lookup by name” 服务的高速缓存。如果你知道对象的名字,就可以在高速缓存中找到这个对象,并获得一个外部引用。这样的高速缓存中的对象是通过一个链表串起来的,而对象在链表中的“存在”就是对象的一个内部引用。不过它并不是一个计数性质的引用。它不表示“对象正在被使用”,只表示“对象被索引以备有人想用它”。这种情况下,只有当此对象的所有外部引用都被解除时,对象才会从链表中删除或者延迟一段时间后删除。很明显,内部引用的存在以及其特性会对RC的实现造成很大的影响。
通过对put操作的实现可以看出不同的RC类型的区别,而get操作上是没有什么区别的。It takes an external reference and produces another external reference. get一般是如下的实现:
assert(obj->refcount > 0) ; increment(obj->refcount);
or, in Linux-kernel C:
BUG_ON(atomic_read(&obj->refcnt)) ; atomic_inc(&obj->refcnt);
注意:get必须在对象已经被至少一处引用时才能使用。一般来说对象初始化时会把RC置1.
“put”操作有三种变种。
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);
}
从中间的开始,2号变种正是kref的类型。这种类型适合在最后一个外部引用解除后就失效的对象。当RC为零时,对象需要被释放。因此需要在此用atomic_dec_and_test()来判断是否满足条件。
适合这种类型的对象不需要担心任何内部引用。sysfs中的对象就是这种类型,它大量的使用了kref。然而,如果一个使用kref类型的对象有内部引用的话,它不允许根据内部引用来创建外部引用,除非存在其他的外部引用。必要的话,可以使用如下的原语:
atomic_inc_not_zero(&obj->refcnt);
如果计数器不为零,它会递增计数器并且返回一个结果来表示是否成功。atomic_inc_not_zero() 是一个相对比较新的函数,出现在2005年的”lockless page cache work”系列patch中。因此它还没有被广泛使用,悲哀的是,kref并没有使用这个原语。
此类引用中没有使用kref,甚至没有使用atomic_dec_and_test() 的两个例子是结构体super_block中的s_count和s_active。
s_active 正好适合kref类型的RC,一个超级块从s_active置1(alloc_super()中置1)开始它的生命周期,并且当s_active变为0时,没有新的外部引用可以再被获得。这个规则存在于grab_super()中,虽然并不是一幕了然。现在的代码由于历史原因,当s_active为非零时,在s_count之上加了一个非常大的值(S_BIAS), grab_super() 中会判断s_count是否大于S_BIAS,而不是判断s_active是否为0。这里其实只要简单地用atomic_inc_not_zero()来判断就可以了,而且可以避免使用spinlocks。
s_count 是另外一种引用,它既有内部的也有外部的。它是内部引用,由于它的语义要比s_active 这种计数的引用更弱。s_count 记录的引用仅仅表示”这个超级块目前还不能被释放” ,而并不声称它是活动的。它是外部引用,因为它很象一个kref,从1*S_BIAS 开始它的生命周期,当它变为0(in __put_super()),超级块就被释放。
因此这两个RC可以被替换为两个krefs, 并且:
spinlock 也可以不要了。
Linux内核并没有”kcref” ,但是它可以用来作为下一类RC的名字。”c”是”cached”的缩写,因为这类RC经常在高速缓存中使用。所以它是Kernel Cached REFerence.
kcref 象上面所说的变种-3一样,使用atomic_dec_and_lock()。因为在最后一次put时,需要释放对象或者做特殊处理。这需要在锁的保护下进行,以防止在满足释放的条件时,又有新的引用产生。
在结构体inode中有一个简单的例子i_count。以下是iput()中的一段重要代码:
if (atomic_dec_and_lock(&inode->i_count, &inode_lock))
iput_final(inode);
iput_final()中检查inode的状态并决定是销毁它呢,还是留在高速缓存中以备后用。
inode_lock用来防止基于内部引用(inode哈希表)创建新的外部引用。所以,只有在持有inode_lock时才能从内部引用创建外部引用。很正常的, iget_locked() (or iget5_locked())就是用来做这个事情的。
另外一个稍微复杂一点的例子是结构体dentry。它的d_count就类似kcref。不过它更复杂的是,在我们确保没有新的引用可被创建时,需要用2个锁来保护,即dcache_lock和dentry->d_lock。这要求我们持有它们中的一个,然后用atomic_dec_and_lock()来锁另外一个(prune_one_dentry());或者我们用atomic_dec_and_lock()来持有dcache_lock,然后请求获得de->d_lock并且重新判断RC(dput())。这是一个很好的例子,可以用来证明,你永远无法保证可以把所有的RC类型都封装起来。需要两个锁的情况是很难预计到的。
一个更复杂的kcref类型的RC是struct vfsmount.mnt_count。它复杂在两个RC直间的相互影响。首先,mnt_count很明显是一个外部引用的计数器。然后,mnt_pinned,它是来自进程统计模块的内部引用的计数器。很特别的是它记录了有多少个统计文件被打开了。这里的复杂在于,当只有内部引用存在时,它们将全部转化为外部引用。你可以深入研究一下细节。
最后一种RC类型是只递减计数器而不做任何其他事情。这种类型在内核中比较少见。理由很简单,把一个不再被引用的对象留在原地并不是个好主意。
struct buffer_head使用了该类RC(fs/buffer.c and
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们都会保留在页中,直到页被释放,到时所有的buffer_heads都会被清除(drop_buffers())
总而言之,”plain”型适用于:开发者知道总有一个内部引用保持对象不会释放,并且在某处这个内部引用会最终被用来找到和释放该对象。
设计模式是被证明有效以及应该被鼓励的方法,反模式是历史证明无效的并不被鼓励的方法。
笔者认为在RC中使用”bias”就是一个反模式的例子。在这里”bias”指在RC上加上或减去一个很大的值,而它仅仅是为了保存一个bit的信息。我们在前面的s_count例子中就看到了bias。在那个例子中,bias仅仅指明了s_active为非零,而这完全可以直接判断。所以bias在此完全没有价值,反而会掩盖代码的真实意图。
另外一个bias例子在struct sysfs_dirent(fs/sysfs/sysfs.h and fs/sysfs/dir.c)。有趣的是sysfs_dirent有两个refcount,就和superblocks一样,而且也叫s_count and s_active。在此,当对象被解除有效时,s_active有一个很大的负bias。这样的信息其实可以放在s_flags中。因为在flag中存储一个bit的信息是最容易理解的。
总而言之,使用bias不能增加代码的可读性,因为它不是一个通用的模式。对于refcount来说它是一个反模式,并且必须避免。
简单地使用”kref” , “kcref” ;以及 “external” , “internal”这样的术语可以很好地看出不同RC类型之间的区别。用代码来具体化它们,并且尽量使用这种封装,这对于开发人员来说,可以帮助它们更好的选择RC的模式;对于review者来说可以更好地看清代码的原意。
本篇所介绍的设计模式:
biased-reference: 当你觉得想要在RC上使用一个bias来标志一些特殊状态的时候,请不要这样做,而应该使用一个flag。