Chinaunix首页 | 论坛 | 博客
  • 博客访问: 541173
  • 博文数量: 75
  • 博客积分: 2374
  • 博客等级: 大尉
  • 技术积分: 933
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-18 14:27
文章分类

全部博文(75)

文章存档

2014年(1)

2013年(17)

2012年(10)

2011年(15)

2010年(23)

2009年(9)

我的朋友

分类: LINUX

2010-03-18 15:56:25

这是翻译版,英文原址:

========

设计模式最早来源于建筑学,后被计算机科学引用。简单来说,一个设计模式描述了某类设计问题,并且针对此类问题给出了一个被实践证明有效的解决方案。

Linux内核的开发中也遇到过很多设计问题,并且针对这些问题,内核开发者给出了很好的通用解决方案,只不过长久以来没有人很好的归纳和文档化这些设计模式,导致不是每个开发者都能对这些设计模式心知肚明。

以下介绍了几种Linux内核的常用设计模式

Reference Counts

使用RC来管理一个对象的生命周期是很普遍的。RC的主要思想就是,使用一个计数器,当对象被引用时递增计数器,当引用被解除时递减计数器。当计数器的值为零时,对象所使用的各种资源可以被释放。

管理RC的机制看起来是很直接的。然而,有些微妙之处会使得这个机制产生问题。由于这个原因(当然还有其他的一些原因),Linux内核中出现了一个数据类型”kref”以及围绕它的一些辅助函数(参考Documentation/kref.txt, , and lib/kref.c). kref封装了一些容易引起问题的“微妙”,值得一提的是,它们还显式地告诉程序员,某个计数器是作为RC来使用的。由于命名对于设计模式来说是非常重要的,所以在这里提供kref这样一个名字给内核开发人员来使用,会让评审者更加轻松。

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);

}

The “kref” style

从中间的开始,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, 并且:

  • S_BIAS 设为 1
  • grab_super() 使用atomic_inc_not_zero()而不是判断是否大于S_BIAS

spinlock 也可以不要了。

The “kcref” style

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,它是来自进程统计模块的内部引用的计数器。很特别的是它记录了有多少个统计文件被打开了。这里的复杂在于,当只有内部引用存在时,它们将全部转化为外部引用。你可以深入研究一下细节。

The “plain” style

最后一种RC类型是只递减计数器而不做任何其他事情。这种类型在内核中比较少见。理由很简单,把一个不再被引用的对象留在原地并不是个好主意。

struct buffer_head使用了该类RC(fs/buffer.c and ). 看一下这个函数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们都会保留在页中,直到页被释放,到时所有的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者来说可以更好地看清代码的原意。

本篇所介绍的设计模式:

  • 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模式。非零的RC必须被当作传递给父对象的内部引用。而内部引用提升为外部引用的规则必须和其父对象的规则一致。例如:struct buffer_head.b_count

biased-reference: 当你觉得想要在RC上使用一个bias来标志一些特殊状态的时候,请不要这样做,而应该使用一个flag。

阅读(1100) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~