阿里linux内核月报201703

目前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,首先要确认内核编译了该功能,确认方法如下:

grepCONFIGMEMCG/boot/config g r e p C O N F I G M E M C G / b o o t / c o n f i g − (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. 将”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值