阿里内核月报2014年3月
目前Linux内核急需的一项功能是在线打补丁的特性。此前被Oracle收购的ksplice一度是Linux上唯一的解决方案。但是在被Oracle收购后,ksplice就闭源了,并且成为了Oracle Linux的一项商业特性。而目前可以拿到的最新版本的ksplice仍然仅仅停留在0.19上,而可以直接使用的内核版本则是2.6.26。对于更新的内核版本则还无法使用。
在目前的实际生产环境中,不论是企业应用还是互联网公司,对于内核bug的修复仍然停留在原始的安排停机,升级内核,重启服务的方式上。这一方式带来的后果就是停机时造成的服务中断。而通过主备切换方式进行停机也有可能造成部分数据的损失和服务的短时间中断。总之,只要你需要停机,就存在服务中断的风险。而这对于企业用户、互联网公司来说属于不能接受或应该尽量避免的。想必是看到了这一点,Redhat和SUSE两家公司分别启动了一个关于内核热打补丁的项目:kpatch和kGraft。
根据目前看到的文献资料,这两个项目解决问题的方案是十分接近的,即通过Linux内核已有的ftrace机制来对有问题的函数进行替换。目前ftrace通过在函数调用过程中插入断点(INT 3)的方式来实现相应的功能。而kpatch和kGraft则将这一断点替换为一个长跳转,从而将有问题的函数替换为新的功能正常的函数。在实现这一功能的过程中,这两个项目的区别是替换时如何解决新旧代码不一致的问题。kpatch的解决方法是通过stop_machine()加以解决的;而kGraft则是通过类似RCU的方式来更新旧代码。此外的一个区别是生成带有补丁的内核模块的方式。kpatch是基于patch的方式来生成内核模块的;而kGraft则可以自动通过源代码来生成带有新补丁的内核模块。
目前这两个项目均可以在网上获取到源代码。kpatch,kGraft。有兴趣的读者可以一探究竟。
Finding the proper scope of a file collapse operation
系统调用的设计绝不简单。当开发者们设计一个接口的时候,经常有一些边角的情况没有被考虑到。涉及文件系统的系统调用似乎 特别容易出现这种问题,因为文件系统实现的复杂性和多样性意味着当一个开发者想创建一个新的文件操作的时候,他将不得不考 虑许多特例。在fallocate()系统调用的推荐添加的讨论中能看到一些这样的特例。
fallocate()是关于一个文件内空间分配的系统调用。它的初始目的是允许一个应用在写这些文件之前给它们分配空间块。这种预 分配确保了在试着往那写数据之前已经有了可用的磁盘空间;它也有助于文件系统实现在磁盘上更有效地分布被分配的空间。后来 添加了FALLOC_FL_PUNCH_HOLE文件操作,用来去分配文件里的块空间,留一个文件空洞。
在二月份,Namjae Jeon推荐了一个新的fallocate()操作FALLOC_FL_COLLAPSE_RANGE,这个草案包括了针对于ext4和xfs文 件系统的实现。它像打洞操作一样删除了文件数据,但又有一个不同,它没有留一个洞在文件里,而是移动文件中受影响区间之后 的所有数据到该区间的开始位置,整体上缩短文件大小。这种操作的直接用户将是视频编辑应用,它能使用这种操作快速而有效地 视频文件中的一段内容。假如被删除的范围是块对齐(这对于一些文件系统是个必须的前提条件),这种删除可能受改变文件区间 映射图所影响,不需要任何实际的数据复制。考虑到那些包含视频数据的文件可能是大的,我们不难理解一个有效的裁剪操作为什 么是有吸引力的。
因此对于这种操作将会出现什么样的问题呢?首先一个问题将可能是与mmap()系统调用的交互,这种操作将映射一个文件到一个进 程的地址空间。被推荐的草案是通过从page cache中删除从被受影响区间到文件结尾的所有页面来实现的。脏页首先被写回磁盘。 这种方法阻止了那些已经通过映射被写入的数据的丢失,而且也有利于节省了那些一旦操作完成而超出了文件结尾的内存页面。这 应该不是个大问题,正如Dave Chinner指出的,使用这种操作的应用通常不会访问被映射过的文件。除此之外,受这种操作文件 影响的应用同样不能处理没有这种操作的其他修改。
但是,正如Hugh Dickins提醒的,tmpfs文件系统会有相关的问题,所有的文件存在在page cache,看起来非常像是一个内存映 射。既然page cache是个后备存储,从page cache里删除所有的文件不可能会有个好的结果。所以在tmpfs能支持collapse操作 之前,还有许多与page cache相关的工作需要做。Hugh不确定tmpfs是否需要这种操作,但他认为为tmpfs解决page cache问题 同样很可能能为其他文件系统带来更稳健的实现。
Hugh也想知道单向的collapse操作是否应该被设计成双向的。
Andrew Morton更进一步,建议一个简单的“从一个地方移动一些块到另外一个地方”的系统调用可能是个更好的想法。但Dave不 太看好这个建议,他担心可能会引入更多地复杂性和困难的极端情况。
Andrew不同意,他说更通用的接口更受欢迎,问题能被克服;但没有其他人支持他这种观点。因此,机会是,这种操作将局限于那 些超出文件的崩溃的块组;也许以后添加一个单独的插入操作,应该是个有意思的用例。
同时,有一个其他的行为上的问题要解答;假如从文件中删除的区间到达了文件结尾将会发生什么?对于这种场景,当前的补丁集 返回EINVAL,一个想法是建议调用truncate()。Ted Ts'o问这种操作是否应该之间变成truncate()调用,但Dave是反对这种想 法的,他说一个包含了文件结尾的崩溃操作很可能是有缺陷的,在这种情况下,最好返回一个错误。
假如允许崩溃操作包含文件结尾,很明显将也会有一些有趣的安全问题出现。文件系统能分配文件尾后的块。fallocate()肯定能 用显式地请求这种行为。文件系统基本上没有零化这些块;代替地,它们被保持不可访问以致无论包含了什么样的不稳定数据都不能 被读取。没有太多的考虑,这种允许区间超出文件结尾的崩溃操作最终会暴露那些数据,特别是假如在中间它被中断(也许被一个系 统崩溃)。Dave不认可这种为文件系统开发者设置陷阱的行为,他更喜欢从一开始杜绝这种带有风险的操作,特别是既然还没有任何 真实的需求来支持它。
因此,所有讨论的最后结果是FALLOC_FL_COLLAPSE_RANGE操作将很可能基本原封不动地进入内核。它将不会包含所有的一些开发者 希望看到的功能,而是支持一个有助于加速一类应用的特性。从长期来看,这是否是足够的依然需要时间的检验。系统调用API设计是 难的。但是,假如在将来需要一些其他的特性,能采用某种兼容的方法来创建一些新的FALLOC_FL命令。
Tracing unsigned modules
在已签名模块中复用“tainted kernel”标志会给在未签名模块中使用tracepoint造成麻烦,这个问题比较容易解决,但其实也有一些阻力,另外内核Hacker们也没有兴趣帮助Linux内核代码外的模块解决问题。
内核的模块加载机制已有近20年的历史(1995年自1.2版本引入),但直到最近才引入加密签名的验证机制:内核只加载被认可的模块。Redhat内核早就支持了这个特性。Kernel的编译者可以指定用于签名的密钥;私有密钥被保存或被丢弃,公有密钥则被编译到内核里。
有几个内核参数可以影响到模块签名:CONFIG_MODULE_SIG控制是否使能签名检查。 CONFIG_MODULE_SIG_FORCE 决定是否所有的模块都需要签名。如果CONFIG_MODULE_SIG_FORCE没有被开启(并且没有设置启动参数module.sig_enforce ),那么没有签名或签名不匹配的模块也可以被加载。此时,内核被标记为“tainted”。 如果用户使用modprobe –force去加载与内核版本不匹配的模块,内核也会设置“taint”标记。强制加载内核非常危险,由于与内核的内存布局不一致,很可能会导致内核crash掉。而且此类的Bug报告肯定会被内核开发者们忽略掉。
但是,加载一个未签名模块并不一定会导致crash,因此并不是很有必要使用“TAINT_FORCED_MODULE”。Tracepoint机制不支持被强制加载的module是因为在不匹配的模块中使用tracepoint很容易挂掉内核。Tracepoint允许TAINT_CRAP与 TAINT_OOT_MODULE,但是如果有其它任何一个“taint”标记,模块中的tracepoint是默认被关掉的。 Mathieu Desnoyers发布了一个RFC patch意图改变此现状。该patch引入了“TAINT_UNSIGNED_MODULE”用以标记是否有此类module被加载。并且允许Tracepoint支持这种“taint”类型。Ingo Molnar立即给了NAK,他不希望外部模块影响kernel的稳定。
但是,有一些发行版的kernel已经打开了签名检查,但是允许通过module.sig_enforce指定是否真的使用。发行版的密钥存在kernel image中。严格的检查只允许发行版自己的模块被加载。这严重限制了用户对模块的使用。
模块子系统的维护者Rusty Russell也没有被此项功能打动。他还在进一步寻找使用案例。
Steven Rostedt指出,如果没有模块被加载,我们为什么还要设置FORCED_MODULE标记。他也经常收到用户们发布的“在未签名模块中不能使用Tracepoint”的Bug报告。
Johannes Berg最终提供了Russell一直在寻找的使用案例。Berg还提供了使用未签名内核的另一个原因:重kernel.org的wiki上backport内核模块用以支持发行版提供者不支持的硬件驱动。
Berg的使用案例足以使Russell同意将此patch加入他的pending tree。我们也将会在3.15版本中看到Desnoyers的最终的patch版本。届时,kernel将会区分两种不同的taint,用户也能够在他们加载的模块中使用Tracepoint,无论是否签名。
Optimizing VMA caching
Linux进程的虚拟地址空间被内核分割成多个虚拟内存区域(VMA),每个VMA描述了这个地址空间的一些属性,比如存储后端,访问控制等。一个mmap()调用创建一个空间映射,这个空间在内核中就用VMA来描述,但是将一个可执行文件加载到内存,需要创建好几个VMA。我们可以通过/proc/PID/maps来查看进程的VMA列表。在内存管理子系统中,通过一个虚拟地址找到对应的VMA是一个非常频繁的操作。比如,每一次缺页中断都会触发这个操作。所以不出大家所料,这个内核操作已经是高度优化过的。可能会令一些读者意外的是这个操作还有优化提升的空间。
内核用红黑树来保存进程的VMA,红黑树可以保证很好的线性查找效率。红黑树有自己的优点,比如扩展性强,能支持大几百个VMA还有很好的查找效率。但是每一次查找都得从树顶遍历到叶子节点。为了在某些情况下避免这种开销,内核毫不意外的用了一个极简的缓存策略,保存上一次查找的结果。这种简单的缓存效果还不错,对于大多数应用基本上都有50%或以上的命中率。
但是Davidlohr Bueso同学认为内核可以做得更好。他觉得一个缓存项太少了,多一个肯定效果会好一些。在去年11月的时候,他提交了一个补丁,就是增加了一个缓存项,指向具有最大空间的VMA。背后的理论很直白,空间越大被查找的概率就越大。多了这个缓存项,一些应用的命中率确实从50%上升到60%了。这是一个不大不小的优化,但是没有进入内核主干。很大的一个原因就是Linus跳出来说,“这个补丁让我很生气。”当然,一般有品味的大神对你说这话,除了看得起你,肯定是有后话的。Linus说了一堆为什么这么做没有品味的原因,最重要的是提出了他自己认可的解决方法,而且一针见血。大意就是要优化就要视野要更宽广,目前VMA查找对于多线程应用来说是很不友好的,一个进程才一个缓存项,所以当务之急是把per-process的缓存变成per-thread的。
Davidlohr同学聆听教诲之后,奋发图强,经过几次迭代,提交了一套很有希望进入主干的补丁。该补丁主要就是把per-process的缓存项变成了per-thread的。优化效果喜人,对于单线程和多线程的应用都有帮助。比如说系统启动,主要都是单线程操作,命中率从51%提升到73%。多线程应用,比如大家都以为无法提升的内核编译,命中率从75%提升到88%。实际的多线程应用,效果可能更好。比如通过观察模拟多线程网页服务器负载的ebizzy,命中率惊人的从1%提高到了99.97%。
有了以上的测试数据,没有任何理由把这个补丁阻挡在内核的门外。大概在3.15内核,大家可以受享到这个VMA查找优化了。
Unmixing the pool
H. Peter Anvin同学在2014年的LSF会上主持了一个简短的讨论会,向大家问了一个简单的问题:你认为硬件特别是CPU的哪些改进可以简化内核内存管理的工作?虽然HPA不保证这个问题会对硬件带来真正的改进,但他说能保证把话带回英特尔。
第一个抱怨更多的是跟底层硬件联系紧密的代码:Rik van Riel提到PowerPC架构没有类似x86上TLB的页表缓存的FLUSH操作。其它的架构(比如SPARC)也有类似的限制。这使得抽象层的共用代码部分很难写。他希望硬件方面能有一些改进,让硬件无关的代码能很方便的更新页表。
对于x86平台,Peter Zijlstra希望能方便的让部分页表失效。另外一个很受期待的请求是支持64KB的页。目前只支持4KB和2MB大小的页。
Mel Gorman希望硬件提供一种对内存页快速的填零操作。目前对于2MB内存页填零是一个耗时的操作,所以这个特性可以提高内存大页的效率。有人建议可以考虑在内核空闲时做填零的操作,但是Christoph Lameter说他已经这样尝试过了,没有什么效果。
另外一个请求是提供一个效率更高更快的iret实现。这将大大提高缺页中断的效率,因为它通过iret返回到用户层。
期间有人讨论了一下处理器之间发送消息的开销,还有希望一个mwait指令让对于用户层的某些应用可能会有帮助。对于处理器未来的变化,让我们拭目以待。
MCS locks and qspinlocks
spinlock是一种简单的锁机制,当某个bit被清除时锁是available的。一个线程需要申请该锁时,通过调用原子操作compare-and-swap指令设置那个bit。如果锁不是available的,线程就会一直spinning。最近几年spinlock开始变得复杂起来,2008年加入了ticket spinlock用于实现公平性,2013年又加入了新特性用于更好的支持虚拟化。
当前spinlock还存在一个比较严重的问题就是cache-line bouncing:每次尝试获取锁的时候都需要将这个锁对应的cache line挪到local cpu上。对于锁竞争特别严重的场景下,这个问题对性能影响很大。之前有过一组patch尝试解决这个问题,但是最终没有被merge
MCS lock
Tim Chen提交了一组patch用于解决这个问题,基于一篇1992年的论文实现的"MCS lock"。其思想是将spinlock扩展为一个per-CPU的结构,能够很好的消除cache-line bouncing问题。
3.15的tip tree里面,MCS lock定义如下:
struct mcs_spinlock {
struct mcs_spinlock *next;
int locked; /* 1 if lock acquired */
};
使用的时候首先需要定义一个unlocked的MCS lock(locked位为0),这个相当于是一个单链表的tail记录头,next记录的是锁的最末尾一项。当有一个新mcs_spinlock锁加入时,新锁的next指向NULL(MCS lock的next为NULL时)或者MCS lock的next的next,MCS lock的next指向新锁。 当一个CPU1尝试拿锁的时候,需要先提供一个本地的mcs_spinlock,如果锁是available的,那么本地mcs_spinlock的locked为0,设置MCS lock的locked为1,本地mcs_spinlock的next设置为MCS lock的next,MCS lock的next设置为本地的mcs_spinlock。如果CPU2也尝试拿锁,那么也需要提供一个CPU2本地的mcs_spinlock,MCS lock记录的tail修改为CPU2的mcs_spinlock,CPU1的next设置为CPU2.
------------
| next |-------------------------
|------------| |
| 1 | |
------------ |
|
V
CPU1 ------------ CPU2 ------------
| next | -----------> | next | --> NULL
|------------| |------------|
| 0 | | 1 |
------------ ------------
当CPU1锁释放之后会修改CPU2对应的locked为0,CPU2就能拿锁成功。这样CPU2上面拿锁spinning的时候读取的都是本地的locked值,同时也能够保证按照CPU拿锁的顺序保证获取锁的顺序,实现公平性。
Qspinlock
MCS lock目前仅仅是用在实现mutex上,并没有替换之前的ticket spinlock。主要原因是ticket spinlock只需要32bit,但是MCS lock不止,由于spinlock大量嵌入在内核的数据结构中,部分类似struct page这种数据结构不能够容许size的增大。为此需要使用一些别的变通方法来实现。
目前看有可能被合并的是Peter Zijlstra提供的一组qspinlock patch。这组patch中每个CPU会分配一个包含四个mcs_spinlock的数组。需要四个mcs_spinlock的原因是由于软中断、硬中断、不可屏蔽中断导致的单个CPU上面会有多次获取同一个锁的可能。
这组patch中,qspinlock是32bit的,也就解决了之前MCS lock替换ticket spinlock导致size增大的问题。
AIM7的测试结果看部分workload有一小点的性能退化,但是其他workload有1~2%的性能提升(对底层锁的优化方面,这是一个很好的结果)。disk相关的benchmark测试性能提升甚至达到116%,这个benchmark在vfs和ext4文件系统代码路径上存在很严重的锁竞争。
Volatile ranges and MADV_FREE
目前kernel的"shrinker"接口是内存管理子系统用来通知其他子系统发生了内存紧张,并促使他们尽可能地释放一些内存,也有各种努力尝试增加一个类似的机制来允许内核通知用户态来收缩内存,但是基本都太复杂而不易合并进内存管理代码。但是大家没有停止尝试,最近就有两个相关的patch。这两个patchset都基于一个新的名词volatile range,它是指进程地址空间中的一段数据可再生的内存区域。在内存紧张时,内核可以直接回收这种区域里的内存,进程在以后需要的时候再重新生成;在内存充裕时,该区域的内存不会被回收,程序也可随时访问这些数据。这些patch的最初动机是为了替换Android上的ashmem机制,当然也可能有其他潜在用户。
Volatile ranges
之前有很多版本的volatile range实现,有的是基于posix_fadvise()系统调用,有的是基于fallocate(),也有一些基于madvise()。John Stultz的实现则增加了一个新的系统调用:
int vrange(void *start, size_t length, int mode, int *purged);
vrange()操作的范围是从start地址开始的,长度为length的空间。如果mode是VRANGE_VOLATILE,这段区域将被设置成volatile;如果mode是VRANGE_NONVOLATILE,这段区域的volatile将会被去掉,但是这个过程中有些volatile页可能已经被回收了,此时*purged会被设置成一个非0值来标识内存区域的数据已经不可用了,而如果*purged被设置了0则表示数据仍可用。如果进程继续访问volatile range的内存且访问了已经被回收的页,它就会收到SIGBUS信号来表明页已经不在了。这个版本的patch还有另外一个地方不同于其他方式:它只对匿名页有效,而以前的版本可以工作在tmpfs下。只满足匿名页使得patch很简单也更容易被review和merge,但是也有一个很大的缺点:不能工作在tmpfs下也就无法替换ashmem。目前的计划是先在基本patch实现上达成一致再考虑实现更复杂的文件接口。vrange()内部是工作在VMA层次上,挂在一个VMA上的所有页要么是volatile的要么不是,调用vrange()时可能会发生VMA的拆分和合并。因为不用遍历range里的每一个页,所以vrange()速度非常快。
MADV_FREE
另外一个实现方法是Minchan Kim的MADV_FREE patchset,他扩展了现有的madvise()调用:
int madvise(void *addr, size_t length, int advice);
madvise()的advice标识了希望内核采取的行为,MADV_DONTNEED告诉内核马上回收指定内存区域的页,而MADV_FREE则将这些页标识为延迟回收。当内核内存紧张时,这些页将会被优先回收,如果应用程序在页回收后又再次访问,内核将会返回一个新的并设置为0的页。而如果内核内存充裕时,标识为MADV_FREE的页会仍然存在,后续的访问会清掉延迟释放的标志位并正常读取原来的数据,因此应用程序不检查页的数据,就无法知道页的数据是否已经被丢弃。
MADV_FREE更倾向于在用户态内存分配器中发挥作用,当应用程序释放了一组页,分配器将会使用MADV_FREE告诉内核。如果应用程序很快在同一段地址空间又分配了内存,它就会使用原来的页,而不需要先释放原来的页在分配并清零新的页。简而言之,MADV_FREE代表"我不再关心这段地址空间里的数据,但是我可能会再次使用这段地址空间"。另外,BSD已经支持MADV_FREE,而不像vrange()只是linux的特性,这显然会提高程序的可移植性。
目前这两种方法都没有收到很多的review,但是在2014 LSFMM上会有一个相关的讨论。
SO_PEERCGROUP: which container is calling?
随着Linux上的各种Container解决方案不断成熟,主流发行版开发者们也开始行动起来了-他们开始构思一个完全由Container组成的运行时环境(还记得Docker吗?)实现这伟大计划的其中一个需求是:负责这些Containers的资源管理、生命周期管理的中控守护进程需要知道和自己通信的各个进程各属于哪个cgroup。Vivek Goyal最近提交了一个实现此功能的Patch,通过给Domain Socket添加新的命令字来让通信的双方知道对方在哪个组里。这patch代码非常简单,但仍然不出意外地引起了一场邮件列表上的大讨论。
这个Patch的大概想法是这样的:给Domain Socket的getsockopt()系统调用添加一个叫做SO_PEERCGROUP的命令字,每次打开这个socket的进程调用这个命令字,就给它返回对端所在的组 - 精确地说是连接建立时对端所在的组,因为它后边可能被移到别的组里了,对于开发者们考虑的典型场景来说,返回连接建立时的情况就已足够。
主要的反对意见来自Andy Lutomirski,他的抱怨真是多,但核心思想主要是一条:“一个位于cgroup控制组中的进程根本不应该知道它自己在组里!”而Vivek的Patch需要组内进程配合才能工作,远不仅仅是“知道”自己在某个组里。他提出了替代解决方案
- 首先,我们可以给每个cgroup组配上一个user namespace,然后就可以通过现有的SO_PEERCRED + SCM_CREDENTIALS获得socket对端的进程的uid的映射,进而间接地推断出它在哪个cgroup组里。Vivek反对这么做,主要原因是user namespace还远未成熟,Simo Sorce也表示这么做不现实,不会在近期考虑给Docker加上user namespace支持。
- 其次,Andy认为我们可以在/proc/{pid}/ns下增加接口,把各个进程的cgroup组信息输出在那儿(这听起来似乎是最简单易行的办法了),然而Simo认为这样做易用性太差,某个进程可能占用了一个pid然后死掉,然后有新进程又占用了这个pid,同一个pid已经换人了,这种情况下用户态程序很难感知到。这一类的race一直不好解决,不然我们直接添加一个/proc/{pid}/cgroup就万事大吉了。
Andy在讨论过程中也向Simo提了一个小问题:既然添加这个命令字主要是为了给Docker用,而Docker是同时使用了namespace和cgroup两种机制的,那么是不是说Docker可以跨network namespace使用domain socket了?比如,在根组里建立一个叫/mysocket的domain socket,再把它bind mount到Container里边的同名路径下,就可以跨container通信了,这样做现在可以吗?*应该*可以吗?这个问题在工作中也曾困扰过淘宝内核组,留给亲爱的读者们做思考题好了 :p
总之,这个线索讨论到最后谁也没有说服谁,我们相信随着Unix标准中不存在的新特性不断进入Linux内核,这样的讨论还会越来越多。内核开发者们不禁开始思考这样一个事情:当我们向内核加入一个新特性的时候,到底应该在多大范围内劝说用户使用它们?到底应该在多大范围内和我们已有的特性结合在一起?毕竟如果我们加入一个新特性,又非常不相信它,以至于我们自己都不愿意使用,那真是非常奇怪的事情。
User-space out-of-memory handling
大家应该对Linux上的OOM killer不陌生,当使用了过多内存且swap都被用完后经常就会发生。Linux kernel发现无法回收上来任何内存后它就会选择kill掉一个进程,而这可能是运行几年之久的web浏览器,媒体播放器或者X window环境,导致一下子丢失了很多工作。有时候这种行为可能是有用的,比如杀掉了一个正在内存泄漏的程序,使得其他程序可以继续正常运行,但是大多数时候它都会在没有任何通知的情况下让你丢失重要的东西。Google的David Rientjes最近提出了一套patchset使得即将OOM时可以给程序发送通知并自己采取行动,比如选择牺牲哪个进程,检查是否发生了内存泄漏或者保存一些信息用于debug。目前的OOM策略是找到并杀死使用内存最多的进程,我们可以改变/proc//oom_score_adj的值对使用的内存量进行加权并影响OOM的决策,但是也仅限于此,我们无法把自己实现OOM kill的行为告知内核,例如我们不能选择杀死一个新生成的程序而不是跑了一年的web服务器,也不能选择一个优先级最低的程序。
目前有两种类型的OOM行为:全局OOM-整个系统内存紧张时发生和memcg OOM-memcg内存使用量达到限制,用户态oom机制可以同时处理这两种类型的OOM。
memcg 简介
Memcg是使一组进程跑在一个cgroup里并对总的内存使用量整体监控,可以用它来防止使用的总内存超过某一限制,因此提供了一个有效的内存隔离机制。当memcg的使用量超过限制且无法回收时,memcg就会触发OOM,这其中分为两个阶段:页分配阶段即页分配器分配空闲内存页的过程,和记账阶段即memcg统计内存使用量的过程。如果页分配阶段失败,这意味着系统整体内存紧张,如果页分配成功但记账失败,就说明memcg内部内存紧张。如果要使用memcg,首先要确认内核编译了该功能,确认方法如下:
$ grep CONFIG_MEMCG /boot/config-$(uname -r)
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
# CONFIG_MEMCG_SWAP_ENABLED is not set
# CONFIG_MEMCG_KMEM is not set
可以看出内核开启了memcg,接下来确认是否挂载:
$ grep memory /proc/mounts
cgroup /sys/fs/cgroup/memory cgroup rw,memory 0 0
也已经挂载了。如果没有,可通过如下命令挂载:
mount -t cgroup none /sys/fs/cgroup/memory -o memory
根挂载点本身就是root memcg,它控制系统上的所有进程,可以通过mkdir来创建子memcg。每一个memcg都有以下四个控制文件:
cgroup.procs or tasks: memcg里的进程pid列表
memory.limit_in_bytes: memcg可使用的内存总量,单位为字节
memory.usage_in_bytes: memcg当前使用的内存量,单位为字节
memory.oom_control: 当memcg内存紧张时,允许进程注册eventfd()并获取通知
David Rientjes增加了另外一个控制文件:
memory.oom_reserve_in_bytes: 可以被正等待OOM通知的进程继续使用的内存量,单位为字节
memcg OOM处理
当memcg内存使用达到限制且内核无法从它以及它的子组中回收任何内存时,基本上OOM就要发生了。默认情况下内核将会在它的进程(或它的子组里的进程)中选择一个使用内存最多的并kill掉,但是我们也可以关掉memcg OOM:
echo 1 > memory.oom_control
但是关掉它以后,所有申请内存的进程都可能会处于死锁状态直到有内存被释放。这个行为看起来没有什么作用,但是如果用户态注册了memcg OOM通知机制,情况就变了。进程可以通过eventfd()来注册通知:
1. 以读方式打开memory.oom_control
2. 通过eventfd(0, 0)创建通知用的文件描述符
3. 将"<fd of open()> <fd of eventfd()>"写入cgroup.event_control
之后进程采取:
uint64_t ret;
read(<fd of eventfd()>, &ret, sizeof(ret));
read()调用将一直被阻塞直到要发生memcg OOM的时候,这时相关进程将会被唤醒而不需要一直去主动查询内存状态。
但是即使进程被唤醒并知道了即将发生OOM,它也什么都做不了,因为基本所有可能的操作都会分配内存。如果该进程是个shell,因为无法分配内存运行ps,ls或者cat tasks都会被阻塞住。所以一个明显的问题是:即使用户态想kill掉一个程序,但是它却无法获取所有的进程列表,这是不是很无奈? 用户态OOM处理机制的目的就是把责任转移到用户态,并允许用户决定后续的操作,而这只有在内存预留后才有可能。他增加的memory.oom_reserve_in_bytes接口就是可以被进程在OOM时超额使用的内存,但是该进程只能是注册了OOM通知的进程。例如:
echo 32M > memory.oom_reserve_in_bytes
那么向memory.oom_control注册了eventfd()的memcg进程就可以超额使用32M内存,这可以允许用户做一些有意义的事:读文件,检查内存使用情况,保存memcg进程列表等。如果可以通过其他方式释放一些内存,用户态OOM处理也不一定要kill掉一个进程。它也可以只是保存一些当前信息例如memcg内存状态,进程使用内存等以便之后debug,并重新开启memory.oom_control来打开OOM killer。当有内存预留时,写memory.oom_control就可以成功。
系统OOM处理
如果整个系统内存紧张,那么就没有预留内存可供memcg使用,包括root memcg也无法允许进程分配内存,因为内核在页分配阶段就会失败而不是记账阶段。对于这种情况我们需要另外一种预留机制:内存管理系统需要单独为用户态OOM处理机制预留一部分内存,使得它可以在OOM时继续分配。目前已经有了per-zone的预留:sysctl min_free_kbytes可以为重要的申请预留一部分内存,例如需要回收的进程或者正在退出的进程。用户态OOM处理的预留内存是min_free_kbytes预留的一个子集。
如果内核决定自己kill掉一个进程,那这些预留也没什么意义。默认下系统全局的OOM killer不能被关闭,而这组patchset允许我们像关闭memcg OOM一样关闭全局OOM,这是通过root memcg的memory.oom_control接口实现的。他增加了一个flag PF_OOM_HANDLER来允许等待OOM通知的进程可以使用预留内存,如果进程属于根组,那么内核允许它在per-zone的min水位之下继续分配。这个flag使用在页分配器的slow path中,也不会产生什么性能影响,对于无关进程就只是多了一条判断而已。这个设计的另一个重要方面是统一了全局OOM和memcg OOM处理的接口,用户态OOM处理也不需要根据它是属于根组还是子组而改变使用方法。
当前状态
2014 LSFMM上有个slot讨论了用户态OOM处理的进展,也有几个问题。Sasha Levin说为啥不用现成的vmpressure机制,当内存开始回收的时候就获得通知而不是等到要OOM时才期待用户态处理,David说vmpressure机制跟这个不一样,我们无法知道进程消耗内存的速度有多快,系统可能快速从"内存充裕"状态迅速变成OOM状态,使得用户态根本没时间反映。另外一个焦点在于用户态OOM机制对全局OOM的处理,Michal Hocko认为改变全局OOM的处理可能从长期看很难维护,我们应该想办法优化现有的OOM机制而不是直接替掉它。但是Google的一个工程师Tim Hockin说改变或升级内核是一个很缓慢的过程,而且很多OOM策略放在内核态也不太合适,所以他们希望能把主动权交给用户态,让用户态程序决定OOM策略而不用更换内核。另外一个大家觉得有争议的地方是在memcg的框架下改变全局OOM的处理,这个机制只能在cgroup被编译进内核时才能使用,但是目前仍有很多用户不cgroup。Peter Zijlstra建议是否可以在/proc下为全局OOM再建一个接口,这样就可以不用依赖memcg。
目前社区对这一套patchset还是不太买帐,对之后的开发方向也没有一致的结论,短期内也应该不会被合并。
PostgreSQL pain points
Linux内核必须让所有的workload都最大限度的优化,但是实际上却并非如此(这也是阿里内核组存在的意义,呵呵),PostgreSQL就是这么一个例子。在今年的LSF上PostgreSQL的开发者Robert Haas, Andres Freund和Josh Berkus给内核开发者分享了一下他们的痛点。PostgreSQL在1996年发起,并运行在很多不同的操作系统上,所以他们只能使用进程,并用System V共享内存来做进程通信。另外PostgreSQL保存着自己的buffer cache,同时也用操作系统的buffer I/O来做磁盘数据的读写。这种使用方式给PostgreSQL的用户造成了很大的麻烦。
Slow sync
PostgreSQL遇到的第一个问题是从buffer cache刷数据到磁盘。PostgreSQL使用自己的journal模式Write-ahead logging,任何修改先写日志,当日志落盘以后,数据库的修改才会落盘。这些逻辑都是由checkpoint进程来实现,它先写日志再刷盘。日志的写入量很小并且都是顺序写入,所以性能不错,太慢现在很满意。问题主要是出在第二步:数据库修改的落盘。checkpoint进程会尽量安排分阶段写入以防止阻塞整个IO,但是一旦它调用fsync,就会导致所有的写入被推到设备的请求队列,由于它一次写入了太多的数据,导致其他进程的读请求也被block住了。
Ted Ts'o问了一个问题:如果我们限制住checkpoint进程所能占据的磁盘带宽是否有意义,Robert反馈说这样不行,因为当没有竞争时checkpoint进程还是希望利用整个磁盘带宽。又有人提议用ionice(这个目前只能用在CFQ上),但是这个对fsync()发起的I/O不起作用。Ric Wheeler建议PostgreSQL的开发者应该自己控制好写入数据的速度,Chris Mason补充说O_DATASYNC应该会有用,但这里的问题是需要用户自己知道磁盘的能力... 于是乎讨论又回到了I/O优先级。可惜很悲剧,如果用户已经一下子发了大量的数据到设备层并占满了设备队列,现在即使进程有很高的I/O优先级也没戏,因为队列已经满了。现在似乎没啥好办法了...
由于没啥结论,囧,最后Ted问PostgreSQL的开发者是否有一个很容易复现的程序,这样内核开发者可以自己来测试不同解决方案的效果。
Double buffering
PostgreSQL既需要控制自己的buffer也需要用buffer I/O。 这会导致一个明显的问题:数据库的某些数据会在内存里面存两份:一份子PostgreSQL的buffer里面,一份子操作系统的page cache里。这会增加PostgreSQL的内存使用,并降低了系统性能。大部分的重复都是可以被做掉的,比如说在PostgreSQL里面被修改过的buffer肯定会比内核里面page cache的内容新,这些page cache后面会在PostgreSQL刷buffer的时候被更新。这样保存这部分page cache就没啥意思了,应该用fadvise(参数是FADV_DONTNEED)来通知内核移除这些内存。Andres提到不知道什么原因,这么做反而会导致一些读的I/O,这个明显是内核的一个BUG。madvise他们也用不了,因为先要把文件map上来太麻烦了,速度也不行。当然,这个问题也能反过来解:把内核的page cache留着,而把PostgreSQL里面没修改的buffer释放掉,这可能通过一个系统调用或者某种特殊的write操作来实现,但是这个没有达成最终的结论。
Regressions
PostgreSQL用户遇到的另一个问题是新内核经常导致性能问题。比如透明大页,这个不但没给PostgreSQL带来帮助,反而显着得降低了性能,囧。另外似乎内存compaction的算法虽然花费了很长时间,但是却没有生成很多大页,最终把透明大页的支持关闭掉以后整个世界清静了。Mel Gorman指出如果内存compaction影响了性能,那就应该是个bug。而且他自己本人已经很久没有看到关于透明大页的bug汇报了,不过他还有一个patch可以限制compaction使用的cpu时间,这个patch目前没有被合并,因为没人发现compaction有问题。不过既然现在PostgreSQL发现了问题,可能后面需要重新考虑合并这个patch。
另一个大问题是"zone reclaim"这个特性:kernel在其他zone里面有内存的时候仍然仅仅在这个zone里面回收内存页。zone reclaim降低了PostgreSQL的性能,所以已经被默认关闭。Andres同时提到他经常由于这个事情去帮别人诊断系统,这个对他的收益有帮助,不过最好还是希望内核把这个解决掉。Mel解释说当时加入zone reclaim的假设是所有的程序都能在一个zone里面,这个假设目前看没啥道理,所以最好把这个关掉。
最后,PostgreSQL开发者指出内核的升级很恐怖,每个内核版本的升级所导致的性能差距没法预期...有人提议说能否找出一个PostgreSQL的benchmark来测试新内核,但是这个最终没达成一致。总得来说,内核开发者和PostgreSQL开发者对这次会晤都很满意。
Facebook and the kernel
今年的LSF summit btrfs的开发者Chris Mason阐述了Facebook是如何使用Linux内核的。他和大家分享了一些facebook内核的数字以及facebook的一些痛点。这些痛点有些和PostgreSQL开发人员遇到的类似,其实也有好些和阿里巴巴也类似,可见大家都是一样的苦逼呀!
facebook的架构大概是这样的:前面有一个web接入层,主要是CPU和网络是瓶颈,用来处理用户请求。后面是一个memcached层来cache MySQL的查询结果。再后面是一个存储层,主要包括MySQL, Hadoop, RocksDB等。 在facebook任何人可以查阅任何代码,facebook.com一天更新代码两次,所以新的代码可以很容易被用户用到。代码变化如此之快,所以如果有性能或者其他的问题,内核开发人员可以通知其他开发人员"你错了",并给出可能的解决方案。
Facebook内部有很多个内核版本,最多的是基于2.6.38的内核,另外一些运行3.2 stable+250patches的版本,还有一些运行着3.10 stable+60patches的版本。这些patches大部分都集中在网络和跟踪子系统,还有少量的内存管理方面的patches。让Mson很惊讶的是Facebook生产环境对错误的容忍性很高, 他举了一个3.10的pipe bug,这个是一个小race,但是Facebook每天会遇到500次,但是用户却根本感知不到。
Pain points
Mason曾经在Facebook内部问了很多人他们认为内核最严重的问题是什么,最终有两个候选答案:stable pages和CFQ。不过btrfs也有stable pages的支持(囧),而且Facebook现在还有Jens Axboe,CFQ的开发者,呵呵!另外一个问题是buffer I/O的延迟,特别是对那些只会追加写的数据库文件。很多时候这些写入很快,但是有时候却非常慢,如果内核能解决这个问题就完美了。他还提到了把内核的spinlock移植到用户态。Rik van Riel提到有可能POSIX locks可以使用自适应的lock,先自旋一阵子再进入睡眠。memcached层有一个用户态spinlock的实现,但是Mason认为这个实现和内核比起来还是太简单了。
精细化的I/O优先级控制也是Facebook的想做的地方:一些做清理工作的低优先级I/O线程往往会阻塞前景的高优先级的I/O线程。有人问Mason需要什么样的优先级才能满足需求,似乎聊下来Facebook也就需要2个优先级:一个低一个高。这时又有人提到了ionice,不过这个解决方案只能在CFQ上用(CFQ已经被Facebook disable了),Bottomley于是乎建议让ionice针对所有的I/O调度器都起作用,这点Mason也表示赞同,不过Ted Ts'o提到了一个可能的问题:回写线程也要有ionice的设置才能让整个环境工作。
Facebook的另一个问题是关于日志的。Facebook记录了很多的日志,这些程序需要不停的调用fadvise()或者madvise()来通知内核是否掉不需要的page cache,而这是可以被优化的。Van Riel建议说新内核的页面置换算法有可能会有帮助。Mason解释说其实Facebook倒是不介意这些,不过不停的调用这些系统调用似乎有点多余了。
Josef Bacik正在对btrfs做一些小的修改从而能够控制buffer I/O的速率,这个对btrfs比较容易,但是其实对内核其他文件系统来说,这个想法同样适用。这时Jan提出仅仅限制buffer I/O是不够的,因为系统中大部分条件下都存在着很多种不同的I/O。Chris对此表示同意,同时他认为这个方案虽然不完美,但至少可以部分解决问题。Bottomley认为ionice实际上已经存在了,我们就应该重用它,并且如果可以让balance_dirty_pages能够拥有正确的ionice优先级应该是可以解决90%以上的问题的。
Mason还提到目前Facebook是将日志文件保存到了Hadoop集群里面,但是用来搜索日志的命令仍然是很简单的grep,最好能有一个更好的方法来给内核日志打标记。对于这一点,Bottomley的意见是Mason最好把这个问题抛给Linus Torvalds,对这个建议,大家都只能呵呵了。
Danger tier
尽管3.10已经很新了,但是Mason还是希望使用更新的内核。基于这个目的,他创建了一个新的层叫危险层,他移植了Facebook原来打在3.10上的60个patches重新打到了最新的内核上并部署了大约1000台机器,这些机器属于前面提到的web层,这样他才可以收集到很多新的性能指标。说到这里,他给大家show了一张请求响应时间图(数据来源于最近3天抓的数据),从这张图里可以看出系统的平均响应时间以及10个最差的响应时间。这样他就能登录这些机器查看真正的问题出在什么地方。这仅仅是一个例子,后面随着项目的进展他还会分享更多的信息这样才能方便诊断新内核的问题并更快的解决它们。
Various page cache issues
Page Cache是文件系统和内存管理系统中的核心组件,它缓存了持久存储上的大量数据,并对系统的整体性能起着关键作用。但目前Page Cache在很多方面已经暴露出了一些问题。今年的LSF/MM峰会上最先讨论的就是和Page Cache相关的几个议题。
Large drives 32-bit systems
最先讨论的议题是由James Bottomley提出的问题是关于32位系统上Page Cache不能寻址超过16TB大小的范围。由于目前Linux页的默认大小为4KB,所以最多只能寻址16TB的磁盘空间。对于这个问题,首先要问的就是真的需要解决它吗?
尽管大多数内核开发者认为没有必要解决这个问题。但是实际情况却并不是这样。随着嵌入式设备的流行,特别是以ARM处理器为核心的设备的大量使用。用户使用超过16TB空间设备的机会大大增加。而这一问题也变得日益严重。
Dave Chinner则指出,这个问题的核心是Page Cache。如果用户程序使用Direct I/O来访问磁盘,由于Direct I/O会跨过Page Cache,因此没有寻址范围的问题。另一个问题是udev会使用buffered I/O来映射磁盘,因此即便应用程序进行了修改,仍然不能解决这一问题。此外,相对应的用户态程序也存在问题。比如在32位的机器上用户无法使用fsck程序检查超过16TB的文件系统。因此对于32位系统寻址范围问题的解决会涉及整个存储应用的方方面面。
最后的一致意见就是,这个问题不解决。
Large pages in the page cache
接下来Dave Chinner和Christoph Lameter主持讨论了关于大物理页的议题,即在Page Cache中存放更大的物理页。这两个人为了解决不同的问题而同时希望能够扩大目前的页大小,而他们采用的解决方法也不一样,不过最终的效果确实扩大了Page Cache中物理页的大小。
Christoph担心的是他的解决方案的性能开销。尽管更大的物理页会减小内核在管理内存页和进行I/O操作时的开销。但是他的解决方案是在分配内存页的时候添加一个Order属性来分配不同大小的内存页。这样做带来的问题就是如何解决内存碎片化的问题。
对于上述内存碎片问题的解决方法,Christoph说可以专门为大物理页保留一些内存页,但是这一做法比较“恶心”。另外一种解决方法则是允许内核移动内存页来消除内存碎片。而且移动内存页的大部分功能在目前内核中已经实现了。目前的问题是有些内存页是不能被移动的,而这些不能移动的内存页会破坏大物理页。很明显,这又是个死胡同。
内存页有很多原因不能被移动。比如内核中通过slab分配的对象。Christoph宣称已经有补丁解决slab中分配的对象不能被移动的问题。但是该补丁还没有被合并。此外,仍然有一些反对的声音,比如这些对象能够移动的前提是所有指向它的指针都已经被修改。Christoph则认为这些问题都可以通过一些数据结构来加以解决。
此外,Mel Gorman指出上面的问题其实也还好。真正的问题是如果通过内存页的移动来解决内存碎片的问题,那以后出现的问题很可能煽了各位内核开发者的脸。因为现在透明大页在使用中也会遇到分配失败的情况。所以问题远没有想象的简单。
Larger blocks in filesystems
Dave Chinner希望扩大内存物理页大小的原因是希望突破文件系统块大小4KB的限制。显然通过连续多个物理页是可以满足他的需要的。这样做的另一个好处是文件系统基本不需要修改。但这意味着修改需要在内存管理代码中完成。所以他只不过是把文件系统的事情拿到内存管理部分来做罢了。
而在扩大物理页上,主要的限制就是通用块I/O栈的限制。此前,单个I/O请求最大大小很长一段时间曾经困扰着快速设备的用户。由于这一大小太小,造成快速设备性能上不去。尽管这一问题目前已经被解决,但是还有其他问题。由于Page Cache目前假定文件系统块大小,所以不会记录文件系统级别的信息,因此文件系统需要自己来处理块大小和内存页大小不一致的信息。这样就造成了目前文件系统磁盘块大小不能超过内存页大小的原因。
Nick Piggin曾经尝试过来解决这一问题。并且从概念上也证明了其代码是可以工作的。但是他的解决方法存在的问题是需要修改所有文件系统。因此Dave选择另外一种方法来解决目前的问题。即不再通过Page Cache来维护内存页和文件系统之间的映射关系。这样就可以解决掉目前的问题。
这一方案的一个好处是彻底废弃buffer_head这一结构。此前buffer_head维护着内存页到磁盘块的信息。而新的方案改用类似于extent的结构来跟踪内存页到磁盘块的映射关系。这样就大大简化了此前的代码。
另外很多开发者提出ELF文件格式的问题。因为ELF格式中有很多对齐规范,而文件系统的磁盘块增大后可能会破坏对齐。但是这一问题除了32位系统外似乎并不严重。
最后的结论就是大家比较支持Dave的方案。
Support for shingled magnetic recording devices
今年LSF/MM上比较重要的一个议题就是SMR硬盘。SMR硬盘是下一代磁盘存储,而它由于具有一些比较特殊的特性而对目前的文件系统、存储带来了挑战。
目前SMR硬盘分为三种,即设备管理,主机感知和主机管理。设备管理类型和此前传统磁盘的使用方法一致,由磁盘自己来对磁盘空间进行映射。但是这样的结果会带来性能下降(与现在Flash设备的擦出类似)。这种类型的硬盘由于和传统磁盘在使用上没有区别,所以不是讨论的重点。讨论的重点自然放到了主机感知(系统需要遵循磁盘的区域来进行操作)和主机管理(主机需要主动根据磁盘的区域来进行操作)两类上。
SMR硬盘在内部会分成两个区域,分别是可以随机读写的区域和只允许顺序读写的区域。在只允许顺序读写的区域,会有专门的指针来记录当前的写入位置。如果写请求要求写入的位置与当前指针位置不一致,会导致磁盘返回IO错误(主机管理)或者磁盘会将该磁盘空间重新映射到别的区域(主机感知)。而重新映射操作会带来很大的延迟。
目前已经定义了两个新的SCSI命令来查询磁盘内部的空间区域和重置记录信息的指针。为了获取更好的性能,SMR的磁盘驱动需要配合磁盘尽量进行顺序写(磁盘内大部分空间是只允许顺序写的空间)。如果驱动程序违反这一规范,驱动器会返回错误或者进行重新映射。大部分内核开发者认为最有可能主机感知类型的SMR硬盘会更快的被广泛使用,因为这类磁盘在目前的系统上还可以使用。
目前T10正在制定SMR硬盘的相关标准和规范。不久这一规范就会被正式确定。Ted指出目前就可以通过T10的网站来获取这一草案。
随后便是对草案内容,特别是接口协议的一些讨论。目前最大的问题是大部分开发者手里并没有SMR硬盘,所以对SMR硬盘的任何评价目前都还是纸上谈兵。
Persistent memory
Matthew Wilcox和Kent Overstreet在LSF上谈到了内核对persistent memory(不知道这个中文应该咋称呼)的支持。一直以来都有传闻说这个东东会有,现在终于我们可以自己买到了(到底哪里有卖的?我也想买)。Wilcox介绍了现在他为persistent memory做的一些支持工作并争取一下大家的建议。
Persistent memory的读写速度和DRAM一样快,并且在断电以后数据不会丢失。为了满足这个物理特性,Wilcox写了一个可以直接访问的块设备层并把它命名为DAX,这个东东的设计灵感来源于内核里面的XIP(execute-in-place原地执行),因为这样可以避免使用page cache。XIP最初来源于IBM,开始的目的是为了在虚拟机之间共享可执行文件和动态库,后来也被用在嵌入式系统里面来直接从ROM里面运行。既然persistent memory和内存一样快,就没啥理由要在内存里面再放置一份copy了。目前,XIP更像是一个起点,但是实际上还有很多工作需要做。所以Wilcox重写了它并重命名为DAX。文件系统可以通过DAX驱动直接访问块设备并避免使用page cache,听起来不错,Wilcox希望DAX能够merge进入主线并希望其他人能够review代码并给予建议。
但是目前DAX还是有很多问题,比如首先现在用户调用msync()去刷一段内存的时候实际上会sync整个文件以及相应的元数据,这个当然不是POSIX的标准而且Wilcox也准备去fix它。但是很显然,这个不是仅仅修改DAX就可以工作的(还需要修改msync),Peter Zijlstra在这时提出警告说任何修改sync的动作都有可能导致用户程序出现诡异的行为,因为用户并不关心内核应该提供什么功能,他们往往更关心的是现在内核提供了什么功能。对于这一问题,Wilcox认为社区应该修复,而不是让用户依赖这个看起来明显有问题的内核行为。另外Chris Mason补充说他也赞成修复msync的错误行为,因为这个会让所有的做文件系统的人很happy,哈哈。
另一个比较大的问题是mmap()系统调用使用的flag MAP_FIXED,它有两层意思,一个是说将mmap的地址映射到指定的地址上,这个一般大家都知道,另一个隐藏的含义是说需要将mmap过程中遇到的其他页都unmap掉...这个功能太奇葩了吧,所以Wilcox建议增加一个flag MAP_WEAK,去掉第二个隐藏的功能。
get_user_pages()对于persistent memory无法正常工作,原因很简单,因为对应每个page没有一个struct page的对象。由于persistent memory设备容量很大以后会有很多页,所以没必要为每个页浪费64bytes来保存struct page。Dave Hansen正在做一个新的get_user_sg(),他的作用是像其他存储设备一样支持scatter-gather list(这玩意不知道咋翻译,不过做存储的人都懂的)类型的IO。这个功能对truncate()的支持还有一些问题,因为有可能truncate和get_user_sg存在一定的race,这样就可能看到脏数据,Wilcox目前的想法也很简单:当get_user_sg运行时阻塞truncate操作的发生,这就排除了race的条件。对于这一点,Overstreet有不同的想法,最近Overstreet一直在重写direct I/O,他认为DAX的mapping操作和direct I/O很类似。他目前重写direct I/O的宗旨就是创建一个新的bio,并干掉get_block(这个玩意确实是一个太tricky的东东),如果DAX也用这个bio就可以避免和truncate的race了。这时有人质疑如果I/O是bio-based就会让NFS/CIFS很悲剧,Overstreet反驳说如果我们可以让buffer I/O架在direct I/O上面就ok了。Chris Mason也认为如果真搞bio based,对NFS来说工作量应该还好。Overstreet也补充说新的bio仅仅是一个包含了很多page的结构体而已,言下之意也是问题不大。
这个session最后没啥结论,可能社区还需要先去review DAX和新的direct I/O的代码然后才能再下结论。
Trinity and memory management testing
Trinity是对内核系统调用进行测试的工具。通过对内核提供随机数据,Trinity已经报出了大量的内核Bug。Trinity的维护者Dave Jones在2014年的Linux Storage Filesystem and Memory Management大会上解释了内存管理track技术以及他如何使用此工具发现了内存管理子系统中的Bug。
他首先介绍了Al Viro的想法:利用mmap()创建一段内存,在该段内存的中间unmap掉一个单独的页,并且将结果传入各种系统调用中,以观察会发生什么。随后,他说:“全乱套了”。出现了大量的bug,内存管理社区需要花很多努力去修复它们。但是,他说在3.14-rc7内核中已经没有什么问题了,这是一件好事。
Sasha Levin也使用Trinity发现了Linux-next tree中的Bug。这些Bug经常会被引入mainline内核。在合并Linux-next之前,必须要先测试它的稳定性了。
Trinity适合发现那些无人使用的代码段中的bug。比如Huge page,page migration以及mbind系统调用。在mbind的支持下,所有的调用者都会先通过一个用户空间的库来检查参数的有效性。而系统调用自身并不做此检查。结果就发现了很多未被修复的bug。
内存子系统中的很多代码目前没有简单的测试方法。Trinity在此方面提供了一些帮助,但它在内存管理测试方面还处在初级阶段。他今年的后续时间里会继续从事此工作。不过,目前Trinity发现bug的速度要远超过bug们被fix的速度。
Fedora用户报告了很多关于大页(Huge Pages)的Bug,它们跟Trinity发现的问题是同源的。用常规手段复现这些Bug很难,因为涉及Java Runtime及诸如此类的应用。但是Dave对此的关注不够。至少在现在,透明大页仅仅简单地关闭了Trinity测试。
Trinity对于crash的复现功能也在不断发展中。该工具能够记录下他所做的一切,但是日志自身是个很重的操作,他记录的时间与crash的时间未必一致。很多crash是kernel内部状态的崩溃导致的。导致crash的操作可能是很久以前发生的。因此,查找crash的原因比较难。
Dave给内存管理的开发者提了2个需求。一个是任何人都能为现有的系统调用增加标记。通过此标记,他可以记录下何时开始测试。另一个是,他希望更多的开发者能够使用Trinity。上手有点难,但这不应该是我们拒绝Trinity的理由。
Compressed swap
目前内核社区有许多通过压缩内存内容来提升内存使用率的方案,其中zswap和zram已经进入了主线内核。这两个实现的共同点是通过压缩内存内容换取空间并最终取代swap,不同点也很明显,zram模拟成了一个特殊的块设备这样可以当作swap设备来是使用,而zswap而是通过"frontswap"来彻底避免swap。
Bob Liu有一个session专门介绍zswap以及它的性能问题。zswap通过将需要交换出现的数据页压缩以后放置到一个特殊的有zbud内存管理器所管理的区域中。当zbud满了以后,他会将页再置换到真正的swap设备中,这个步骤就会牵涉到数据解压,写数据到swap设备两个操作,并会显着得降低系统的效率。为了解决这个问题,Bob有几个方案:一个是将zswap做成一个写透的cache,任何写入到zswap的page会被同步的写入到真正的交换设备,这样经zswap的数据清除会非常简单,但是基本上和原来的swap设备区别不大了。另一个方案是让zswap可以动态调整大小,这样就可以根据需求来变大,但是即使做到这样,我们也仅仅是推迟了问题的出现并没有最终解决上面的问题。
Bob希望社区能够有一些建议如何来解决这些问题,可惜的是,Mel Gorman却指出无论是zram还是zswap都没有一个关于它们潜在收益的系统分析。当人们做一些性能测试的时候,他们一般会选择SPECjbb,这个并不适合对zram或者zswap做测试,而内核的编译就更不能算作对内存压缩系统的测试了。所以其实现在内存压缩子系统最缺乏的其实是一个合理的性能测试workload,这个才是Bob他们应该关注的地方。
Memory management locking
内存管理子系统作为kernel的一个核心系统,对锁的竞争非常敏感,2014 LSFMM Summit中有一个由Davidlohr Bueso主持的slot专门讨论内存管理中的锁。其中有两个锁的竞争非常严重,一个是anon_vma lock,它控制对匿名虚拟地址空间的访问,另一个是i_mmap_mutex,用来保护address_space结构中的几个域。这些锁之前是mutex,但是由于大多是只读访问,因此开发者将其换成了读写信号量(rwsems),然而一旦需要修改这些数据结构时就会引起明显的性能下降。某些应用程序还可能出现"rwsem stealing"的现象,即一个线程插队抢走了另一个线程正在等待的锁。Davidlohr说应该在mutex上加上一定程度的spin等待,这样即使一个mutex是睡眠锁,线程也可以在获取它时尝试spin等待一会以期望它马上就会被释放。他说为rwsem的实现加上spin等待可以使所有的workloads性能提升到原来的水平,目前社区没有人反对合并这些代码。
对于anon_vma,一个强烈的需求是不使用睡眠锁。rwlock可以满足这个需求,但是rwlock有公平性问题。Waiman Long在queue rwlock上做了一些工作,可以解决这个问题并提升性能。Peter Zijlstra说他又重写过一遍这些patch,公平性问题解决了,但是没有合适的benchmark来评价效果,而且这些锁在虚拟系统下仍然有一些问题,但是这并不妨碍代码合并进upstream。但是Sagi Grimberg说,有些code是在拿到anon_vma lock后需要睡眠的,比如invalidate_page(),因此转换成无睡眠锁会带来新的问题。InfiniBand和 RDMA也有这类需求,它们需要在拿到锁后进行睡眠。Rik van Riel 建议尽快将相关代码合并,但是Davidlohr并不认为使用睡眠锁带来的性能损耗很严重。Peter说对于这些无睡眠锁的patch应该发给Linus,并详细解释可能引起的问题,然后由Linus决定是否合并。
Davidlohr也建议重新开始mmap_sem信号量的讨论。mmap_sem保护进程地址空间的很多部分,因此经常被锁的时间很长,导致延迟增大,而且也在锁里处理了太多的工作。他认为如果只是处理地址空间的一部分区域就没必要锁住整个空间,也许我们应该看一下range lock是否可以解决mmap_sem锁竞争的问题。Michel Lespinasse认为虽然range lock可能有用,但是我们应该先解决mmap_sem锁太长时间的问题。Rik建议我们是否应该换成per-virtual-memory-area锁,但是Peter说这个方法以前试验过,但是结果会从mmap_sem锁竞争变成另一个"big VMA lock"的竞争。Jan Kara提出了在mmap_sem锁下MM子系统调用文件系统代码的一些问题,除了性能问题,这种使用方法也可能带来lock inversion。他为了解决这个问题研究了一阵,目前已经快ready了,但是还有一些其他问题,例如page fault代码,但解决办法还是有的。另一个问题是调用get_user_pages(),这需要调用者先拿到mmap_sem锁,Jan把调用换成了不需要锁的get_user_pages_fast()。这种方法在大部分情况下可以工作,但是还有一些比较难处理的情况,比如调用get_user_pages()时mmap_sem被高层代码锁住了,Video4Linux videobuf2的代码就包含了这类难处理的使用方法。另外一个担心是关于uprobes,它需要在它加载进内存的代码段中插入断点,这会导致在mmap()里
最后更新:2017-06-05 21:31:54