博客

Scale Fail (part 2)

May 20, 2011
This article was contributed by Josh Berkus
Scale Fail (part 2)
翻译:刘晓佳

上一部分中,讨论了若干影响网站扩展性能的问题,而大部分其实是管理的问题。上一篇文章主要涉及到了公司所做出的一些欠缺长期考虑的决策-或者叫它“反模式”,比如compulsive trendiness, lack of metrics, "barn door troubleshooting", and single-process programming。这一期,我们将探索导致停工的一些更一般的技术管理失败。

No Caching

“报告显示每秒完成了7000次读查询。你确定它们都能被缓存吗?”
“我们已经安装了memcached”
“memcached是如何配置的?缓存了哪些数据?如何使缓存更新?”
“我…不太确定。我们都留给Django解决吧。”

我有时对web公司愿意在更快的硬件上花多少钱,和愿意在一些能够使得他们的应用在平稳的方式下运行的更快的简单事上所花的努力感到非常吃惊。比如,如果你打算扩展一个网站,首先应该问问自己:“我该在哪加一个更有用的缓存呢?”

上面提到的memcached,并不仅仅指简单的数据cache。对任何一个真正可扩展的网站,你可以加入多级缓存,每一级都大有用处:

  • database connection, parse and plan caching
  • complex data caching and materialized views
  • simple data caching
  • object caching
  • server web page caching

详细分析不同类型的缓存和如何使用这些缓存需要一系列的文章来阐述。然而,每种形式的缓存都共享了将数据发给用户而不管用户在哪和减少响应次数的能力。更为重要的是,通过降低重复应用请求所需要的资源,能够提高你的平台的效率,从而使得它更具扩展性。

看起来很明显,不是吗?但我一只手就能数出在雇佣我们之前就已经具备有效缓存策略的公司。

客户在数据缓冲方面常犯的一个错误是让ORM来完全决定缓冲。问题主要在于作为一个即开即用的方案, ORM无法有效的使用cache,对于完全一致的用户请求才请求缓存,导致命中率几乎为0

我所见过的一种最坏的情况是一个在线拍卖的应用程序。用户做的每件事,如每次点击,每次翻页,每次鼠标移动,都会导致对后台PostgreSQL数据库的查询。加上拍卖的桌面精灵轮询查询拍卖数据库,每用户每秒更新30次。这意味着每个程序的用户每秒将会完成查询内核事务处理数据库100次。

缺乏缓存是个非常常见的错误,下面是另一个我喜欢称之为反模式的一种症状:
伸缩不可能的事情

“在发展的第三阶段,我们会将数据表根据统计出的用户分组分割成12个shard。
没有被分割的数据需要在每个shard上复制,并且我也发明了一种复制机制来
完成这个任务。”
“看起来很复杂。你考虑过仅仅缓冲最常用的搜索吗?”
“那不行。我们的数据太动态了。”
“确定吗?我做了一些分析,当前数据库的90%的访问属于四种模式之一。”
“我告诉过你,它将不工作。我才是这里的CTO!”

你的应用程序所需要的某些东西很难扩展,但消耗了大量的系统资源,管理时间和员工的创造性来做这种无意义的事情。这包括事务性数据库,队列,共享文件系统,复杂的web框架(如Django或Rails),和ORMs。

你的基础架构的其他部分很容易扩展许多用户的请求,如web服务器,static content delivery,缓存,本地存储,客户端软件(如javascript)。

基本上,当一个架构越状态化,复杂化,特色化,每个应用的用户使用的资源就会越多,越容易出问题-因此也就越难扩展。通过这个,你也许会认为与快速解决扩展化问题斗争的公司会首先集中扩展容易扩展的,并尽可能推迟那些难以扩展的。

这你就错了。

相反,开发部门的领导貌似热衷于先解决架构中最难扩展的问题。数据库Sharding,负载均衡主从服务器复制,转发事务性队列,200个节点的集群文件系统-这些都会令IT民工们兴奋,而且得到大量的资金上的支持。而有时只需要通过增加一个低成本/简单的Varnish(Varnish是一款高性能的开源HTTP加速器)节点就能解决问题,或修补程序中资源消耗的大户。

比如,我们有个客户的Django服务器过载后down掉了。他们的服务器数量从四台增加到八台,仍旧需要定期重启,需要将应用服务器的数量再加一倍,而这需要扩展数据库服务器。相反,我们做了一些流量分析,发现大部分Django服务器的资源利用在服务静态图像上。我们就把所有的静态图像移到了CDN上,然后就能够减少服务器的数量了。

故障的单点/SPoF

“我们如何负载平衡中间件服务器和数据库服务器之间的连接?”
“通过Zeus负载均衡群。“
“从web服务器到中间件服务器呢?“
“同样是Zeus群。“
“Web服务器到网络文件存储呢?数据中心之间的VPN呢?SSH访问?“
“Zeus。“
“该网络上的一切都通过Zeus吗?“
“是的,相当的多“
“好吧,什么可能导致错误呢?“

SPoF,当然,代表着Single Point of Failure。特别的是,它指的是这样一个部件,如果它失效,将会导致整个系统不能工作,而不管其它地方有多么冗余。令人失望的是多少公司花费了大量的硬件和工程时间来使得它们的很多部件高可靠性,却没能移除SPoFs。最终你的系统的可靠性是由最不可靠的部件决定的。

上面对话中的公司在那次会话后的几周的某天就垮掉了。一个系统管理员下载了一个buggy配置到Zeus上,立马整个网络就瘫痪了。数据库服务器,web服务器,和其它服务器都正常运行着,但是甚至就是管理员都无法访问它们。

有时,你的SPoF是一个人。比如,你有一台服务器或甚至一个数据中心,它们只有通过手动操作才能失效,仅有一个员工了解相关知识或者能够登录进去操作。更险恶地是,这些SPoF经常潜伏在你的开发或恢复过程中。我曾目睹过一个公司为了防御DDOS攻击而部署了一套热代码,仅仅让热代码仓库down掉,系统就无法修复了。

"Cascading SPoF"是指那些看起来有冗余措施的SPoF。这里有个简单的数学练习:你有三台应用程序服务器。每台都工作在它们最大能力的80%上。当其中一台失效后,且它的流量均衡到另外两台上面会发生什么情况呢?

并不是说某个组件是唯一的才成为单点;它仅仅需要成为导致失效的一种情况而已。如果所有的组件都工作在接近最大能力的情况下,那么你就有麻烦了,因为少数的服务器故障或者中等的流量增加就可以导致一系列级联的服务器失效。

云成瘾

“……所以如果你一直待在AWS平台上,你将不得不主要进行
横行的扩展,这将需要40K美金的咨询费的工程。如果你转
到传统的托管上,你大约需要10K美金的咨询费用完成迁移,
同时得到更好的应用性能。云服务的价格大约是自己租用机
架服务器的三倍。“
“不到明年我绝对不考虑迁移“
“所以你想花40K美金的合同吗?“

今年年初我曾参与一个火热的讨论,我逐渐发现了一种新的反模式,我们叫它“云成瘾“。我们的几个客户拒绝移走他们的云托管,甚至当它们几乎要毁掉它们的业务时。最糟糕的是AWS,因为亚马逊不支持仅迁移云的一部分(要么就全迁移走),其它的公有云也有类似问题。

云托管的优势在于它不需要预先的基础设施的投资就能启动新的应用和服务用户。作为一种降低创新门槛的方式,云托管拥有巨大的优势。

当应用扩展达到云服务器的资源限制时问题就出现了,不得不转换到新的平台。通常公司会通过超时,运行中断,逐渐增加服务器数量却不能改善性能等方式发现这些限制。提到限制,我指的是这种大规模的公有云自身的内存,处理能力,存储吞吐量和网络配置方面的限制,而且购买繁忙云实例的费用高昂。云服务支撑一个项目起飞是足够的,但是当你需要每个节点上严格的性能需求时就会启动失败。

那就是关于云的伸缩性问题。之所以会这样,是公司没有经验来管理基础设施,没有系统方面的人员,没有迁移的预算。更危险的是,管理人员没有做任何关于基础设施的决策。Advice that a change of hosting is required are met with blank stares or even panic.下一财政年度在启动,实际上是委婉的说永远不会启动。

结论
当然,这些并不全是扩展反模式。个人管理不善,没有预期到需要峰值,缺乏部署工程,依赖不可靠的第三方社团,以及其它一些问题,和我之前所概括的那八个问题一样都可能造成严重的损害。也可能有许多不扩展的方式。我无法一一列出。

只是希望这篇文章能有助于你认识到发生在你自己公司或客户中的这些“scale fail“的模式。我所概括的这些问题主要归结于失误的决策-而不是任何技术方面的限制。就我所经历的,技术方面的问题很少抑制web生意的增长,往往都是来自管理方面的问题造成的。如果你能识别出反模式,你就能够犯更少的错误。

Topic: sohulinux

prefetch的问题

By Jonathan Corbet
May 24, 2011
The problem with prefetch

翻译:曾怀东

随着经验的增长,软件开发者会发现微优化的努力并不值得,尤其是在缺少针对具体问题的硬数据(hard data)的时候。性能问题通常不是出在我们认为的位置,所以没有头绪地进行调整试图获得更好的效果可能是徒劳的,甚至可能使事情变得更糟糕。这是内核开发人员得到的教训。

在内核层面,性能通常受缓存行为的影响。真正高性能要求只有命中cpu缓存才能够满足,内存访问相比较显得过于缓慢了。内核尽量地使用cache-hot memory;以及其它一些其它重要的工作,例如调整数据结构使得经常被访问的数据位于同一条cache line中。作为通用的准则,这些优化方法对性能的提升很明显。

百分百命中缓存是难以达到的,但是也可以想办法尽量提高命中率。如果内核知道最近将要访问的数据所处的位置,它可以使用CPU提供的 prefetch 指令将数据放入缓存。这种指令是通过内核prefetch()函数实现的;开发者已经在广泛使用这个函数。例如通常使用的
宏:

#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)

这个宏是用于遍历一个链表。Prefetch()的思想是在处理当前实体的同时开始获取链表中下一个实体。希望在下一次循环开始前能够获取到数据,或者至少使这个数据已经开始传输。众所周知,链表是对缓存不友好的数据类型,所以这种类型的优化能够有效提升速度。

但实际上这样做并不能提升速度,至少在x86处理器上不能。

Andi Kleen可能是第一个对这一优化方法提出疑问的,去年九月他尝试移除list操作中的prefetch。他的patch引发了小规模的讨论,但是显然误入了歧途。最近,Linus在自己最喜欢的workloads(内核builds)之一上做了一些分析然后发现prefetch 指令集占据了极大比例。执行Prefetching所消耗的时间超出了缓存带来的好处;将prefetch()移除能够让made和build更快。

Ingo Molnar,牛人 Ingo,也关注了这件事情,他进行了很有意义的研究。通过使用perf和一些细微的kernel调试,他证明了使用prefetch()结构造成了0.5%的性能下降。这不是一个简单的性能退化,它本来被指望带来更快的速度,一定有什么地方没有按照人们想象的方式运行。

Linus指出其中最明显的一个问题:他的测试引入了大量对单链哈希表(singly-linked hlist hash table lists)的遍历。这些列表比较短,所有没有足够的范围实现prefetching;事实上,大部分时间中,唯一的prefetch操作仅是在尝试使用指向列表末端的空指针。Prefetching 一个空指针看起来没啥消耗,但实际上是有代价的: 在 x86 上(ARM 也是如此)的每一条这样的指令都会导致 a translation lookaside buffer miss and a pipeline stall. Ingo 测算得出空指针的 prefetch 大约消耗 20 个 CPU 周期.

很明显,空指针 prefetch 不是个好主意。如果CPU可以简单的忽略对使用空指针的prefetch尝试那么会好一些。但是,从软件层面解决并不是最好的办法。Ingo对只prefetch非空指针版本的prefetch()函数进行了测试。这个版本确实性能更好。但是仍不如不使用prefetch的方案。

CPU设计者很清楚内存等待的代价;他们花费了大量的努力将任何可能的代价都降到最小。CPU有自己的内存prefetch单元,这些单元尝试预测下一次需要的内存并提前对内存进行遍历。Ingo在他的测试中提到,即使没有任何软件层面的prefetch操作,cpu进行的prefetch操作数量几乎是相同的。所以硬件prefetcher一直处于繁忙状态-并且在决定fetch哪些内容方面上它比软件层面表现得更优秀。将显式的prefetch操作混入其中只会影响硬件的prefetch操作。

Ingo总结如下:所以即便将NULL的部分刨除,prefetches也明显是有害的。

他工作的成果之一:2.6.40(现在被起名叫3.0了),将prefetch()操作从链表,哈希表以及sk_buff表的遍历操作中移除,正如Andi Kleen在九月份尝试做的那样。或者其他的prefetch操作也被移除的话性能也有提升的几率。内核中仍然存在prefetch()操作,不过只存在于特定的能明确提升性能的场景。如同我们尝试的其他底层优化(立刻能想到的就是likely()),我们自以为prefetch能带来帮助,但并不是我们真正需要做的工作。

这个事情的另一个经验是numbers matter。Andi在移除这些操作的时候是正确的,但是他不能说服社区接受自己的补丁。这一次,除了Linus注意到它并进行了研究外,更重要的原因是基于性能的补丁确实需要用数据来证明自己能够达到设定的目标。如果Andi花些时间量化他的做法带来的影响,上一次可能就已经被大家接受了。

Topic: sohulinux

WebGL 的安全问题

By Jake Edge
May 25, 2011
WebGL vulnerabilities
翻译:李凯

最近的报道强调指出,在WebGL中某些现存以及潜在的安全漏洞正在被广泛传播。显然,这种允许网页内容与复杂的3D图像硬件之间互操作的新特征很会导致一系列的问题。该特性肯定会在浏览器上全面支持,事实上已经有几个浏览器已经具备了这个功能。

WebGL是一种底层的3D图像API,该API基于目前多数3D图像卡库采用的OpenGL ES 2.0标准。支持WebGL的浏览器,JavaScript 控制的 canvas元素将可以被硬件加速显示。对网页浏览器而言,无论是玩游戏还是探索三维景观,当然也包括大量其他的用途,WebGL都将会是一种非常受欢迎的扩展。但是,允许互联网中的内容与复杂的硬件进行交互很可能会导致一些安全问题。

典型的图形硬件是由通过驱动连接的一个或多个图形处理器组成的。驱动提供了与高层库之间的某些标准化接口,这些高层库采用了诸如OpenGL之类的图像标准。为了给图像程序员提供更多的灵活性,把特定处理模型以库方式提供的称为shaders。shaders是用来对复杂的图形处理进行渲染,图形库和驱动把那些程序转换为硬件中的GPU运算。

本质上,那意味着恶意网站可以编写出运行在用户硬件上的semi-arbitrary程序。仅仅这一点就应该足够让我们停下来好好考虑考虑安全的问题了。例如一个恶意的shaders可能完全控制了图形硬件,从而破坏任何试图写入到显示器的东西(如其它窗口)。在最糟糕的情况下,这可能会导致用户不得不重新启动图像硬件。

上述拒绝服务可能是极其恼人的,但并不会直接影响桌面的安全问题。它不会泄露用户数据到恶意网站,尽管它可能会由于用户的某些行为而导致数据丢失。这有点像某些恶意的JavaScript,比如无限循环程序,可锁住一个浏览器(但一般不是整个桌面)。现代浏览器把每个标签作为一个独立的进程运行,(例如Chromium就是这么干的,Firefox正在朝这个方向努力)这在很大程度上限制了类似JavaScript问题。

但是这并不是Context(一个英国安全咨询公司)报道概述的唯一的问题。潜在的问题是跨域的图片偷窃。当canvas元素包含跨域的内容,且声明一幅图像来源于其他网站时,有一个“origin-clean“标志将会在浏览器中被清除掉,这样做是用来避免通过JavaScript函数从外域提取图像数据。然而,恶意的canvas元素可以创造shaders从而很容易泄漏影像内容。

这种袭击依赖于一种通过时序分析提取加密密钥的技术。通过计算shaders描绘一个像素的“亮度”要花多长时间,JavaScript可根据该值再生图像。这是一个复杂的攻击,并且要找到真正使用它的exploit有些复杂,但这正好是跨域的一个缺陷。

Mozilla 的黑客 JP Rosevear 认为跨域的图片信息盗窃是一个真实的威胁,即使实践中很难找到漏洞。"While it is not immediately obvious that it can be exploited in a practical attack right now, experience in security shows that this is a matter of when, not if." 他对此的建议是 CORS 草案(cross-origin resource sharing),即让站点明确哪些外域可以包括它们的内容。

防止拒绝服务问题更加的麻烦。唯一真正防御恶意shaders的方法是用审查shaders代码。所以Rosevear建议在需要展示WebGL内容前进行用户确认。

也有一些在硬件层面上努力着试图处理拒绝服务的问题。GL_ARB_robustness(和GL_ARB_robustness_2),是硬件制造商用来检测这些问题和当出现问题时重启硬件的一种机制。但就像Context的FAQ里所说,这可能并不是真正解决问题的办法:

  1. 重置图形卡和驱动应该被看作是当发生异常情形时,维持OS稳定性的辅助手段,并不是一种用来保障用户免受恶意代码的机制。重置图形卡并不能保证整个操作的顺利进行,图形子系统的用户需要正确地处理这一事件。图形攻击需要确保在防范另一个应用程序滥用硬件资源前,任何的硬件资源都可以被重建。这一行为不能直接引起拒绝服务,却仍然可以间接影响整个系统和运行在上面的应用程序。

从安全角度来说,在系统硬件上允许运行从任意网站上发送来的代码,永远是有问题的。Rosevear 指出 WebGL 组件应该分成若干部分以提供必要的隔离: "Nevertheless, claims of kernel level hardware access via WebGL are speculative at best since WebGL shaders run on the GPU and shader compilers run in user mode." That assumes that the libraries and drivers don't have exploitable bugs of their own, of course. 正如Rosevear指出,“对[WebGL的]重大攻击是可能的”。这显然是值得关注的一个领域。

Topic: sohulinux

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
订阅 RSS - 博客 | BT的花