Archive for the ‘translation’ category

[译文] Avro: 大数据的数据格式

November 3rd, 2009

Monday, November 2nd, 2009 at 8:00 am by Doug Cutting, filed under data collection, general, hadoop, mapreduce.
王旭 [ gnawux(at)gmail.com , @gnawux, http://wangxu.me ]于11月2-3日译

译注:Doug Cutting 是 Hadoop 的大佬,目前在 Cloudera,Avro 基本上将成为未来 Hadoop 的数据描述和 RPC 的基础,今天看到这篇,就立刻翻译了,水平有限且译的比较草,请见谅,且欢迎纠错。

Avor 是 Apache 的 Hadoop 项目族的一个新成员。Avro 定义了一种用于支持大数据应用的数据格式,并为这种格式提供了不同的编程语言的支持。

背景

我们希望处理大数据的应用可以更加动态化:人们应该能够快速的从不同的数据源合并数据集。我们希望能够让新颖的、创造性的数据分析变得更方便。比如说,有人需要完美地将销售各个网点的交易、网站的访问量以及外部的统计数据关联在一起,而不需要很多的准备工作。这应该可以使用脚本和交互工具即时完成。

目前的数据格式通常都做的不是很好。XML 和 JSON 可以承载很多信息,但它们本身就很大,处理起来很慢。当你处理上 PB 的数据的时候,尺寸和速度绝对是个大问题。

Google 使用一个称为 Protocol Buffers 的系统来解决这个问题。(还有其他的系统,比如 Thrift,与 Protocol Buffers 很类似,我不会在这里深入讨论它们,但我对 Protocol Buffers 的评价对它们同样适用。)Google 已经开放了 Protocol Buffers,但它对我们的目的来说也不完美。

通用数据

在 Protocol Buffers 中,用户首先定义数据结构,然后生成可以有效读写这些数据的代码。不过,如果你需要在一个脚本语言中直接使用 Protocol Buffer 的数据,你必须首先确定数据结构定义的位置;为它生成代码;最后,在获取数据之前装载代码。这么做可能还算是不错,但如果我们想要一个能浏览任意数据集的通用工具,它将不得不首先定位定义,再为每个数据集生成与装在代码。这让很多本来简单的事情变得复杂了。

与 Protocol Buffer 不同,Avro 格式将数据结构的定义以一种易于处理的形式存储在数据之中。这样,Avro 的实现可以在运行时使用这些定义,将数据以一种通用的方式展现给应用,而不需要生成代码。

代码生成在 Avro 中是可选的:如果某些编程语言要对某些数据结构是由代码生成的话也相当不错,比如需要频繁串行化的数据类型。但是,对于像 HivePig 这样的脚本来说,代码生成可能会是一种过分的负担,所以,Avro 不需要代码生成。

把数据结构定义存储在数据中的一个附加的好处是允许数据可以被更快和更小巧地存储。Protocol Buffers 为数据增加了注释,以保证在定义和数据不完全匹配的时候仍然能够得到处理。然而这些注释会让数据略微增大、处理稍稍变慢。一些评测结果显示,Avro 这样不需要这些注释的数据和其他串行化系统相比,更小而且处理起来更快。

Avro Schemas

Avro 使用 JSON 来定义数据结构的 schema。比如,一个二维平面上的点可以定义为如下的 Avro 记录:


{"type": "record", "name": "Point",
"fields": [
  {"name": "x", "type": "int"},
  {"name": "y", "type": "int"},
]
}

指针的每个实例都包含两个整数,不包含附加的每个记录或每个域的注释。整数使用变长 zig-zag 编码存储。所以小的正负值可能只需要两个字节:100个点可能只需要两百个字节。

在记录和数值类型之外,Avro 支持数组、map、每句、变长与定长二进制数据以及字符串。它还定义了一个容器文件格式,这样可以更好的支持 MapReduce 和其他大数据处理框架。对于更多的细节信息,可以参考 Avro 的规范

兼容性

应用程序都在演进,在这一过程中数据结构是可以改变的。我们希望应用的新版本仍然能够处理老版本创建的数据,反之亦然。Avro 和 Protocol Buffers 处理这个问题的方式比较类似。当应用需要的域没有出现的时候,Avro 会提供一个 schema 中规定的缺省值。Avro 忽略掉数据中的意外值。这并不能处理所有的后向兼容问题,但它让大部分的兼容性问题更容易处理。

RPC

Avro 也定义了一个 RPC 协议。尽管 RPC 中使用的数据类型和数据集中的类型通常是不同的,使用一个通用的串行化系统仍然是有用的。大数据需要一个分布式的基于 RPC 的框架。所以,在我们需要处理数据集文件的所有地方,我们也需要能够使用 RPC。这样,将 RPC 和数据集都建立在同一个基础之上将会最小化地减少那些代码可以处理数据,却无法使用分布式框架来这么做的可能性。

与 Hadoop 集成

我们希望在 Hadoop MapReduce 中可以简单地使用 Avro 数据。目前这项工作仍然在进展中。目前 issue MAPREDUCE-1126MAPREDUCE-815 在处理这个问题。

注意,Avro 数据结构可以定义它们的排序方式,所以一个编程语言中创建的复杂的数据可以在另一个语言中被排序。Avro 可以在不还原序列化的情况下进行排序,这可以加快处理速度。

我们希望 Avro 可以替换掉 Hadoop 中现有的 RPC。Hadoop 目前需要客户端和服务器必须使用相同版本的 Hadoop。我们希望使用 Avro 可以允许一个 Hadoop 应用能和多个运行不同版本的 HDFS 和/或 MapReduce 的多个集群交互工作。

Finally, we hope that Avro will permit Hadoop applications to be more easily written in languages besides Java.  For example, once Hadoop’s built on Avro, we hope to support native MapReduce and HDFS clients in languages like Python, C and C++.

最后,我们希望 Avro 可以允许 Hadoop 应用更容易的使用 Java 之外的语言开发。比如,一旦 Hadoop 构建在 Avro 之上,我们希望支持 Python,C 和 C++ 等语言的 HDFS 和 MapReduce 的原生客户端。

Hadoop World 访谈

很多话题都已经包含在了我最近在 Hadoop World 的访谈,我很乐于在这篇 blog 里包含这段视频。(译注:译者撞墙看不到视频,不贴了哈。)

[译文] XI2 食谱(1)

September 28th, 2009

作者:Peter Hutterer
原文发布时间:2009年6月26日
原文来源:http://who-t.blogspot.com/2009/05/xi2-recipes-part-1.html
译者:王旭 ( http://wangxu.me , @gnawux )
翻译时间:2009年9月28日

XI2 (X Input 2)目前已经进入 XServer 的主线了。在接下来的几天里,我将发布出来一组“食谱”,来介绍如何和这些新功能打交道。这里的例子仅仅是一些片段,每部分的完整的示例都将放在这里

09年7月13日更新:调整到新的 cookie event API

在第一部分中,我将介绍一些总体的信息,初始化以及事件选择。

为什么需要 XI2?

 

引入 XI2 的一个重要原因是要将 MPX 并入 X Server。当前的 1.5 版本的 X Input 扩展余地非常有限,很难进行扩展以完全支持 MPX。在 XI 1.5 上编程非常痛苦,所以,我们开始寻找一个替代方案,从而让支持多设备的程序更容易写一些,并且能灵活地应付将来的应用场景。

XI2 的协议时相当保守的,只增加了一些新的请求和事件,但它也留下了很多的发展空间。XI2 和它的 API 与 X 核心 API 的模式非常接近。从客户应用(X Client)的角度看,与 XI 的最大不同在于调用需要附带一个设备ID参数,不需要去打开设备,而事件类型和掩码都是常数(更多的在下面)。

现在,惟一的 XI2 绑定是 Xlib 绑定。尽管如此,我仍鼓励你开始采用 XI2 并思考程序如何使用它。通过现在进行测试,你可以在 2.0 发布之前帮助发现问题和当前版本中的缺陷。当然,在发布前修正 bug 可以惠及所有人。

MPX

 

MPX允许同时使用多个鼠标指针和键盘焦点。这将有助于引领相当时髦的用户界面——双手操作、多人操作……所有你能说出来的。它还抛弃了当前 GUI 设计的一些假设,但我将在将来再去谈这些。

MPX引入了一个显式的主/从设备层次关系。最简单的记住这个层次关系的方式是:物理设备是从设备,而鼠标指针或键盘焦点是主设备。

从设备附着到一个主设备上,每当从设备产生一个事件,这个事件将被通过主设备路由到客户应用上。主设备总是成对出现(一个指针,一个键盘焦点)。

这样,一个常见的配置可能是一个笔记本有四个主设备(两个键盘焦点和两个鼠标指针),触控板和内建键盘控制第一对主设备,一个USB无线键鼠套装控制第二对主设备。标准配置是有一堆主设备,所有设备都附着到这对主指针和焦点上。这个方案意外地(或无意外地)和我们自从 X Server 1.4 的配置是一样的。这也意味着除非你建立一对新的主设备,否则 MPX 根本就不可见。

我将在后面的文章里更多的介绍 MPX,从大多数客户应用的角度看,主设备是惟一需要关心的问题,只有配置工具和一些其他的特殊程序才真的需要去关心从设备。

XI2 初始化

 

一个典型的 XI2 程序如下面这么开头:

/* Connect to the X server */Display *dpy = XOpenDisplay(NULL);

/* XInput Extension available? */int opcode, event, error;if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) {   printf("X Input extension not available.\n");   return -1;}

/* Which version of XI2? We support 2.0 */int major = 2, minor = 0;if (XIQueryVersion(dpy, &major, &minor) == BadRequest) {  printf("XI2 not available. Server supports %d.%d\n", major, minor);  return -1;}

首先,客户应用连接 X Server,之后询问可用的 XI 扩展的版本。XQueryExtension 不仅告诉我们 XI 扩展是否存在,也会返回扩展的 opcode。这个 opcode 在事件处理时需要用到(所有的 XI2 事件都用到了这个 opcode)。这个 opcode 是在 X Server 启动时设置的,所以,在跨 X Server 的应用中,你不能把它当做个常量。

最后,我们宣布我们支持 XI 2.0,服务器返回它支持的版本。 XIQueryVersion 是一个纯 XI2 调用,即使你在一个不支持 XI2 的服务器上调用,它的实现也不会返回一个 BadRequest 错误(这是在 Xlib 中的实现,所以如果你使用xcb,这个行为可能会不同)。

XIQueryVersion 不仅返回支持的版本,X Server 也会存储你的客户应用支持的版本。随着 XI2 的进程,使用这个调用变得非常重要,服务器将根据支持的版本来区别对待不同的客户应用。

事件的选择


初始化GUI之后,客户应用通常需要选择事件。这一工作可以通过 XISelectEvents 实现。


XIEventMask eventmask;

unsigned char mask[1] = { 0 }; /* the actual mask */

eventmask.deviceid = 2;

eventmask.mask_len = sizeof(mask); /* always in bytes */

eventmask.mask = mask;

/* now set the mask */

XISetMask(mask, XI_ButtonPress);

XISetMask(mask, XI_Motion);

XISetMask(mask, XI_KeyPress);

/* select on the window */

XISelectEvents(display, window, &eventmask, 1);

XIEventMask 定义了一个设备的掩码。掩码定义为(1 事件类型),匹配位必须在 eventmask.mask 域中设置。eventmask.mask 的尺寸可以是任意大小的,倡导可以满足所有需要设置的掩码。XI_LASTEVENT 定义为当前 XI2 协议中的最高的事件类型,所以你可以使用它来确定需要设置的掩码的位数。在本例中,我们只需要6位,所以一个字节就够了。

XISelectEvents 携带了多个事件掩码,所以,你可以一次提交多个事件的选择(比如每个对应一个设备)。

如上所示,每个 XIEventMask 都带有一个设备ID。要么是一个设备的数字ID,或是 XIAllDevices 或 XIAllMasterDevices 这样的特殊 ID。如果你选择 XIAllDevices 的事件掩码,所有的设备都会送来选择的事件。XIAllMasterDevices 也是一样,但只是所有的主设备。

XIAllDevices 和 XIAllMasterDevices 的事件掩码是特定设备事件掩码的一个补充。例如,你可以选择所有设备的按下事件,所有主设备的松开事件,以及设备2的移动事件,设备2将会向客户应用报告所有三类事件(有效掩码是按位或的)。

并且,XIAllDevices 和 XIAllMasterDevices 对所有设备都会生效,即使设备在客户应用选择事件之后才加入也会生效。所以,你只要设置事件掩码一次,无需关心当前有多少设备可用,将来还会进入多少设备。

检查事件的非常简单:

XEvent event;XNextEvent(display, &event);if (ev.xcookie.type == GenericEvent &&    ev.xcookie.extension == opcode &&    XGetEventData(dpy, &ev.xcookie)){    switch(ev.xcookie.evtype)    {        case XI_ButtonPress:        case XI_Motion:        case XI_KeyPress:            do_something(ev.xcookie.data);            break;    }}XFreeEventData(dpy, &ev.xcookie);


对每个事件的数据进行更加细节的分析将在后面的文章中被介绍。

注意,你需要检查 XI 扩展的 opcode,和 XQueryExtension(3) 返回的 opcode 进行比较(如前所述)。

就这么简单,有了这些信息,你已经可以开始写简单的监听所有设备的事件的程序了。在下一部分,我将介绍如何列出输入设备,并监听主/从设备层次结构发生的变化。

[译文] 空指针的乐趣(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的时候还会遇到一定的反对意见。

Java SE 6 Hotspot [TM] 虚拟机垃圾回收调优

November 16th, 2008

来源:http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html
王旭  [ gnawux (at) gmail.com ; http://www.wangxu.me ] 于2008年11月译
译者按:有些术语实在很别扭,不过译者文采有限,没法找到合适的中文词汇,不太影响理解,凑合看吧。

1. 概述

Java 平台标准版(Java SE™)被广泛应用于各种应用,从桌面上的小小的 applet 到大型服务器上的 Web Service 无处不在。为了支持各种不同的部署场景,Java HotSpot™ 虚拟机提供了多种垃圾回收器,每种都为满足不同的需求而设定。这是也为了满足大大小小不同应用需求的一部分。不过,那些需要高性能应用的用户、开发者和管理员们也被选择适合他们应用的恰当的垃圾回收器的繁琐困扰着。取消这些额外操作的重要一步是在 J2SE™ 5.0 中作出的:垃圾回收器会根据应用运行的计算机类型而作出选择。

这个垃圾回收器的“更好的选择”总的说是一种进步,不过,这并不意味着对所有的应用这都是最好的选择。对于有极端的性能或其他需求的用户,仍需要显式地指定垃圾回收器,并调优某些参数,以达到满意的性能。本文就为这些需求提供了一些相关信息。首先,本文会基于串行的 stop-the-world 垃圾回收器来介绍垃圾回收器的一般性特征和基本调优开关。接下来会介绍其他垃圾回收器的特点和如何选择一个垃圾回收器。

何时选择垃圾回收器?对于一些应用,这个答案可能是“永远不”。也就是说,在有低频率、短时的垃圾收集器造成的停顿的情况下,大部分程序都运行良好。不过,这并不适用于很多程序,特别是那些处理大量数据(若干GB)、很多线程和需要处理很多事务的情况。

Amdahl 观察到,大部分工作负载并不能被很好的并行化;有部分情况下总是会被顺序执行,无法从并行化中获益。这对 Java™ 平台也是如此。特别的,在 J2SE 1.4 以前,Sun Java 平台的虚拟机并不支持并行垃圾回收,这样,在多处理器系统中,垃圾回收会对并行应用产生严重影响。

下图显示了一个除了垃圾回收以外均为完美可伸缩的理想系统的性能曲线。红色曲线是一个在但处理器系统中会花费 1% 的时间在垃圾回收上的程序。它在 32 处理器的系统中,将损失 20% 的吞吐量。而一个花费 10% 时间在垃圾回收上的应用(不考虑单处理器系统中额外的垃圾回收时间)在系统扩张到 32 处理器系统中时,会损失超过 75% 的吞吐量 。

这意味着在小型开发系统中微不足道的速度问题当扩张到大规模系统中就可能成为严重的性能瓶颈。从另一个角度看,减少这样的性能瓶颈的小改动就可以获得很大的性能收益。对足够大规模的系统,选择合适的垃圾收集器并进行必要调优是绝对值得的。

对于大多数“小”应用(在现代处理器上大约需要100MB堆内存的应用)来说通常是足够的。其他垃圾收集器会带来额外的负载或复杂性,这回让系统的某些行为付出一定的代价。如果一个应用不需要一个垃圾收集器的某个功能。那么就使用串行的垃圾收集器好了。一个不应该使用串行垃圾收集器场景是一个超多线程的大程序运行在一个大型的、有大量内存和两个或多个处理器的系统中。当应用运行在这些服务器级的计算机上的时候,并行垃圾收集器会被缺省选择(参见下面的功效学 )。

本文以 Solaris™ 操作系统(SPARC(R) 平台版本)中的 Java SE 6 作为参考。不过,文中所述的概念和建议适用于所有支持的平台,包括 Linux, Microsoft Windows 和 Solaris 操作系统(x86 平台版本)。此外,文中的命令行参数也对所有平台有效,虽然它们的缺省值在各个平台可能有所不同。

2. 功效学(Ergonomics)

“功效学”是一个 J2SE 5.0 引入的概念。引入功效学概念是为了通过不设置或设置很少的几个命令行参数的情况下提供更好的性能,这些参数包括:

  • 垃圾收集器,
  • 堆尺寸,
  • 和运行时编译器

这里的参数选择假定应用所运行的主机类型和应用的类型一致(也就是说,大型应用运行在大型的机器上)。这些选项简化了垃圾回收的调优。选择并行垃圾回收器,用户可以指定应用的最大中断时间和希望的吞吐量。这和指定堆大小来调优性能是相对应的。最常用的功效学相关的内容在可以参考 “Ergonomics in the 5.0 Java Virtual Machine” 这篇文章。建议在尝试本文提到的细节配置之前尝试该文章中介绍的功效学手段。

本文中的功效学特性被作为并行垃圾回收器的自适应尺寸策略的一部分。这包括指定垃圾回收性能的目标和性能调优的一些附加选项。

3.代

J2SE 平台的优势之一是它将内存分配、垃圾回收这些繁复的细节屏蔽了起来。然而,一旦垃圾回收成为主要的瓶颈,那么理解一下这些隐藏在背后的细节就变得有必要了。垃圾回收器对应用程序对对象的使用方式进行判断,这个判断会反映在可调优参数中,他们可以被调整,以提高性能而不牺牲掉抽象性。

当一个对象不再可能被从其他任何地方访问到的时候就会被认为是垃圾了。最直接的垃圾回收算法就是简单地迭代所有可找到的对象。任何没有被跌带到的对象都可以被认为是垃圾了。这个方法的用时和活着的对象数量成正比,这对于那些维护着大量活数据的程序来说是不可接受的。

从 J2SE 1.2 开始,虚拟机就引入了各种不同的垃圾回收算法,这些算法都使用分代垃圾收集。尽管原生的垃圾回收会检查堆中的所有活着的对象,分代垃圾收集采用了很多观测到的大部分应用程序的经验特征,用来最小化发现废弃的对象的工作量。最重要的经验特征是 weak generational 假设,该假设认为大部分对象都只存活一少段时间。

下图中的蓝色区域是对象生存期的典型分布。横轴是对象被分配后的生存期。纵轴方向计算的字节数是相应生存期的对象的总字节数。左侧的尖峰表明,对象在分配之后不久就被废弃了。比如,迭代器对象常常只会在一个循环中被用到。

当然,有些对象确实活得要长一些,于是,分布曲线延伸到了右边。比如,典型情况下,有些对象在初始化的时候被创建,并一直存活到进程结束。在这两种极限情况之间,那些对象活的时间也是中等的,在图中表现初来的就是从开始的峰值泄漏初来的蓝色区域。有些应用可能会有看起来十分不同的分布曲线,不过绝大多数的进程都是这个常见的形状。大部分对象都会“英年早逝”这个事实让高效的垃圾收集变得具有可能性了。

为了为这样的应用环境优化,内存被按照“代” (generation)进行管理,或者说,内存池中存放不同年龄的对象。当一个年龄断被填满后,就对该代的垃圾进行回收。在内存池中的大部分对象都是年轻的对象(年轻的代),而大部分对象也会在年轻的时候就成为垃圾。当年轻代被填满的时候,会导致一次“小回收”(译注:原文minor,似乎“未成年”更贴切一些,不过咱们读起来会很别扭),这里只有年轻代的对象惠北回收,而其他年龄断的垃圾则不与理会。该回收算法的成本是,一阶情况下,正比于被回收的活的对象的数量;年轻代因为满是死对象,所以回收非常迅速。而在“小回收”中存活下来的对象于是乎就会被转移到所谓的年老代(tenured generation)。最终,当年老代被填满而需要回收的时候,就会导致一次主回收,这时整个堆都会被回收。主回收通常会运行锝比小回收慢很多,因为大量的对象都会被处理。

如上文记述,对不同的应用,“工效学”会动态选择垃圾收集器来提供较好的性能。串行垃圾收集器用于哪些数据量比较小的程序,而且它的缺省参数也让大多数小程序能够高效工作。而大吞吐量垃圾收集器用于那些有中到大数据量的数据集。工效学选择的堆尺寸参数和自适应尺寸策略用于为服务器提供更好的性能。这些选择的大多数而不是所有的情况下工作得很不错。这就引出了本文的核心宗旨:

如果垃圾收集器成为了瓶颈,你可能不得不调整整个堆的大小乃至每个代的尺寸。检查垃圾收集器的详细输出,然后检查垃圾收集器对你关注的各个性能指标的影响。

(并行垃圾收集器之外的)缺省的代排布大概就是这样的。

初始化的时候,最大的地址空间虚拟地保留住而没有分配出去,直到真的需要的时候为止。整个保留的对象地址空间被分给了年轻的和年老的代。

年轻代包括“伊甸园”和两个幸存者空间。大部分对象最初在伊甸园里被分配出来。一个幸存者空间在任意时刻都是空的,作为伊甸园中的活对象的目的地,另一个是用于下一次收集。对象在幸存者空间之间停留到足够老之后,就会被复制到年老代去了。

另一个和年老代有密切关系的代是永久的(permanent)代,这里保存着虚拟机需要的用来描述那些 Java 语言层面没有等价物的对象。比如,那些描述类和方法的的对象就存放在永久代。

3.1 性能考虑

对于垃圾回收的性能,主要有两种量度方法:

  1. 吞吐量。吞吐量是在一段足够长的时间中,没有花费在垃圾回收上的时间占总时间的百分比。吞吐量包含了花在空间费配上的时间(不过空间分配速度的调优一般是没有必要的)。
  2. 延时。延时是由于等待垃圾回收而导致的程序没有响应的时间。

不同的用户对垃圾收集有不同的需求。比如,对于一个web server而言,吞吐量是合理的量度,因为垃圾收集带来的短时时延是可以容忍的,或者说是很容易就被网络时延所掩盖了。不过,对于交互的图形界面程序而言,极短的停顿都会影响用户的使用体验。

有些用户对其他的因素很敏感。Footprint是一个进程的工作集,由页和cache line来量度。对于内存相对于进程数量很有限的系统而言。Footprint会影响到程序的可伸缩性。Promptness是对象死掉和该块内存重新可用之间的时间间隔的量度,这是分布式系统的一个重要考虑因素,包括远程方法调用(RMI)。

总的说,一个特定的代的尺寸选择是上述这些因素之间的权衡的结果。比如,一个非常大的年轻代的大小可以最大化吞吐律,但会以Footprint、Promptness和延时作为代价。而年轻代延时可以通过缩小该代的大小来达到最小化,但同样会损失吞吐量。近似地,调整一个代的尺寸不会影响到其他代的垃圾收集频率和时延。

没有一个简单的方法来设置代的尺寸。最好的选择由程序使用内存的方式和用户的需求来决定。这样,虚拟机对垃圾收集器的选择并不总是最优的,而且可以通过后面介绍的命令行参数来调整。

3.2 测量

使用应用特定的量度,吞吐量和footprint很容易被测量。例如,web服务器的吞吐量可以使用一个客户端负载生成器来测量,而该服务器的 footprint 则可以在 Solaris 操作系统中使用 pmap 命令来测量。另一方面,垃圾收集导致的时延可以方便地通过监测虚拟机自己的诊断输出来估算出来。

命令行参数 -verbos:gc 可以送出每一次垃圾收集时的堆和垃圾收集信息。比如,这是一个大型服务器应用的输出:

[GC 325407K->83000K(776768K), 0.2300771 secs][GC 325816K->83372K(776768K), 0.2454258 secs][Full GC 267628K->83769K(776768K), 1.8479984 secs]

这里是两次小回收和之后的一次主回收。箭头前后的数字(比如第一行的325407K->83000K)分别指垃圾回收前后的所有活着的对象占用的空间。在小回收之后,这个尺寸之中仍然包含一些没有被回收的垃圾(死掉的对象)。这些对象要么存在在年老代中,要么被年老或永久代中的对象所引用。

后面的括号中的数字(比如第一行中的 (776768K))是全部提交的堆大小,也就是虚拟己不向操作系统申请内存的情况下,全部 java 对象可用的存储空间。注意,这个数字不包括幸存者空间中的一个,因为幸存者空间在一个给定时间只有一个可用,同时也不包括永久代的空间,这里面是虚拟机使用的元数据。

最后一个数字(比如 0.2300771 secs)是垃圾收集所用的时间;这个例子里大约是四分之一秒。

第三行中主垃圾回收的格式也是类似的。

-verbos:gc 输出的格式可能在将来的版本里有所改变。

通过-XX:+PrintGCDetails参数可以查看更多垃圾回收相关的信息。下面是串行垃圾收集器使用该参数打印出来的信息。

[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]

这个信息显示,这次小回收收回了 98% 的 DefNew 年轻代的数据,64575K->959K(64576K) 并在其上消耗了 0.0457646 secs(大约45毫秒)。

整个堆的占用率下降了大约51% 196016K->133633K(261184K),而且通过最终的时间 0.0459067 secs 显示在垃圾收集中有轻微的开销(在年轻代之外的时间)。

选项-XX:+PrintGCTimeStamps会提供每次回收开始时间的时间戳。这对于查看垃圾回收频率非常有用。

111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs] 111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs]  26282K->2311K(32704K), 0.1293306 secs]

如上,垃圾回收在程序运行后111秒开始。小回收同时启动。信息中还显示了主回收中的年老代的垃圾回收信息。年老代的空间使用率下降了大约 10% 18154K->2311K(24576K) ,用时 0.1290354(大约130毫秒)。

和 -verbose:gc 一样,-XX:+PrintGCDetails 的输出格式在将来的版本里也可能会有所变动。

4. 代的尺寸

很多参数会应想到代的尺寸。下图是堆中的提交空间和虚拟空间的差别。虚拟机初始化的时候,整个堆空间都是保留的。保留空间可以通过参数 -Xmx 指定。如果-Xms参数小于-Xmx参数,那么不是所有的保留空间都会立刻提交到虚拟机之中。未提交的空间在途中标记为 virtual。堆的不同部分(永久时间段、年老时间段和年轻时间段)可以按需生长到虚拟空间的限制为止。

一些参数可以调整堆的不同部分的比例,比如参数NewRatio指定年老代对年轻代的比例。这些参数将在下面讨论。

4.1 全部堆

注意,下面的关于堆的生长、收缩和缺省堆大小都不适用于并行垃圾收集器,并行垃圾收集器请参考相关章节。不过,用于控制整个堆大小和代尺寸的参数对并行垃圾收集器都是适用的。

因为垃圾收集是发生在代被填满的时候,所以,吞吐量反比于可用此内存数量。总可用内存数是影响垃圾收集性能的最重要因素。

缺省情况下,虚拟己在每次垃圾收集后增加或减少堆尺寸,来尽量保持可用空间对活的对象之间的比例在一个区间之内。这个目标区间通过参数-XX:MinHeapFreeRatio=<minimum>和-XX:MaxHeapFreeRatio=<maximum>来设置,而总的堆大小的界限由-Xms<min>和-Xmx<max>来确定。这些参数在 32 位 Solaris 系统(SPARC 平台版本)中的缺省值如下表所示:

Parameter


Default Value


MinHeapFreeRatio

40

MaxHeapFreeRatio

70

-Xms

3670k

-Xmx

64m

64位系统中的堆尺寸的参数会大 30% 左右,这个增长用来补偿64位系统中更大的对象所带来的开销。

通过设置这些参数,当一个代的可用空间低于 40%,虚拟机就会把可用内存扩展到 40%,直到代的最大尺寸。同样的,如果可用空间超过 70%,代就会被缩小,使得只有 70% 可用空间,直到达到代最小的空间为止。

大型服务器程序在使用这些缺省设置时,经常遇到两种问题。其一是慢启动问题,初始的堆尺寸过小,经常需要经历多次主回收才能达到稳定值。另一个更现实的问题是,对于大多数服务器应用来说,这个缺省的最大堆大小太小了。对于服务器程序而言,设置的一般原则是:

  • 除非遇到了时延问题,给虚拟机尽量多的内存。缺省尺寸(64MB)通常都太小了。
  • 把-Xms 和 -Xmx 设置成相同的值,把最重要的尺寸决定从虚拟机收回来,从而增强可预见性。
  • 一般地,随着处理器数量的增加而增加内存,因为内存分配可以被并行化。

作为参考,有一个单独的页面会介绍各个命令行参数

4.2 年轻代

影响位居次席的是用于年轻代的堆比例。年轻代越大,小回收的次数也就越少。不过,在一定的堆大小的情况下,年轻代越大,年老代也就越小,这就增加了主回收的频率。最佳选择依赖于应用中分配的对象的生存期分布。

缺省的,年轻代的尺寸由 NewRatio 控制。比如,设置-XX:NewRatio=3意味着年轻代和年老代的比例是1:3。换句话说, eden 和幸存者空间的总和是整个堆大小的四分之一。

参数 NewSize 和 MaxNewSize 约束了年轻代的上下界限。可以把这两个参数设成相同的值来固定年轻代的大小,设置 -Xms 和 -Xmx 一样来设置堆大小为固定值。这样可以比使用NewRatio更细粒度地调整年轻代的大小。

4.2.1 幸存者空间

如果需要,SurvivorRatio 可以用来调整幸存者空间的大小,不过这对于性能一般影响不大。比如,-XX:SurvivorRatio=6 会设置幸存者空间和eden的比例是 1:6。换句话说,每个幸存者空间将是 eden 的六分之一,是整个年轻代空间的八分之一(不是七分之一,因为一共有两个幸存者空间)。

如果幸存者空间过小的话,拷贝收集到的幸存者将会直接溢出到年老代的空间中去。如果幸存者空间太大的话,他们也就是空着浪费掉。每次垃圾收集中,虚拟机会选择一个对象在成为年老的之前被复制的次数门限。这个门限的设置会保证幸存者空间是半满的。命令行参数-XX:+PrintTenuringDistribution 可以显示这个门限和年轻代中对象的年龄。这对于观测应用中对象的生存期分布也是有用的。

下面是 SPARC 上的 32 位 Solaris 的各个参数的缺省值,在其他平台上可能有所差异。

Default Value


Parameter


Client JVM


Server JVM


NewRatio

8

2

NewSize

2228K

2228K

MaxNewSize

not limited

not limited

SurvivorRatio

32

32

年轻代的最大尺寸通过最大堆尺寸和 NewRatio 计算而得。所谓的“无限制”的缺省值是说这个计算的值不会受到 MaxNewSize 的约束,除非命令行中指定了这个值。

服务应用的设置准则是:

  • 首先确定可以提供给虚拟己的最大堆尺寸。然后根据性能需求来确定年轻代的尺寸,来找到最佳设置。
    • 注意:最大堆尺寸一定要小于系统中的内存数量,以防止过多的缺页错误和换页。
  • 如果总的堆尺寸是确定的,增加年轻代的尺寸就会减少年老代的尺寸。一定要保证年老代的尺寸,使之可以容纳所有在应用全程都要用到的活对象,并留有一定裕量(10-20%或更多)。
  • 依照上述年老代的约束:
    • 给年轻代分配足够的内存。
    • 如果有多个处理器,那么分配更多的内存给年轻代,因为内存分配可以并行化。

5. 可用的垃圾收集器

到目前为止,我们讨论的还都是串行垃圾收集器。不过 Java HotSpot 虚拟机一共支持了三种不同的收集器,每种提供不同的性能特性。

  1. 串行垃圾收集器使用单线程进行所有垃圾收集工作,因为没有线程间通信的开销,串行垃圾收集器相当高效。串行垃圾收集器最适合于单处理器系统,因为它不会从多处理器硬件中获益,尽管在小数据量的应用中(不大于100MB的),它对于多处理器系统也是游泳的。串行垃圾收集器在一定的硬件和操作系统的配置时会缺省使用,也可以显式地用 -XX:+UseSerialGC 参数来指定。
  2. 并行垃圾收集器(或吞吐垃圾收集器)并行进行小垃圾收集,这会显著减少垃圾收集的的开销。它适用于中等或大尺寸数据的运行在多处理器或多线程硬件上的应用。并行垃圾收集器也会在一定的硬件和操作系统配置下被缺省使用,同时,也可以使用 -XX:+UseParallelGC 参数来指定。
    • 更新:“并行压缩”是 J2SE 5.0 update 6 以上版本的新特性,并在 Java SE 6 之中得到加强,该特性允许主回收也并行收集。如果不使用并行压缩,主回收仍然会单线程运行,这会严重限制系统的可伸缩性。并行压缩可以使用命令行参数-XX:+UseParallelOldGC 来打开。
  3. 并发垃圾收集器并发地进行大部分垃圾收集工作(也就是在应用运行当中进行)来尽可能煎炒垃圾收集带来的应用停顿。它是为哪些拥有中到大量数据的、对响应时间要求高于吞吐量要求的应用,因为最小化时延的技术会让吞吐能力付出代价。并发垃圾收集器通过 -XX:+UseConcMarkSweepGC 参数来启用。
5.1 选择垃圾收集器

除非你的应用有非常严酷的时延要求,那么就运行你的应用,并让系统自己选择垃圾收集器好了。如果有必要的话,就调整堆的大小来增进性能。如果性能仍然无法达到你的目标,那就按照如下设置来选择一个垃圾收集器。

  1. 如果应用的数据很少(大约不超过100MB),那么
    • 使用-XX:+UseSerialGC选择串行垃圾收集器。
  2. 如果应用运行在单处理器系统中,并且没有什么时延要求,那么
    • 让虚拟机选择垃圾收集器,或者
    • 使用-XX:+UseSerialGC选择串行垃圾收集器。
  3. 如果(a)程序峰值性能是第一位的,并且(b)没有时延要求,或时延要求是一两秒或更长,那么
    • 让虚拟机选择垃圾收集器,或者
    • 使用-XX:+UseParallelGC选择并行垃圾收集器,乃至(可选)通过 -XX:+UseParallelOldGC启用并行压缩。
  4. 如果响应时间比总体吞吐量更为重要,并且垃圾收集时延需要控制在1秒以内,那么
    • select the concurrent collector with -XX:+UseConcMarkSweepGC. If only one or two processors are available, consider using incremental mode, described below.
    • 通过 -XX:+UseConcMarkSweepGC 参数启用并发垃圾收集器。进当你有一个或两个处理器可用的时候,考虑使用下文将要介绍的“增量模式”。

这些指导意见仅仅是选择垃圾收集器的起点,因为性能依赖于堆的尺寸、应用中活数据的数量,以及处理器的数量和速度。时延参数对这些因素尤为敏感,所以,所谓的1秒门限值只是个大致数值:在很多硬件和数据量的组合情况下,并行垃圾收集器可能会导致停顿时间超过1秒;同样,在某些组合下,并发垃圾收集器也不能保证停顿小于1秒。

如果推荐的垃圾收集器没有达到期望的性能,首先应该尝试堆和代的尺寸,以期达到目标。如果仍然不成功的话,尝试更换一个垃圾收集器:使用并发垃圾收集器来减少停顿时间,使用并行垃圾收集器来增加多处理器系统中的吞吐量。

6. 并行垃圾收集器

并行垃圾收集器(也被称为吞吐量收集器)和串行收集器类似,也是一种分代垃圾收集器;其最大的不同在于它使用了多线程来加快垃圾收集的过程。并行垃圾收集器可以通过参数 -XX:+UseParallelGC 指定。缺省的,只有小回收会并行运行,主回收仍然单线程运行。不过,通过参数-XX:+UseParallelOldGC启动并行压缩可以让主回收和小回收都并行运行,从而进一步减少垃圾收集开销。

在一个有N个处理器的计算机上,并行垃圾收集器使用N个垃圾收集器线程。不过,这个数量可以在命令行参数里指定(参见下文)。在一台单处理器的计算机上,由于线程开销(比如同步),并行垃圾收集器的性能应该不如串行垃圾收集器。然而,当应用程序有中等或大尺寸的堆的时候,它在一个双处理器的机器上就会略优于串行垃圾收集器,而如果有多于两个处理器的话,它就能远胜于串行垃圾收集器。

垃圾收集器线程数的多少可以用-XX:ParallelGCThreads=<N>参数来控制。如果要使用命令行参数显式调整了堆的尺寸,使用并行垃圾收集器的情况下需要的堆的尺寸和使用串行垃圾收集器情况下的堆的尺寸是一阶相等的。使用并行垃圾收集器仅仅是让小回收造成的停顿更短一些。因为有多个垃圾收集器线程参与小回收的过程,有极少的可能性可能会在将年轻代移动到年老代的过程中造成一些碎片。每个垃圾收集线程都有一块专属的年老代的空间,用于年轻代向年老代的移动,将年老代的可用空间划分为“移动缓冲”(promotion buffer)的过程可能会造成一定的碎片效应。减少垃圾收集器线程的数量可以减少碎片、增加年老代的空间。

6.1 代

正如上面提到的,并行垃圾收集器的代的排布方式和串行垃圾收集器略有不同。其分布如下图所示。

6.2 功效学

自 J2SE 5.0 以来,并行垃圾收集器成为了server级机器的缺省垃圾收集器,详细资料可以参考“Garbage Collector Ergonomics”。此外,并行垃圾收集器使用一种自动调整机制来指定期望的行为而不是指定代的大小和其他底层调整细节。这些行为包括:

  • 最大垃圾收集停顿时间
  • 吞吐量
  • Footprint (也就是堆尺寸)

最大停顿时间的目标由参数-XX:MaxGCPauseMillis=<N>来指定。这个参数被解释为指定停顿时间不得大于<N>毫秒;缺省情况下没有最大停顿时间目标。如果指定了一个停顿时间目标,堆尺寸和其他垃圾回收相关参数就会被相应调整,以便保持垃圾回收时间小于指定的值。注意,这些调整可能会导致总体吞吐量的降低,而且,在某些情况下,要求的停顿时间目标可能无法达到。

吞吐量目标测量垃圾回收时间和非垃圾回收时间(也就是应用时间)的比例。这个目标时间可以用命令行参数-XX:GCTimeRatio=<N>来指定,这样,垃圾回收时间和应用时间的比例将是1 / (1 + <N>)。例如,-XX:GCTimeRatio=19设置1/20活5%的时间用于垃圾回收。缺省值是99,目标是1%的时间用于垃圾回收。

最大堆footprint使用已经存在的 -Xmx<N> 参数。此外,如果没有其他的优化目标的话,垃圾收集器有一个隐式的最小化堆尺寸的目标。

6.2.1 目标的优先级

目标的优先级顺序如下:

  1. 最大停顿时间目标
  2. 吞吐量目标
  3. 最小堆尺寸目标

最大停顿时间目标会被首先满足。仅当最大停顿目标被满足的情况下,才会去满足吞吐量目标。类似的,仅当前两个目标都会满足的情况下,才会考虑去满足footprint目标。

6.2.2 时间段尺寸调整

每次垃圾收集结束的时候,垃圾收集器都会更新其保存的平均停顿时间之类的统计参量。同时它会检查各个目标是否被满足了,是否有调整代尺寸的需要。这之中的意外情况就是显式的垃圾收集(比如调用 System.gc())会在统计和调整判断中被忽略掉。

增加和缩小一个代的大小是通过增加活缩小一个固定的百分比来达到的,这样一个代要分步来达到需要的尺寸。增加活所见是以不同的比率来进行的。缺省情况下,一次增加 20% 活减少 5%。年轻代和年老代增量的比例分别通过命令行参数 -XX:YoungGenerationSizeIncrement=<Y>和-XX:TenuredGenerationSizeIncrement=<T>来设定。而缩小比例的要通过-XX:AdaptiveSizeDecrementScaleFactor=<D>参数来设定。如果增量是X%,那么每次减小量就是(X/D)%。

如果垃圾收集器决定在启动的时候增加一个代的大小,会有一个额外的百分比的增量。这个附加的增量随着收集的次数而减少,不会长期影响。这个额外增量意在提高启动速度。缩小代的尺寸是没有这个额外的增量。

如果最大停顿时间目标没有达到,会有且仅有一个代的大小被缩小。如果两个代都在目标之上,停顿时间较大的那个代会首先被缩小。

如果总体吞吐量目标没有达到,那么两个代的大小都会增加。每个都按照各自对垃圾回收时间的贡献比例分别增加。比如,如果年轻代的垃圾回收时间占去了25%的总垃圾回收时间,并且年轻代的全部增量应该是20%,那么这时它的增量就是5%。

6.2.3 缺省堆尺寸

如果没有在命令行中进行设置,初始和最大堆尺寸会通过计算机内存计算而得。如下表所示,对大小占用的内存的比例是由参数 DefaultInitialRAMFraction和DefaultMaxRAMFraction来控制的。(表中的 memory 代表计算机的系统内存数量。)

Formula


Default


initial heap size

memory / DefaultInitialRAMFraction

memory / 64

maximum heap size

MIN(memory / DefaultMaxRAMFraction, 1GB)

MIN(memory / 4, 1GB)

注意,缺省的最大堆尺寸不会超过1GB,不论系统中到底有多少内存。

6.3 过多的GC时间和OutOfMemory错误

当有过多的时间花费在垃圾收集上的时候,并行垃圾收集器会跑出 OutOfMemoryError 错误:如果超过 98% 的时间花费在垃圾收集上并且只有 2% 的堆被释放的话,就会抛出一个 OutOfMemory。这个功能是用来防止堆太小导致程序长时间无法正常工作而设计的。如果必要,这个功能可以使用命令行参数-XX:-UseGCOverheadLimit来关闭。

6.4 测量

并行垃圾收集器的垃圾收集器详细输出和串行垃圾收集器是一样的。

7. 并发垃圾收集器

并发垃圾收集器适用于那些需要更短的垃圾收集停顿,并能为此付出程序运行期处理器资源的应用。典型情况下,那些拥有较多长期存在的对象(年老代比较大),并且运行在拥有两个活更多处理器的应用可能会因此获益。不过,在任何要求很低停顿时间的应用都应该考虑这个垃圾收集器;比如,拥有较小年老代的交互程序在但处理器上使用并发垃圾收集器就可以收到明显的好处,特别是使用增量模式的时候。并发垃圾收集器可以通过命令行参数-XX:+UseConcMarkSweepGC来启动。

和其他垃圾收集器类似,并发垃圾收集器也是分代的;所以也有小回收和主回收。并发垃圾收集器通过使用独立的垃圾收集线程于应用本身的线程并发执行跟踪所有可及的对象,以期降低主回收导致的停顿。在每个主回收周其中,并发垃圾收集器会在垃圾收集的开始让所有应用线程暂停一下,并在回收中段再暂停一次。第二次暂停相对而言会更长一些,在此期间会有多个线程来进行收集工作。剩下的收集工作包括大部分的活对象跟踪和清除不可及的对象的工作都由一个或多个和应用并发的垃圾收集器线程来进行。小回收会在进行的主回收周其中穿插进行,其模式和并行垃圾收集器十分类似(特别需要说明的就是,在小回收期间,应用线程是会有停顿的)。

并发垃圾收集器的基本算法在技术报告 A Generational Mostly-concurrent Garbage Collector里有介绍。主义,实际的实现细节在不同版本里手有细微的变化的,因为垃圾收集器也在一直进步。

7.1 并发性的开销

并发垃圾收集器的短主回收停顿时间是以处理器资源作为代价的(这些资源如果不用在收集器上肯定就要用在应用上了)。最明显的开销就是并发地使用了一个或多个处理器资源。在N处理器系统中,垃圾收集的并发部分会使用K/N的可用处理器,其中 1<=K<=ceiling{N/4}。(注意,K值的上限将来可能会有变化。)并发垃圾收集器不仅在并发阶段使用处理器,还引入了其他的开销。所以,尽管并发垃圾收集器显著减少了程序的停顿,但和其他垃圾收集器相比,应用的总体吞吐量会受到轻微的影响。

在拥有多个处理器的计算机上,在并发垃圾收集器运行的时候,应用程序仍然能使用到CPU,所以,并发垃圾收集器并没有让程序停顿。这通常意味着更短的停顿,谈也意味着更少的应用可用的处理器资源,并且让它运行得相对比较慢,特别是当应用可以完全的利用多个CPU核心的时候更是如此。随着N的上升,垃圾收集器导致的损失会相对变小,而从并发垃圾收集的获益则相对提高。下一节“并发模式失败”会讨论这种规模扩张的潜在局限。

因为在并发阶段至少有一个处理器用于了垃圾收集,所以在单处理器(单核)系统中,并发垃圾收集器一般不会带来什么好处。不过,并发垃圾收集有一个分离模式可以在单处理器或双处理器系统中显著减少停顿时间;后面的增量模式中将会进一步介绍其细节。

7.2 并发模式失败

并发垃圾收集器使用一个或多个垃圾收集线程在应用线程执行的同时运行,从而在年老代和永久代变满之前就完成垃圾收集。如前文所述,在一般的操作中,并发垃圾收集器的大部分跟踪与清理工作是在程序运行的同时进行的,所以,程序线程只有极少的停顿。但是,如果并发垃圾收集器在年老代变满的时候仍没有完成垃圾清除工作,或是年老代中的可用空间无法满足一次分配操作的需要的时候,应用就不得不被暂停下来以等待应用线程结束了。这种无法并发地完成垃圾收集的情况被称为“并发模式失败”,这就需要对并发垃圾收集器的参数进行调整了。

7.3 过多的GC时间和OutOfMemory错误

并发垃圾收集器会在垃圾收集消耗时间过多的时候抛出 OutOfMemoryError 错误:如果多于 98% 的时间被花费在了垃圾手机上,并且仅有少于 2% 的堆被回收的话,就会抛出 OutOfMemoryError。这个功能是用来防止堆太小导致程序长时间无法正常工作而设计的。如果必要,这个功能可以使用命令行参数-XX:-UseGCOverheadLimit来关闭。

这个策略和并行垃圾收集器是基本一致的,惟一的区别就是并发的垃圾收集时间并未计算在内。也就是说,只有哪些程序停顿下来进行垃圾收集的时间才被计算在内了。这些垃圾收集常常是由于并发模式失败或是显式垃圾收集请求(如调用 System.gc())导致的。

7.4 浮动垃圾

并发垃圾收集器与 HotSpot 中的其他垃圾收集器一样,是一种识别至少所有在堆中可以被访问到的对象的跟踪收集器。按照Jones and Lins的说法,是一种增量更新(Incremental Update)垃圾收集器。因为应用现成和垃圾收集器线程在主回收过程中并发执行,那么那些垃圾收集器跟踪的对象就可能在垃圾收集完成之后变成垃圾这些无法访问却还没有被回收的对象被称为浮动垃圾(floating garbage)。浮动垃圾的数量取决于垃圾收集周期的长度和程序中引用更新的频率,也被称为转化率(mutation)。而且,另一个原因是年轻代和年老代的收集是独立的,彼此都是对方的根。一个粗略的配置规则是为年老代的浮动垃圾多预留出20%的空间来。一个垃圾回收周期中的堆中的浮动垃圾会在下一个垃圾回收周期中被回收。

7.5 时延(停顿)

并发垃圾收集器在一个并发回收周期中会两次暂停应用。第一次会从根从根(比如从对象线程栈和寄存器、静态对象等的引用)和堆的其他部分(如年轻代)开始标记所有直接可达的活的对象。第一次停顿被称为“初始标记停顿”(initial mark pause)。第二次停顿发生在并发跟踪阶段末尾,用来发现由于在垃圾收集线程跟踪完一个对象之后又被应用线程更新了其引用而没有被并发跟踪到的对象。这次停顿被称为“重标记停顿”(remark pause)。

7.6 并发阶段

可达对象的并发跟踪图发生在初始标记停顿和重标记停顿之间。在并发跟踪阶段中,一个或多个并发垃圾收集器线程会使用那些本来可能会被应用使用的处理器资源,所以尽管不会停顿,计算密集型应用可能会在此阶段和其他并发阶段受到相当的吞吐量损失。在重标记停顿之后,还有一个并发清理阶段,会收集所有标记为不可达的对象。一旦手机周期结束了,并发收集器就会进入等待阶段,这时就基本不会消耗任何计算资源了,直到下一个主回收周期开始为止。

7.7 开始并发收集周期

在串行收集器中,每当年老代满了的时候都会引发一次主回收,所有应用现成都会在主回收期间暂停运行。并发垃圾收集器与之不同,它需要在足够早的时间开始垃圾收集,以便能在年老代变满之前完成垃圾收集;否则的话就会因为并发模式失败而导致较长的时延。有很多种条件可以触发并发垃圾收集器启动。

基于最近的历史记录,并发垃圾收集器维护了一个年老代变满的预期剩余时间和一个垃圾收集周期的预期时间。基于这些动态估计,并发垃圾收集周期会以让垃圾收集周期在年老代变满之前完成为目标开始并发垃圾收集周期。因为并发模式失败的代价非常惨重,这些估值都流出了安全裕量。

并发垃圾收集在年老代的已用百分比超出了一个初始占有率值(initiating occupancy)的时候也会启动。这个初始占有率阈值的缺省值大约是 92%,不过这个值可能在不同版本中略有不同。它也可以通过命令行参数-XX:CMSInitiatingOccupancyFraction=<N> 来手工设置,其中N是一个0-100的整数,代表年老代的占用百分比。

7.8 调度中断

年轻代和年老代的垃圾收集的停顿发生彼此间是独立的。他们不会重合,但可能会连续发生,这样也就让一个垃圾收集的停顿连上下一个垃圾收集的停顿了,从外界来看就是一个长停顿了。为了避免这种情况,并发垃圾收集器会调度重标记停顿的时间,使之发生在前后两个年轻代停顿之间。这个调度目前还不应用于初始标记停顿,因为它通常会比重标记停顿短很多。

7.9 增量模式

并发垃圾收集器可以在这样一种模式下工作:并发阶段以增量的方式进行。回忆一下,在并发阶段,垃圾回收线程会使用一个或多个处理器。所谓增量模式是指减少长并发阶段的影响,周期性中断并发阶段,将处理器资源还给应用程序。这种模式又称为“i-cms”,将垃圾收集器的并发工作划分到小块时间,在年轻代垃圾收集之间进行。这个功能对于那些工作在没那么多处理器的机器上(1或2个处理器的)需要并发垃圾收集器的低时延应用非常有用。

并发垃圾收集周期通常包括如下几步:

  • 停止所有的应用线程,标记从根开始可达的对象集,然后继续所有的应用线程
  • 在应用线程运行的同时,使用一个或更多的处理器,并发跟踪可达的对象图
  • 使用一个处理器,并发跟踪对象图中在上一步开始之后的各个改动的部分
  • 停止所有的应用线程,重新跟踪根和对象图中自从上次检查开始发生了变化的部分,然后继续运行线程
  • 使用一个处理器,并发地把不可达对象清理到用于分配空间的 free list 上面去。
  • 使用一个处理器并发地调整堆的大小,准备下一个回收周期所需的数据结构

正常情况下,并发垃圾收集器在并发跟踪阶段使用一个或多个处理器,不会让出它们。类似的,在清理阶段也会始终独占地使用一个处理器。这对于对于一个程序的响应时间可能是个不小的影响,特别是系统中只有一两个CPU的时候。增量模式通过将并发阶段分解为一系列的突发行为来降低这一影响,这些突发行为会散布在小回收之间。

i-cms 使用占空比来控制并发收集器自发的放弃处理器之前的工作量。占空比是年轻代收集之间的允许并发垃圾收集器运行时间的百分比。i-cms 可以根据应用的行为自动计算占空比(这也是推荐的方法,称为自动步长(auto pacing)),当然,也可以通过命令行指定一个固定的值。

7.9.1 命令行参数

下面是控制 i-cms的命令行参数(参考下文的初始设置建议):

参数


描述


缺省值


J2SE 5.0 及以前


Java SE 6 及以后


-XX:+CMSIncrementalMode

启动增量模式。注意,并发垃圾收集器必须也被选择(-XX:+UseConcMarkSweepGC) ,否则此参数无效。

disabled

disabled

-XX:+CMSIncrementalPacing

打开自动步长,这样,增量模式占空比将根据JVM统计到的信息自动调整。

disabled

enabled

-XX:CMSIncrementalDutyCycle=<N>

两次小回收之间的允许并发收集器运行的时间的百分比(0-100)。如果打开自动步长,那么这个值就是初始值。

50

10

-XX:CMSIncrementalDutyCycleMin=<N>

自动步长打开后,占空比值的下限 (0-100)。

10

0

-XX:CMSIncrementalSafetyFactor=<N>

计算占空比值时使用的一个裕量(0-100)

10

10

-XX:CMSIncrementalOffset=<N>

在小回收之间,增量模式中占空比开始的时间,或说是向右的平移量(0-100)

0

0

-XX:CMSExpAvgFactor=<N>

当进行并发回收统计,计算指数平均值时,当前采样所用的权值(0-100)

25

25

7.9.2 建议参数

要在 Java SE 6 里使用 i-cms,需要使用如下命令行参数

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \-XX:+PrintGCDetails -XX:+PrintGCTimeStamps

前两个参数分别启动并发垃圾收集器和 i-cms。后两个参数不是必须的,它们只是要求垃圾收集器将诊断信息打印到标准输出,这样,垃圾收集器的行为就可以被看到并用于以后分析了。

注意,对于 J2SE 5.0 和之前的版本,我们建议 i-cms 使用如下的初始命令行参数:

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \-XX:+PrintGCDetails -XX:+PrintGCTimeStamps \-XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0-XX:CMSIncrementalDutyCycle=10

这样,就是用了和 Java SE 6 一致的参数了,多出的三个参数用于自动调整占空比。这些多余的参数值完全是使用的 Java SE 6 的缺省值。

7.9.3 基本问题处理

i-cms 的自动占空比计算模式使用了程序运行时收集到的统计信息进行占空比计算,以保证并发垃圾收集器可以在堆占满之前完成。不过,使用过去的行为预测将来的变化的估计方式可能并不总是足够准确,可能在某些情况下无法阻止堆用满。如果需要收集的垃圾太多,可以尝试下面这些步骤,一次使用一个:

Step

Options

1. 增加保险系数

-XX:CMSIncrementalSafetyFactor=<N>

2. 增加最小占空比

-XX:CMSIncrementalDutyCycleMin=<N>

3. 关闭自动占空比计算,使用固定占空比

-XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N>

7.10 测量

下面是使用-verbose:gc和-XX:+PrintGCDetails参数时,并发垃圾收集器的输出,一些小细节已经被去掉了。注意,并发垃圾收集器的输出里掺杂着小回收的输出;典型情况下,很多小回收会发生在并发收集周期之中。其中的CMS-initial-mark表征了一个并发垃圾回收周期的开始。CMS-concurrent-mark: 标志着并发标记阶段的完成,而CMS-concurrent-sweep则标志着并发清除阶段的完成。之前没有提到过的预清除阶段以CMS-concurrent-preclean为标志。预清除可以和重标记阶段CMS-remark的准备工作同时运行。最后一个阶段是CMS-concurrent-reset,这是下一个并发收集周期的准备工作。

[GC [1 CMS-initial-mark: 13991K(20288K)] 14103K(22400K), 0.0023781 secs][GC [DefNew: 2112K->64K(2112K), 0.0837052 secs] 16103K->15476K(22400K), 0.0838519 secs]...[GC [DefNew: 2077K->63K(2112K), 0.0126205 secs] 17552K->15855K(22400K), 0.0127482 secs][CMS-concurrent-mark: 0.267/0.374 secs][GC [DefNew: 2111K->64K(2112K), 0.0190851 secs] 17903K->16154K(22400K), 0.0191903 secs][CMS-concurrent-preclean: 0.044/0.064 secs][GC [1 CMS-remark: 16090K(20288K)] 17242K(22400K), 0.0210460 secs][GC [DefNew: 2112K->63K(2112K), 0.0716116 secs] 18177K->17382K(22400K), 0.0718204 secs][GC [DefNew: 2111K->63K(2112K), 0.0830392 secs] 19363K->18757K(22400K), 0.0832943 secs]...[GC [DefNew: 2111K->0K(2112K), 0.0035190 secs] 17527K->15479K(22400K), 0.0036052 secs][CMS-concurrent-sweep: 0.291/0.662 secs][GC [DefNew: 2048K->0K(2112K), 0.0013347 secs] 17527K->15479K(27912K), 0.0014231 secs][CMS-concurrent-reset: 0.016/0.016 secs][GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs]

初始标记停顿在典型情况下比小回收的停顿时间还要小。而如上例所示,并发阶段(并发标记、并发预清除和并发清除)通常会比小回收长很多。不过注意,应用并没有在这些并发阶段中停顿下来。重标记停顿通常和一个小回收的长度相当。重标记停顿挥手道应用的某些特征(如高对象修改频率可能会增加这个停顿)和上一次小回收的时间(即,更多的年轻代对象可能会增加这个停顿)的影响。

8. 其他考虑

8.1 永久代尺寸

在大部分应用中,永久代对于垃圾回收性能没有显著的影响。不过,一些应用会动态的生成与加载很多类;比如,一些 JavaServer Pages(JSP)页面的实现。这些应用可能需要很大的永久代去存放一些多余的类。如果这样的话,最大永久代的尺寸可以用命令行参数-XX:MaxPermSize=<N>来增大。

8.2 Finalization; Weak, Soft and Phantom References

一些应用使用 finalization 和 weak, soft, phantom 引用与垃圾收集器交互。这些特征可以 Java 语言层带来性能影响。一个例子是通过 finalization 来关闭文件描述符,这会导致一个外部资源依赖于垃圾收集器。以来垃圾收集器来管理内存之外的资源是个坏主意。

参考资料章节中的文章深度讨论了一些finalization的常见错误和用来避免这些错误的技术。

8.3 显式垃圾回收

应用程序和垃圾回收器的另一个交互途径是显式调用 System.gc() 进行完整的垃圾回收。这回强制进行一次主回收,即使没有必要(也就是说一次小回收可能就足够了),所以应该避免这种情况。显式垃圾回收对性能的影响可以通过使用 -XX:+DisableExplicitGC 进行比较来进行测量,这样虚拟机会无视 System.gc() 的。

最常见的显式调用垃圾回收的场景是 RMI 的分布式垃圾回收。使用 RMI 的应用会引用到其他虚拟机中的对象。在这种分布式应用的场景下,本地堆中的垃圾可能不能被回收掉,所以 RMI 会周期性强制进行完整的垃圾回收。这些回收的频率可以使用参数来控制。如

java -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 …

这里指定了垃圾回收每小时运行一次,而不是缺省的每分钟一次。不过,这可能会导致某些对象的清除消耗太长时间。这些参数可以被设置到高达Long.MAX_VALUE来让显式垃圾回收的间隔时间无限长,如果没有合适的DGC上限时间的话。

8.4 Soft References

Soft reference在虚拟机中比在客户集中存活的更长一些。其清除频率可以用命令行参数 -XX:SoftRefLRUPolicyMSPerMB=<N>来控制,这可以指定每兆堆空闲空间的 soft reference 保持存活(一旦它不强可达了)的毫秒数,这意味着每兆堆中的空闲空间中的 soft reference 会(在最后一个强引用被回收之后)存活1秒钟。注意,这是一个近似的值,因为 soft reference 只会在垃圾回收时才会被清除,而垃圾回收并不总在发生。

8.5 Solaris 8 替换 libthread

Solaris 8 操作系统提供了一个替代的线程库,libthread, 它将线程直接绑定成了轻量级进程(LWP)。有些应用能够从中极大获益,并潜在的对所有多线程应用都或多或少的有好处。下面的命令会为 java 启用替换的 libthread(BASH 格式)

LD_PRELOAD=/usr/lib/lwp/libthread.so.1export LD_PRELOADjava ...

这个方法仅对 Solaris 8 适用,因为对 Solaris 9 操作系统来说,这是缺省的,而 Solaris 10 中,这是惟一的线程库。

9. 相关资源

  1. HotSpot VM Frequently Asked Questions (FAQ)
  2. GC output examples 介绍了如何解释不同垃圾收集器的输出。
  3. How to Handle Java Finalization’s Memory-Retention Issues 介绍了一些容易犯的错误和避免他们的方法。
  4. Richard Jones and Rafael Lins, Garbage Collection: Algorithms for Automated Dynamic Memory Management, Wiley and Sons (1996), ISBN 0-471-94148-4

在本网站中,名词“Java Virtual Machine” 和“JVM” 都代表 Java 平台虚拟机。

[译文] 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 建议这个话题。)

Xen 组网

August 4th, 2008

XenWiki 之 XenNetworking 译文版
王旭 译

[译注:这是一篇 wiki 文章,由多人多次编辑完成,其中难免有些不协调的痕迹,但内容非常详实,很有帮助,译文保持了原文的状态,较少添加译注。]

目录

  1. 虚拟以太网接口
  2. MAC地址
  3. 桥接
    1. 桥接中的包流向
    2. network-bridge_脚本
    3. vif-bridge_脚本
    4. 补充说明
    5. 相关链接
  4. 路由
    1. network-route脚本
    2. vif-route脚本
  5. 虚拟网络
    1. 相关链接
  6. 接口命名
  7. VLAN

虚拟以太网接口

     缺省情况下,Xen 为 dom0 创建7对"对接的虚拟以太网接口"。就是使用交叉线对连的以太网口。veth0 连接到 vif0.0,veth1 连接到 vif0.1 等等,知道 veth7 连接到 vif0.7。你可以在 veth# 端配置 IP 和 MAC 地址,而另一端,vif0.# 被加入到了一个网桥之中。

     每当建立一个 domU 实例的时候,就会分配一个新的域 ID。很遗憾,这个号码不能选择的。第一个 domU 的 id就是 1,第二个就是 2,即使第一个已经关机了也不会重新使用它的 id。

     对于每个新的 domU,Xen 就会为它新建一对“对接的以太网口”,一端在 domU 里面,而另一端在 dom0 里面。对于 Linux 的 domU,这个设备一般会被命名为 eth0。这个虚拟以太网接口对在 dom0 之中的另一端称为 vif<id#>.0。举个例子,domU 5 的 eth0 连接到 vif5.0。如果为一个 DomU 建立多个网口,也就是 eth0, eth1….那么,在 dom0 之中对应的便是 vif<id#>.0, vif<id#>.1…

     当 domU 关机的时候,对应的虚拟以太网接口也会被删除。

MAC地址

虚拟以太网接口也具有以太网的 MAC 地址,缺省情况下,xend 会随机分配地址,这样,同一个 domain 的不同实例会具有不同的地址。如果你需要让一个 domain 具有固定的 MAC 地址(比如用于 DHCP),那么可以在 vif 配置中使用 "mac=" 选项(如 vif= ['mac=aa:00:00:00:00:11'])。

在选择要使用的 MAC 地址的时候,要确保你使用的是一个单播地址。也就是说第一个字节的最地位应该是0。比如,一个 aa 开头的地址是正确的,而 ab 是不正确的。最好将地址标明是“本地分配的” (而不是全局性地分配给硬件厂商的)。这样,要让第一个字节的次低位是 1。比如 aa 是正确的,而 a8 就不是。

总之,一个地址应该是这样的形式

XY:XX:XX:XX:XX:XX

其中 X 是任意 16 进制数,而 Y 则是 2、6、A、E 之一。

另外,建议使用 00:16:3e:xx:xx:xx 区段的 MAC 地址。这段地址是为 Xen 保留的。

桥接

依照缺省的 Xen 配置,会在 dom0 中进行桥接来允许所有的域像独立的主机那样出现在网络上。如果在 dom0 中大量使用了 iptables (比如构筑防火墙),那么就可能影响桥接,因为桥接的包会通过 PREROUTING, FORWARD 和 POSTROUTING 三条 iptables 链。也就是说,从客户域桥接到外面的包需要通过这三条链。最常出现问题的可能是 FORWARD 链被配置为拒绝包或丢包 (这与在内核中的 IP forwarding 不同)。

iptables FORWARDING 可以设为禁止转发所有包,这样就禁止了 dom0 作为一个路由器的能力:echo 0 > /proc/sys/net/ipv4/ip_forward。

更安全一点的手段是允许在外部物理接口与 vif 们之间转发包。例如,一个只有一块以太网卡的计算机可以这么设置:

iptables -A FORWARD -m physdev --physdev-in eth0 --physdev-out '!' eth0  -j ACCEPTiptables -A FORWARD -m physdev --physdev-out eth0 --physdev-in '!' eth0  -j ACCEPT

(这里需要 ipt_physdev [就是 xt-physdev] 模块可用)。

ebtables 项目有一篇关于桥接与 iptables 互动的有趣文档

桥接中的包流向(By Ernst Bachman)

当包到达硬件的时候,由 dom0 中的以太网驱动来归口处理,并出现在了 peth0 上面。peth0 被绑定在网桥上,所以,包被送到网桥上来。这一步在以太网层(第二层)运行,peth0 或网桥没有配置 IP 地址。

现在,网桥将分发来包,正像一个交换机那样。在这一阶段的过滤可以由 ebtables 之类的工具进行。

接下来,因为有一组 vifX.Y 连接到网桥上,它将会根据 MAC 决定把哪些包放到哪个接收端上。

vif接口会把包交给 Xen,由它把包送给 vif 对应的端口(dom0中也是同样进行的,这时走的是 vif0.0->(v)eth0 这对接口)。

最终,dom0/domU 中的目标设备是有 IP 地址的,你可以对它们使用 iptables 进行过滤。

network-bridge 脚本

当 xend 启动的时候,它会运行 network-bridge 脚本,这个脚本会

  1. 建立一个称为 xenbr0 的网桥
  2. 关闭真正的以太网接口 eth0
  3. 将 eth0 的 IP 和 MAC 地址拷贝给虚拟以太网接口 veth0
  4. 将真实的接口 eth0 重命名为 peth0
  5. 将虚拟以太网接口 veth0 重命名为 eth0
  6. 将 peth0 和 vif0.0 连接到网桥 xenbr0
  7. 将网桥、peth0、eth0 和 vif0.0 都激活

将物理以太网接口和 dom0 的接口分开是个有益的设计,比如,这样你就可以为 dom0 配置防火墙而不会影响  domU 们(仅仅保护 dom0 自己)。

vif-bridge 脚本

当一个 domU 启动的时候,dom0 中运行的 xend 会运行 vif-bridge 脚本,这个脚本会:

  1. 将 vif<id#>.0 连接到 xenbr0
  2. 激活 vif<id#>.0
补充说明
  • 你可以在 xend-config.sxp 文件中改用如下配置,并重启 xend 来修改网桥 xenbr0 的名字:
    (network-script 'network-bridge bridge=mybridge')
  • 当然要记得在 domU 的配置文件中修改要连接到的网桥的名字:
    vif=[ 'bridge=mybridge' ]

    或者是这样的配置:

    vif=[ 'mac=00:16:3e:01:01:01,bridge=mybridge' ]
  • 也可以创建多个网络接口,并连接到不同的网桥之上:
    vif=[ 'mac=00:16:3e:70:01:01,bridge=br0', 'mac=00:16:3e:70:02:01,bridge=br1' ]
  • 如果你希望有多个网桥,必须自己手工建立他们,可以是手工,也可以是通过启动脚本建立,或者是修改 network-bridge 脚本。比如

    $ cd /etc/xen/scripts$ cp network-bridge network-custom$ cp vif-bridge vif-custom$ vi /etc/xen/xend-config.sxp        (network-script network-custom)        (vif-script vif-custom)$ vi network-custom        # whatever you want
  • 在将物理接口连接到网桥之前,记得重置它的 mac 地址并关闭 arp。比如:
    # ip link set eth1 down# ip link set eth1 mac fe:ff:ff:ff:ff:ff arp off# brctl addif br1 eth1# ip link set eth1 up
  • 对于 Xen 3.0,添加网桥的最佳途径是使用一个略加修改的 Xen 缺省配置脚本。按照 XenBug #332 所述。例如,对一个连着 eth0 和 eth1 两个网桥的配置,可以建立这样一个 /etc/xen/scripts/my-network-script:

    #!/bin/shdir=$(dirname "$0")"$dir/network-bridge" "$@" vifnum=0"$dir/network-bridge" "$@" vifnum=1
    • (steve_from_moreover 的附加说明——可能是显而易见的,但一定要记住,让 /etc/xen/scripts/my-network-script 的访问权限为 755,否则重启之后你会发现它仍旧保持沉默而没有运行)。
    • 至少对于 SuSE Linux,每个接口都需要配置 /etc/sysconfig/network 之下的 ifcfg 脚本。否则 network-bridge 将会创建没有人何接口连接于其上的网桥。
    • 修改脚本后不要忘记修改 /etc/xen/xend-config.sxp 中的网络配置为 (network-script my-network-script)。
    • 同样的源里可以用于没有物理以太网卡的环境中。可以使用 dummy 接口:
      "$dir/network-bridge" "$@" vifnum=2 netdev=dummy0
相关链接

邮件列表中一些相关话题的讨论:

路由

本节仅当你选用了 network-route 和 vif-route 而不是 network-bridge 和 vif-bridge 时有意义。

通过路由,在 dom0 和 domU 之间建立点对点连接。到每个 domU 的路由会添加到 dom0 的路由表当中,这里 domU 必须有一个确定的(固定)IP。DHCP 无法工作,因为无法创建路由,DHCP offer 也就无法送到 domU。

network-route脚本

当 xend 启动的时候,它会运行 network-route 脚本,进行如下工作:

  1. 打开 dom0 的 ip forwarding
vif-route脚本

当 domU 启动的时候,xend(在 dom0 之中)运行 vif-route 脚本,进行如下工作:

  1. 将 eth0 的 ip 地址拷贝到 vif<id#>.0
  2. 激活 vif<id#>.0
  3. 为 domU 的配置文件中给出的 domU 的 IP 地址增加一个主机固定路由,指向 vif<id#>.0 接口。

虚拟网络

目前虚拟网络是一种非标准配置。

虚拟网络配置将所有 domU 放在 dom0 的一个虚拟网络中。这样允许 domU 使用 dom0 提供的 DHCP 服务器,不允许 domU 的 DHCP 请求出到物理网络之中。(据我所知,vif0.0 和 dummy0 都不是必须的。)

相关链接

接口命名

Xen 系统的缺省配置是使用桥接。当 xend 启动的时候,它会建立一个称为 xenbr0 的网桥。xend 将会把 eth0 的 ip 地址之类的分配到 xenbr0 (作为 dom0 连接到网桥上的接口的名称)。所以,现在 dom0 的对外的接口已经是 xenbr0 了,注意防火墙之类的配置要指向它。(译注:新版本 xen 中,或说译者使用的版本中,这个接口被重命名成了 eth0,而物理的 eth0 早被重命名成了 peth0 了。)

VLAN

如果将 802.1Q VLAN 支持配置到 dom0 当中来,Xen 可以支持多标签 VLAN。每个需要的 VLAN 都需要 dom0 中的一个本地接口,虽然不一定需要一个 IP 地址。每个 VLAN 都可以配置一个网桥,客户 OS 可以连接到合适的网桥。

JamesBulpin 倾向于将网桥定以为一个不自动激活的接口(比如,在 Debian 的 /etc/network/interfaces 中,不使用 "auto" 选项)

iface xen-br293 inet manual    up vconfig add eth0 293    up /etc/xen/scripts/network start netdev=eth0.293 bridge=xen-br293 antispoof=no    up /sbin/ifconfig eth0.293 up    down /etc/xen/scripts/network stop netdev=eth0.293 bridge=xen-br293 antispoof=no    down vconfig rem eth0.293

之后,可以添加一个 init.d 脚本在 xend 和 xendomains 之间启动这个接口。

XenNetworking (PhilipGarrett 于 2008-07-06 18:49:56 最后一次编辑本文)

Switch to our mobile site