选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……
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),即驱动程序在读写内存前后通知硬件自己将要执行的操作,并由硬件来确保相应的结果。例如,网卡驱动在读写映射环或映射链的时候,应在读前以及写前后分别进行内存栅操作,确保自己没有读到过期数据,并确保设备没有读到过期数据。
这一类问题也非常隐蔽。对于负载不高的情形,很可能驱动程序的开发者不会注意到任何问题。甚至,对于负载较高的情形,这类问题会表现为响应慢而不是不稳定或数据损坏。
Read more...C程序设计中,内存操作相关的错误可以说是最常见,同时也是非常隐蔽的一类错误。这类错误往往导致程序莫名其妙地崩溃、耗尽系统资源,或是形成严重的安全弱点。
在 FreeBSD,以及多数其他 BSD 派生的系统中,重复 free() 在默认情况下都会导致 C 函数库调用 abort() 终止程序。除了 malloc(3) 函数族本身的设计之外,这也是一项非常重要的安全特性。与此相反,包括 *BSD 在内的多数系统的 C 函数库并不对堆进行审计,也就是说,从 API 设计者的观点来看,内存泄漏并不被认为是非常严重的程序设计问题。
为什么会有这样的区别呢?事实上,内存泄漏同样可以导致比较严重的问题,例如响应速度变慢、进程由于占用的资源太多而被 OS 杀掉导致 DoS 等等。为了回答这个问题,我们来观察一下两种问题出现的场景。
内存泄漏 是指这样一种场景:程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针)。从 OS 的角度,它知道进程持有的内存数量;然而,从进程的角度,它可能并不完全知道自己持有哪些内存。换言之,内存泄漏就是通过遍历进程内所有可以从栈上,或以静态变量形式存于堆上的指针及其后继,无法到达所有全部已分配内存的情形。
如果程序不存在其他问题(例如缓冲区溢出),此时程序访问内存时,任何时候都不会在无意中覆写超出范围的数据。即,将数据覆写到程序其他部分保存数据的内存单元。
而 重复释放 则指这样一种场景:程序分配一块内存之后,经过使用将这块内存释放,但并没有将指向这块内存的所有指针抹零或回收,并在其他部分再次将指向同一块内存单元的指针交给内存分配器去进行释放操作。这种情况下,我们可以断言:
因此,这应被看作立即停止程序运行的一项致命错误,因为程序行为已经出现了异常,而C函数库拥有的信息不足以纠正这种异常行为,而另一方面,程序可能已经发生了堆缓冲区溢出。
为了削弱这类问题带来的实质性安全影响,现代的内存分配器往往会将尺寸接近的内存块放在一起(这样做还能够抑制内存碎片的产生,并提高CPU的数据缓存命中率,因为通常程序会倾向于一次性地访问相近的内存结构),从而能够在一定程度上减轻由于向已经释放的内存块继续写数据导致的损害(因为这些内存很可能被分配给同样的数据结构,这类写操作的危害往往会低于向其他类型的数据结构写数据,特别是当这些数据中包含一部分用户输入的时候)。
当然,彻底消除这类问题,需要为程序设计语言增加一些新的基础设施(例如强类型、托管内存等)。现代程序设计语言如Java、Python和.net系列等,都采用了避免这类问题的措施。然而,也正因为如此,通过这些语言入门并准备撰写 C 程序的开发人员就更需要注意这类问题。
Read more...总算是发公告了,可以说具体的事情了。
FreeBSD昨天发布了2项安全公告和1项Errata Notice:SA-09:13.pipe、SA-09:14.devfs和EN-09:05.null。两个安全公告修正的是同一类问题,也就是我们常说的多线程程序中的竞态条件(Race Condition);EN-09:05.null则是增加了一个使这类问题不再那么容易被利用达到特权提升目的的功能,但默认并不启用。
在 C 程序中,NULL 指针是一项很有用的特性。NULL有很多功能,例如:
等等。与用户态程序类似,内核也使用 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 来启用这一特性。
Read more...不知道为什么我总是觉得PIXAR的电影都是主旋律电影,或者说很大程度上都在宣传美国的价值观。
电影开头的部分其实是一段悲剧—-老人和妻子从幼年相识直到暮年,希望通过存钱来完成他们周游南美洲的梦想,但因为一次次意外而不得不放弃,到最后老太太快要去世的时候,老爷子用他们童年时候一样的蓝色气球—-老人后来成了一个钉子户,因为房子是他和妻子一生的回忆。
一个胖胖的小男孩出现了,老人不想受到打扰而编了一个谎言把小孩骗走,此时建筑工人出现,在争执中老人将对方打伤,法院判决让老人住进老人院,离开房子。老人又找到了年轻时候的那本探险日志,一夜未眠,第二天老人院工作人员来接老人时,他终于放起了一大片的气球,将房子拔地而起,开始了旅行。
影片余下的部分主要是探险的经历,这里就不剧透了。影片的配乐非常不错。
Read more...蛇头GG发来的一篇文章,值得一读:The Difference Between Art and Design。摘录一下标题:
先把想法记下来。
解决的问题:
dawnh在之前的人肉traceback中提到了 另一种DSR结构。即:
(此VLAN划分是出于性能方面的考虑,不划分VLAN并不会导致整个系统不能用)。
这种做法保持了在外网网卡上直接绑一个未用公网IP之后,不需要绑定路由器MAC地址的优点,而在这种配置中,服务器直接连接网络的网卡上都不需要绑定公网IP地址。缺点是需要有一个能够在VLAN-Outgoing到Internet之间进行路由的路由设备,或能够直接控制到Internet那一跳的路由器(当然,这种情况,路由器本身甚至也可以不占用同网段IP,即,VLAN-Internet与VLAN-Outgoing合并、负载平衡设备公网网口绑VLAN-Outgoing上的内网IP,在路由器上配置指定的Internet地址段路由到VLAN-Outgoing网口)。
Read more...FreeBSD先前的vesa framebuffer驱动有个问题,就是滚屏的时候会比较慢。jkim大长辈于是改了一行代码。
好吧,我承认我之前一直以为是console驱动想要锁&Giant的问题。其实真正的原因是默认的pmap_mapdev并不做写合并,所以应该呼叫更低阶的pmap_mapdev_attr并传入PAT_WRITE_COMBINING参数。
现在VESA console的滚屏几乎和文本模式一样了。活到老学到老吧……
Read more...因为有人在我前一篇blog《使用DSR模式实现单IP服务冗余》里提了个问题,这里解释一下。
DSR比较常见的两种配置,一种是我之前文章中提到的禁止外网网卡ARP的方法,另一种是把虚拟IP绑定到lo0上。这两种方法各自有一些优缺点。一般来说,我喜欢用前一种方法。
实现DSR结构的关键是,通往Internet路由器的那个网络上,只有负载平衡设备在网络上宣示虚拟出来的那个IP的MAC地址,这样,当请求进来的时候,数据会发到负载平衡设备,而不是某一台服务器上。
禁止某块网卡上的ARP的时候,由于这块网卡完全忽略ARP(不仅是禁止发出ARP请求,同一广播域上的ARP广播也会忽略)。禁止网卡上的ARP,但将虚拟IP绑在网卡上的语义是这样:
而另一种方法,也就是网卡上绑与虚拟IP同网段的另一个IP(服务器之间不冲突),但将虚拟IP绑在lo0上的做法的语义则是这样:
第一种做法的优点是无需在与虚拟IP同网段的网络上再占用一个IP(第二种方法需要:这个IP必须和虚拟IP是同一个网段上的,以便让系统知道通往路由器的网卡是这一块)。很明显,DSR的时候你不会希望使用 RFC 1918 规定的的私有IP,除非这个服务是针对内部网络的。
第二种做法的优点是无需在服务器上维护路由器IP与MAC地址的对应表。这个特性可以减少维护的压力。例如,如果网络上只有一个路由器,而没有做VRRP或CARP的时候,在做路由器割接时,由于路由器的MAC地址变动,第一种做法会需要一次性修改全部服务器上的MAC绑定。当然,如果网络上的路由器是冗余的,这个优势可能就不那么明显了;另一方面,IPv4地址已经快用完了,所以,为了世界和平,还是建议大家用第一种做法。lo上绑定的IP地址掩码最好是/32(仅此IP)。
当然,两种做法,*nix系统都是支持的。FreeBSD支持,Linux也支持。
Read more...今天在 Matrix67 的 blog 上看到这么一道题:
一个人有两个小孩儿,其中有一个生于星期二的男孩儿。问另一个是男孩儿的概率是多少?
答案当然不是1/2或者1/4,因为我们已经知道了有一个是生于星期二的男孩,因此这是一个条件概率问题。
Read more...