October 2009 Archives

客户端应该去计算什么?

| 3 Comments

这是一个很有意思的话题:随着计算机技术的发展,客户端的计算能力越来越强。想要提高在服务器端运行的系统的负载能力,最直接有效的办法就是把计算任务尽可能交给客户端去做,并减少两者之间的交互;然而,另一方面,这样做又可能会带来一些其他问题,例如,客户端完成某些计算任务的时候可能会比较慢(因为在客户端可以用到的资源比较少,想要保持兼容性最好的办法就是只使用普适的Java Script子集),或者,作为安全系统的一个最基本的原则,任何来自外界的数据都是不应被信任的,等等。

我们可以把数据根据一些规则来进行分类。一般来说数据会具有一些这样的属性:

  • 传输方向。数据是服务器发给客户端的?还是反过来?或者,只是在客户端兜个圈?或者在两台服务器之间传递,只不过客户端是一个中间的载体(当然一般来说,这样做的系统的架构师应该去看看神经科的医生)?
  • 敏感性。如果数据在传输过程中(从客户端到服务器,或反过来)被人截获,是否会产生安全威胁?
  • 尺寸。这个主要是从经济方面考虑,不过多增加计算量的前提下,通常我们会希望传输的尺寸越小越好。

这样一来,判断计算是否应在客户端进行就比较简单了:我们首先要看的第一个问题是:放在客户端进行,是否会增加服务器的计算量?例如,服务器是否需要再增加一些额外的步骤,才能够完成具体的操作?这些额外的步骤与原先的计算相比是否更少?通常来说,这个答案都是"是的",这样我们可以继续考察第二个问题。

第二个问题是,这样做是否有助于减少通讯量?例如,对于用户输入信息的合法性检查,比如说,用户输入的内容是否可能导致SQL注入,或者,他是否在一个该输入数字的地方输入了其他字符?这些检查很显然必须在服务器端做(因为数据跨越了安全边界),但是,假如客户端也进行了这些检查的话,那么那些无效输入就可以在与服务器交互之前被拦住,从而减少通讯量并减少服务器的负担。

第三个问题是,这样做是否会占用太多客户端资源?这个问题可能不太容易评估,一般来说,计算密集型的任务使用Java Script去做的效果有可能会不太好,有时,为了改善响应时间,有可能会希望这些操作由服务器去完成(由于可以采用任意的软件,因此服务器完成某些任务可能更高效)。简单的排序操作在客户端做有助于减少服务器负载,但如果很多客户端需要请求同样的内容,那么将排序结果在服务器端直接缓存起来就会更好一些,等等。

以上仅代表个人经验供参考。

新的开始

无内容。

看文档时候最恶心的事

| No Comments

看文档的时候,最恶心的事情就是明明那不是一份作弊条文档,却只罗列作者是怎么把一件事做起来的,譬如说列出命令行选项,但是不说这些选项是干什么的,以及为什么选上这些选项。

一篇不说是作弊条,却又写成作弊条的文档,特别是,假如这张据说是课后辅导书,结果实际上是作弊条的东西还有错(常有的事),就太让人郁闷了。

ZFS性能的一些优化结论

| No Comments

最近几天测试了一下盘很多(具体说是24块盘,其中2块热备的JBOD)的时候ZFS的性能特点。一些结论

a) ZFS的随机读性能比较差(相对于顺序读写)。这一点除了改为用mirror而不是raidz1/z2之外似乎没什么太好的办法。同样多的硬盘做成两组raidz1(11+11+2),与做成两块盘一组的11组mirror的pool相比,针对同样的数据集的随机读性能相差可达10倍多,当然,mirror的结果是顺序写性能会差一些。作为副作用,mirror时的读性能可提高大约4倍左右。

b) 告诉ZFS数据集常用的数据块尺寸可以提高读写混合的操作的性能。例如如果应用程序多数时间都在操作16K的数据块,将块尺寸改为16K(默认为128K)可将混合操作的性能提高十几倍。

暂时还没测试SSD做ZIL/cache对性能的具体影响,先记下一笔。现在看来如果给数据库用的话,比较好的配置应该是若干对mirror+热备组成zpool,然后把具体存放数据库的zfs的recordsize设置为16k,改天拿实际的数据库在上面跑跑看。

原则是什么

| No Comments

前一段日子,偶然又看了几集 千王之王重出江湖。其中有一集说到了沈胜天和龙四的决裂,两个人各执己见最后断绝师徒关系。

原则是什么?原则是一个人判断事情的准则,或者说价值观,但又不完全是。我想,还需要再加上一条:不会轻易改变。没有原则的人,要么对于事情的是非没有观念,要么对于是非的判断会经常发生变化,和这样的人长期合作会比较危险。

DMA设备驱动的常见问题

| 4 Comments

DMA (Direct Memory Access) 是一种提高计算机系统并发能力的技术。简单地说,它允许外围设备以异步方式操作内存,从而减少了CPU在I/O操作中的参与。

目前的微机和PC服务器都广泛采用了 DMA 技术。由于 DMA 是一种异步操作,因此在撰写驱动时,有很多需要注意的问题。

第一类比较常见的问题是,并不是所有的 DMA 控制器或设备都有能力访问全部物理内存,或对访问有限制。例如,许多低端存储设备和网卡往往只能访问物理内存的前4GB,甚至更小的范围。而另一些设备可能只能同时访问同一段整4GB内存,例如,它可能只能访问物理地址为 0~4G-1,或4G~8G-1的内存,而不能同时访问两部分内存。还有一种比较常见的限制是设备只能按照整数次方幂边界来访问内存,例如它可能要求映射长度为4K的DMA起始地址为4K的整数倍,等等。

这一类问题比较隐蔽。如果驱动程序没有考虑这些问题,导致的结果往往是在运行一段时间之后突然发现数据损坏等问题。如果硬件手册中没有特别指出硬件的限制,我们往往可以通过设计一些特别的用例来强制系统映射位于某些可能导致问题的边缘内存来发现这类问题。

针对这些限制,操作系统往往提供了一些绕过限制的方法,例如比较常见的bouncing page,即在较低的物理内存地址进行DMA映射,然后由OS在DMA完成之后将这些数据复制到其他地方。这些方法都会导致性能下降,因此,对于希望承担高性能任务的系统而言,应尽量避免使用这样的硬件。

第二类比较常见的问题是,驱动程序(CPU)在不适当的时候读写内存。许多设备都支持主动发起DMA(Bus Mastering),这种时候,CPU可能没有办法知道设备是不是正在写内存。解决这种问题的方法是引入内存栅(Memory Barrier),即驱动程序在读写内存前后通知硬件自己将要执行的操作,并由硬件来确保相应的结果。例如,网卡驱动在读写映射环或映射链的时候,应在读前以及写前后分别进行内存栅操作,确保自己没有读到过期数据,并确保设备没有读到过期数据。

这一类问题也非常隐蔽。对于负载不高的情形,很可能驱动程序的开发者不会注意到任何问题。甚至,对于负载较高的情形,这类问题会表现为响应慢而不是不稳定或数据损坏。

C程序设计中,内存操作相关的错误可以说是最常见,同时也是非常隐蔽的一类错误。这类错误往往导致程序莫名其妙地崩溃、耗尽系统资源,或是形成严重的安全弱点。

FreeBSD,以及多数其他 BSD 派生的系统中,重复 free() 在默认情况下都会导致 C 函数库调用 abort() 终止程序。除了 malloc(3) 函数族本身的设计之外,这也是一项非常重要的安全特性。与此相反,包括 *BSD 在内的多数系统的 C 函数库并不对堆进行审计,也就是说,从 API 设计者的观点来看,内存泄漏并不被认为是非常严重的程序设计问题。

为什么会有这样的区别呢?事实上,内存泄漏同样可以导致比较严重的问题,例如响应速度变慢、进程由于占用的资源太多而被 OS 杀掉导致 DoS 等等。为了回答这个问题,我们来观察一下两种问题出现的场景。

内存泄漏 是指这样一种场景:程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针)。从 OS 的角度,它知道进程持有的内存数量;然而,从进程的角度,它可能并不完全知道自己持有哪些内存。换言之,内存泄漏就是通过遍历进程内所有可以从栈上,或以静态变量形式存于堆上的指针及其后继,无法到达所有全部已分配内存的情形。

如果程序不存在其他问题(例如缓冲区溢出),此时程序访问内存时,任何时候都不会在无意中覆写超出范围的数据。即,将数据覆写到程序其他部分保存数据的内存单元。

重复释放 则指这样一种场景:程序分配一块内存之后,经过使用将这块内存释放,但并没有将指向这块内存的所有指针抹零或回收,并在其他部分再次将指向同一块内存单元的指针交给内存分配器去进行释放操作。这种情况下,我们可以断言:

  • 程序逻辑并不清楚这块内存已经被释放;并且,
  • 有理由相信,对这块内存进行的写操作,可能已经影响了程序其他部分的行为,因为这块内存可能已经分配作为其他用途。

因此,这应被看作立即停止程序运行的一项致命错误,因为程序行为已经出现了异常,而C函数库拥有的信息不足以纠正这种异常行为,而另一方面,程序可能已经发生了堆缓冲区溢出。

为了削弱这类问题带来的实质性安全影响,现代的内存分配器往往会将尺寸接近的内存块放在一起(这样做还能够抑制内存碎片的产生,并提高CPU的数据缓存命中率,因为通常程序会倾向于一次性地访问相近的内存结构),从而能够在一定程度上减轻由于向已经释放的内存块继续写数据导致的损害(因为这些内存很可能被分配给同样的数据结构,这类写操作的危害往往会低于向其他类型的数据结构写数据,特别是当这些数据中包含一部分用户输入的时候)。

当然,彻底消除这类问题,需要为程序设计语言增加一些新的基础设施(例如强类型、托管内存等)。现代程序设计语言如Java、Python和.net系列等,都采用了避免这类问题的措施。然而,也正因为如此,通过这些语言入门并准备撰写 C 程序的开发人员就更需要注意这类问题。

NULL指针引用和内核bug的利用

| No Comments

总算是发公告了,可以说具体的事情了。

FreeBSD昨天发布了2项安全公告和1项Errata Notice:SA-09:13.pipeSA-09:14.devfsEN-09:05.null。两个安全公告修正的是同一类问题,也就是我们常说的多线程程序中的竞态条件(Race Condition);EN-09:05.null则是增加了一个使这类问题不再那么容易被利用达到特权提升目的的功能,但默认并不启用。

在 C 程序中,NULL 指针是一项很有用的特性。NULL有很多功能,例如:

  • 表示链表结束,或指针没有引用数据对象。因为NULL是0,因此判断一个指针是否是NULL要比判断一个指针是否指向某一个特定的对象要高效。
  • 作为调试工具:引用NULL会立即触发缺页(Page Fault),通常系统会将用户进程虚拟地址空间中地址为0开始的至少一页不予映射,因此这次缺页将让OS直接向进程发出SIGSEGV,这可以让程序尽早崩溃,以帮助开发人员找到问题。(映射一整页能够让访问 NULL 和 NULL 附近的内存时都产生缺页,例如指向结构 struct mystruct 的指针 struct mystruct *p,在访问 p->m_member 的时候,实际访问的内存地址是 offsetof(struct mystruct, m_member) 而未必是 0。

等等。与用户态程序类似,内核也使用 NULL 完成相同的目的。当内核自己引用空指针内存(NULL deferencing)时,x86/amd64硬件会产生异常12。

不过,传统上 Unix 系统并不明确地禁止用户在虚拟地址0上映射自己的内存页。因为对于程序来说,它也完全可以把地址 0 看作一个再普通不过的内存地址。另一方面,有些应用程序,特别是模拟器一类的程序,很可能会需要利用这一特性来完成一些功能,因为如果靠截取 SIGSEGV 并进行特殊处理的话,效率会很差。

事实上,用户程序能够在地址 0 上映射内存页这件事本身并不是一个问题。真正的问题在于,为了改善性能,许多操作系统(如果不是全部支持 x86 的通用操作系统的话)在内存分配方面会采用一个小技巧,将内核内存和用户内存映射在同一个虚拟地址空间,从而避免在执行系统调用进入和退出内核时进行完整的地址空间切换,因为这个操作在x86硬件上的代价是比较大的,而另一方面,由于同样的原因,这个技巧也会改善内核访问用户地址空间中内存的性能。

然而,这一切能够相安无事的前提是,内核没有bug,或者用户态运行的程序是"友善"的。由于欠锁等原因,内核可能会引用 NULL 指针(例如,在没有获取对象引用的情况下调用对象中的函数指针,而此时另一线程已经将对象的内存释放并进行了抹零操作)。这种竞态条件通常会导致系统崩溃,但如果用户能够控制虚拟地址为0的那一小块内存,则这类竞态条件就有可能被利用来执行用户所指定的代码,进而导致特权提升问题。

从 FreeBSD 8.0-RELEASE 开始,FreeBSD将默认禁止用户程序映射虚拟地址为0的内存页(返回EINVAL,个人认为应返回EPERM)。修正日期之后的 FreeBSD 其他版本如 6.x 和 7.x,可以通过将 security.bsd.map_at_zero="0" 增加到 /boot/loader.conf 来启用这一特性。

About this Archive

This page is an archive of entries from October 2009 listed from newest to oldest.

September 2009 is the previous archive.

November 2009 is the next archive.

Find recent content on the main index or look in the archives to find all content.

Pages

OpenID accepted here Learn more about OpenID
Powered by Movable Type 5.01