qyb的博客

Linaro 的最新情况

By Jake Edge May 25, 2011
Updates from Linaro

翻译:李潇

在过去的一个多月左右,我参与了一些有关Linaro及改善ARM Linux状况的相关工作的不同演讲。虽然Linaro是由六个主要的ARM公司合作的,可是它的成果对所有ARM公司,开发者和用户都是可用的。也许会重点关注合作公司的需求,但Linaro的影响和成果会推广到这些需求之外的其他地方。Linaro努力着眼于整个ARM生态系统的兴趣集中点——那样的话你们也许就不会对ARM,Ltd.和大部分ARM硅制造者IBM的参与感到奇怪了。

自从Linaro正式成立已经过去大概十一个月,这个组织已经扩大了它的范围,变成每月发布一个版本,介入并努力帮助阻止ARM树的一场危机。它也在许多自身问题方面取得进步,比如内核、工具链、图形化。但是要做的当然比这些要多。

Linaro 开发者峰会

Linaro CEO George Grey在Linaro开发者峰会的开幕上做了简短的发言,描述了他所预见的Linaro将迎接的工业界趋势和事物。Grey提到过去的十二个月显示了在现实世界产品使用开源软件的一些非常奇怪的变化。“尤其Android令很多人吃惊”,他说。在那段时间,Android的智能手机市场份额从5%上升到25%,这在以前从来没发生过。

设备生产商以前只能拿到过时的BSP(board support package),而且每个BSP都有一套特定的开发工具链。现在,他们在寻找有产品质量保证的开源平台,这正是Linaro所要宣扬的。LAVA(Linaro自动验证架构)——一个测试和验证的平台——将在提高Linaro发布的软件质量过程中起到重要作用。

Grey说到,使开发平台进入到开源代码开发者的手头上是Linaro与众不同的另外一些地方。Grey说,在过去,小角色因为难以引起SoC厂商的注意,所以很难触及到这些开发平台。但现在,Texas Instruments, Freescale, 和 ST-Ericsson(这些都是Linaro成员)都有价格合理的ARM SoCs开发平台,这样的平台也会越来越多。

这解决了硬件问题,并且Linaro将会提供运行在这些ARM板上的软件。这意味着预算紧张的公司或开发者能够获得完整的配有多种I/O设备的硬件和有产品质量保证的软件。Grey激动地憧憬着社区利用这些资源能够做的一切。

Grey同时宣布将不会有Linaro 11.11(以前是在Ubuntu发布之后的一个月发布),而是每个月发布一次(或者更频繁)。Linaro各个工作组将持续的将他们的代码推向上游,同时每天会发布一些代码,这些主要是内部使用。每个月将会有给外部使用的内核和工具链发布

展望未来,Grey提到对Cortex A15处理器的支持工作正在取得进展。其目的是要在发布之日让Linux支持该处理器,而不是像过去那样一两年后才被支持。他同时说到,将会有一个基于ARM的服务器市场,并且Linaro也将在这个市场做一些前期工作。

Linaro工程设计副总裁Kiko Reis在Grey之后做了发言,主要是关于LDS(Linaro Developers Summit)运作的具体细节,还有参与者怎样充分利用它。他回顾了Linaro的第一年,在那一年,尤其是在与各ARM厂商的交流和合作方面,没有取得很多成就。启动Linaro并不容易,他说,还有更艰难的事情要做,包括将整个ARM社区团结在一起。

Reis特别提到了仍然需要去完成的ARM内核合并的工作。Linaro花了一年时间,但并不真正是在做这些,而现在是时候去完成它了。决定嵌入式图形设备内存管理的正确过程同样是要做的事情中优先级比较高的一项。实现它将用掉不止一年的时间,但是如何去计划是这周首先要做的。

Linaro组织

Linaro CTO David Rusling 在此前四月份的嵌入式Linux大会上的演讲中回顾了Linaro这个组织,强调了它在开始的十个月里取得的一些成就,并对未来做了一些计划。Rusling在一开始阐明Linaro是一个工程组织,而不是一个夏威夷的茶点房。他同样指出,Linaro是候选名字里面最不让人讨厌的名字,这是基于Dirk Hohndel在早前选择"Yocto"作为项目的名称所做解释的反应。

Rusling说他大约在2009年,当时还在ARM,Ltd.时,认识到ARM跟Linux的命运是缠绕在一起的,但是有很多公司并不合作。在各种SoCs相同的事情上明显需要更多的合作,但致力于解决这些问题的工程师并不多。Linaro由此应运而生。

Linaro开始时大约有20个工程师,它被其他人想象成“高高在上的工程师”组织。聪明的做法是让所做的事情公开化,他说。所有东西都在wiki,虽然一些东西很难找到,“高高在上”是我们的恶咒。

Rusling说,Linaro所做的大部分是“社会化工程”。人们对Linux和开源有很多需要消除的误解,包括认为开源是难以处理的这种想法。虽然在Linux和其他项目上有非常严格的代码管理者,但是有兴趣的组织和厂商参与项目过程非常容易。真正消极的观点是认为开源比较便宜,其实参与开源社区工作需要更深层次的投入,来影响发展的方向。

该组织的六个公司成员想为Linaro推动这个技术日程,而Linaro则通过已确定好的工作组来推动工作进展。这些工作组是非常独立的,这些公司成员不会直接运行项目,而是让这些工作组在上游解决他们存在的问题。

同样也有一个平台集合建立、测试、并给工作组已完成的工作定基准点。这是为了证明新的内核特性能正常工作或者工具的变化能通过创建并测试评估版本工作的更快。他说,任何时候你做的任何改变你都要度量一下那些改变造成的影响。对应每个SoC厂商成员(Samsung, Texas Instruments, ST-Ericsson, and Freescale)都有一个团队负责从平台团队获取输出成果并将其转变成其用户可使用的版本。这些团队是Linaro内的团队,不对社区参与者工作。

不仅仅是内核方面,工具链工作组也会积极把在GCC和其他工具上做出大量工作推向上游。对ARMv7a, Thumb 2, Neon, 和 SMP的支持将被加入到GCC 4.7中,直到2012四月,这个GCC版本才会发布,大约到2012年10月,4.7.1发布之后,才会出现在各操作系统中。在这期间,工具链工作组将会在GCC发布之前让“合并”能被ARM开发者使用。除了GCC方面的工作,工作组也会向gdb,QEMU,和valgrind增加功能。

11月发布第一个版本后,两个新的工作组加了进来,以解决图形和多媒体问题。Rusling说,另外,其他的工作组开始查看长期的问题。内核工作组开始为所有Linaro成员增加设备树支持。对GCC的向量化支持是工作链工作组关注的焦点,电源管理组开始处理分段内存问题,这样能让部分内存掉电休眠。他说,所有这些事情都是有技巧的,并需要ARM领域以及上游社区的协调。

对多媒体问题,许多工作都需要测试,benchmark,并为ARM调整各种各样的编码。OpenMax媒体库的标准化和GStreamer框架是Linaro将要走的方向。他说,Android在多媒体支持上已走出了一条自己的道路。

同Grey一样,Rusling也指出了在LAVA上的工作不仅对Linaro非常重要,对社区也很重要。这是一个能够被ARM社区其他人或者更多人使用并完全开放的测试和验证系统。

Rusling说,在Linaro的第一年有一些惨痛的教训。从各公司那里零星的得到几个工程师,并想组建一个新的工程组织是比较难的。另外一方面,人们认为ARM社区不会合作但Linaro已经证明事实并不是这个样子。所有事花的时间比他想的要长,而且仍然有很多要学习的东西,他说。“开源是极好的”,但如何使用开源是有挑战性的。

很明显,ARM已经成为Linux的一个非常重要的架构,并完全统治者低功耗移动设备市场。这种现状将长期持续, ARM在服务器领域所做的努力也许在接下来的几年会尝到甜头。当然能耗(以及相关的发热)不仅在手持设备方面,在数据中心也很重要。迄今为止,Linaro已经做出了许多正确的举动来确保ARM在linux有良好的技术支持并具有可维护性。

Topic: sohulinux

What Every C Programmer Should Know About Undefined Behavior #3/3

Saturday, May 21, 2011
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html
翻译:刘晓佳

本系列的第一篇文章中,我们介绍了什么是C程序中的未定义行为,并且分析了在某些情况下,这种未定义行为能够使C语言比“安全”语言有更好的性能。在第二篇文章中,我们着重于那些未定义行为导致的,令人感到惊讶的Bug,和那些C程序员对此所持有的误解。本文,我们来看看编译器所面临的一些挑战,即为这些设计陷阱提供警告。此外在带走我们的惊讶的同时,还要讨论一些帮助我们赢得性能的LLVM和Clang提供的工具。

当基于未定义行为做优化时,为什么不能发出警告?

人们常常疑问,当编译器充分利用未定义行为做优化时,明明知道这种情况极有可能是用户代码的一个bug,为何不产生发出警告呢。这样做带来的挑战是1)很可能会产生太多没用的警告信息-因为当没有bug时这些优化总是执行,2)仅仅当人们需要时才发出警告是相当棘手的一件事,3)没有一种很好的方式告诉用户如何将一系列优化结合起来揭示优化的时机。让我们逐一解释:

  1. 发出的警告很难真正有用
    我们来看个例子吧:当优化“zero_array”(系列第一部分中的)时,产生一个“优化器认为p和p[i]不是别名的”警告时没有多大用处的,即使这样无效的类型转换bugs是通过基于别名分析得到的。
    1. float *P;
    2. void zero_array() {
    3.    int i;
    4.    for (i = 0; i < 10000; ++i)
    5.       P[i] = 0.0f;
    6. }

    除了上述误判问题之外,优化器并没有足够的信息来产生一个合理的警告。首先,它致力于完全不同于C的一种代码的抽象化的表示形式(LLVM IR)。其次,优化器试图用循环外部的P来存储数据,却并不知道更高层面上,编译器的TBAA通过分析已经解决了指针别名查询。是的,这就是那些编译器使用者所抱怨的,但它确实很难解决。

  2. 很难做到在人们需要时才发出警告
    Clang为一些简单而明显的未定义行为实现了警告,比如“x人们并不想从死代码中得到任何未定义行为的警告(这里还有另外一个例子
    死代码有几种形式:以出人意料的方式进行展开的宏,cases语句无法运行到的部分(我们也曾抱怨某些情况下需要控制流分析来发现这一点)。基于C中Switch结构本来就没有适当的组织的事实,警告也没什么用。
    Clang中解决该问题的方式是增加了为处理“运行时行为”警告的功能,同时如果后面运行时我们发现这些块儿是不可执行的,这些警告将不会被通告。对于程序员来说,这就好比一场军备竞赛,因为总是会遇到一些我们未预料到的语法,而在前端这样做的后果就是,无法捕获到所有想要捕获的情况。
  3. Explaining a series of optimizations that exposed an opportunity
    如果从前端产生好的警告有难度的话,我们可以从优化器来产生。这里产生有用的警告面临的最大问题是数据的追踪。编译器优化器完成了许多优化操作,每一个操作都修改了代码使得它们更加规范化,运行的更快。比如,如果内联器打算内联某个函数时,也就揭示了一个现在是优化“X*2/2”的时机。
    我提供了几个相对简单而完备的例子来阐释这些优化操作,它们生效的地方往往是宏实例化,内联,和其他一些抽象清除活动。事实上,确实人们一般不会犯如此糊涂的错误。对于这意味着为了将问题反馈给用户代码,它将不得不精确重现编译器是如何得到正在运行的中间代码的。我们需要像下面这样表达:
    1. 警告:三级内联之后(可能出现在文件的链接时间优化期间),将一般的子表达式消除,将这些事情放到循环之外来做且证明这13个指针不会重名后,我们发现一种未定义行为的情况。这可能是由于代码里有一个bug,也可能因为宏展开和内联操作和之前我们并没有证明是死代码的无法运行到的无效代码。

    不幸的是,我们并没有内部跟踪程序来产生这些警告,即使我们这样做了,编译器也没有一个很好的用户接口来向程序员传达这些信息。
    最终,未定义行为对优化器来说很有价值,因为它说“该操作无效-你可以假定什么也没发生过”。在“*P”的情况下告诉优化器P不能为空指针。在“NULL”情况下(常量传递或内联之后产生),会告诉优化器该代码无法运行到。这里重要的是,因为无法解决中止问题,编译器无法知道代码是真正死掉了,还是被一系列优化所引起的一个bug。因为没有更好的方式来区分这两种情况,几乎所有的警告都是误报。

Clang处理未定义行为的方法

之前对于未定义行为的无能为力,你可能会很好奇Clang和LLVM如何来改善这种状况。我已经提及一部分内容:Clang静态分析器Klee工程,和-fcatch-undefined-behavior标志,这类工具用来跟踪上面那些bug特别有用。但是这些工具被接受度很低,因此我们能做的是在编译器里直接提供这些功能来代替使用这些工具,即便编译器受限于没有动态信息和将花费更多的编译时间。

Clang改善代码的第一步是默认打开大量的警告提示。受过良好训练的开发者会加上“-Wall –Wextra”之类的选项来编译,而大部分人并不了解这些标志,或者嫌这样太麻烦。默认打开更多的警告更多时候会引起更多的bug。

第二步是为代码中很明显的未定义行为产生许多警告来捕获一般的错误(包括null的解引用,越界的移位,等等)。上面提及的这些警告,实际上貌似运行的很好。

第三步是LLVM优化器使得未定义行为不那么自由了。尽管标准认为任何未定义行为对程序没有约束效应,但这并没有什么用处,对开发者来说并不是什么可利用的友好行为。相反,LLVM优化器以一些完全不同的方式来处理这些优化:

  1. 如果采取好的方式,某些未定义行为可以变换成隐含的陷阱操作。例如,在Clang中,该C++函数:
    1. int *foo(long x) {
    2.     return new int[x];
    3. }

    编译为X86-64的机器代码:

    1. __Z3fool:
    2.         movl $4, %ecx
    3.         movq %rdi, %rax
    4.         mulq %rcx
    5.         movq $-1, %rdi        # Set the size to -1 on overflow
    6.         cmovnoq %rax, %rdi    # Which causes 'new' to throw std::bad_alloc
    7.         jmp __Znam

    而GCC产生的机器代码是:

    1. __Z3fool:
    2.         salq $2, %rdi
    3.         jmp __Znam             # Security bug on overflow!

    区别在于Clang中我们决定花一些时间来阻止潜在的可能会引起缓存区溢出和漏洞利用(new运算符花费的代价比较昂贵,开销不被注意到)的整数溢出bug。至少在2005年GCC已经意识到这个问题了,但到目前为止还没有解决。

  2. 针对一个未定义值的运算应当考虑产生一个未定义值而不是未定义行为。区别在于,未定义值不会格式化你的硬盘分区或者产生其它的无法预料的结果。有效的改进是当给定任何未定义值时四则运算产生同样的输出结果。比如,优化器假定“undef & 1”的结果是高位有一些0,最低位是未定义的。这意味((undef & 1) >> 1),在LLVM中定义为0,而不是未定义的。
  3. 动态执行未定义操作(比如有符号整数溢出)的四则运算会产生一个逻辑陷阱值,从而影响了基于它的后续计算,但这并不会破坏你的整个程序。这意味着从未定义操作后续的操作都可能被影响,但不会破坏你的整个程序。这也就是为何优化器最后要删除操作未初始化变量的代码。
  4. 向NULL地址读写都转变成一个__builtin_trap()调用(该调用最终像x86上的“ud2”指令一样转变为自陷指令)。这一过程在优化的代码中一直进行(就像其它的转换如内联和常数传递),而且由于这些语句是很明显的无法运行的代码,我们用该转换来删除含有它们的语句块。

    从学术的角度讲这是正确的,我们很快发现人们确实偶尔解引用null指针,让落入下一个函数开头的代码执行 使得该问题很难理解。从性能的角度讲,揭示它最重要的方面在于殃及后续的代码。为此,clang把它转变成运行时trap:如果其中之一实际中可运行了,程序就立即停止,而且可以被调试。这样做的缺点在于通过这些操作和这些条件使代码变得膨胀起来。

  5. 如果程序员的意图很明确(比如当P是一指向float类型的指针而去进行(int *)p操作)优化器会努力做正确的事。在很多情况下都很有用处,但实际上你并不想依赖它,有很多这样的例子,你可能认为它们是很明显的,不会经过很长一系列的变换。
  6. 像第一部分描述的zero_array和set/call这样没有任何对用户的暗示就被优化的例子,优化很少会发生这种情况。我们这样做是因为没有任何有用的东西要说,真实的代码被优化器破坏的情况很罕见。

我们所能做的主要的改进是有关trap插入的。我认为增加一警告标志将非常有趣,每当产生一个trap指令时都会引起优化器警告。这对一些代码库来说也许是噪音,但对别的也许很有用。这里首要限制因素是使优化器产生警告的部件:除非调试信息打开,否则并没有有用的源码位置信息。

另一个更重要的限制因素是警告不会含有任何追踪信息能够解释某操作是三次展开一个循环,然后通过四层函数调用内联它的结果。顶多我们能指出原始操作的文件、行、列,这在某些情况下很有用,在另外一些情况下极其混乱。不管怎样,对于我们这并不是优先要实现的,因为a)这可能并不是很愉快的事情,b)默认不能够开启,c)有大量的工作要实现。

使用更安全的C方言(和其它选项)

如果你并不关心“终极性能”,最终的选择是使用不同的编译器标志来使用C中那些晦涩的语法消除未定义行为。比如,使用-fwrapv标志来消除有符号整数溢出导致的未定义行为(注意这并不会消除整数溢出这一安全缺陷)。-fno-strict-aliasing标志使基于重名分析的类型失效,因此你可以忽略这些类型准则。如果有需要的话,我们能增加一个标志到Clang中来把所有的局部变量归零,插入“与”操作在每次移位之前,等等。不幸的是,并没有一种易于处理的方式在不改变ABI和不损害性能的前提下来完全消除C中的未定义行为。另外如果这样作,那这就不是C语言,而是一种类似C但是却是不可移植的晦涩的类C语言。

如果说不在乎是否在编写写C的不可移植的晦涩代码,-ftrapv和-fcatch-undefined-behavior标志就是你的武器库中很有用的查找这种bug的武器。在调试模式下使用它们是一种很好的查找相关bug的方式。如果你要建立严格安全的应用,这些标志在你的产品代码中非常有用。尽管它们不能保证能找到所有的bug,但它们确实在寻找bug方面非常有用。

最后,一个现实的问题是C并不是一门安全的语言,尽管很流行很成功,但许多人并不理解这门语言是如何工作的。在1989年之前的几十年演化过程中,C从一种在PDP之上的底层系统编程语言逐渐演化变成一门试图靠打破人们的期待来提供合适性能的底层编程语言。一方面,这些C语言存在的"作弊行为"使得代码的性能更好。另一方面,它们也常常是最突然的,出现在最坏的时间把你弄个措手不及。

C远远不是一个可移植汇编语言。希望该讨论至少能够从编译器实现的角度来帮助理解C的未定义行为背后的问题。

Topic: sohulinux

What Every C Programmer Should Know About Undefined Behavior #2/3

Saturday, May 14, 2011
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
翻译:李凯

在该系列的第一部分,我们讨论了什么是未定义行为,以及它如何使C和C++编译器产生比“安全”语言更高性能的应用程序。本文讨论什么是“不安全”C,以及未定行为引起的一些令人意想不到的影响。在第3部分,我们将会探讨友好的编译器如何做才能使一些令人意想不到的那些影响减少,尽管他们并不需要这样做。

我喜欢把这叫做"Why undefined behavior is often a scary and terrible thing for C programmers"

编译器优化的相互作用导致出乎意料的结果
一个现代化的编译器的优化单元按一定顺序执行许多优化方法。有时优化会被反复执行,或者执行顺序因为编译器的演进而变化。不同的编译器经常有本质上不同的优化方法。因为编译器是分阶段优化的,因此如果前一阶段的优化改变了代码则会产生突发的影响。

为了让这更形象化,我们来看一个简单的例子(从Linux内核曾经出现的一个漏洞简化而来):

  1. void contains_null_check(int *P) {
  2.   int dead = *P;
  3.   if (P == 0)
  4.     return;
  5.   *P = 4;
  6. }

在这个例子中,这些代码很明确地检查了空指针。如果编译器在“Redundant Null Check Elimination”通过之前碰巧运行到“Dead Code Elimination”,那么我们将会看到代码将执行下面两步:
void contains_null_check_after_DCE(int *P) {

  1.   //int dead = *P;     // deleted by the optimizer.
  2.   if (P == 0)
  3.     return;
  4.   *P = 4;
  5. }

然后:

  1. void contains_null_check_after_DCE_and_RNCE(int *P) {
  2.   if (P == 0)   // Null check not redundant, and is kept.
  3.     return;
  4.   *P = 4;
  5. }

但是,如果优化器碰巧被设计成别的顺序,程序将会在DCE之前运行RNCE。如下两步所示:

  1. void contains_null_check_after_RNCE(int *P) {
  2.   int dead = *P;
  3.   if (false)  // P was dereferenced by this point, so it can't be null
  4.     return;
  5.   *P = 4;
  6. }

然后dead code elimination运行:

  1. void contains_null_check_after_RNCE_and_DCE(int *P) {
  2.   //int dead = *P;
  3.   //if (false)
  4.   //  return;
  5.   *P = 4;
  6. }

对于许多程序员来说,从这个函数中删除空指针检测将会使他们非常吃惊(而且他们也许会给编译器提交一个bug:)然而,根据标准,“contains_null_check_after_DCE_and_RNCE” 和 “contains_null_check_after_RNCE_and_DCE” 都是 “contains_null_check” 完全有效的优化方法,而且这些所涉及到的优化方法对各种应用程序的性能都非常重要。

尽管这是一个非常简单的例子,但是这种情况在内联时总是发生:内联函数时往往会产生一些二次优化的情况。这意味着如果优化器决定内联一个函数,那么各种各样的本地优化行为都可以kick in,这样会改变代码的行为。根据标准,这些都是完全有效的,而且对在实际应用中的性能表现很重要。

不要把未定义行为和安全性搅在一起
C被广泛用来写安全性级别要求很高的代码,像内核、setuid 后台程序、web浏览器等等。这种语言面临这样一种问题,当有恶意的输入和bug时会导致各种各样的安全性问题。C语言有一个被广泛提及的优势-----很容易从代码中理解上下文。

然而,未定义行为把这种特性的优势丧失了。毕竟大多数程序员都认为“contains_null_check”会做一个空检查。然而这种情况并不十分可怕(如果通过了一个空检查,代码可能会在内存中冲突,这样相对容易调试),因为在相当大的范围内,C语言的碎片是被认为完全无效的。这种问题已经使得许多项目(包括Linux Kernel, OpenSSL, glibc等),甚至导致了CERT发布vulnerability note来对抗GCC(尽管我个人认为被广泛应用的优化的C编译器都容易受到它的攻击,并不仅仅是GCC)。

让我们来看一个例子。研究一下这段认真写下的C代码:

  1. void process_something(int size) {
  2.   // Catch integer overflow.
  3.   if (size > size+1)
  4.     abort();
  5.   ...
  6.   // Error checking from this code elided.
  7.   char *string = malloc(size+1);
  8.   read(fd, string, size);
  9.   string[size] = 0;
  10.   do_something(string);
  11.   free(string);
  12. }

这段代码做了相应的检测,以确保有足够大的内存容下从文件中读取的数据(因为一个空的终止字节需要加进来),如果产生了整型数溢出错误就跳出。然而,这就是我们之前给出的例子,在那个例子中编译器被允许(有效地)优化这个检测。这意味着对编译器来说完全有可能把这段代码转变为如下:

  1. void process_something(int *data, int size) {
  2.   char *string = malloc(size+1);
  3.   read(fd, string, size);
  4.   string[size] = 0;
  5.   do_something(string);
  6.   free(string);
  7. }

当在一个64位平台上built时, “size”是INT_MAX(也许是硬盘上一个文件的大小)时,这非常有可能是一个可利用的bug。让我们来分析一下这是个多么严重的错误:一个代码审计师在阅读这个代码时会理所当然地认为这是一个正常的溢出检查。人们测试这个代码时会发现并没有错误,除非特别地只测试那个error path。这段安全码看起来已经运行了,直到有人进一步调试并发现这个漏洞。总的说来,这是一个令人惊讶并相当可怕的一类bug。幸运的是,在这种情形下,解决方法很简单,只要令"size == INT_MAX"或者类似的操作都可以。

事实证明,有很多原因可以说明整型数溢出是一个安全性问题。即使你在使用完全定义的整型运算(-fwrapv或无符号整型数),仍然有可能会出现一个完全不同的整型溢出bug类。幸亏这种类在代码中是可见的,有见识的安全审计员经常能意识到这种错误。

调试已优化的代码可能没有任何意义
一些人(比如喜欢看生成的机器代码的底层嵌入式程序员)在优化器一直开启的状态下进行他们所有的研发。因为代码在正在开发时经常有bug,这些人最终会看到数量及其大的意想不到的优化,这会在运行时导致调试程序非常困难。例如,在第一节的“zero_array”例子中偶然地忘记写“i=0”,这使得编译器完全地跳出循环(把zero_array编译为“return”),因为它使用了一个未定义的变量。

另一个有趣的例子是使用(全局)函数指针时发生的。一个简化的例子如下所示:

  1. static void (*FP)() = 0;
  2. static void impl() {
  3.   printf("hello\n");
  4. }
  5. void set() {
  6.   FP = impl;
  7. }
  8. void call() {
  9.   FP();
  10. }

这可以优化为:

  1. void set() {}
  2. void call() {
  3.   printf("hello\n");
  4. }

这种做法是可以的,因为空指针的调用未定义,这允许它假设set()必须在call()之前调用。在这种情形下,开发人员忘了调用“set”,在引用空指针的情况下程序并没有崩溃,而当其他人运行debug时他们的代码会遭到破坏。

其结果是,这是一个可以解决的问题:如果你怀疑一些奇怪的事情会发生,可以尝试在-O0建立,在这里编译器不太可能进行任何优化。

当编译器演化或改进后,以前正常代码中使用未定义行为也许会异常
我们已经看过许多例子,在一个应用程序看起来能工作的时候,当一个新的LLVM用来建立应用程序或应用程序从GCC转移到LLVM时,就出问题了。尽管LLVM偶尔自身会有一两个bug,但是这种事情却会经常发生的,因为如今应用程序中的潜在错误被编译器暴露出来了。这可以以各种不同的方式产生,下面是两个例子:

  1. 一个未初始化的变量,被luck “before”初始化为0,现在它得到其他一些不是0的寄存器。这通常是由寄存器分配的变化而体现出来的。
  2. 一个堆栈中的数组溢出,是从那个实际中很重要的变量开始,还不是已经无用的变量。当编译器重新整理它往堆栈中存东西的方式,或者对于那些在运行时间的变量不重叠的变量,能更积极主动地获得共享堆栈空间时,它们才会体现出来。

一个很重要且很恐怖的事情将会显现,那就是在未来任何时间内任何基于未定义行为的优化都可能会触发buggy代码。内联,循环展开,等等其他优化方法会越来越好,它们存在的一个有意义的原因是像上面第一个一样体现二次优化。

对我来说,这是远远不能满足的,一部分原因是由于编译器最终都无可避免的会被指责,也有一部分原因是由于这意味着庞大的C代码等待着被摧毁。这甚至会更糟糕,因为……

没有可靠的方式来说明一个大型的代码库中是否有未定义行为
一个让这块“雷区”成为更糟糕的地方的事实是:现在没有一个好的方式来决定一个大型的应用程序是否有未定义行为,这种状况在未来也不容易打破。现在有许多工具能帮助找到一些bugs,但没有一个能有足够信心保证你的代码在将来不会中断。让我们来看一下其中一些options,并一起看一下它们的优点和弱点:

  1. Valgrind memcheck tool是一种找到所有种类的未定义变量和内存bug的奇妙方法。由于Valgrind运行时非常慢,所以这种方法受到了限制,它只能找到还存在于生成的机器码中的bug(所以不能找到优化器去掉的bug),同时它不知道源语言是C语言(所以它不能找到移出范围的或有符号整数溢出的bug)。
  2. Clang有一个实验性的-fcatch-undefined-behavior模式,这种模式通过运行时插入检查来找错误,比如移位超出范围,一些简单的数组越界等。这种方式因为降低了应用程序的运行速度而且不能解决随机指针的引用问题(Valgrind可以)而受到限制,但是它能找到其他重要的bug。Clang还完全支持-ftrapv标志(不要和-fwrapv弄混),这种标志能在运行时对有符号整型溢出bug设置陷阱(GCC也有这个标志,但是以我经验来看它完全不可靠)。下面是-fcatch-undefined-behavior的快速演示:
    1. /* t.c */
    2. int foo(int i) {
    3.   int x[2];
    4.   x[i] = 12;
    5.   return x[i];
    6. }
    7. int main() {
    8.   return foo(2);
    9. }
    1. $ clang t.c
    2. $ ./a.out
    3. $ clang t.c -fcatch-undefined-behavior
    4. $ ./a.out
    5. Illegal instruction
  3. 编译警告信息对找到一些类型的bug有帮助,比如未初始化变量和简单整型溢出bug。它有两个基本的限制条件:1)它没有代码在执行时的动态信息,2)它必须运行的很快,因为它做的任何分析都会降低编译时间。
  4. Clang的静态分析器进行了更深入的分析来试图找到bug(包括未定义行为的使用,比如空指针调用)。你可以把它想像成产生编译器警告信息的模块,因为它不受正常警告的编译时间的限制。静态分析器的最基本的不足如下:1)当程序运行时它没有程序的动态信息,2)对许多开发者来说,它并不是被熟练使用的工具链(它被纳入LLVM "Klee" Subproject使用象征分析方法,通过一段代码来"试着找到各种可能的路径",找到代码中的bug并产生一个测试用例。这是一个伟大的小项目,绝大多数时间段内,受限于不能有效地运行在大型应用程序上。
  5. 尽管我从来没有尝试过,Chucky Ellison 和 Grigore Rosu研究的C-Semantics tool非常有意思,这种工具能显著地找到某一类的bug(比如sequence point violations)。它目前仍然是一个研究课题,但是也许会在(小的并且self-contained)程序中找bug有用处。我推荐读一下John Regehr关于它的文章来获取更多的信息。

最终的结果是我们的工具箱中有许多方案可以找到一些bug,但是没有更好的方法来证明一个应用程序不受未定义行为的影响。在本系列的最后一部分,会总结当处理未定义行为时C编译器所可能考虑的各种选择,尤其是Clang。

Topic: sohulinux

What Every C Programmer Should Know About Undefined Behavior #1/3

Friday, May 13, 2011
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
翻译:王鑫

当开始编译器优化的时候,llvm 编译出的代码偶尔会产生 SIGTRAP 信号。随着对此问题的深入研究,人们发现,clang会产生一个“ud2”的指令(假设在X86平台下),这个指令同__builtin_trap()产生的一样。与之相关的问题全都是关于C代码的未定义行为以及LLVM如何处理它的。

本文试图通过解释这些问题,使得你能够更好的理解编译器优化中的权衡和复杂度,也许还能学到一点关于C语言的隐蔽特性。C并不像许多有经验的C程序员(尤其是那些关注底层的人)所想的那样是一个“高级汇编”,而且C++和Objective-C从中直接继承了许多同样的问题。

Introduction to Undefined Behavior
无论是LLVM IR(intermediate representation)和C语言都有“未定义行为”这个概念。未定义行为是一个有着许多微妙之处的广阔话题。我发现的对此的最佳介绍是John Regehr博客里的一篇文章。这篇极其精彩的文章概括了一件事情:许多在C中貌似合理的事情实际上都有着不确定的行为,并且这通常都是程序中BUG的源头。除此之外,C中的任何的未定义行为都有可能(无论是编译期还是运行期)产生一些格式化你的硬盘的、做一些你完全想象不到的事情的代码,或者更糟。我再次强烈推荐读一下John的那篇文章。

基于C的语言中一般都存在着未定义行为,这是因为C的设计者想要把它作为一种极其有效的低级编程语言。相反,像Java(还有其它许多“安全的”语言)就规避了此类未定义行为,因为他们想要的是安全性和一致性,并且为此可以牺牲性能。不管“哪个目标才是正确的”,如果你是一个C程序员的话,你确实应该了解一下什么是未定义行为。

在我们详细深入这个话题之前,我认为有必要简单地说一下为了使C的应用程序获得更好的性能,编译器都干了些什么。概括来看,编译器产生高性能的应用程序一般通过:

  1. 基础计算优化(比如寄存器分配、调度)
  2. 许许多多的小技巧(比如窥孔优化、循环转换)
  3. 擅长消除一些不必要的抽象(比如,由于C中的宏而产生的冗余,内联函数,在C++中消除临时对象等)
  4. 不把任何事情搞糟

也许这些优化可能听起来微不足道,但是在一个关键循环中哪怕只是减少一次循环都有可能使代码运行快10%或者少消耗10%。

未定义行为带来的好处
在讲述未定义行为的阴暗面,以及LLVM编译器的策略和行为之前,我打算先介绍一些特定的未定义行为是如何比像Java一类的安全语言获得更好的性能的。要么是因为未定义行为使得优化成为可能,要么是因为被完备定义会带来额外的开销。编译器的优化器有时可以消除这些开销,但为了达到这个目的需要解决很多“有意思的挑战”。

同样需要指出的是,无论Clang还是GCC都将一部分标准C中遗留的未定义行为确定了下来。下面将要讲到的例子无论是根据标准还是被这两个编译器的缺省所对待的,都是未定义的(行为)。

使用未初始化变量:这通常被认为是C程序的问题之源,而且现在也有许多工具可以捕获它:从编译器警告到静态或动态分析。这种方法通过当变量进入自己的有效范围(像Java那样)时而不必要求所有变量初始化为0而达到改进性能的目的。对于大多数的scalar变量,这将基本不会引起系统开销,但是若是对栈数组或malloc分配的内存调用memset,代价则非常昂贵,尤其是当该存储空间经常被完全改写的时候。

有符号的整数溢出:如果一个‘int’类型的变量运算溢出了,结果是不确定的。一个例子就是不能保证“INT_MAX+1”等于“INT_MIN”。这种行为对于某些代码而言,开启特定的优化器是重要的。例如,知道了INT_MAX+1是未定义的,那么允许优化“X+1 > X”的结果为“true”。知道了乘法“不能”溢出(因为如果溢出的话结果将是不确定的),那么开启优化“X*2/2”等于“X”。虽然这些可能看起来都是微不足道的小事,但是这类事情总是由于内联和宏展开而成为不受保护的。一个更重要的优化是针对for循环中的“

  1. for (i = 0; i <= N; ++i) { ... }

在这个循环中,编译器会假设当“i”溢出成为不确定值的时候,这个循环会精确地迭代N+1次,随后有更多的循环优化可能。另一方面,如果变量确定是在溢出周围的话,那么编译器必须假设这个循环可能是无穷的(当N是INT_MAX的情况),循环优化就无法进行了。这点尤其影响64位的平台,因为如此之多的代码都把“int”用作归纳变量。

而无符号数的溢出被保证为2进制数的补码溢出,因此你可以总是使用它们。把有符号整数溢出的行为确定下来的代价是这类优化器失效。(例如,通常表现为64位目标机循环中符号位的扩展)。Clang和GCC全部接受“-fwrapv”标志来强迫编译器把有符号整数的溢出的行为确定下来

超过范围的移位:将一个uint32_t移32或更多的位是未定义的。我猜这最初是因为在不同CPU上的移位操作是不同的:比如,X86平台会将移位值截断为5位(因此移动32位是同移动0位一样的)【译者注:可参考 https://blog.delphij.net/2011/06/post-603.html】,但是PowerPC将移动值截断为6位(因此移动32位的结果为0)。由于硬件的差异性,这个行为在C中也是完全无法确定的(因此在PowerPC上移动32位可能会格式化掉你的硬盘,它并不保证一定产生0)。消除这个未定义行为的代价就是编译器在变量移位时不得不加入一些额外的操作(比如“与”操作),相对于普通的CPU,这将产生两倍的代价。

野指针的解引用以及数组越界访问:解引用随机指针(比如NULL,或者已被free掉的指针等)和越界数组访问在C程序中毫无疑问地都是一个bug。为了从根本上消除此种未定义行为,数组访问将不得不进行边界检查,并且将不得不改变ABI来保证任何指针的范围信息都能够被映射为指针的运算。这对于许多数值和其他应用来说,将花费极高的代价,就好像破坏掉现存的每一个C库的二进制兼容性一样。

解引用一个NULL指针:不同于普遍的看法,在C中解引用一个空指针是未定义的。它并未被定义为自陷(trap),如果你mmap一个页面在0,它并不一定会去访问那个页面。这放弃了原来拒绝解引用野指针和把NULL当作哨兵用的习惯。NULL指针的解引用成为未定义的,这将开启大范围的优化:相反,对于任何一个不能被优化器证明为非空的对象指针的解引用,Java会带来一个副作用,即都会把它当作对于编译器是无效的。这极度地影响了调度策略和其它优化。在基于C的语言中,把NULL当作未定义将开启许多简单的标量优化处理,那是由于宏展开和内联而被展现出来。

如果你在使用一个基于LLVM的编译器,如果你愿意,你能够解引用一个“volatile”null指针来得到一个崩溃,因为volatile的加载和存储通常是不通过优化器的。当前还没有任何标志可以把随机的NULL指针的加载当作有效的访问来对待,或者使随机加载知道它们的指针是“被允许为null”的。

违背类型规则:把一个int*转换成一个float*并且解引用它(把这个“int”当作“float”来访问)是一个未定义行为。C要求这类的类型转换是通过memcpy来完成的:使用指针转换是不正确的并且结果是不确定的。这个规则是有十分细微差别的,而且我不想在这里详细说它(char*是一个特例,向量有着特殊的性质,所有元素一起改变。这个行为引起一个被称之为“基于类型的别名分析”(TBAA)的分析,它在编译器中被用作大范围的内存访问优化,并且能够显著地改进生成代码的性能。比如,这条规则将允许clang把下面这个函数优化为"memset(P, 0, 40000)"。

  1. float *P;
  2. void zero_array() {
  3.     int i;
  4.     for (i = 0; i < 10000; ++i)
  5.         P[i] = 0.0f;
  6. }

这个优化也使得许多加载都不用进入循环,消除掉一般的子表达式等。这类的未定义行为可以通过传递-fno-strict-aliasing标志来禁止掉,这将禁止这个分析。当传递此标志时,Clang将会把这个循环编译成10000个4字节存储(将会慢好多倍),因为它不得不假设任何存储都有可能改变其值,就好像下面这样:

  1. int main() {
  2.   P = (float*)&P;  // cast causes TBAA violation in zero_array.
  3.   zero_array();
  4. }

这种滥用类型是十分不常见的,这也是为什么标准委员会决定为了显著的性能允许“合理的”类型转换的难以预料的结果。值得说明的是,Java从基于类型的优化中受益但并没有这些缺点,因为在这门语言中完全不存在不安全的指针转换。

不管怎么说,我希望你能了解一些通过C中的未定义行为开启的这些优化。也有许多其他类型的例子,包括以下这些,比如“foo(i, ++i)”,多线程程序中的竞争条件,违反“restrict”,除0等。

在我们的下一篇文章中,我们将讨论为什么当性能不再是你唯一的目标的时候,C中的未定义行为是一件十分可怕的事情。在本系列的最后一篇文章中,我们将讨论LLVM和Clang怎样处理它的。

Topic: sohulinux

ARM kernel consolidation

Paul McKenney
May 18, 2011
http://lwn.net/Articles/442570/

翻译:李潇

你最近可能听说了内核中ARM架构存在的一些尴尬。鉴于ARM Linux的合并是Linaro特别要着手处理的问题之一,很自然你会想问“Linaro将怎么面对这个问题?” 所以当该话题成为了最近在匈牙利布达佩斯举行的Linaro开发者峰会的主旋律时,千万不要感到惊奇。

代码重复以及游离于主干开发流程之外的补丁,使得内核中的 ARM 部分尤其难以使用和开发。因此,Linaro正在合并代码并将代码推向主干。这将会让内核能更好的处理ARM板和SoCs。然而,ARM Linux内核合并并不是Linaro独自面对的问题,在整个ARM Linux内核社区,ARM Soc,ARM板和系统供应商也同样存在这些问题。因此,尽管我希望Linaro能起到关键作用,但最终的解决方案需要整个ARM社区的努力。另外很重要的一点是,这种努力只是一种尝试性的建议,而不是必须遵守的命令。

代码组织

如果我们想要取得任何进步,我们必须从某些地方开始做起。一种极好的出发点是根据功能来组织ARM Linux内核代码而不是根据Soc/board实现。将有相同目标的代码聚集在一起将比较容易观察到相同的模式,甚至是相同的代码。比如,如今许多ARM SoCs用相似的“IP blocks”(比如I2C控制器),但是在相应的arch/arm/mach目录下每个SoC会提供一个完全不同的I2C驱动。我们期望在不同ARM板和SoCs上,相同硬件的“IP blocks”驱动能够合并为一个单独的驱动,并且该驱动能够使用相应IP block工作在任何系统上。在某些情况下,将一个给定的IP block与SoC或者ARM板连接起来有不同的方法,这样会带来些复杂度,但这样的复杂度总是可以被搞定的。

这样又出来了一个问题,相似的代码应该移到哪儿去。大家都同意的回答是,不应当移到arch/arm目录下。驱动理所当然要放到顶层drivers目录树的相应子目录下面。另外,ARM SoCs有内置多种不同的设备,从触摸屏到GPS接收器,再到加速感应器,而且新型设备还会不断出现。所以某些时候,将代码移动到某个驱动分类也不完全合适,而最好是在drivers树目录中创建一个新的分类。

但对非驱动代码该怎么办呢?它应该放在哪?看一看以下例子应该会有启发:(1)Jeremy Kerr,Russell King,Thomas Gleixner,还有很多其他人一直在开发的struct clk代码。(2)Grant Likely领导的device-tree代码。(3)Thomas Gleixner一直在开发的通用中断的芯片实现。

struct clk代码是由许多SoCs和ARM板里elaborate clock trees受到启发的。这些树在其他地方会用到,在性能和能源效率方面权衡,根据SoC或者ARM板上单个设备的需要作出设置。struct clk代码在提供插件去适应指定SoC或ARM板的同时,还允许这些树以通用的格式表示。通用中断芯片实现也有相似规则,但它跟时钟树无关,而是跟中断分布有关。

设备树的目的是用来允许通过数据而不是代码来表示ARM板的硬件配置,这能够减轻创建一个在不同ARM板上启动的单一Linux内核二进制的任务压力。设备树基础架构补丁已经在近期被Russell King接受,这说明从特定ARM板代码到设备树描述的转变将开始。

struct clk代码已经被同时用在了ARM和SH CPU架构中,所以它不是只对ARM适用的,而是更核心的Linux内核代码。同样的,设备树代码也不是只对ARM适用的,它同样被PowerPC, Microblaze和SPARC架构用到,甚至被x86架构用到。因此设备树也是Linux的核心内核代码。虚拟中断使用的更广泛,在所有CPU架构中都很普遍。这给我的启示是ARM内核代码合并不一定要限制在ARM上。事实上,一段特定代码支持的架构越多,愿意对它贡献代码和测试的开发者就会越多,这段代码就会越健壮,越具有可维护性。

当然,也需要有一些只针对ARM的代码,但是这些代码最终会限制为ARM核心架构代码和ARM SoC核心架构代码。而且,ARM SoC核心代码最终会演变为由针对核心Linux内核框架的小插件组成,这将会极大减轻新ARM板和SoCs的开发和维护。

很容易将这些做法写出来,但真正去实现它却是另外一回事。毕竟,尽管有一大批极有天赋和激情的ARM开发和维护人员,但是其中很多是ARM新手,对Linux内核也不熟,自然也不知道新代码该放在哪里。这些人可能很容易继续将他们的大部分代码放在SoC和ARM板子目录下,这将使得现存的ARM Linux内核难题继续存在。

解决方案的一部分是增加文档,特别是指导ARM驱动和ARM板端口开发的文档。Deepak Saxena,这位Linaro内核工作组的新领导者,即将开始这项工作。虽然只有当真正读到它时,文档才会发挥作用。但幸运的是,正如在计算机科学里任何问题似乎都可以通过增加额外的间接层来解决一样,任何维护上的问题似乎也都可以通过增加额外的git树和维护人员来解决。这些维护者将会帮助生成通用代码,在代码可用后自然也就成为文档的开发人员。

Git trees

可以使用Nicolas Pitre现成的Linaro内核git树。然而,Nicola的git树是integration tree,就是说将最新的ARM相关代码合并到一个静态的主线版本。相反,maintainership tree面向的是计划合并进入上游的补丁,这些补丁是以下一个主流发布候选版本为根据的。如果我们用单一的git树来处理整合和维护,那我们要么不必要的将不相关的核心内核bug暴露给用户,要么我们不能足够紧密得跟踪主线内核用以维护,这将使得在每个合并窗口期开始的那一小段时间内,强制进行一个完整的rebase和测试期。

当然,理论上我们可以在同一个git树上有维护和整合分支,但是将这两个很不同的功能分开到不同的git树可能是初期最好的选择。

这个新的git树(公布于五月18)将在每一个参与的ARM子架构至少有一个分支,并且这些分支将不会受到rebase合并,这让在该git树上的开发变得容易。遵循惯例,参与ARM子架构的维护者将宣布该维护工作组发送该git树的 pull request。同样,遵循惯例,所有分支的合并请求将会发送到Stephen Rothwell的 -next 分支。这些分支也有可能通过Russell King的ARM树,独立推送给Linus Torvalds。

每个分支独立推送给Linus,这看起来很令人奇怪,但Linus的确希望看到由此产生的冲突。这样的冲突可能会帮助Linus找到需要他注意的地方。

无疑,这个新的git树将不会限制在Linaro内部,但也不仅仅给Linaro之外的同仁使用。我会非常高兴地看到一些Linaro之外的维护者表示有兴趣参与进来。

布达佩斯的会议推举出了这个git树的维护组成员,即Arnd Bergmann, Nicolas Pitre和Marc Zyngier,另外还有Thomas Gleixner提供帮助。Russell King当然也会拥有对该树的写权限。这个树将及时启动以应对2.6.41合并窗口期。该计划打算从小处开始不断迭代向前进化而不是一开始就设计一个完美模型。

正如文章开始提到的,这种努力是一种实验,而不是必须遵守的命令。尽管这个建议性的实验不能期望解决每个和任何ARM Linux问题,但他们将提供一个好的开始。每一点帮助,每一次清理释放一些时间来启动下一次清理。有理由希望这份努力会帮助减少无数的毫无意义的并让Linus Torvalds在上个月动怒的平台代码

Topic: sohulinux

Scale Fail (part 1)

http://lwn.net/Articles/441790/
By Josh Berkus
May 6, 2011
翻译:王鑫

让我告诉你们一个秘密:我从来不去修正数据库,我去修正程序。

许多公司总是请我去“修正数据库”,因为他们总是认为造成他们性能和宕机的问题是出在数据库上。其实,数据库很少出现问题。可伸缩性糟糕的应用总是由一系列错误的管理决策所造成的。事实上,这些错误的决策是如此常见以致于可被称之为反面模式。

  1. 译者注:所谓“模式”,是指遵从某种规则或规律所反复出现的思维方式或表现。Alexander把模式的集合称为模式语言(Pattern Language)。构成模式语言的各模式是针对某一特定前提的求解,记述频繁发生的现实问题及其基本解法。这些解法可以反复使用,只要出现同类问题就可以使用同一解法,而不必总是一切从头做起。
  2. 反面模式(Anti-Patterns)的概念产生于1996年,是米切尔•阿克鲁在研究软件开发的既存问题及其解法时归纳产生。反面模式主要研究最常见的拙劣软件模式特征及其识别方法,以及反面模式“再构”的改善方法。
  3. 具体参考:<a href="http://www.job108.com/App/show_article.aspx?m=00276AEAA000058F6

">www.job108.com/App/show_article.aspx?m=00276AEAA000058F6[/geshifilter-code]
 
我在上一次的MySQL会议上做了一点关于反面模式的讨论,先去看看再继续来讨论。当你已经看过了这个五分钟的小短片(希望你笑了),下面介绍几个解释怎样识别和避免反面模式的细节。

Trendiness

  1. “现在,你们为什么还要去迁移数据库呢?你们三个月没有出过问题,而且我们计划在未来两年内继续延长这个时间。迁移数据库可能会导致服务中断或者混乱。”
  2.  
  3. “好吧……我们的CTO是每周CTO午餐会中唯一使用PostgreSQL的。其他的CTO总是在这个上面开他玩笑。”

这是不是听起来就好像你的CTO一样?这可是源于一次我真实的对话。它说明了越来越多的技术负责人开始思考一个问题:比起站点是否仍在运营或者公司正常运转而言,他们更关注个人形象和职业。如果你开始在一些基础会议上听到诸如“流行”、“热点”、“前沿”、“最新技术”或者“流行产品”,那么你就得当心了。频频引用杂志上的调查结果或者产业趋势是另外一个危险信号。

Scaling 应用本质上就是关于资源管理和重复性的日常工作。这意味着使用你的员工熟悉的,已经被证实为可靠的,并且就是按照你所想的而设计的技术。热门新特性不如无人值守的可靠运营。web世界里总把一个个崭新的技术推到前台,尽管它们匮乏文档、不稳定、不能同其它组件集成,并且充满各种漏洞。

还有另一种趋势值得被注意,常常听到这样的言论,“如果Google或者Facebook在做某件事情,那它一定就是正确的选择。”首先,除非你的应用程序或平台同他们的非常类似,那么对他们而言是正确选择的事情不一定对你也是正确的选择。

此外,并不是每件Google和Facebook做的事情,如果让他们从来一遍他们还会去做。像其他的公司一样,这些顶级的互联网公司也做过错误的技术决策。所以当你将要模仿那些巨头们的行为时,请首先确定一下他们的员工对这个技术的看法。

No metrics

  1. “我们确实检查过了网络延迟吗?”
  2.  
  3. “我确信问题是出现在HBase中。”
  4.  
  5. “恩,但是我们检查过没有呢?”
  6.  
  7. “我告诉你,我们根本不必检查。这个总是HBase的问题。”
  8.  
  9. “给个面子,告诉我。”
  10.  
  11. “或许吧。恩……哦!我想网络可能是有点问题。”

 
Scaling 应用是一个数学问题。如果一个用户在web服务器上消耗X个CPU时间,那么你想支持100,000个用户的并发访问时,你需要多少个web服务器呢?如果数据库每天增长Y,而其中Z%的数据是“active”,那么这些常用数据的大小超过RAM的容量需要多少时间?

明显地,你至少需要知道X、Y、Z的近似值才能对此类问题作出估计。如果你在做scale,你应该考虑你的应用程序栈的每一个部分,从存储硬件到JavaScript。而那个你忘了监控的东西很有可能就是让你整个站点挂掉的原因。近年来的大多数软件都有一些监控它们性能的方法,软件不应该成为你规避的部分。

尽管这是一个常识,我们的客户中仍然有相当多的一部分仅仅用Nagios监控它们的硬件。这意味着当出现响应时间的问题或者其它一些超过Nagios能力范围内的问题时,他们无法去诊断到底是什么原因引起这种问题的,而且到头来,他们总是去修正错误的部分。

更糟的是,如果你没有你的应用程序实际消耗资源的统计的话,那么你就不知道当你去扩充你的站点时,你需要多少、哪种类型的服务器。那意味着你将对某些组件进行大规模地重建,并且花费两倍的金钱,而其它部分只能缩减。

有多少个公司没有测量数据,或者跳过这个问题,他们是怎么做决策的?呃……

Barn door decision making

  1. “当我在亚马逊的时候,我们使用squid做反向代理。”
  2.  
  3. “Dan,你在亚马逊的时候是一个广告销售经理吧。”

 
缺乏数据的情况下,员工一般都根据他们的经验来排解故障,而这往往是错误的。尤其当出现突发事件时,一般都会去解决上次出过问题的那个部分。当然,上次出问题的地方,不一定是这次造成这次问题的原因。

当你计划扩容的时候,这种思想会更糟糕。我见过许多IT员工依靠他们上一个项目的经验甚至上一份工作的经验来购买设备,提供服务,配置软硬件,部署网络。这意味着,当现有应用程序的可用资源不能够完全匹配应用程序的需求时,你要么为此提供更多,要么挂掉。

当然你应该从你的经验中学习。但是你应该学些合适的教训,比如“不要指望VPN总是连通的”。不要误用知识,比如把应用到图片站点的cache策略应用到在线银行站点上来。反例通常类似如下的格式的断言:

  •  “当我在XXX(原来的公司名)的时候……”
  •  “当我们原来遇到XXX(其实并不是很相似的问题)的时候,我们用XXX(某种软件或技术)”
  •  “XXX(差别很大的项目)使用XXX(某种软件或技术),因此我们也改使用它。”

(For non-native English speakers, "barn door" refers to the expression "closing the barn door after the horses have run away")

下面我们检讨程序设计中的问题

Single-threaded programming

  1. “因此,当我在Rails中monkey-patch一个普通类的时候,什么时候该改动会影响到这个正在运行的并行进程呢?”
  2.  
  3. “立刻就会!那很神奇的!”

 
并行处理的框架对大多数开发者来说都是一个挑战。我见过上百次这样的故事了:一个开发者把他的代码写成了单线程的形式,然后他在自己的笔记本上,在单用户单并发的情况下测试,之后他把这份代码部署到了有200个服务器的站点上,然后这个站点挂了。

单线程是可扩展性的敌人。你的应用程序的任何一部分,如果限制了同一时刻并发执行同样代码的能力,那意味着把你限制在单机单核的吞吐量上。我并不是在这里讲应用程序代码中关于互斥的部分,尽管那也可能很糟糕。我在讨论的是由于等待某个独占锁式的组件而阻塞了整个应用程序的设计问题。

比如,一个入门新手常犯的错误就是把所有的异步任务放到一个单向队列里去。这样整个应用都受限于这个队列处理的速度。其它常见的错误包括:frequently updated single-row "status" table, explicit locking of common resources, and total ignorance of which actions in one's programming language, framework, or database require exclusive locks on pages in memory.

我当前工作的应用是一个分布式的,拥有240台服务器的数据处理集群。然而,把数据分块交给服务器运算的部分是一个只跑在一台服务器的单进程程序。于是整个云每分钟只能处理4000个任务,75%的时间是空闲的

更糟糕的例子是我以前咨询过的一个很流行的体育站点。赛事信息是从互联网上其它的服务获取的,更新信息的时候它会在数据库事务里保持一个独占锁,然后等待远程服务器响应。客户无法明白为什么增加的应用服务器越多,用户的超时就越严重。

设计可伸缩的应用之前,问问自己:"how would this work if 100 users were doing it simultaneously? 1000? 1,000,000?"。学习一门函数式语言或者 map/reduce. 这会训练你思考并行的能力

=============
等待下集

Topic: sohulinux

Fedora 15里如何关机

今早我无意中发现了F15可以下载了,于是就开始download

到了100多兆的时候,杨松同学出现了,然后问我是否要 F15 的安装盘。。我心里一喜,终止了下载任务

杨松随即又抱怨,F15的ACPI很成问题,halt 无法关机。我问他,为啥不用菜单里的关机选项来关机。杨老师很泄气的回答,因为找不到这个菜单项

我哈哈一下,你必须要按 Alt 才能看到 shutdown

==== 这是故事的分割线 ====

为什么我知道 Alt 这件事?我明明没有用过 F15 啊

很简单,在下载那100多兆的时候,我一直都在看 Release Notes

别的项目不敢说,RedHat 制作的 Release Notes 一向都是做得很漂亮的。而且要明白最近半年开源软件比较重量级项目的版本变化,大部分都囊括在这个文档里了

即使你在用 ubuntu,也非常值得阅读。

Topic: 技术

谁在维护RPM? (2011版)

Who maintains RPM? (2011 edition)
By Jonathan Corbet
May 3, 2011
翻译:李凯

早在2006年,LWN 就讲述过关于 RPM 这个软件维护过程的复杂故事。考虑到该软件对于所有基于 RPM 的发行版的重要性,RPM 缺乏一个如何进行维护的清晰描述确实很令人沮丧。当年晚些时候,Fedora工程宣布成立一个新的,社区驱动的 rpm 项目。从那以后,这件事情就悄悄的进行着,但是最近的一些事件表明,rpm的故事还没有走到尽头。

上述提到的rpm工程运转在rpm.org上4.9版本已经在三月初发布了。该代码也在积极的维护中,同时可以看到新增加的一些小小的特征。但是这个项目对是否有一个大的发展计划并没有发出任何实质的信号。只有少数的几个提交者为代码仓库工作;他们中的绝大多数人是为RedHat工作的。从所有迹象来看,rpm至少一半是维护模式。

但是rpm.org不是唯一的 RPM 站点,还有一个fork项目rpm5.org。这个版本主要由Jeff Johnson来维护,他之前是Red Hat的雇员,在公司之外发起了这个项目,希望创建一个更好的 RPM。这个版本增加了更广泛的可移植性,还有一些特性比如新的压缩格式和许多其他的一些东西(rpm5 并没有一个完整的特性列表,俺也懒得在ChangeLog上搜寻显要的变化)。一个重要的特征是包的事务管理(也叫做“RPM ACID”),其思想是确保任何rpm操作,要么成功要么完全失败,无论中间过程发生了什么。依照Jeff所说,rpm4操作中如果被杀掉,可使整个系统被损坏;但rpm5消除了这种可能性。

RPM5 fork可以说已经有了更多的发展以及增加了一些有趣的新功能。尽管如此,它仍然是一个相对模糊地项目;它已经被诸如 Alt Linux,ArkLinux 和 Unity Linux 所选择,但还没有得到较大的 distributor 的重点考虑。然而最近几个月这些情况已经有了变化:在 Mandriva 2011 plans中就包括了切换至rpm5。这已经为这个fork带来了更多的关注并产生了一些有趣的结果。

并不是在Mandriva阵营里的每个人都认为这个切换是一个好主意。早在去年11月份,Evgeni Dadonov就开始对这种变化背后的原因展开讨论

(据我所知,Fedora/RedHat/CentOS and SUSE/OpenSUSE 等主要的供应商都支持RPM4,Alt Linux and Unity Linux使用RPM5.  RPM4主要的特征是稳定,以及众所周知的那些长期悬而未解决的问题以及它的行为表现。RPM5正在不断的发展中,它有很多特点,但没有太多的安装的基础。)

不论是拥护还是反对,许多人就这个问题进行相当激烈的讨论,有时还会产生一些不愉快。转向 RPM5 的决定似乎是基于若干考虑的。某些新的功能对Mandriva很有吸引力。开发RPM5的团体看起来似乎比RPM4的团体更加的活跃、开放;rpm4主要由RedHat主导,相对比较封闭。Mandriva 有一堆没有被 RPM4 接受的补丁,现在可以 upstream 到 RPM5。与其他大型基于RPM的发行版保持 RPM 包的兼容性只是一个虚幻的概念,每个发行版都根据自己的需求对rpm4进行大量的修补,所有发行版几乎都无法安装非本发行版的其它 RPM 包装(译者:所以 RPM5 不兼容也不是大不了的事情)。重要的是Mandriva的包维护者Per Øyvind Karlsen希望采用其它的模式

(我已经说的很清楚了,我已经计划去维护rpm5,我是一个维护者,有最好的知识。如果你更新到我的测试版本里的话,会体会这种改变不会对你产生消极影响。我是包的维护者,它不应该对别人造成不便。我觉得我应该做出个关于是否应该继续维护已经被彻底打乱了的rpm.org版本的决定,它陷入补丁中,并痛苦的重新生成每一个版本。)

Per Øyvind断言,“这种改变不会对你产生消极影响”。这个断言没有变成现实,从此以后,Mandriva的 cooker 邮件列表就一直被关于rpm5相关问题的讨论占据。其他小的系统变化,例如 systemd 的改变,被湮灭在滔滔的 RPM5 讨论中。这种类型的切换一直都不容易,与Mandriva的更高级别的包管理工具的整合使得整个大工程愈发的复杂化。去适应 Mandriva 这样一个巨大复杂的发行版是 RPM5 不可避免的成长之痛。

必须指出的是,很明显 Jeff 花了大量时间支持 Mandriva 的这个计划。人们很容易得到这么个印象,他被公司聘用作为这个项目的一部分。问题一个个被提出,Jeff 一个个努力解决。很少有开发项目拥有这样努力去支持它的用户,即使是高知名度的牛人也难以如此投入;Jeff已经真正的为Mandriva服务。

这种支持在四月底的时候有些动摇,Jeff突然宣布,rpm5的邮件列表(以及许多其他的网站内容)归档已经被撤下。在被质疑为什么会发生这样的情况时,他的反应非常过火。事后他自己承认,他想尽快停止这种讨论。这个网站已经重新上线,包括一个重定向到rpm5.org的rpm6.org。

这是怎么回事远没有明朗,但从Jeff指责一名Mandriva的前开发者“应该对破坏rpm5品牌负责”的邮件里可得到一些暗示。他真的对于处理报道的问题以及抱怨有关rpm5的事情感到疲惫,而其中许多确实与rpm5的问题无关。大部分问题貌似是由于 Mageia(a fork of Mandriva)没有随着Mandriva迁移RPM5所至。Mageia的立场,正如AnneNicola描述的那样,似乎是合理的:Mageia的开发者为了保证紧跟 Mandriva 发行节奏就已经忙不过来了。鉴于所有需要所做的事,把精力投入到一个低级别的包管理器系统的变化看起来似乎不是最好的主意。

这样看来,依然有些坏的血液在Mandriva和Mageia阵营中。有些迹象表明,相比较公共的列表而言,私下的讨论不那么友好,Jeff和rpm5很可能在那里被议论。至少,可以从类似的评论中可能得的一条结论:

(你已经花了数月时间推销rpm5但并不成功。RPM5被评为惨败...被很多你绝不喜欢的词语诅咒。What you don't like has nothing whatsoever to do with this "tourist with camera" wandering the dungeons hacking on RPM.

我可以告诉你,从我的第一手经验而言,fork 是对时间和精力的巨大浪费。我还可以说,Mandriva 和 Mageia 的相似之处比相异处更多。我强烈建议你该干嘛干嘛去)

自从那以后,最糟糕的事情已经过去了,Jeff已经回来讨论有关使rpm5工作的更好的话题了。这个事件已经让某些在Mandriva社区中的同志警醒,开始担忧2011版本建设基础。

观看这里讲发生什么是一件有趣的事情。现在Mandriva看起来似乎很好的承受了rpm5的过渡,一切貌似已经稳定下来了。如果RPM5在2011版中表现的比较好,其它发行版的用户将会问,为什么会我们还坚持用"旧的版本";反之,其它发行版也可能看到Mandriva经历的痛苦,以及一个不那么如意的结果,他们就继续快乐的使用相对无聊的RPM4好了。

无论哪种方式,好戏还没有完全结束呢。

Topic: sohulinux
订阅 RSS - qyb的博客