Posts Tagged ‘kernel’

BeijingLSF笔记

October 25th, 2009

BeijingLSF = Linux Storage Filesystem Workshop in Beijing,这是 Coly 等大牛组织起来的一次国内 Linux 内核存储相关部分的主要开发者的聚会,会议由 Novell 提供了一定的资助,在 linuxfb 的老巢北邮教三楼召开,参与者来自 Novell, EMC, Oracle, Intel, Fujisu, Freescale, Lenovo, Xteam 等公司,2/3 的第二天 AKA 大会的讲演者都在 BeijingLSF 上介绍了他们的工作,来参加 AKA 大会的 Herbert Xu 也大驾光临,直接从机场来到了会场。作为 Coly 的信徒,我有幸参加了这一峰会,当然写Blog是必须的了。

正如我在最后所说的,我本来是跟着 Coly 来看大牛的,结果一不小心还被大牛围观了,这基本是由于我来自一个让中国的开源社区感到陌生而又熟悉的公司——中国移动。嗯……稍后再来感慨,先谈谈这次会议的实况。

第一个时段是 Coly 亲自主持的关于 ocfs2 和 DLM 的讨论,Coly 分享了很多开发过程中遇到的问题,Oracle 和 Novell 的同学基本上主导了这个话题的讨论,其他公司的同学也参与其中了。给我留下比较深刻印象的是那个32/64位 lvb 的问题以及最后的那个 race condition 的问题。

第二个时段应该是马涛主持的讨论,但他把这个时段几乎完全贡献给大家作为自我介绍时段了,也就是在这个时段,我从瞻仰变成了被围观,呵呵。不过,说实话,我最感兴趣的是马涛介绍的 reflink,对于大量使用虚拟机的情况,这一特性无疑非常具有吸引力,而且也获悉 btrfs 也具有类似特性,我确实希望能尝试一下这个文件系统了。

接下来胡欣蔚介绍了 clvm 等内容,之前尝试过 drbd8,不过还是被 md,dm 这些模块搞得有些晕晕的,呃……对于我们的某些应用来说,多节点构成一个逻辑卷的需求是切实存在的,以后可能有机会常和这些东东打交道。

下午,吴锋光首先介绍 writeback 和 readahead,大家对 SSD 的讨论很热闹,这也是世界范围的关注重点,头一次听说 SSD 的随机读取速度可能会超过连续读,非常有趣的情况,我想,下个月我会测试一下SSD的实际情况,可能会有更多可发言的东西,呵呵。

接下来,归剑锋介绍了 cgroup iocontroller,了解了很多相关的开发进程以及背后的八卦信息,当年我在翻译LWN的CFS组调度的文章的时候接触过 cgroup 这个概念,这次听归剑锋的介绍,对 pdflush 的问题印象比较深刻。

最后的一个session是联想研究院的卢亿雷介绍他们的网盘系统的实现和一些问题,这个系统和我们的应用有些类似的地方,我也参与了一些性能相关的讨论。

结束前的light talk环节,EMC的同学们还提出下次可以分享一些不涉及机密的非开源产品的技术内容,其他参与者也表达了美好的憧憬,这让我也不由得期待明年还能有机会参与其中,呵呵。

本来没想在这里一直一天的,毕竟由于上周出差已经两个星期没和儿子有一整段时间在一起了,不过,内容和嘉宾太吸引人了,Herbert Xu这样的名人要来,如果不住在河北省,谁会早走呢,呵呵,不仅学到很多东西,而且认识了很多大牛,不虚此行。感谢 Coly,感谢组委会,感谢 Novell,感谢志愿者彭涛和李劼……

[译文] 空指针的乐趣(2)

September 14th, 2009

作者:Jonathan Corbet
原文发布日期:July 21, 2009
来源:http://lwn.net/Articles/342420/
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年9月14日

译者注:本来没准备翻译本系列的2,翻译文章实际上是译者的一种自我消遣方式,不过,翻译了1之后,有很多朋友的反应很不错,也有朋友在期待2,所以,就接着把2翻译了,时间仓促,翻译得不一定到位,请各位将就着看吧 :)

本系列的第一篇详细分析了一长串的偶然错误,利用它们可以用一个空指针解引用来对内核进行攻击。清除这个 bug 本身可以直接修改;实际上在这个问题的本质被大部分人所理解之前就已经修正了。从这个意义上讲,这个问题本身相当小,很少有发布版带有有问题的内核版本。但是,这个攻击证明了,内核中会有一大类类似的问题,在被修正之前,绝对有攻击者有机会可以发现这些错误并利用它们。

一个显而易见的问题是,当安全模块机制被配置入内核的时候,管理员指定的最小合法用户控件虚拟地址会被忽略,安全模块允许覆盖管理员指定的最小合法用户空间地址限制((mmap_min_addr)。这个行为颠覆了人们对安全模块工作的理解:它们被认为能够限制权限而从不增加权限。而在这种情况下,SELinux 的存在实际是增加了权限,大部分部署的 SELinux 都没能关闭这个漏洞(攻击代码的注释中指出,AppAmor 也不会更好)。

此外,当完全不配置安全模块的 时候,mmap_min_addr 也没有被完全强制执行。目前主线(译注:2.6.31)内核有一个补丁来保证 map_min_addr sysctl 设置永远可用,这个补丁同时也被提交进入 2.6.27.27 和 2.6.30.2 更新了(以及其他的地方)。

问题同时也在SELinux层面进行了修复。Red Hat 未来的版本的 SELinux 将不再允许无限制的(在其他方面却无特权的)进程将页面映射到地址空间底端。虽然这里仍然有一些未解决问题,特别是像WINE这样的程序将陷入混乱。目前仍然不确定如何让系统安全地支持一小撮需要映射到0页面的程序。有个想法是让 WINE 以 root 权限运行,这样,或许太过类似 Windows 的行为了,这些想法只收到很少的关注。

另一个关于 map_min_addr 的方法也必须要谈到:一个带有 SVR4 个性化的特权进程在 exec() 的时候,会有一个只读页面映射到0页面。在十分罕见的情况下,一些老的 SVR4 程序会需要那里有这么一个页面,但是,它实际让空指针攻击成为了可能。于是,另一个被并入主线和稳定版更新中的补丁用来在 setuid 的程序运行的时候重置 SVR4 个性化(或者,至少重置那个关于映射0页面的部分)。

这个改变对有些用户仍然不够,他们要求能够完全关闭这个个性化功能。这个能力用于直接运行基于 386 的 Unix 的程序,其实已经没有它 1995 年的时候所拥有的重要性了,所以,很多人质疑是否值得为这个个性化特征付出这样的代价了。Linus 答道

我们可能可以扔掉那个愚蠢的特征。它已经不再那么重要了。有人真的关心么?同时,这么多年来,我们已经发展出了很多其他个性化标志,其中有些仍然是有用的。

特别的,禁止地址空间随机化(一个个性化特征)的能力在很多情况下是很有用的。所以 personality() 应该被留下来,不过,0页映射的特征可能要一去不返了。

这一连串错误中的一个就是被编译器优化掉的空指针校验。这个校验可以阻止攻击,但 GCC 却把它优化掉了,因为理论上说这个指针不应该是空的(因为它已经被正常的解引用过了)。GCC(本身)有一个标志可以禁止某种特殊优化;所以,从现在开始,内核将缺省使用 -fno-delete-null-pointer-checks 编译开关。由于在内核中空指针的确可能是个合法的指针,无限地禁止这种优化史合理的。

有人可能会提出,虽然所有上面的改变都是好的,但可能部分地没有切中要害:高质量的内核不应该在第一次发生的地方允许解引用空指针。这些解引用本身确实是 bug,这里才是最该修正的地方。这有些有趣的历史,实际上内核开发者们常常建议忽略空指针校验。比如,如下 BUG_ON() 代码:

    BUG_ON(some_pointer == NULL);
    /* dereference some_pointer */

经常被以这样的注释而删除掉:

如果我们解引用 NULL,那么内核将基本上会和 BUG 一样显示一个信息,也会采取相同的措施。所以添加一个 BUG_ON 实际无法得到任何收益。

这是因为,解引用空指针会导致一个内核 oops。表面上,这是合理的:如果我们的硬件可以检测到空指针解引用,那么添加软件开销来检测它没什么意义。但这个原因实际上不是无懈可击的,这次的攻击代码就说明了这一点。映射到0页确实是有一定原因的,所以,一个空指针未必总是非法的。你可能会假设所有相关开发者都理解这一点,但实际上内核中仍然有很多地方确实有游泳的空指针检验代码被删除掉了。

虽然如此,大部分内核中的空指针问题可能仅仅就是失察造成的。如果没有方法在相关代码中实际使用空指针的话,大部分的空指针问题是不可被利用的,缺少检验并不能带来什么。不过,如果能把它们全都修复肯定是件好事。

发现这些问题可能可以通过 Smatch 静态代码分析工具。Smatch悄无声息很多年了,不过 Dan Carpenter 正在让它死灰复燃;他最近发布了 Smatch 为他发现的一个空指针 bug。如果 Smatch 能成为一个通用的发现此类错误的工具的话,内核将会更加安全。但不幸的是,这个检查器似乎没有吸引开发者们的很多兴趣,自由软件依旧落后这个领域中的先进技术很远,这让我们都很受伤。

另一个方法由 Kulia Lawall 所采用,她使用一个 Coccinelle “语义补丁”来发现并修复 TUN 驱动中发现的这种先解引用再检查的 bug。一系列补丁()被发布出来,用于修复一系列类似 bug。指针在检查之前被解引用可能只是内核中的空指针问题的一个子集,但每个都被程序员们认为有可能出现空指针,并且是有问题的。所以它们显然是应该被修复的。

总之,这个攻击代码可以看做是内核社区的一个起床号,十分幸运,它将帮助清理很多代码,并消灭很多安全问题。攻击代码的作者 Brad Spengler 还明确的希望得到更多:他经常会发布一些对于内核安全问题的关切:严重的内核安全 bug 被悄悄地修复,或在最差情况下被变为拒绝服务的问题。这个问题是否会改变现状呢,在内核环境中,很多 bug 会造成安全隐患,这些在 bug 被修复的时候并不会立刻显现出来。所以我们无法看到更多 bug 被以安全问题的方式被发布出来,不过幸运的是,我们将会看到更多的 bug 被修复。

[译文] 空指针的乐趣(1)

September 4th, 2009

作者:Jonathan Corbet
原文发布日期:July 20, 2009
来源:http://lwn.net/Articles/342330/
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年9月2-3日

现在,大部分读者都已经知道了 Brad Spengler 发布的“the local kernel exploit”(本地内核漏洞利用)。这一漏洞会影响 2.6.30 内核(以及 RHEL5的一个测试版 2.6.18 内核),受到了多方关注。本文将详细分析如何利用这一漏洞,以及让这个漏洞得以成真的令人震惊的一连串错误。

TUN/TAP 驱动提供了一个虚拟的网络设备,它会建立一个隧道;这一驱动在多种场合都有很有用,包括虚拟化、VPN 等很多地方。使用 TUN 时,程序通常打开 /dev/net/tun,然后使用 ioctl() 调用来建立网络端点。Herbert Xu 近来注意到,缺少对包的审计可能会导致恶意程序能够占用大量的内核内存,并导致系统性能下降。他的解决方案是通过一个补丁给该设备添加一个“伪 socket”,使它可以使用内核的审计机制。问题是解决了,但是,回头看来,它的代价是已入了一个更严重问题。

TUN 设备支持 poll() 系统调用。(在 2.6.30 内核中)实现这个功能的函数的开头是这样的:

    static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
    {
	struct tun_file *tfile = file->private_data;
	struct tun_struct *tun = __tun_get(tfile);
	struct sock *sk = tun->sk;
	unsigned int mask = 0;

	if (!tun)
	    return POLLERR;

上面有下划线的的那行代码是 Herbert 的补丁中添加的,这正是惹祸的开始。精心编写的内核代码都会小心的避免对指针的解引用,以避免 NULL;事实上,这里只在那个条件语句那里检查了 tun 指针。并且,这是件好事;现在看来,如果进行了 configuring ioctl() 调用,tun 确实将是 NULL。这时,按照预期,本来 tun_chr_poll() 应该返回一个错误状态。

但 Herbert 的补丁添加的指针解引用是在检查之前的,这显然是一个 bug。在正常操作中,这个 bug 的影响会比较有限,如果 tun 是 NULL 的话,会导致内核 oops。oops 会首先杀掉进行这个系统调用的进程,并将回溯信息加入系统日志,此外就不应该发生什么其他的事情了。最坏情况下,这应该也就是个拒绝服务问题。

依照上述推理,这是一个小问题,虽人 NULL (0)可能确实是个合法的指针地址。缺省的,不论在用户空间还是内核空间,虚拟地址空间的底部(“0页”和它上面的一些页面)都是不允许任何访问的,以用来捕捉空指针错误(如上描述)。不过,使用 mmap() 系统调用将真实的内存映射到虚拟地址空间的底部仍然是可能的。这个功能有一些合法的用例,包括运行一些过时的程序。尽管如此,大部分现代的系统都通过使用 mmap_min_addr sysctl 设置来禁止映射到0页。

安全模块检查被认为可以作为内核已经进行了的检查的一个补充,但这次,它并没有如愿工作。

这一设置应该阻止用户空间的程序区映射零页,这也就保证了空指针的解引用只会导致一次内核的oops。但是,不知何故,如果安全模块机制被配置入内核的话,2.6.30 中的 mmap() 的代码会显示地拒绝执行 mmap_min_addr 。取而代之的是将这个工作留给特定的安全模块来进行。安全模块的检查工作被认为是内核中已有的检查工作的一个补充,但它此时却并不工作。对于 0 页,安全模块会授权访问,而其他情况下则会拒绝访问。这个错误的最后一步是,Red Hat 的缺省 SELinux policy 允许映射 0 页。这样,运行 SELinux 实际是降低了系统的安全性。

但没有 SELinux 的生活也不是就一马平川了。在没有 SELinux 的时候,攻击行为会被 mmap_min_addr 限制,这似乎足够让一切结束了。但这是可以通过使用 personality() 系统调用绕过的。打开 SVR4 个性化会在程序被 exec() 调用的时候将一个只读页面映射到 0 地址,但只有进程有 CAP_SYS_RAWIO 能力的时候才会这样。所以,需要一个更进一步的欺诈行为:顶级的攻击代码设置 SVR4 更兴华,然后使用 exec 运行有特殊插件的 pulseaudio 服务器。pulseaudio 服务器是 setuid root 的,所以它将会在调用时映射到 0 页面。当调用到插件代码的时候,pulseaudio 将会放弃它的权限,但是,这时 0 页已经对攻击代码可用了,攻击代码可以让 0 页可写,并将其自己的数据放在这里。

上面这些攻击的结果就是,用户空间进程是有可能映射0页而不让 tun_chr_poll() 发生内核 oops。不过,你可能会想,攻击者还不能高兴得太早,毕竟接下来 tun 就会检查空指针。这正是这一系列错误中的下一个:GCC编译器缺省会优化掉 NULL 的彻底检验。原因在于,因为这个指针已经被解引用过了(而且也什么都没发生),所以它不可能是 NULL。所以,没有理由再去检查它了。于是,尽管这个逻辑本来在大部分情况下都有效,但是在 NULL 是一个合法指针的时候却是错误的。

所以,攻击者这时就能通过一个空 tun 指针而成功进入 tun_chr_poll() 内部了。接下来需要指出如何利用这种情况控制内核。tun_chr_poll() 中后面的下一步代码是这样的:

	if (sock_writeable(sk) ||
	    (!test_and_set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags) &&
	     sock_writeable(sk)))
		mask |= POLLOUT | POLLWRNORM;

注意,sk 的值来自于 tun 的解引用,所以它位于攻击者的控制之下。SOCK_ASYNC_NOSPACE 是 0,所以 test_and_set_bit() 调用可以用于设置内存中任何字的最低权重位。这是个小小的内存冲突,但这已经被证实是足够的了,在 Brad 展示的攻击代码中,sk->sk_socket->flags 指针指向了 TUN 驱动的 file_operations 结构;特别的,它是指向了 mmap() 函数。TUN驱动不支持 mmap() 调用,所以这个指针通常应该是 NULL,在 poll() 调用之后,它就是 1 了。

攻击代码的最后一步就是调用这个打开的 TUN 设备的文件描述符的 mmap() 调用。由于内部的 mmap() 已经不是空了(刚刚被我们设置成了 1),内核将会跳到哪里。那个地址已经在攻击代码所映射的0页面中了,所以,它在攻击者的控制之下。于是,攻击代码使用下一个跳转跳到其自己的代码处即可。这样,当内核调用(它以为的)TUN 驱动的 mmap() 函数的时候,结果就是任意代码都可以在内核模式下运行;这里,攻击代码获得了完全的控制权。

在一个良好设计的系统中,一个单独的错误很少导致灾难性的故障。而这里就是这样一个例子。很多东西都出错才导致了这个攻击成为可能:安全模块能够不顾系统策略而授权访问地位内存,SELinux 策略允许这些映射,pulseaudio 可以被攻击代码利用从而让这一映射可以被攻击代码使用,空指针在解引用之前未被检验,并且检验被编译器优化掉了,代码以某种方式使用空指针可以获取系统的控制权。这是一条长长的错误链,其中的每一环节都是让这一攻击成功的必要条件。

这个漏洞如今已经被关闭了,不过几乎可以肯定还有类似的问题。本系列的下一篇文章将会介绍内核开发者们如何应对这一攻击行为。

[译文] Linux 内核设计模式 (1)

August 30th, 2009

作者:Neil Brown
原文发布日期:June 8, 2009
来源:http://lwn.net/Articles/336224/
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年8月29-30日

[译注:初稿,未经审校,欢迎意见建议。]

内核社区中,始终受到关注的一个话题便是维持代码质量。显而易见,我们需要维护乃至提高内核的质量,不过,如何能更好的做到这一点却不那么显而易见了。一个已经取得一定成果的普适方法便是提升内核的各个方面的可见性。这将让这些方面的质量更加透明,进而可以提高它们的质量。

可见性的提高以不同的形式发生:

  • checkpatch.pl 脚本会检查代码的各式风格,将不一致的风格高亮标出,这会帮助(还记得使用这个脚本的)人们来修正代码风格上的错误。这样,通过提高代码风格指南的可见性,我们统一了代码的外观,于是在某种意义上说,提高了质量。
  • 在打开“lockdep”系统时,它可以动态计算锁(以及相关状态,比如是否允许中断等)之间的依赖性。如果有什么东西看起来比较异常的话,它会进行报告。这些异常并非总意味着可能死锁或有类似问题,但很多时候确实如此,而且这种死锁可能性是可以被消除掉的。这样,通过提升锁依赖关系图的可见性,可以提高代码质量。
  • 内核还包含着其他的可见性改进,如对不用的内存空间进行“poisoning(下毒)”,这样非法访问将更为明显,或者通过在 stack trace 中直接使用符号名称替代十六进制地址,从而使得 bug 报告能更有意义。
  • 在更高层面上,用于跟踪内核变化的”git”版本管理系统让观察补丁提交的时间和补丁作者变得十分容易。它鼓励让每个补丁的提交者都附加一个说明,由此可以获知为什么代码应该是这样的。这个可见性可以帮助理解代码,并且由于更多开发者能更好地了解代码的含义,从而可以提高质量。

除此之外,还有很多种方法可以提高其他方面的可见性,从而提高代码质量。在本系列文章中,我们将探讨一个特定的领域,这让作者们可以感到可见性可以发生质的改变的方式改进。这个领域就是阐明内核特定的设计模式。

设计模式

“设计模式”这个概念最早见于建筑学,1994年出版的《设计模式:可重用面向对象软件元素》一书将这个概念引入到了计算机工程,特别是面向对象编程领域之中。Wikipedia上有关于此主题的更加深入的介绍

简而言之,设计模式描述了一类特定的设计问题,并描述了已经被验证有效的解决这一类问题的方法细节。设计模式的一个特别的好处是,它将问题描述与解决方法的描述结合在一起并进行命名。一个模式具有一个简单好记的名称非常有好处。如果开发者和审阅者都知道一个模式的名字,那么重要的设计决策就可以用一两个词就做到有效沟通,决策问题也就变得非常具有可视性了。

在 Linux 内核代码中,很多设计模式都已经被证实是有效的了。不过,它们中的大多数都还从来没有被文档化过,对其他开发者来说也就不怎么可用了。我的希望就是显式地描述这些模式,让这些模式被更广泛地使用,进而开发者可以更快更有效的解决常见问题。

在本系列文章的后面的部分,我们将考察三个问题域,并从中发现适用范围和重要性迥然不同的各种设计模式。我们的目标不仅是描述这些模式,而且要介绍这些模式适用的范围和所具有的价值,这样,其他人也可以尝试来描述他们遇到的模式。

这个系列中将引用很多 Linux 内核中的例子,因为例子是解释模式的一个重要部分。如无特殊声明,这些例子都取自2.6.30-rc4。

引用计数

使用引用计数来管理对象的生命周期的想法非常普遍。核心思想就是使用一个计数器,当有一个新引用的时候就加一,释放引用的时候就减一。当计数器到达0的时候,这个对象所占用的所有资源(如用于存储它本身的内存)就都可以释放了。

管理引用计数的机制看起来非常直接。不过,实际上有一些陷阱会使得它非常容易出错。部分的处于这个原因,Linux 内核(自从 2004 年)就有了一个“kref”类型和一组相关机制(参考 Documentation/kref.txt, <linux/kref.h>, 和 lib/kref.c)。这些方法封装了一部分陷阱,特别的,让一个计数器明确地作为引用计数被以一种特定的方式来使用。如上所述,设计模式的名称非常有价值,开发者只提供要使用的设计模式的名字对于审阅者非常有好处。

Andrew Morton 语录

我更希望开发者们能这么说:“啊,这里用了kref。这样,我理解它用了 refcounting,我知道它被很好的 debug 了,并且知道它能处理一般的错误了。”这比下面这样强多了:“哦,这个东西实现了自己的refcounting — 这样我就得在这里对常见错误进行审阅”。

Linux 内核中引入 kref  为其显式支持设计模式既打了一个勾也打了一个叉。勾就是 kref 就是具体实现了一个重要的设计模式,良好的文档化,并在使用的时候使得代码清晰可见。而这个叉则是因为 kref 仅仅封装了部分关于引用计数的内容。有些引用计数的应用并未被 kref 模型所惠及,稍后我们将看到。一个没有提供所需的方法的引用计数具有“blessed”机制实际可以导致错误,因为人们可能会在 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” 风格

从中间一种方法开始,第2项是 kref 使用的风格。这个风格适用于对象没有其外部引用活得长的情况。当引用计数到达0的时候,对象需要被释放,否则就要进行处理,这就需要使用 atomic_dec_and_test() 来检查引用计数为为0的情况。

符合这一风格的对象通常都没有需要顾虑的内部引用,大部分 sysfs 中的对象就是这样,它们也使用了大量的 kref。但如果一个使用 kref 风格引用计数的对象有内部引用,那么它就不允许用一个内部引用来创建外部引用,除非能确定仍有其它的外部引用。如果有此必要,可以使用如下原语:

     atomic_inc_not_zero(&obj->refcnt);

这样,在计数器不为零的情况下自加,返回值指示操作是否成功。在 linux 内核中,atomic_inc_not_zero() 是一个较新引入的操作,在2005年末作为不加锁的 page cache 的一部分引入。因而,这个原语的使用还不够广泛,一些本可以使用这个原语的代码使用了自旋锁。遗憾的是,kref 包也没使用它。

这个风格的引用有一个没用 kref 的有意思的例子,甚至连 atomic_dec_and_test() 都没有用(虽然实际可以用并且确实应该用),这就是 struct super 里面的两个引用计数:s_counts_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 引用计数的意思仅在于“超级块还不能立刻被释放”,而不检查它是否真的处于 active 状态。而它却又非常类似 kref 那样,从 1 开始生命周期(实际是 1*S_BIAS),当它到 0 的时候(在 __put_super() 中)超级块就被销毁了,从这个意义上讲又是外部引用计数。

只要进行如下操作,这两个引用计数就可以用两个 kref 所代替:

  • S_BIAS 置为 1
  • grab_super() 使用 atomic_inc_not_zero() 原语,而不是和 S_BIAS 进行比较

这样,很多自旋锁都可以不用了。具体细节可以作为练习留给各位读者了。

“kcref” 风格

Linux 内核并没有 “kcref” 对象,但这个名字似乎适用于接下来的这种引用计数风格。“c”是指的“缓存(cached)”,这种风格经常用于缓存之中。所以可以称为 Kernel Cached REFerence。

kcref 引用计数如上面第三种情况使用 atomic_dec_and_lock() 。这是因为,最后一次 put 的时候,需要释放掉资源或检验是否需要其他特定的处理。这需要进行一次加锁来保证当前状态被重新求值期间不会再产生新的引用。

一个简单的例子是 struct 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() (或 iget5_locked())。

略有一点复杂的例子是 struct dentry,这里的 d_count 是用的类似 kcref 的方式管理的。它更复杂一些的原因在于在我们获得引用之前,需要先获取两个锁—— dcache_lockde->d_lock。这要求我们必须先获得一个锁,然后再用 atomic_dec_and_lock() 来获取另一个(如 prune_one_dentry 之中),或者先用 atomic_dec_and_lock() 然后请求另一个锁并重新检查引用计数,如 dput() 中的操作。这是一个很好的例子,它说明了你永远不能肯定你已经封装了所有的可能的引用计数风格。需要两个锁的情况很难被预见到。

一个更复杂的例子是 struct vfsmount 中的 mnt_count。这里的复杂性源于两个引用计数的相互影响:mnt_count 是一个典型的外部引用计数,而 mnt_pinned 是进程审计模块的内部引用计数。特别地,它统计文件系统中打开的审计文件的数量(实际应该使用个更贴切的名字)。复杂性来自于当只有内部引用存在的时候,它们将全被转化为外部引用。关于这个例子的细节同样留作练习,留给感兴趣的读者。

“plain”风格

最后一种引用计数涉及到的风格就是直接对引用计数进行减值操作(atomic_dec())而不做其他任何事情。这个风格在内核中并不常见,必须有充分的理由才会使用。毕竟随意放着一个无人饮用的对象不是个好主意。

这个风格的一个例子出现在 struct buffer_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)。它们会被保存着,直到页面本身被释放,这时,所有的 buffer_heads 都会被清除(通过 try_to_free_buffers() 调用 drop_buffers())。

通常,”plain”风格适用于那些内部引用会一直存在,对象不会丢的情况,这个内部引用的所在的进程会最终找到并释放对象。

反模式(Anti-patterns)

现在要回顾一下这个引用计数来作为设计模式的介绍,我们将要讨论反模式的相关概念。设计模式都是已经经过检验可以工作的方法,并应该鼓励使用,而反模式则是那些已经证明不能良好工作,并应被慎用的。

作者建议把在引用计数中使用”偏置(bias)“作为一个反模式的例子。偏置是一个大数,在引用计数中进行加减,它被用于保存一些信息。我们已经在超级块的 s_count 中见过这种方式了。在这个例子中,偏置的存在表示 s_active 是非零值,这很容易直接检验。所以偏置实际没有任何价值,只是使得代码的真实用意更不清楚了。

另一个使用偏置的例子是 fs/sysfs/sysfs.hfs/sysfs/dir.c 中的 struct sysfs_dirent 。十分有趣,struct sysfs_dirent 和超级块一样,有两个以用,也是叫做 s_counts_active。这里,当选项被去激活时,s_active 有一个大的负数偏置。同样信息可以被有效并更清晰地存储在一个标志字 s_flags 中。在标识中存放一些信息比用偏置的方法存放在计数器中更为易懂,也更应该被推荐。

总之,使用偏置不能增加清晰度,不是一个常用模式。它不能比一个单独的标志位提供更多的信息,极端缺少内存、无法发现其它可用内存的情况下使用偏置来存放信息的情况非常罕见。因此,引用计数中,偏置应该被视为反模式并尽力避免使用。

总结

到时候结束我们对各种引用计数相关的设计模式的介绍了。简单列出如”kref”与”kcref”,”外部“与”内部“引用这样的术语是非常有帮助的。像我们一样找到 kref 和可以用 kcref 的代码,并在所有可用的地方使用它们,这对开发者和审阅者都有好处,开发者可以在一开始就找到正确的方法,而审阅者也更容易知道代码的用意。

我们本文中涉及的设计模式包括

  • kref: 当对象的生命周期仅仅延续到最后一次释放外部引用的时候,kref 是恰当的设计模式。如果对象有内部引用,那么它们只有使用 atomic_inc_not_zero() 才能变成外部引用。例子:struct super_block 中的s_actives_count
  • kcref: 如果对象的生命周期超出最后一次释放外部引用,那么应该使用 atomic_dec_and_lock() 和 kcref。内部引用只有在获得了子系统锁的时候才能转化为外部引用。例如:i_count 中的 i_count
  • plain: 当对象的生命周期挂靠在其他对象之上的时候,应该采用plain 引用模式。对象的非零引用计数必须被看作是对父对象的内部引用,内部引用转化为外部引用必须遵循和父对象相同的规则。例子:struct buffer_head 中的 b_count
  • biased-reference: 当你想要在引用计数中使用一个大的偏移值来表征一些特殊状态的时候,停下,别这么干。使用个别的标志位吧,这是反模式。

下个星期我们将换一个领域看其中 Linux 内核已经证明了的成功的设计模式,并看一些复杂的数据结构略多的地方。

练习

作者在准备这个系列的时候就被提醒到,没有比直接研究代码更能让人理解这些问题了。所以也给感兴趣的读者留了一些练习。

  1. 使用 kref 在 struct super 中替换 s_active,抛弃 S_BIAS。比较使用 trifecta 进行正确性、可维护性和性能检查的结果 。
  2. mnt_pinned 和处理它的相关函数选择一个更贴切的名字。
  3. 给 kref 库添加一个使用 atomic_inc_not_zero() 的函数,并使用它(或相反的操作)去掉 net/sunrpc/svcauth.c 中使用的atomic_dec_and_lock() ,这里它破坏了 kref 的抽象。
  4. 检查 struct page (如 mm_types.h 中) 的 _count 引用计数,观察它的行为更像 kref 还是 kcref (提示:肯定不是 plain)。这应该包括标记所有的内部引用和相关的锁定规则。指明为什么 page cache (struct address_space.page_tree) 有一个引用计数或为什么不应该有。浙江包括理解 page_freeze_refs() 和它在 __remove_mapping() 中的使用,以及 page_cache_{get,add}_speculative()。

补充:一个系列的最小的自包含的补丁来实现上述这些研究结果的改变被证实是有用的。

[译文] Xen: 完成工作

March 22nd, 2009

http://lwn.net/Articles/321696/
By Jonathan Corbet
March 4, 2009
王旭 2009年3月22日译

曾几何时,Xen是炙手可热的虚拟化技术。Xen的开发者们拥有一个自由软件中最为领先的虚拟化解决方案,Xen看起来正是Linux虚拟化的未来。很多风投在追逐这个神话,各个发行版也竞相提供基于Xen的虚拟化平台。然而,在这条路上,Xen似乎已经走失了。XenSource的开发者们看起来似乎没有兴趣让他们的代码进入主线内核,而其他人的努力也遇到了没完没了的障碍。于是,Xen在多年以来就一直停留在主线之外了。Xen首次对外发布是2003年的事了,可Xen的核心代码仅仅在2007年10月才进入了2.6.23。

就在同时,KVM出现了,而且一下子抢走了众多的注意力。它以难以置信的速度一下子就进入了主线内核,而且很多内核开发者都毫不掩饰的表达了他们对于KVM锁采用的方法的青睐。就在最近,Red Hat发布了他们的KVM虚拟化时间表,让这种偏爱更加官方了。同时,lguest 成为了那些想要尝试虚拟化的人的简单选择。

Xen的故事是“上游优先”策略产生的原因的一个经典实例,该策略要求代码在被提供给客户之前应该首先进入主线。发布者们忙不迭地首先发布Xen,然后就发现他们自己正在支持一段out-of-tree的代码,这些代码常常根本不被他们的开发者锁支持。特别地,对外发布的Xen通常只支持相当老的内核,希望提供一些更新的内容的发布者需要做很多工作来让它们一起工作。现在,至少部分的发布者(发行版)已经开始走向其他的选择了,而高层的内核开发者则在问一个问题——事到如今,是否还值得来把剩下的Xen代码merge进来。

所有人都在说,Xen已经岌岌可危了。不过,或许更严重的所谓Xen已经死掉了的流言有些言过其实了。

主线中的代码实现了 Xen 的 DomU 概念——一没有访问硬件的权限的非特权域。而一个完整的Xen则需要更多功能;需要一个用户空间(原文如此)的hypervisor(是以GPL许可发布的)和内核中的Dom0代码。Dom0是hypervisor启动的第一个域;定性情况下,它比其他Xen guest拥有更多的特权。Dom0用于在管理策略的管理下小心的向其他域提供那些诸如访问硬件、网络等的特权。一个实用的Xen系统必须包含Dom0代码——目前是一大块out-of-tree的kernel代码。

Jeremy Fitzhardinge 希望改变这一局面。他发出了一组Xen Dom0 patch ,希望能够进入 2.6.30。在审阅者中,Andrew Morton问了这个问题

我讨厌成为说着句话的人,不过,我们确实应该坐下来弄明白把它merge到Linux当中来是否真的合适。我觉得Xen是实现虚拟化功能的老方法了,世界已经专项新方向KVM了。

三年之内,我们是不是会后悔我们merge了Xen?

Andrew的问题本质上是:(1)完成这个工作需要什么代码(这次提交的代码之外),和(2)这么做的原因是什么?第一个问题的答案 是“再有2-3个尺寸差不多的补丁集就将提供引导dom0所需的所有代码”。之后有一些其他的不同内容可能不会进入主线。不过,Jeremy说,让核心代码进入主线将会减少发行版需要打的out-of-tree的补丁,让所有人的生活变轻松一些。对于第二个问题,Jeremy回应到:

尽管kernel周期中有来自kvm的影响,Xen还是拥有庞大并且仍在增长的用户群的。此时,他们都工作在大量的out-of-tree补丁之上,这对大家来说都十分不快。让他们成为主线内核的一部分再好不过了。你知道,这和我们争论的所有其他事情都一样。

此外,Jeremy表示,Xen仍然有其存在的价值。它的设计在很多方面和KVM有本质的不同;这封邮件 精辟的解释了这些差异。所以,Xen作为另一个解决方案已然有意义。

Jeremy指出的一些Xen的优势包括:

  • Xen的实现页表的方法消除了shadow页表或guest中嵌入页表的需要;这样,可以让许多负载获得更好的性能。
  • Xen的hypervisor是非常轻的,并且可以独立运行;而KVM的hypervisor则就是Linux内核本身。似乎有些厂商(HP 和Dell被点到名了)在他们的很多系统的firmware中会提供一个Xen的hypervisor。这些代码于是和可以和其他的东西一样具有可以立即使用的特性了。
  • Xen的半虚拟化方式可以使用不支持虚拟化的硬件一起工作。而KVM则需要硬件支持。
  • 分离hypervisor, Dom0和 DomeU的方式让安全性检查更简单了。各个域之间的隔离还允许让每个设备都在一个独立的域中提供服务,这可以看做是一种重量级的微内核架构。

与此不同,KVM相对更简单、更易用,并且可以访问所有的内核特征。在Jeremy看来,两个系统在Linux中各有不同的位置。

这个讨论在最后重归沉默,这表明Jeremy的解释相当不错。Xen历史上犯过错误,但这个项目仍然活着,而且仍然有它的原因继续存在下去。你的编辑(译 著:编辑就是本文原文作者)预测,Dom0的代码在尝试进入2.6.30的merge window的时候还会遇到一定的反对意见。

[译文] Linux: The Journaling Block Device

August 21st, 2008

June 21, 2006 – 2:40am

Submitted by Kedar Sovani on June 21, 2006 – 2:40am.
http://kerneltrap.org/node/6741
原作者与版权信息:

Amey Inamdar (www.geocities.com/amey_inamdar) is a kernel developer working at Kernel Corporation. His interest areas include filesystems and distributed systems.

Kedar Sovani (www.geocities.com/kedarsovani) works for Kernel Corporation as a kernel developer. His areas of interest include filesystems and storage technologies.

Copyright (c) 2004-2006 Kedar Sovani and Amey Inamdar

王旭 (gnawux(at)gmail.com, http://wangxu.me/blog/) 于2008年8月19-21日译

原子性是操作的一种属性,标明这个操作要么完全成功,要么完全失败,不会处于中间状态。磁盘可以保证扇区级的原子性。这意味着写一个扇区的操作,要么完全成功,要么根本没写。不过,当一个操作涉及到多个扇区的时候,就需要高层机制了。这种机制应该确保全部的扇区修改都是原子性的。如果不能做到原子性的话将导致数据的不一致性。本文就将讨论 Linux 中的日志块设备(JBD)的实现。

首先来看看这些文件系统的不一致是如何产生的。假设一个应用程序建立了一个文件。文件系统内部于是就减少一个 inode 数量,初始化磁盘上的 inode,并为文件的父目录添加一个对应于新文件的条目。但如果在上述操作进行到一半的时候计算机崩溃了,那会怎样呢?在这种情况下,不一致性就被引入到文件系统当中了——可用的 inode 数量减少了,但磁盘上 inode 的初始化可能还没有进行。

要发现这种不一致性的惟一方法便是扫描整个文件系统,这个程序称为 fsck (filesystem consistency check)。对于很大的系统,一致性检查可能需要相当长的时间(可能高达数小时)来检查并修复这些不一致问题。和你想的一样,这么长的宕机时间是难以接受的。更好的手段当然是在第一时间就避免不一致性的产生,这可以通过为操作提供原子性来达到。日志就是为这些操作提供原子性的一种手段。简单地说,使用日志就像使用了一个草稿本。你在草稿本上进行操作,当操作正确,你满意了之后,可以把他们誊到最终版本上。

对于操作系统而言,所有的元数据和数据都存储在文件系统所在的块设备上。日志文件系统使用一个日志区域作为草稿。日志可以是同一个块设备的一部分,或者是一个单独的设备。一个日志文件系统首先记录所有在日志中的操作。一旦这些作为一个原子操作的操作们都被记录到日志之中了,它们才会被一起写到实际的块设备中。在后文中,“磁盘(disk)”将指代“真实的块设备”,而“日志(journal)”将指代“日志区域”。
日志恢复场景
这个例子中,根据上面的需求,改动了3个块——inode 计数块,包含了磁盘上的 inode 的块包含了用于插入条目的目录所在的块。所有这些块首先都被写入到日志中。之后,一个特殊的块——提交记录,被写入到日志中,提交记录用于指示写到日志中的所有的属于一个原子操作的块。

下面三个基本场景就反映了日志文件系统是如何工作的:

  • 当第一个块被写入日志的时候机器崩溃了。在这种情况下,当计算机重启之后检查日志,它会发现一个有没有提交的操作。这就标明可能有一个未完成操作。那么,既然现在还没有对磁盘进行修改操作,也就保持数据一致性。
  • 当提交记录被写入日志的时候机器崩溃。在这种情况下,机器重新启动并检查日志,它会发现一个操作和它的提交记录正在那里。提交记录指明有一个完成了的操作可以被写入到磁盘之中。所有的属于这个操作的块都会依据日志被写到他们在磁盘中的实际位置。
  • 所有的三块都被写入日志,但提交记录还没有进入日志的时候机器崩了。即使在这种情况下,因为没有提交记录,磁盘上还是没有任何修改。这个场景中实际和第一个场景区别不大。

类似的,任何其他的崩溃场景都可以归到上面的头两种场景之中。这样,日志确保了文件系统的一致性。用于查找和重放日志的时间与文件系统一致性检查的时间相比简直是微不足道的。

日志块设备

Linux 日志块设备(JBD)提供了这个用于提供操作原子性的草稿纸。这样,由控制着一个块设备的文件系统可以在同一个设备或是其他块设备上使用JBD来保障一致性。JBD 实现为一个提供一组供这些应用使用的 API 的模块。下面的章节将描述 2.6 内核中的 Linux JBD 的原理和实现。

在转入到 JBD 的实现细节之前,我们需要了解一些 JBD 用到的对象。日志(journal)是管理一个块设备的更新的内部记录(log)。正如上文提到的,更新首先会放到日志之中,然后反射到它们在磁盘上的真实位置。日志区域被当作一个环状链表来管子。也就是说,当日志记满的时候会重用之前用过的区域。

handle 代表一个原子更新。需要被原子地完成的全部一组改写被提取出来引用为一个 handle。

不过,将每个原子更新(handle)都写入到日志之中可能不那么高效。为了更高的性能,JBD 将一组 handle 打包为一个事务(transaction),并将事务一次写入日志。JBD 保障事务本身是原子性的。这样,作为事务的组成部分的 handle 们自然也是原子性的。

事务最重要的属性是它的状态(state)。当事务正在提交时,它的生命周期经历了下面的一系列状态。

  1. 运行(running):事务当前是活着的,并且可以接受新的句柄。在一个系统中,仅有一个事务可以处于运行状态。
  2. 锁定(locked):事务不再接受新的 handle,但现有 handle 们还没有完成。一旦所有 handle 都完成了,事务将进入下一个状态。
  3. 写入(flush):事务中的所有 handle 都完成了,这时事务将它自己写入到日志中去。
  4. 提交(commit):整个事务的记录都被写到日志里面这之后。事务会写一个提交块,来指到日志中的事务记录已经完成了。
  5. 完成:事务完整的写到日志种种之后,它会留在那直到所有的块都被更新到磁盘上的实际位置。

Transaction Committing 与 Checkpointing

一个处于运行状态的事务在一段时间之后会被写(write)到日志区域。这样,一个事务可能是在内存中(running)或是在磁盘上。将事务写入(flushing)到日志中并标记特定的事务已完成的过程称为事务的提交。

日志只控制一小块有限的区域,并且需要进行重用。对于已经提交的事务,也就是那些已经把全部块都写到磁盘上的事务,就不再需要停留在日志里了。这里,Checkpointing(这词儿咋翻译?咬不准啊)就是用于将完成的事务写入磁盘并重新声明日志中对应的空间可用的过程。

实现概要

JBD 层用于进行元数据的日志记录,期间数据简单的不经过日志直接写到磁盘上。但是这并不阻止应用程序记录数据的日志,因为它可以告诉 JBD,它自己是元数据。本文以 2.6.0 内核为例。

Commit

[journal_commit_transaction(journal object)]

Kjournald 线程与每个进行日志的设备相关联。Kjournald 线程保证运行中的事务会在一个特定间隔后被提交。事务提交的代码分成了如下八个不同阶段。图1 给出了日志的逻辑结构。

阶段0:将事务的状态从运行(T_RUNNING)变为锁定(T_LOCKED),这样,这个事务就不能再添加新的 handle 了。事务会等待所有的 handle 都完成的。当事务初始化的时候,会预留一部分缓冲区。在这一阶段,有些缓冲区可能没有使用,就被释放了。到这里,所有事务都完成了之后,事务就已经准备好了可以被提交了。

阶段1:事务进入到写入状态(T_FLUSH)。事务被标记为正在向日志提交的事。这一阶段中,也会标记该日志没有运行状态的事务存在;这样,新的 handles 请求将会导致新的事务的初始化。

阶段2:事务实际使用的缓冲们被写入到磁盘上。首先是数据缓冲。由于数据缓冲不存储到日志记录区域,所以没有什么麻烦的。相反,它们会直接写到磁盘的真实位置。这一阶段在这些缓冲都被接收、收到 IO 操作完成提示后结束。

阶段3:这时所有的数据缓冲都已经写入到磁盘上了,但它们的元数据还在内存之中。元数据的写入不像数据缓冲那么直接,因为元数据需要先写到日志区域之中,并需要记录它们实际要存放的位置。在这个阶段会首先写这些元数据缓存,这里需要一个日志描述块。日志描述块以标签(tag)的形式存储日志中的每个元数据缓存到它的实际位置的映射。之后,元数据缓存写入日志。一旦日志描述符中装满了标签,或者所有的元数据缓存都写入到日志之中了,日志描述符就会被写入到日志之中。现在,所有的元数据缓存都存在日志中了,而且它们在硬盘中的实际位置也被记录下来了。如果系统宕机,再启动的时候,这些要被永久化写入磁盘的数据就可以用来恢复数据了。

阶段4 和阶段5:这两个阶段分别在等元数据缓冲和日志描述符的 I/O 描述通告。一旦收到 I/O 完成消息,内存链表中对应的缓冲就可以释放掉了。

阶段6:现在所有数据和元数据都已经安全地保存着了,数据就在它们的实际位置,而元数据在日志中。现在,事务要被标记为已提交,这样就表明所有的更新都妥善保存在日志之中了。因此,日志描述块再次分配。一个标签会被写入,以示事务已经被成功提交,这个块是同步写入到到日志之中的。这之后,事务便进入了已提交状态,T_COMMIT。

阶段7:当很多事务被写入到日志中但还没有写入到磁盘中的时候。当前事务中的一些元数据缓存可能是一些之前的事务的一部分。这就不需要保存之前的事务了,因为我们的当前提交的事务已经有了更新的版本了。这些缓冲于是就被从老的事务中删除了。

阶段8:事务被标记为完成状态,T_FINISHED。日志结构被更新以将当前事务标记为最新提交的事务。同时也将自己标到要被 checkpoint 的事务列表中去。

Checkpointing

当日志要被写入磁盘的时候,checkpointing 就会被启动了——比如卸载文件系统,或者在新的 handle 开始的时候也可能会启动 checkpointing。一个新的 handle 可能发现需要保证的缓冲数量不足了,于是它就可能需要启动 checkpointing 进程来释放一些日志空间。

checkpointing 进程将写入那些还没有写到硬盘里实际位置上的元数据缓冲。于是这些事务就可以被从日志中删除了。一个日志区域可以有多个 checkpointing 事务,而每个 checkpointing 事务都可以有多个缓冲。checkpointing 进程处理每个提交的事务,对每个事务找出需要写入磁盘的元数据缓冲。所有这些缓冲被一批写入磁盘。一旦所有事务都被处理完,他们的记录就会被从日志中删掉了。

恢复
[journal_recover(journal object)]

当系统在崩溃之后启动的时候,它会发现日志条目不是空的,这就表明上次文件系统卸载并不成功,或者根本就没有进行。这就需要尝试进行修复了。图2 绘制了日志的物理结构的一个例子。修复分三个阶段进行。

  1. PASS_SCAN: 发现日志记录的尾部。

  2. PASS_REVOKE: 为日志记录准备一串要被撤销的块。

  3. PASS_REPLAY: 未被撤销的块以确保磁盘一致性的顺序被重新写入(重放)。

对于恢复而言,可用信息已经在日志中提供了。但是日志的实际状态是不清楚的,就像我们不知道系统在那个具体的点崩溃的一样。这样,最后的事务可能在 checkpointing 或是提交的状态。一个在运行中的事务是不可能被发现的,因为它还在内存中。

对于提交状态的事务,我们不得不忘掉这些更新,因为可能不是所有的更新都在这里。这样,在 PASS_SCAN 阶段中,最后的记录条目会被发现。到这里,恢复进程知道了哪些事务需要被重写。

每个事务可以有一组撤销(revoked)块。这些块非常重要,通过它们可以防止同一个块的老的日志记录被重放在新的数据之上。在 PASS_REVOKE 过程中,会准备一个撤销块的哈希表。每当我们要找出某个快是否需要被在重放中写入到磁盘的时候,这个表就会被用到。

在最后一个阶段,所有的需要被重演的块都需要被处理。每个块都会检查是否存在在解除块撤销表之中。如果这个块没在表里,那么它就可以被安全的写入到磁盘上的实际位置之中。而如果这个块在表中,那么只有最新的版本才会写入到磁盘中。注意,我们还没有改变磁盘上的日志的任何内容。这样,如果在修复过程中系统再次崩溃的话不会造成任何破坏。同样的日志可以在下次用来重新恢复,在恢复的过程中,无关的操作不会被进行。


[译文] GEM vs. TTM

August 8th, 2008

By Jonathan Corbet
May 28, 2008
王旭于2008年8月8日译
http://lwn.net/Articles/283793/

在 Linux 下,即使是在有了基础硬件的编程接口信息的情况系,得到高性能的 3D 渲染仍然是非常具有挑战性的。这个问题的一个原因就是内存管理:一个 GPU 本质上说是一个拥有它自己的独立的内存的计算机系统。要想管理GPU 的内存 —— 以及以它的视角看到的系统内存,让系统能够跑起来就必须要小心,更不要说还要获得可接受的性能了。

不久以前,这个问题看上去要被翻译表映射(TTM)子系统 解决了。TTM 目前仍然没有进入主线内核,依赖于它的所有驱动自然也没有进入。最近的一个关于 TTM 如何才能进入主线的提问引发了一场有趣的讨论,最终使得局面急转直下,TTM 可能根本就不是未来的显存管理系统。

很多关于 TTM 的抱怨都已经浮现出来了。它的 API 远大于任意一个开源 Linux 驱动的需求;换句话说,大量的代码都是用于二进制发布的驱动的。隔离机制(fence, 管理 CPU 和 GPU 的并发性的机制)非常复杂而难于使用,而且并不是总能得到很好的性能。大量的使用内存映射缓冲会给它自己带来性能问题。TTM API 尝试着提供所有情况下的一切东西;结果对于很多开发者来说很难让特定的硬件去匹配这些 API,很难开始使用 TTM,也缺乏足够的灵活性。对于使用TTM的开源驱动来说,这是一个重大的不足。所以 Dave Airlie 担忧地说 :

现在,我希望有一个 radeon 或是什么新的驱动已经使用了 TTM,或者,至少有个 demo 展示了如何使用它,可是现在什么也没有,这让我很痛苦……真正的问题是 TTM 是不是真的适合于 Linux 桌面和嵌入式驱动的开发者们,至少我在桌面方面还没看到足够的正面反馈。

所有这些但有看来都是徒劳的,毕竟 TTM 已经在那了,而且也没有什么其他的替代品。然而,事情发生了,一个转机发生了,它就叫做图形执行管理器(这名字翻译的好难听阿,对不起观众了),简称 GEM。Intel 资助的 GEM 项目已经有一个月了(译注:原文写于08年5月底,目前 GEM 形势大好阿)。GEM 的开发者其实还没完全准备好来对外公布他们的工作,不过 TTM 的讨论让他们提前走上了前台。

Keith Packard (译注:Intel 的开源图形驱动项目的带头人)的 GEM 介绍 中包含了一个已有 API 的描述文档。GEM 在处理上面有很多显著不同。首先 GEM 使用普通、匿名、用户空间内存来用于分配图形缓冲对象。这意味着这些缓冲区可以在必要的时候被交换出物理内存。这个方法有其显著的优越性,不仅仅是内存使用的灵活性:它让挂起和恢复的实现可以直接通过恢复所有缓冲对象的方式自动完成了。

GEM API 尽量避免将缓冲区到用户空间的映射。映射工作代价高昂而且而且带来了 CPU 与 GPU 之间的缓存一致性问题。所以,GEM 用简单的 read() 和 write() 调用来取代映射,访问缓冲区对象。或者,至少,在 GEM 开发者能给每个缓冲对象附加一个文件描述符的时候的处理方法。而内核不会那么轻易管理这么多文件描述符,这样,实际的 API 使用了不同的缓冲对象 handle 和一系列的 ioctl() 调用。

也就是说,GEM可能映射一个缓冲对象到用户空间。不过,用户空间的驱动必须显示地承担管理缓存一致性的责任。为此,有一组 ioctl() 调用用于管理缓冲的“域”;所谓“域”,本质上说就是描述了系统中的哪个组件拥有这块缓冲区并有权操作它。改变一个缓冲的域(两类域,其一用于读访问,而另一个是写)将会进行必要的缓存刷新。在某种意义上说,这个机制重现了流 DMA API,DMA API 中的 DMA 缓冲区的所有权就可以在 CPU 和 外设控制器之间交换。不必惊讶,他们解觉得也是类似的问题。

GEM API 不需要显示的屏蔽 (fence) 操作。相反,当 CPU 操作需要访问缓冲区的时候,如果必要的话 CPU 就会简单地等待 GPU 完成对该缓冲区的操作。

最后,GEM API 并没有想要独自解决所有问题;很多重要操作(比如执行一组 GPU 指令)被留给硬件特定的驱动来实现了。GEM 本身在目前非常适于 Intel 驱动的需求;它不需要达到所有的 TTM 所有达到的目标。Eric Anholt 描述 到:

TTM 的问题是它要对所有硬件提供一套统一的 API,即使我们的驱动不希望这样的时候也是如此……我们尽量从另一个方面来达到这一目的:实现一个驱动。当其他人实现了另一个驱动,又发现这里面有些代码应该是公共的,那么就将它移动到支撑库里并共享它。

这种方法的有点是更容易来给 Intel 新加入一些东西。而且这可能是个好的开始,一组工作的驱动总比没有强。另一方面,这也意味着要让 GEM 支持其它硬件的驱动还需要大量的工作。对于这些工作如何去做,目前大概有两种观点:(1) GEM 为新的驱动的需求增加新的能力,或者 (2) 让驱动使用它自己的内存管理器。

第一种方法在很多情况下看起来更漂亮。但它也预示着 GEM API 会经常发生很多变化。这可能会延缓整个系统进入主线内核的脚步;GEM API 会输出到用户空间,所以必须保持变化的兼容性。看起来必须要不断演进的 API 会在快速的进入主线内核的时候受到阻力。

而对于第二种方法,Dave Airlie 给出了最好的解释 :

嗯,事情是我不能相信我们还不足够知道如何以一种通用的方法来做到这点,不过 TTM vs GEM 可能已经证明了这是不可能的了。那么我们可以赌一赌每个驱动一个内存管理器,不过我怀疑这将成为一个维护的噩梦,所以如果人们决定了这就是下一步要走的,我会很高兴地看到他发生。不过提交第 n+1 个内存管理器的人必须解事情出接口后面的机制,而且解释清楚为什么不能复用内存管理器 1 到 n。

一个还没被讨论过的问题就是性能。Keith Whitwell 给出了一组评分结果 ,该结果显示对于 i915 驱动,使用 TTM 或 GEM 的时候,性能会比不使用的情况显著降低。而 Keith Packard 的到了不同的结果 ,他的结果显示 GEM 驱动的性能明显更好。显然,目前需要一组一致的 benchmark;图形性能是重要的,但如果不能有效地测量也就没法优化。

使用匿名内存也会引起一些性能考虑:如果第一人称射击游戏 (FPS) 的血腥像素们要不停地被换页的话,体验可能就不尽如人意了。匿名内存可以位于高位内存,而且这样就不必使用32位指针来访问。一些 GPU 硬件不能访问高位内存;这将可能影响 kernel 的 bounce buffer (不好意思啊,这个不知道怎么翻译好了)。最后,GEM 还需要去证明它可以提供良好的性能,GEM的开发者们非常主动地使他们的硬件看起来很好,这也让工作在上面的东西有了一个更好的机会。(这句话翻译的比较晕)

所有这些能得到的结论是GPU的内存管理问题还不能简单认为是已经解决了。GEM可能最终称为这个答案,但它还是非常新的API,仍然需要大量的工作。这个领域中似乎还有很多的工作需要做。

(感谢 Timo Jyrinki 建议这个话题。)

2.6.26内核新特性:Read-only bind mounts

July 14th, 2008

原来还真没注意,原来的 bind mount 不能只读挂载,嗯,无所谓了,2.6.26的可以了。

我一般是在这么几种情况下用 bind mount 的:

  • chroot 的时候 bind mount  /dev 和 /proc,不过经检验 proc 不用 bind,直接再 mount 一个出来就行,不过 /dev 似乎还是要 bind 的吧。whatever,这里应该不用只读
  • 复制整个系统的时候 bind mount 一下 /,这样,/ 下面的其他分区(如果有的话,比如 /home, /usr, /var, /boot 之类的)就不会出现在新挂载点下面了,这样 tar 不会涉及到其他分区,正好适合备份。这种情况下,read-only bind mount 应该比较有用,而且更安全

嗯,就是这样。

哦,kernel.org 上,2.6.26 已经发布了,2008-07-13 22:44 UTC,距离现在两个多小时吧,算是个新闻哦。

[译文]What is RCU, Fundamentally?

June 1st, 2008

http://lwn.net/Articles/262464/

December 17, 2007

Paul E. McKenney, IBM Linux Technology Center

Jonathan Walpole, Portland State University Department of Computer Science

王旭 [gnawux(at)gmail.com] 翻译,2008年5月26日–6月1日

[编者注:本文是解释 read-copy-update 工作机制的三部曲的第一部。十分感谢 Paul McKenney 和 Jonathan Walpole 允许我们发表这些文章。剩余的两片将在随后几周中陆续发出。]


《RCU是什么?》第一部分

概述

Read-copy update (RCU) 是一种 2002 年 10 月被引入到内核当中的同步机制。通过允许在更新的同时读数据,RCU 提高了同步机制的可伸缩性(scalability)。相对于传统的在并发线程间不区分是读者还是写者的简单互斥性锁机制,或者是哪些允许并发读但同时不允许写的读写锁,RCU 支持同时一个更新线程和多个读线程的并发。RCU 通过保存对象的多个副本来保障读操作的连续性,并保证在预定的读方临界区没有完成之前不会释放这个对象。RCU定义并使用高效、可伸缩的机制来发布并读取对象的新版本,并延长旧版本们的寿命。这些机制将工作分发到了读和更新路径上,以保证读路径可以极快地运行。在某些场合(非抢占内核),RCU 的读方没有任何性能负担。

问题1:seqlock 不是也允许读线程和更新线程并发工作么?

这个问题可以归结到 “确切地说,什么是RCU?” 这个问题,或许还是 “RCU 可能是如何工作的?” (再或者,不太可能的情况下,问题会变为什么情况下 RCU 不太可能工作)。本文从几个基本的出发点来回答这些问题;之后还会分批地从使用的角度和 API 的角度来看这些问题。最后一篇连载还会给出一组参考文献。

RCU 由三个基本机制组成,第一个用于插入,第二个用于删除,而第三个则用于让读线程可以承受并发的插入或删除。这三个机制将在下面的三节中介绍,讲述如何将 RCU 转化为链表:

  1. 订阅发布机制 (用于插入)
  2. 等待已有的RCU读者完成 (用于删除)
  3. 维护多个最近更新的对象的版本 (为读者维护)

这三个章节之后还有上重点回顾与快速问题答案。

订阅发布机制

RCU的一个关键特性是它可以安全地扫描数据,即使数据正被同时改写也没问题。要提供这种并发插入的能力,RCU使用了一种订阅发布机制。举例说,考虑一 个被初始化为 NULL 的全局指针变量 gp 将要被修改为新分配并初始化的数据结构。下面这段代码(使用附加的合适的锁机制)可以用于这个目的:

1 struct foo {
2 int a;
3 int b;
4 int c;
5 };
6 struct foo *gp = NULL;
7
8 /* . . . */
9
10 p = kmalloc(sizeof(*p), GFP_KERNEL);
11 p->a = 1;
12 p->b = 2;
13 p->c = 3;
14 gp = p;

不幸的是,没有方法强制保证编译器和CPU能顺序执行最后四条语句。如果gp的赋值早于p的各个域的初始化的话,那么并发的读操作将访问到未初始化的变 量。内存屏障(barrier)可以用于保障操作的顺序,但内存屏障以难以使用而闻名。这样我们将他们封装到具有发布语义的 rcu_assign_pointer() 原语之中。最后的四条将成为这样:

1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);

rcu_assign_pointer() 将会发布新的结构,强制编译器和CPU在给p的各个域赋值之后再把指针赋值给gp。然而,仅仅强制更新操作的顺序是不够的,读者也必须强制使用恰当的顺序。考虑下面的这段代码:

1 p = gp;
2 if (p != NULL) {
3 do_something_with(p->a, p->b, p->c);
4 }

尽管这段代码看起来不会受到顺序错乱的影响,不过十分不幸,DEC Alpha CPU 和投机性编译器优化可能会引发问题,不论你是否相信,这的确有可能会导致 p->a, p->b, p->c 的读取会在读取 p 之前!这种情况在投机性编译器优化的情况中最有可能会出现,编译器会揣测p的值,取出 p->a, p->b 和 p->c,之后取出 p 的真实值来检查拽侧的正确性。这种优化非常激进,或者说疯狂,不过在确实会在profile-driven优化时发生。

毫无疑问,我们需要在CPU和编译器上阻止这种情况的发生。rcu_dereference() 原语使用了必要的内存屏障指令和编译器指令来达到这一目的:

1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();

rcu_dereference() 原语可以被看作是订阅了指针指向的值,保证接下来的取值操作将会看到对应的发布操作(rcu_assign_pointer())发生之前被初始化的值。 rcu_read_lock() 和 rcu_read_unlock() 绝对是必须的:他们定义了 RCU 读方临界区的范围。他们的目的将在下一节 解释,不过,他们不会自旋或阻塞,也不阻止 list_add_rcu() 的并发执行。事实上,对于非抢占内核,它们不产生任何代码。

虽然 rcu_assign_pointer() 和 rcu_dereference() 在理论上可以用于构建任意 RCU 保护的数据结构,但实际上,使用高层构造常常更好。因此,rcu_assign_pointer() 和 rcu_dereference() 原语被嵌入到了 Linux 的链表维护 API 中的特殊 RCU 变量之中了。Linux 有两个双向链表的变种,循环链表 struct list_head 和线性链表 struct hlist_head/struct hlist_node。前者的结构如下图所示,绿色的方块表示表头,蓝色的是链表中的元素。

Linux list

将上面的指针发布例子放到链表的场景中来就是这样:

1 struct foo {
2 struct list_head list;
3 int a;
4 int b;
5 int c;
6 };
7 LIST_HEAD(head);
8
9 /* . . . */
10
11 p = kmalloc(sizeof(*p), GFP_KERNEL);
12 p->a = 1;
13 p->b = 2;
14 p->c = 3;
15 list_add_rcu(&p->list, &head);

第15行被使用某种同步机制保护住了,通常是某种所,以组织多个 list_add() 实例并发执行。然而,这些同步不能组织同时发生的RCU读者。订阅一个 RCU 保护的链表非常直接:

1 rcu_read_lock();
2 list_for_each_entry_rcu(p, head, list) {
3 do_something_with(p->a, p->b, p->c);
4 }
5 rcu_read_unlock();

list_add_rcu() 原语发布一个节点到制定的链表中去,保证对应的 list_for_each_entry_rcu() 调用都正确的订阅到同一个节点上。

问题2:如果在 list_for_each_entry_rcu() 运行时,刚好进行了一次 list_add_rcu(),如何防止 segfault 的发生呢?

Linux 中的另一个双向链表,hlist,是一个线性表,也就是说,它的头部仅需要一个指针,而不是向循环链表一样需要两个指针。这样,使用 hlist 作为大型哈希表的 hash-bucket 数组的容器将仅消耗一半的内存空间。

Linux hlist

将一个新元素添加到一个 RCU 保护的 hlist 里面与添加到循环链表里非常类似:

1 struct foo {
2 struct hlist_node *list;
3 int a;
4 int b;
5 int c;
6 };
7 HLIST_HEAD(head);
8
9 /* . . . */
10
11 p = kmalloc(sizeof(*p), GFP_KERNEL);
12 p->a = 1;
13 p->b = 2;
14 p->c = 3;
15 hlist_add_head_rcu(&p->list, &head);

和上面一样,第15行一定使用了锁或其他某种同步机制。

订阅一个 RCU 保护的 hlist 也和循环链表非常接近。

1 rcu_read_lock();
2 hlist_for_each_entry_rcu(p, q, head, list) {
3 do_something_with(p->a, p->b, p->c);
4 }
5 rcu_read_unlock();

问题3:为什么我们需要传递两个指针给 hlist_for_each_entry_rcu(), list_for_each_entry_rcu() 可是只需要一个指针的啊?

RCU 发布与订阅原语在如下表中列出,同时给出了 “取消发布”或是撤回的原语

类别
发布
撤销
订阅

类别
发布
撤销
订阅

指针
rcu_assign_pointer()
rcu_assign_pointer(…, NULL)
rcu_dereference()

循环链表
list_add_rcu()
list_add_tail_rcu()
list_replace_rcu()
list_del_rcu()
list_for_each_entry_rcu()

双向链表
hlist_add_after_rcu()
hlist_add_before_rcu()
hlist_add_head_rcu()
hlist_replace_rcu()
hlist_del_rcu()
hlist_for_each_entry_rcu()

注意,list_replace_rcu(), list_del_rcu(), hlist_replace_rcu(), 以及 hlist_del_rcu() 增加了一些复杂度。什么时候释放被替换或删除掉的数据元素才是安全的呢?具体地说,我们怎么能知道所有的读者都释放了他们手中对数据元素的引用呢?

这些问题将在下面的章节中得到回答。

等待已经存在的RCU读者完成

RCU的最基本的功能就是等待一些事情的完成。当然,还有很多其他方法也是用于等待事情完成的,包括引用计数、读写锁、事件等。RCU最大的好处在于它可以等待所有(比如说)两万件不同点事情,而无需显式地跟踪它们中的每一个,也不需要担心性能的下降、可伸缩性限制、复杂度死锁场景,以及内存泄露等所有这些显式跟踪手法所固有的问题。

RCU 中,被等待的东西被叫做“RCU读方临界区”。一个RCU读方临界区始于 rcu_read_lock() 原语,止于 rcu_read_unlock() 原语。RCU 读方临界区可以嵌套,也可以放入很多代码,只要这些代码显式阻塞或睡眠即可(有一种称为“SRCU”的特殊RCU允许在它的读方临界区中睡眠)。只要你遵守这些约定,你就可以使用RCU来等待任何期望的代码段的完成。

正如其他地方对经典RCU实时RCU的描述,RCU 通过间接确定这些其他事情的完成时间来达到这一目的。

具体地说,如下图所示,RCU是一种等待已经存在的RCU读方临界区结束的方法,包括这些临界区中执行的内存操作。

Grace periods extend to contain pre-existing RCU read-side critical sections.

注意,开始于一个给定宽限期开始之后的RCU读方临界区能够、并可以延续到该宽限期结束之后。

下面的伪码展示了使用RCU等待读者的基本算法形式:

  1. 进行改动,比如,替换链表中的一个元素。
  2. 等待所有已经存在的RCU读方临界区完成(比如,使用synchronize_rcu()原语)。关键点是接下来的RCU读方临界区将无法得到新近删除的元素的引用了。
  3. 清理,比如,释放上述所有被替换的元素。

下面的代码段是从前一节修改而得的,用于说明这一过程,这里面的域a是这个搜索的键值。

1 struct foo {
2 struct list_head list;
3 int a;
4 int b;
5 int c;
6 };
7 LIST_HEAD(head);
8
9 /* . . . */
10
11 p = search(head, key);
12 if (p == NULL) {
13 /* Take appropriate action, unlock, and return. */
14 }
15 q = kmalloc(sizeof(*p), GFP_KERNEL);
16 *q = *p;
17 q->b = 2;
18 q->c = 3;
19 list_replace_rcu(&p->list, &q->list);
20 synchronize_rcu();
21 kfree(p);

第19、20 和 21 行实现了上面所说的三个步骤。第 16-19行展现了 RCU 的名字(读-复制-更新):在允许进行并发读操作的同时,第16行进行了复制,而第17-19行进行了更新。

乍一看会觉得 synchronize_rcu() 原语显得比较神秘。毕竟它必须等所有读方临界区完成,而且,正如我们前面看到的,用于限制RCU读方临界区的rcu_read_lock() 和 rcu_read_unlock() 原语在非抢占内核中甚至什么代码都不会生成。

这里有一个小伎俩,经典RCU通过 rcu_read_lock() 和 rcu_read_unlock() 界定的读方临界区是不允许阻塞和休眠的。因此,当一个给定的CPU要进行上下文切换的时候,我们可以确定任何已有的RCU读方临界区都已经完成了。也就是说,只要每个CPU都至少进行了一次上下文切换,那么所有先前的 RCU 读方临界区也就保证都完成了,即 synchronize_rcu() 可以安全返回了。

因此,经典RCU的 synchronize_rcu() 从概念上说可以被简化成这样:

1 for_each_online_cpu(cpu)
2 run_on(cpu);

这里,run_on() 将当前线程切换到指定 CPU,来强制该 CPU 进行上下文切换。而 for_each_online_cpu() 循环强制对每个 CPU 进行一次上下文切换。虽然这个简单的方法可以在一个不支持抢占的内核上工作,换句话说,对 non-CONFIG_PREEMPT 和 CONFIG_PREEMPT,但对 CONFIG_PREEMPT_RT 实时 (-rt) 内核无效。因此,实时RCU使用了一个(松散地)基于引用计数的方法。

当然,在真实内核中的实现要复杂得多了,因为它需要管理终端,NMI,CPU热插拔和其他实际内核中的可能有的风险,而且还要维护良好的性能和可伸缩性。RCU的实时实现还必须拥有良好的实时响应能力,这就使得(像上面两行那样)直接禁止抢占变得不可能了。

虽然我们了解到了 synchronize_rcu() 的简单实现原理,不过还有很多其它问题呢。比如,RCU读者们在读一个正在被并发地更新的链表的时候究竟读到了什么呢?这个问题将在下一节讲到。

维护多个版本的近期更新的对象

本节将展示 RCU 如何为多个不需要同步的读者维护不同版本的链表。我们使用两个例子来展示一个可能被给定的读者引用的元素必须在该读者处于读方临界区的整个过程中保持完好无损。第一个例子展示了链表元素的删除,而第二个例子则展示了元素的替换。

例1:在删除时维护多个版本

要开始这个“删除”的例子,我们先把上节这个例子的 11-21行改成如下的形式:

1 p = search(head, key);
2 if (p != NULL) {
3 list_del_rcu(&p->list);
4 synchronize_rcu();
5 kfree(p);
6 }

这个链表以及指针p的最初情况是这样的:

Initial list state.

表中每个元素的三元组分别代表域a, b, c。红色的便捷表明读者可以获取它们的指针,而且因为读操作和更新操作不是直接同步的,读者可以在这个删除的过程中同时发生。这里我们为了清晰没有画出双向链表的反向指针。

在第三行的 list_del_rcu() 完成的时候,5,6,7 这个元素已经被从链表中删除了(如下图)。由于读者并不直接和更新操作同步,读者可能同时正在扫描这个链表。由于访问时间不同,这些并发读者可能看到、也可能没看到新近删除的元素。不过,那些在获取指针之后延迟了读操作的读者(比如因为中断、ECC内存错误,或在 CONFIG_PREEMPT_RT内核中因为抢占而延迟了的)可能仍然会在删除之后的一段时间内看到那个老的链表的版本。下图中 5,6,7 元素的边框仍然是红色的,这意味着仍然有读者可能会引用它。

After deletion.

这里注意,在退出读方临界区之后,读者们就不能再持有 5,6,7 这个元素的引用了。所以,一旦第4行的 synchronize_rcu() 完成了,所有已有读者也就保证都完成了,这样就没有读者会访问这个元素了,下图中,这个元素的边框也变黑了。我们的链表也回到了一个单一的版本了。

After deletion.

这之后,5,6,7 这个元素就可以被安全的释放了:

After deletion.

这里,我们完成了删除 5,6,7 这个元素的操作,下一小节将介绍替换操作。

例2:在替换的过程中维护数据的多个不同版本

在开始替换的例子钱,我们再修改一下前面例子的最后几行:

1 q = kmalloc(sizeof(*p), GFP_KERNEL);
2 *q = *p;
3 q->b = 2;
4 q->c = 3;
5 list_replace_rcu(&p->list, &q->list);
6 synchronize_rcu();
7 kfree(p);

这个链表的初始状态和指针p和删除的那个例子是完全一样的:

Initial list state.

和之前一样,每个元素里面的三元组分别代表域 a, b 和 c。红色的边框代表了读者可能会持有这个元素的引用,因为读者和更新者没有直接的同步,读者可能会和整个替换过程并发进行。再次说明,这里我们为了清晰,再次省略了反向指针。

第一行的 kmalloc() 生成了一个替换元素,如下:

List state after allocation.

第二行把旧的元素的内容拷贝给新的元素:

List state after copy.

第三行,将 q->b 更新为2:

List state after update of b.

第四行,将 q->c 更新为3:

List state after update of c.

现在,第5行进行替换操作,这里,新元素最终对读者可见了。到了这里,如下所示,我们有了这个链表的两个版本。先前已经存在的读者可以看到 5,6,7 元素,而新读者将看到 5,2,3 元素。不过,任何读者都被保证可以看到一个完整的链表。

List state after replacement.

第6行的 synchronize_rcu() 返回后,宽限期将完成,所有在 list_replace_rcu() 之前开始的读者都将完成。具体地说,任何可能持有 5,6,7 的读者都已经退出了他们的读方临界区,这就保证他们不再持有一个引用。因而也在没有任何读者持有老元素的引用了,途中,5,6,7 元素的边框也就变黑了。对于读者来说,目前又只有一个单一的链表版本了,只是新的元素已经替代了旧元素的位置。

List state after grace period.

第七行的 kfree() 完成后,链表旧成为了如下的样子:

List state after grace period.

尽管 RCU 是以替换而命名的,但内核中的大多数使用都是前面小节 中的简单删除的情况。

讨论

这个例子假设在更新操作的过程中保存着一个互斥量,也就是说,这个链表在一个给定时间最多有两种版本。

问题4:如何修改删除的例子,来允许超过两个版本的链表可以同时存在?

问题5:在某一时刻,RCU最多可以有多少个链表的版本?

这组例子显示了RCU使用多个版本来保障在存在并发读者的情况下的安全更改数据。当然,一些算法是无法很好地支持多个版本的。有一个参考文献 介绍了如何对这些算法进行改造以使用RCU,不过,这超出了本文的讨论范围了。

小结

本文介绍了基于RCU的算法的三个基本部分:

  1. 对与添加新数据的发布-订阅机制
  2. 等待已有RCU读者完成,以及
  3. 维护多个版本以便在不顺坏或严重延迟RCU读者的情况下,允许更改。

问题6:如果 rcu_read_lock() 与 rcu_read_unlock() 之间没有自旋锁或阻塞,RCU更新者会怎样延迟RCU读者?

这三个RCU的组成部分允许数据在并发读者访问的同时更新数据,并可以以多种方式实现基于RCU的算法,一些算法将会在接下来的“What is RCU, Really?”系列中继续介绍。

致谢

我 们十分感激本文草稿的审阅者,他们极大地提高了本文的价值,这些审阅者是 Andy Whitcroft, Gautham Shenoy, 和 Mike Fulton。同时,我们要感谢 Relativistic Programming 项目的成员和 PNW TEC 的成员,他们带来乐很多有价值的讨论。我们还要感谢 Dan Frye 的支持。最后,本文基于 NSF CNS-0719851 资助项目的工作。

本文仅代表作者的个人观点,并不反映 IBM 或波特兰州立大学的观点。

Linux 是 Linus Torvalds 的注册商标。

其他涉及到的公司、产品和服务的名称是各自的商标或服务标志。

问题解答

问题1:seqlock 不是也允许读线程和更新线程并发工作么?

:是或不是。虽然 seqlock 的读者可以在 seqlock 写的同时并发访问,不过一旦写操作发生,read_seqretry() 原语都会强制要求读者重试。这意味着任何与 seqlock 更新者并发发生的 seqlock 读者所做的工作都将被放弃并重做。所以,seqlock 读者可以与更新者同时运行,但不能真的在这种情况下做什么工作。

与此相反,RCU 读者可以在存在并发 RCU 更新者的同时完成有效地工作。

问题2:如果在 list_for_each_entry_rcu() 运行时,刚好进行了一次 list_add_rcu(),如何防止 segfault 的发生呢?

:在所有运行着的 Linux 系统中,从指针读取或写入指针都是原子操作,也就是说,如果一个存储到指针和一个从指针读取操作是同时发生的,那么,读取要么读回初始值,要么读回存入值,而不会返回两者的混合体。另外,list_for_each_entry_rcu() 总会前向遍历链表,而从不反向遍历。因而,list_for_each_entry_rcu() 要么看到 list_add_rcu() 加入后的元素,要么看不到,不论如何,都会看到一个正确的链表。


问题3:为什么我们需要传递两个指针给 hlist_for_each_entry_rcu(), list_for_each_entry_rcu() 可是只需要一个指针的啊?

:因为一个 hlist 必须检查 NULL 而不是检查是不是回到头部了。(尝试编一个单指针的 hlist_for_each_entry_rcu()。如果你能得到一个不错的答案,那当然是非常好的事情啦!)

问题4:如何修改删除的例子,来允许超过两个版本的链表可以同时存在?

a答:一个方法可以通过如下代码达到:

spin_lock(&mylock);
p = search(head, key);
if (p == NULL)
spin_unlock(&mylock);
else {
list_del_rcu(&p->list);
spin_unlock(&mylock);
synchronize_rcu();
kfree(p);
}

注意,这意味着 synchronize_rcu() 可以等待多个并发的删除操作。


问题5:在某一时刻,RCU最多可以有多少个链表的版本?

:这依赖于同步时如何设计的。如果用一个信号量来保护宽限期内的变更操作,那么最多有新旧两个版本。然而,如果仅仅搜索、更新,并用锁来保护 list_replace_rcu(),那么可以有任意数量的活动版本,只受到内存和宽限期内可以完成多少个更新操作的限制。不过,要注意,那些如此频繁更新的数据结构不是实施RCU的好场所。这只是说,RCU 可以在必要时应付高更新速率。


问题6:如果 rcu_read_lock() 与 rcu_read_unlock() 之间没有自旋锁或阻塞,RCU更新者会怎样延迟RCU读者?

:给定的 RCU 更新者进行的改动将导致对应的 CPU 丢弃当前的 cache lines,强制 CPU 运行并发 RCU 读者,从而导致 cache 重新映射造成的昂贵代价。(你能设计一个算法来改变一个数据结构而不导致并发读者的昂贵的 cache 丢失么?对接下来的其他读者呢?)

[译文] 技术预览:基于内核的显示模式设定

April 22nd, 2008

原文链接:http://www.phoronix.com/scan.php?page=article&item=kernel_modesetting
原文发表时间:2008年4月19日
原作者:
Michael Larabel
翻译时间:2008年4月22日
译者:王旭 gnawux(at)gmail.com

译者按:2008年9月19日,证实了一下,由于 TTM 到 GEM 的变化,kernel modesetting 没有进入到 2.6.27 之中,并且,刚刚尝试了一下,发现这个还很不成熟,不知道什么时候才能真的实用起来,可能需要 X driver 和 kernel, drm 等同时步进到一个稳定状态才行,这样看来,可能要到明年了。

X.Org 社区之中,不少新颖的技术都在萌芽之中——其中有Gallium3D (译注:新一代Linux 3D图形驱动框架), TTM 显存管理器(译注:用于GPU的显存管理),MPX (允许 X 拥有多个鼠标指针和键盘焦点)。不过,这些特色之中那个已经向着列表顶端进发、并给最终用户带来可以看到的好处的正是基于内核的显示模式设置(mode- setting)。正如其名字所表述的,基于内核的模式设置将显卡的模式设置代码从用户控件的X服务器驱动挪到了Linux内核。这个话题对最终用户似乎没什么意义,不过,将模式设置代码移动到内核将展现一个更简洁更漂亮的启动过程,增强挂起和恢复的支持,并提供更可靠的虚拟终端切换(等等)。内核的模式设置还没有进入主线内核,它的API也还没有冻结,不过,下个月将要发布的 Fedora 9 就将成为首个带有这一支持的主流发布版。本文中,我们将近距离观察配合 Intel X.Org 驱动的内核的模式设置及其工作时的视频(译注:译文将略过视频,请见谅)。

Radeon 驱动的移植工作 仍在进行中(驱动命名为 radeon_ms),不过 Fedora 9 将只包含带有这项支持的 Intel 开源 X.Org 驱动。内核的模式支持交互不是主线内核的内容,不过 Fedora 9 Intel 启动基于 intel-kernelmode 分支。intel-kernelmode 分支目前正在转向基于 intel-batchbuffer 分支。而 Intel-batchbuffer 也正是包含初始 DRI2 支持,TTM 现存管理,Render 改进,以及其他新鲜的改进的分支。在 Mesa/DRM 这边有一个 modesetting-101 分支。

因为内核不再依赖于外部资源来恢复显卡了,挂起和恢复的支持将有很大提高。因为这个过程如今在内核之中了,它能自动并且更快速恢复显示模式。类似地,虚拟终端的切换也因此被提高了。内核模式设置还将提升 debug 表现,这将可以消除所谓的“硬挂(hard hang)”,并将使显示图形化的错误信息成为可能(想象一下在 Linux 中来一个蓝屏死机)。有了这项技术,启动时只需要设置一次显示模式,而无需在开始启动过程的时候(对于Fedora来说,就是RH图形化启动)启动或关闭,然后再在X Server启动、显示GDM时再次初始化设备。基于内核的模式设置是 Intel 的 Keith Packard 列出的让人满意的 Linux 桌面的应有功能的项目列表之一。

基于内核的模式设置组间被缺省包含在 Fedora 9 之中了,但 Intel 的内核模式设置并不会缺省启动。要打开这个设置,需要在启动时在 grub 内参数列表里添上 i915.modeset=1,或是修改 /boot/grub/grub.conf (译注:对其他发布版可能应该是 menu.lst,另外,怀疑这个参数可以通过 modprobe.d 和 initramfs 设置,未经试验,纯属译者猜测)。除非有一天内核的显示模式设置被缺省使用了,这行命令才能省掉,否则总需要这条命令来让系统使用内核态模式设置而不是用户空间的 X 驱动。

Fedora 9 是惟一的一个今年春天发布的带有内核态模式设置功能的发布版。2.6.26 内核的 merge window 马上就要打开了,不过大家的一致看法是这一功能恐怕无法在 2.6.26 进入主线内核。这样,它进入主线内核的时间表将是这个秋天的 2.6.27,不过这恐怕对 Ubuntu 8.10 "Intrepid Ibex" 来说太迟了。Fedora 9 里面包含的只是个功能演示版。Red Hat 和 David Airlie 计划在 Fedora 10 中用内核态的模式设置来支持快速用户切换。

Intel-kernelmode 驱动支持 Intel 915 IGP 及更新的 Intel 显示芯片。为了这篇文章,我们在华擎ConRoe1333-DVI/H 主板上测试了刚刚发布的 Fedora 9 预览版,板载 Intel 945G 整合显卡,带有 VGA 和 DVI 接口。当连接到数字显示器并打开内核态模式设置时,启动过程是白了。RH图形化启动程序无法正确初始化显卡,到了启动GDM的时候,屏幕将闪动数次,但无法成功启动。而使用模拟显示器的时候,i915.modeset=1 可以顺利启动。不过,支持仍然不很完美。当使用内核态的模式设置驱动的时候,drmWaitVBlank 和 IRQ 仍有问题。

下面几页(译注:两页,四段 youtube 视频,略了)是内核态模式设置的工作视频。

基于内核的模式设置是 Linux 和 X.Org 将带给最终用户的显而易见的体验的巨大进步——干净而没有屏幕闪烁的启动过程、快速而稳定的虚拟终端切换、更好的挂起与恢复支持,而且很快将让“快速用户切换”更快。而这些仅仅是冰山一尖,更多的好处,比如图形化调试功能,也将会作为基于内核的显示模式设置功能的引入而显现出来。不过,现在还不是庆祝的时间。Redhat 和所有参与到为 Fedora 9 添加这一功能的出示支持的开发者都应得到赞赏,但仍然需要一段时间才能让更多图形驱动转移到内核态模式设置和并让其他发布版也支持这个功能。将模式设置代码转移到内核中并不是一个快速而直接的过程,但到今年年底,有希望达到一个比较好的状态。Intel 向 X.Org 提交的内容让他们成为了这个恐怖功能的第一个可以工作的驱动。由于内核态模式设置终将被采用并进入主线内核,我们将继续向大家展现更多亮点。

如果你尝试了 Intel 内核态模式设置,别忘了来 Phoronix 论坛 分享你的成果。

Switch to our mobile site