阿里内核月报2014年5月-06月-阿里云开发者社区

开发者社区> 开发与运维> 正文

阿里内核月报2014年5月-06月

简介: 长久以来,重启操作系统来安装一个内核补丁一直是一个烦人的事情。很多时候,重启系统的时机会受到其他条件的限制。此外,用户则更希望能够在不重启系统的情况下完成内核补丁的安装工作。2008年为了迎合这一需求Ksplice诞生了。

The initial kGraft submission

长久以来,重启操作系统来安装一个内核补丁一直是一个烦人的事情。很多时候,重启系统的时机会受到其他条件的限制。此外,用户则更希望能够在不重启系统的情况下完成内核补丁的安装工作。2008年为了迎合这一需求Ksplice诞生了。但它并没有被合并进主线内核,甚至在Oracle收购其后便消失在Linux开源社区的视线内了。最近其他一些解决方案陆续提交到了Linux内核社区,kGraft便是其中之一。

kGraft是由SUSE的Jiri Kosina和Jiri Slaby两人共同开发的。该解决方案相比于Ksplice要简单很多。当然,与此同时也意味着某些功能上的缺乏(比如:向数据结构中添加影子成员)。kGraft的补丁仅有600行,十分简单。这也意味着使用kGraft后对系统的影响是很小的。

kGraft的工作方式是通过替换掉内核中的整个问题函数来实现内核代码升级的。通过使用专门的工具,一个开发者可以轻松地将一个补丁变为多个需要替换的函数列表并将这些函数编译为一个单独的内核模块。在加载这个内核模块的时候,kGraft会将已有的问题函数替换为没有问题的新函数。

函数的替换是kGraft中的核心。对运行着的操作系统内核进行补丁升级时十分危险的。然而好消息是这一问题已经被较为完美的解决了。ftrace也需要对操作系统内核进行类似的操作。为此ftrace的开发者已经完成了类似的函数替换功能,用来调试和解决那些奇怪的错误。因此kGraft开发者要做的工作便是使用ftrace机制将问题函数替换为新函数。

另外一个较为重要的难点是如何保证在升级的过程中,没有进程正执行在问题函数之中。如果这一情况发生了,会造成不可预知的结果。kGraft开发者解决这一问题的方法是保证没有进程会同时看到两个版本的函数。

为了解决上述问题,kGraft会在每个进程的thread_info结构中添加一个标记用来追踪该进程在升级开始后是否离开或者返回用户态空间。当系统截获对问题函数的调用后,一个叫做“slow stub”的模块会去检测当前运行的进程目前的标记。如果进程进入或者退出内核空间,则意味着该进程运行在有问题的上下文忠,因此需要调用旧的问题函数。否则该进程需要调用新函数。一旦系统中的所有进程都进入到了新的上下文后,“slow stub”模块就可以被卸载,同时新函数可以无条件的被调用了。

接下来的问题是如果有进程在一定时间内没有完成上述状态转换怎么办?举个例子,一个进程可能花费很长的时间来等待网络IO。Vojtech Pavlik在今年Collaboration Summit上提到过一个方法,即向这些进程发送信号强制他们转换当前的状态。这一机制并没有包含在目前提交的补丁中。另一种解决方案是在/proc目录中显示当前有问题的进程,从而方便管理员的识别。

上面的问题似乎解决了,那么内核线程该如何解决呢?我们知道内核线程是不会返回用户态空间的。大部分内核线程在等待某个时间时,会调用kthread_should_stop()来判断是否需要退出。kGraft利用了这一点,它通过修改该函数来重置上面的标记。对于没有调用kthread_should_stop()函数的内核线程,kGraft会插入一个kgr_task_safe()函数用来标记当前的内核线程是否到达一个适当的状态。

最后的一个问题是关于中断的。kGraft解决中断处理函数替换的方法是定义一个per-CPU数组用来标记对应CPU是否运行在进程上下文。该数据的初始值为false,当schedule_on_each_cpu()被调用的时候,kGraft在其中插入了一个新的函数用来检查该CPU上的中断是否都已经进行了处理。在该CPU上存在没有处理的中断时,kGraft会让所有CPU都运行在就的有问题的上下文中,知道所有中断处理完毕为止。

目前kGraft的补丁已经提交到社区了,社区并没有特别的反对声音。这一功能应当说确实是十分有价值的。但是由于存在竞争对手,内核不可能同时合并两个热打补丁的解决方案。因此,目前需要有人来合并两种解决方案,或者需要有人站出来做决定。

The possible demise of remap_file_pages()

remap_file_pages()是一个有些怪的系统调用,它允许在任务地址空间和特定文件之间创建一个复杂的、非线性的地址映射。这样的映射方式也可以通过多次调用mmap()来完成,但是明显后者的代价要高一些,因为每次都会在内核中创建一个单独的VMA(virtual memory area),而remap_file_pages()则只创建一个VMA就够了,如果有很多很多不连续的内存映射,这两种方式的不同点在内核中将变得很大。

据说有很少开发者在使用remap_file_pages(),以至于Kirill Shutemov发布了一个patch把remap_file_pages()完全移出内核,他说“非线性映射维护起来很痛苦,并且,目前64位的系统可以充分的被使用,这时再使用它有些不太合理”。他目前还不打算将这个patch合并了,而这个patch仅表明他提出过这个观点了。

这个patch吸引之处就是,它可以删除600多行内核中难以琢磨的代码。但是如果这样做造成了某些应用程序无法使用,那么这些代码还必须待在内核中。一些内核开发者相信即使remap_file_pages()被移除了,也不会有人注意到的。但去相信一定不会造成应用程序无法使用是不可能的。所以,一些人建议在内核中增加一些警告。Peter Zijlstra 建议增加一个开关来激活remap_file_pages(),如果目前使用remap_file_pages()的开发者自己意识到这个变化的话就更好了。目前讨论这些希望可以避免以后的一些麻烦。

The first kpatch submission

正直北半球的春天,一个年轻的内核开发者正在思考着如何给内核动态的打补丁。上周我们看到了SUSE的kGraft动态打补丁方案。随后Red Hat的解决方案kpatch就呈现在我们面前了。这两个解决方案,在某些方面十分相似,但也有一些显著的不同。

与kGraft类似,kpatch也是对问题函数进行整体替换。kpatch通过用户态工具将补丁文件转换为一个可加载的内核模块。在该模块加载的时候,kpatch_register()函数被调用,并使用ftrace机制来截获对问题函数的调用,并调用新函数。从这里看,kpatch的工作原理与kGraft十分相似。但是,还是让我们来看看kpatch的内部细节。

与kGraft使用的复杂方法来保证函数替换的正确性不通,kpatch直接调用stop_machine()让所有CPU都暂定,虽有kpatch检查所有进程的栈以确保没有问题函数在执行。随后kpatch将问题函数完全替换为新函数。不想kGraft,这里没有任何状态上的记录,所有进程都一次性的进入到新状态中。

kpatch目前的方式有一些不足。首先stop_machine()杀伤力太大,内核开发者都在竭力避免使用该函数。此外,如果有问题函数正在被执行,kpatch会直接失败返回;而kGraft则会等待并且再尝试进行替换。这就是说,对kpatch来讲,那些总是被执行的函数(比如:schedule(), do_wait(), irq_thread())是无法进行替换的。对于一个常见的系统来说,有上千个函数会造成热升级的失败。

对于kpatch使用stop_machine()的这一方法,很多内核开发这表达了自己的想法。其中Ingo Molnar支出可以使用进程冻结的方法来确保完全没有进程在运行状态,但这意味着大补丁的过程会更漫长一些。Ingo指出如果Linux发行版开始使用热升级方案,首先要保证的是热升级的安全,其次才是快速。

kpatch的开发者Josh Poimboeuf则指出内核中有很多不能冻结的线程。Frederic Weisbecker建议使用内核线程暂定机制来代替进程冻结的方案。Ingo则指出无论如何都需要一种机制来保证进程能够到达一个安全的状态。目前社区的意见是首先保证安全,然后再提高性能。

另一个问题是关于补丁中对数据结构修改的。kGraft表示可以通过上下文机制来处理简单的数据结构修改。根据Jiri Kosina的说明,kGraft可以使用一种被称为创可贴函数的方式来让内核同时理解新旧数据结构知道所有进程都已经替换为新代码。在这一过程结束后,旧版本的数据结构就可以被废弃掉了,但是读取旧数据结构的函数仍然需要保留。

反观kpatch这边,目前并没有明确提出一种可行的修改数据结构的方法。目前kpatch有计划提供一种毁掉函数的机制在进行热升级的过程中对所有涉及的数据结构进行修改。这一方法意味着不需要维护旧数据结构的任何状态。

目前看,似乎情况还不算太糟糕,毕竟内核补丁中对数据结构的修改并不常见。正如Jiri所说:
根据他们的分析,需要热升级的补丁几乎都是非常短小的补丁。这些补丁仅会增加额外的便捷检查。很多年才可能出现一两个需要格外处理的补丁。

目前看来思考如何安全的修改数据结构仍然为时尚早。目前的重点是如何找到一种能够热升级内核的方法。今年八月份举行的Kernel Summit上开发者们将会讨论这一议题。目前看,大家都认为应该寻找一种可靠地方法来解决目前热升级的问题。

Braking CPU hotplug

最近的一组patch引发了对CPU hotplug子系统相关的讨论。为一个正在运行的系统动态增减CPU有很多需求:硬件上支持物理上的增加和移除CPU,或者需要移除一颗异常的处理器。在虚拟化场景里面,CPU热插拔是一个常见用于在用户虚拟机运行状态中动态调整虚拟机处理能力的手段。这个特性无疑很有意义,但是没有人对CPU hotplug目前的实现感到满意。

对一个正在运行系统的CPU进行插拔是一件复杂的工作,有大量的per-CPU状态需要管理。为此,拥有一套完整的机制将很不错,将这些复杂的工作细分为一个个简单步骤,同时能确保这些步骤按序执行。但不幸的是Linux内核并没有这种机制,而是使用了一系列难以修改的通知和回调实现,导致bug很难发现。

事实上在这一块的bug很多,Borislav Petkov希望让它们更难发现。他的patch介绍如下:

我们有一群热心的测试哥们,在对CPU热插拔不了解的情况下,拼凑脚本猛烈的压测CPU热插拔,然后报告他们触发的bug.

当然,首先,大部分,不是所有的,他们触发的bug是和CPU热插拔相关。但是我们知道热插拔全身布满了输管和“棕色纸袋”。

最终我们耗费了大量时间处理一个在最开始就有问题的机制。

他的解决方案很简单:在每个CPU热插拔操作前加入1秒的延迟。这样使得能够测试的操作数量及操作之间的并发量尽量减少。也能够漂亮的减少源源不断的bug报告。

当然,这有一个小小的瑕疵:这个patch并没有实际上解决任何bug,它只是将问题掩盖起来不被发现。Andrew Morton指出这个patch将导致CPU热插拔的bug解决得更少。Thomas Gleixner认为这也许是一件好事:“如果有人能够花相同的时间重写热插拔这团混乱的东西,我们将会获得更大的收益。但是可惜没有,我们更愿意为它插上更多的管道或者扎个绷带”。

2013年2月Thomas曾经试图重写,这块工作在这里。他花时间将热插拔操作拆分为一长串离散的步骤,然后建立了一个以定义好的顺序运行这些步骤的系统。但是距离完整的解决这个问题还有很远,大部分已经存在的热插拔代码依旧存在,仅仅是调用点不一样。但是一个随着时间推移能够不断重写的框架已经提供出来。

唯一的问题是:没有人做这个重写。Thomas已经转移到其他任务去,没有时间继续,同时也没有其他人将这块工作挑起来,因此这部分patch仅仅有最初始的发布。大量这一块的bug修复仅仅定位到特定的bug,并没有全盘考虑这一复杂而且难以维护的系统,事实上他们是的这些问题更加糟糕。

导致Borislav用于延迟热插拔系统patch出现的原因是不断的“管道和棕色纸袋子”带来的挫折。最终,这块的开发者不希望再有更多的bug修复,他们希望这部分代码更加简单而且易懂,他们不希望源源不断的bug修复确是不断增加代码的复杂度。让这个子系统的bug更难发现对于将开发者的注意力转移到其他方面是一个很大的帮助。

如果这个patch能够被merge,将会让人惊讶。在这样一个世界--内核子系统维护者不能强迫开发者在特定子系统领域,同时没有公司管理者指导他们的员工解决CPU热插拔的问题--一个人有时需要些创造性才能够让事情顺利解决。有人也许会希望这组patch能够给一个足够强的提示以使得有人能够在这个问题上继续解决。不幸的是Thomas已经不经意间破坏这种努力,他说加入没有其他人重写热插拔CPU子系统,他将跳回来自己来做。

Tux3 posted for review

在经过多年的开发和一些错误的开始之后,Tux3文件系统已经进入代码评审的阶段,希望它可以在不久的将来就能并入mainline。Tux3开始就提出了一些下一代文件系统的特性和高度的可靠性。提交代码评审是Tux3的一大进步。但是恐怕距离真正进入mainline还有一段时间。

目前唯一一个进行了代码评审的开发者是Dave Chinner,而他也没有感到非常满意。还有一些工作要做,Dave认为Tux3中的很多对文件系统核心的改变需要单独评审。其中一个Tux3的关键的机制“page forking”,当年并没有被2013 LSFMM接收,并且Tux3的开发者Daniel Phillips也没有对这点做出什么大的改进。

Dave还担心Tux3提出的一些特性目前正处在开发中,几年前,btrfs在还处于未完成的状态下被合并,以希望这样能促进它的开发。Dave说这种错误最好不要再犯了。

btrfs的开发说明了把一个还处于原型的文件系统合并到mainline中并不会促进它加速提高稳定性和性能,事实上它还是单独开发直到所有特性都基本完成会更好些。急于并入mianline只会减速它变的功能完善和稳定。

总之,这个文件系统目前会受到冷遇。Daniel面临将代码准备好合并的挑战,只有做到了这点才是时候将Tux3合并到内核。

BPF: the universal in-kernel virtual machine

最近关于ktap动态跟踪系统的讨论大多聚焦在添加一个lua解释器和虚拟机到内核中。 在内核空间运行虚拟机似乎是不合适的。 但实际上,内核已经包含了不止一个虚拟机。其中一个, BPF解释器已经在特性和性能上都有了发展;它现在似乎正在扮演着 超出初始目的的角色。在此过程中,它也许会导致内核网络子系统中解释器代码的精简。

“BPF”本来表示“伯克利包过滤器”;它最初是作为一种简单的语言用于为一些像tcpdump的工具写包过滤代码的。Jay Schulist 在内核2.5中添加了BPF的支持。自那以后很长时间,BPF解释器都是没有太多变化,似乎仅有一些性能调整和添加了一些访问 包数据的指令。在内核3.0中Eric Dumazet为BPF解释器添加了及时编译器功能。在内核3.4中,“secure computing”被增加 来,以便为系统调用支持来自用户的过滤器。那种过滤器也是用BPF语言写的。

在内核3.15中,BPF有了另一个重要的变化。它被分拆成2个变体,“经典BPF”和“内部BPF”。后者将可用的寄存器从2个扩 展到了10个,添加了许多与真实硬件匹配的指令,实现了64位寄存器,使BPF程序调用一组或多组内核函数成了可能。内部BPF 更轻易地编译成了快速机器代码并且更容易将BPF挂进其他子系统。

现在,至少内部BPF整个对于用户空间是不可见的。包过滤和安全计算接口依然接受经典BPF语言写的程序。这些程序在他们第 一次执行之前被翻译成内部BPF。这个想法似乎是内部BPF是内核特定的实现,也许随着时间会改变,因此它将不会很快被暴露 得用户空间。

内核3.16以后,也许网络意外的子系统也许也会使用BPF。 Alexei最近提交了一个将BPF用作跟踪过滤器的补丁。这个改变几乎 删掉为可观地提升性能而添加的代码。

内核中的跟踪机制允许一个有合适特权的用户每次执行遇到特定跟踪点时能接收详细的跟踪信息。正如你想象的,来自某些跟 踪点的数据可能是相当大的。这就是为什么需要过滤机制。
过滤器允许将布尔表达式与任何给定的跟踪点关联起来;仅当在执行的时候表达式为真,跟踪点才会触发。一个例子像下面:

   # cd /sys/kernel/debug/tracing/events/signal/signal_generate
   # echo "((sig >= 10 && sig < 15) || sig == 17) && comm != bash" > filter

在上面例子中,那个跟踪点被触发仅当特定的信号在跟定范围内产生并且产生这个信号的进程没有运行“bash”。

在跟踪子系统里,像上面的表达式被解析且表示成一个简单的数,它的每个内部节点都表示一个操作码。每次跟踪点被遇到, 将会遍历那个树,用此时的特定数据来评估每个操作。加入结果在树顶是真的,这个跟踪点将被触发并且相关的信息将被发出。 换句话说,跟踪子系统包含了一个小的分析器和解释器用于特定的目的。

Alexei的补丁留下了分析器却去掉了解释器。代替地,分析器产生的可预测树被翻译成一个内部BPF程序,然后丢弃。BPF被 及时编译器翻译成机器码。结果被执行无论何时跟踪点被遇到。从Alexei发布的benchmark来看,它是值得努力的。大多数 过滤器的执行时间都被减少近20倍,有些更多。考虑到跟踪的开销经常可能掩盖掉跟踪正试着找的问题,开销上的锐减是受 欢迎的。

这个补丁集真正是欢迎的,但不太可能被合入内核2.16。它当前依赖其他3.16改变。那些改变被合入了net-next树;那个树 没有正常地用作内核中其他改变的依赖。因此,合入Alexei的改变进入跟踪代码树导致了编译失败。

根原因是BPF代码被深深地嵌入了网络子系统。但BPF的使用不在仅限于网络代码;它正被其他内核子系统像安全子算和跟踪 使用。因此是时候将BPF移到一个更忠心的位置,以便它能独立于网络代码被维护。这个改变很可能设计到不仅仅是一个简单 文件的移动。在BPF解释器中依然有许多网络特定的代码需要被重构。那将是个大的工作,但对于一个将要被演进成更通用的 子系统来正常。

在这个工作被做之前,合入那些对非网络代码的BPF改变是困难的。因此,为解释代码,将BPF作为主要的虚拟机加载进内核, 那将是逻辑上的下一步工作。仅有如此一个虚拟机是有意义的,能更好地调试和维护。对于这个角色,没有其他可信的竞争者, 因此,一旦它为整个内核使用被重新打包后,BPF很可能将扮演这个角色。在那之后,将会很有意思地看到将会有什么其他的 用户出现。

Expanding the kernel stack

每个进程即使退出时,在内核里都要占用一定数量的内存,尽管占用的数量不大。其中有些内存被用来存放每个进程的内核栈。

因为每个进程可能同时在内核里运行,因而每个进程必须有他自己的内核栈空间。如果有系统里有大量的进程,内核栈所消耗的空间

总和也不少,而且内核栈还要求在物理上是连续的,这都给内存管理很大的压力。这些方面的考虑,也成为保持小的内核栈的一个强烈动机。

对linux的历史而言,在绝大多数架构上,内核栈大小是8K,两个连续的物理页。2008年曾有些开发者尝试把栈缩小到4K,事实证明这样的努力是不现实的。现在的内核函数的调用深度已经超越了4k的栈。

在x86_64系统上,越来越多的调用栈已经无法装入到8K栈。最近,Minchan Kim追踪到一个由于栈溢出导致的crash。最为回应,他建议是时候把x86_64系统上的栈大小翻倍,变成16KB了。之前,这样的建议是被抵制的,这次也是一样被抵制。Alan Cox争论说可有其他的解决方法,但是这种观点比较孤单。

Dave Chinner 经常处理栈溢出的问题。因为XFS文件系统上,更容易发生这类问题。

他非常支持这种改变:

在x86-64系统上,对linux io来说,8K栈从来都不足够大。但除了文件系统和io开发者,没有人乐意接受这个观点,尽管文件系统不得不一次次把栈溢出问题规避掉。

Linus一开始也不相信这个事实, 他澄清说降低内核栈足迹的工作(栈使用的深度?)还得继续,更换stack大小不是一个可靠的解决办法。

我基本计划使用这个patch,我同时还想确认我们确实是修订了一个我们见过而不是推论出来的问题。

我承认8KB有些痛苦和受限,并且变得相当痛苦。但是我不想当我们有一个深的stack使用的例子,就完全放弃。

Linus也澄清他不会在3.15里更改栈的大小。但是3.16 merge的窗口近期就会打开, 我们可以期待这个patch。

Seccomp filters for multi-threaded programs

seccomp是一种通过限制进程能够使用的系统调用来实现应用沙箱的方案。早期的seccomp对进程实行系统调用白名单制度,只允许进程使用固定的open/close/read/write四个调用,随后演化成可以使用灵活的过滤器组合(filter),并将过滤器逻辑使用一种类似汇编的特定语言(BPF)写下来,上传至kernel space去执行。每一个系统调用,连带用户传来的参数,都会被送到过滤器组合中,各个过滤器可以独立地决定放行还是拒绝,只要有一个过滤器决定拒绝,这个系统调用就不会被允许执行。seccomp之类的方案适用于很多PaaS的场景,或者现代浏览器的Sandbox,或者移动客户端上执行受限的应用。

在目前的内核里,一个进程可以通过一个prctl()系统调用来给自己加上这种过滤器,格式如下:

 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter);

filter是一个指向struct sock_fprog结构体的指针,代表要被加载的过滤器。过滤器一但加载就不能被卸载。一般来说添加过滤器(即使给自己)也需要sudo root,但这里有一个例外:如果一个进程在添加过滤器前调用 了: prctl(PR_SET_NO_NEW_PRIVS, 1); —这表示它放弃了以后获得任何privileges的机会,包括获得新的capability,调用setuid/setgid等等—它就可以给自己添加过滤器。

目前的seccomp实现有一点和cgroup很像:一但一个线程添加了一个过滤器,它以后派生的所有子线程都会继承这个过滤器。但是之前派生的线程或者兄弟姐妹们则完全不受影响。考虑到有时候我们不太容易去修改一个线程组中第一个被创建的那个线程,如果可以提供一个接口说:现在添加的过滤器要应用到我所在的进程中所有的线程上,而不仅仅是给我自己,则会方便许多。Kees Cook提的就是这么一个patch,他引入了一个新的接口

  prctl(PR_SECCOMP_EXT, SECCOMP_EXT_ACT, SECCOMP_EXT_ACT_FILTER, flags, filter);

如果flags传了0进去,这个接口的行为就和刚刚说的的PR_SET_SECCOMP完全一样,如果传了常量SECCOMP_FILTER_TSYNC进去,新建的这个过滤器就会被应用到整个线程组上去。

另外,Kees Cook还添加了一个接口:

 prctl(PR_SECCOMP_EXT, SECCOMP_EXT_ACT, SECCOMP_EXT_ACT_TSYNC, 0, 0);

这个接口的语义是说:不添加新的过滤器,而是把我自己这个线程已有的所有过滤器都添加到线程组中的其他线程上去。
通过以上两套新接口,seccomp对线程组的支持增强了。我们最早有望在3.16看见这套patch被合并。

Locking and pinning

内核通过mlock()系统调用可以实现将页锁在物理内存上,但是实际上却不止这一种方式,而这些方式的行为也多少有些不同,这使得在资源计数和内存管理方面有些混乱。Peter Zijlstra的一套patch定义了另一种称为"pinning"的锁页操作。

锁内存的一个问题是不能满足所有用户的需求。一个被mlock()调用锁住的内存页会一直占据系统物理内存,因此从表面上看,当应用程序访问这些页时不会发生缺页异常。但是这却没有要求这些页必须一直在同一个位置,而内核则可以根据需要对页迁移。当页发生迁移后,应用程序再次访问时会发生一次软缺页(不会发生任何IO)。大部分情况下这不是一个问题,但是硬实时程序的开发者要求更苛刻,他们要避免软缺页带来的延迟。但是内核目前还没有这种形式的锁内存功能。

锁内存也无法满足一些内核内部的需求。例如内核用来做DMA缓存的内存不能被迁移,这些页不能使用锁机制,但是通过增加引用计数或调用get_user_pages()来达到了固定页的目的。对于这些变相被锁住的页,它们是怎么和资源计数机制交互的呢?系统管理员可以对用户可锁页的数量设置一个上限,但是创建和用户态共享的DMA缓存却是应用程序的行为。因此对于一些用户而言,可以通过创建RDMA缓存来变相达到锁页的目的,而又绕过计数机制,这使得想限制所有锁页数量的系统管理员和开发者很不爽。这些通过“后门”创建的锁页也带来了其他问题。内存管理子系统对可迁移页和不可迁移页做了区分,这种情况下的页是以正常匿名页方式分配出来的可迁移页,但是把它们强制锁住使得它们不可迁移。当内存子系统试图迁移内存页来创建大块连续内存时,这些页便不能被操作从而无法创建更大的连续内存。

Peter的patch便是尝试解决以上问题,他提出了“pinned”页的概念,这些页只能在当前的物理位置上。pin住的页被放在了一段单独的VMA中,同时带有VM_PINNED标志。内核里可以通过mm_mpin()函数来pin住页:

   int mm_mpin(unsigned long start, size_t len);

在调用进程的资源限制允许的情况下,该函数会将内存页pin在内存里。内核代码访问这些页时也仍需要调用get_user_pages(),且在mm_mpin()之后。

一个长期计划是使内存pin功能对用户态可用,新加一个类似于mlock()的新系统调用mpin(),来保证页不会被迁移也不会发生缺页异常。另一个当前未实现的功能是在锁页前先将该页迁移到不可迁移的内存区域,因为mm_mpin()调用明确告知了该页不能被迁移,因此内核事前将其分配在不可迁移区域是有益的,这避免干扰以后的内存压缩操作,可以增大创建大块连续内存的概率。最后,将被pin的页放在单独的VMA可以更方便的追踪,也可以被记账从而避免了刚才提及的后门。

目前来看,似乎没有人对这套patch强烈反对,在上轮讨论中,有人提到改变锁页的计数机制可能对即将达到限制的用户带来regression,但是这个问题也没有其他的解决办法,可以选择继续让pinned页不在这个限制内,或者给它单独设置一个限制。

Another attempt at power-aware scheduling

2013年的power-aware scheduling mini-summit提出希望CPU power-aware scheduling能够集成包括CPU frequency和CPU idle governors等子系统。5月23日,Morten Rasmussen发的Energy cost model for energy-aware scheduling patch set。这组patch弃用之前的启发式算法,改而尝试测量计算每个调度决策将带来的power消耗。

通过这组patch,将可以实现类似以下这样一个函数接口:

int energy_diff_util(int cpu, int utilization);

即计算得到一个指定的负载(通过利用率代表)加到给定CPU时将带来的power消耗。
现实条件下这个接口实现还面临以下一些困难:

  1. 内核并不知道即将被调度的特定task的CPU利用率。因此这组patch使用的是load进行衡量,但这毕竟不是一样的量,load并没有考虑进程的优先级。
  2. 调度器并不知道CPU frequency governor将会对哪些CPU执行什么动作。主要是因为还没有将这些子系统集成起来。
  3. CPU唤醒对节能调度的影响。除了简单的CPU利用率会影响CPU能耗外,另外将CPU从睡眠状态唤醒过程本身带来特定的能耗(取决于CPU睡眠的深度,这也是一个当前调度器无法获取的量)。一个特定的进程不可能知道自己唤醒一个睡眠CPU的频繁程度,但是可以通过计算处理器本身从睡眠状态唤醒的频繁度进行计算。通过估算有多大的概率进程的唤醒会发生在CPU处于睡眠状态,可以估算进程唤醒导致CPU从睡眠状态唤醒带来的开销。

拥有这些条件之后,剩下的就是在需要为一个给定进程选择CPU时执行能耗计算。遍历所有的CPU开销过大,因此要求尽可能快的定位到一个尽可能低层级的group进行计算,最低能耗的CPU所在的group将会被选择。在这组patch中,find_idlest_cpu被修改执行该计算,其他进程放置策略选择(比如负载均衡)并没有修改。

这组patch提供一小组benchmark信息,针对特定的负载,在big.LITTLE系统上,显示节能效果能从3%提升到50%。同时进程切换开销也差不多是原来的4倍,这是当前不可接受的,需要继续一些优化工作。

截止目前,这组patch的讨论还在默默继续。过去对power-aware调度的patch进行reviewer人员不足一直是个问题,这组patch将使得相关review工作变得简单。然后我们将会看到这个想法是否代表一个可行的前进方向

The BFQ I/O scheduler

块设备层IO调度器的作用是向存储设备分发IO请求,使得吞吐量最大化且并使延迟最小化。Linux内核目前包含几个不同的调度器,但是近些年这方面的改动很小,既没有提出新的调度器也没有对现有调度器进行较大的改动。但是最近出现了一个新的"budget fair queuing" (BFQ) IO调度器,提出了一些有趣的想法。

BFQ介绍

BFQ已经被开发使用了好几年,从很多方面上它都参考了内核里的CFQ调度器。CFQ对每一个进程的IO请求都单独维护了一个队列,并轮转服务这些队列来公平地划分可用带宽,CFQ工作的很好且通常是旋转磁盘的选择。但是CFQ在优化改进性能的同时代码也越来越复杂,尽管加了一些启发式算法但仍会产生一些较大IO延迟。

BFQ调度器同样对每个进程都维护IO请求队列,但是不采用CFQ的轮询方式,而是给每一个进程都分配一个"IO预算"。该预算表示当进程下一次访问设备时允许传输的sector数目。预算的计算方式有些复杂,但是整体上是基于每个进程的IO权重以及该进程过去的行为。IO的权重函数类似于一个优先级参数,通常被管理员设置且是一个常量,具有相同权重的进程将会获得相同的IO带宽。不同的进程拥有不同的预算,但是BFQ会尽力保持整体的公平性,因此一个有较小预算的进程会比有较大预算的进程更快的获得调度机会。当决定服务哪些请求时,BFQ会检查各个进程的预算,去选择会尽快释放设备的那一个,因此具有较小IO预算进程的等待时间会小于大预算进程。当选择一个进程后,它会排它的占有这个磁盘设备直到预算里的sector传输完毕,但是也有一些例外:

  1. 正常情况下如果一个进程不再有任何请求,则它对磁盘的访问就结束了。但是如果最后一个请求是同步请求(例如读请求),BFQ会idle一会来给该进程一个机会产生新的IO请求。这是因为进程可能正在等待该读请求的完成然后再产生后续新的IO,而这些IO很大概率上是和上一个请求连续的,因此服务起来也很快。这听起来有点不合情理,但是通常情况下在一个同步请求后等待一会会提高吞吐量。
  2. 每个进程完成请求的时间也有限制,如果它的IO完成的很慢,比如很多随机IO,那么在完成所有预算前可能会停止它继续访问设备,但是这种情况下仍然会记账它使用了整个预算,因为它影响了整个设备的IO吞吐量。
    关于每个进程的预算分配算法,简单来说是它上次被调度时传输的sector数目,但有一个全局最大值。因此起起停停传输量较小的进程会倾向获得较小的预算,而IO密集型进程则有较大的预算。预算较小的进程对延迟响应更敏感,被调度地更加频繁。预算较大的进程会做较多IO且等待时间较长,但是会获得加时时间片来提高设备的吞吐量。

关于启发式算法

BFQ的一些使用经验显示上面描述的算法有不错的效果,但是仍有提升空间。目前发出的代码增加了一些启发式算法来改善系统这方面的行为,具体包括:

  1. 刚启动的进程会获得一个中等的预算和递增的权重,使得它们能以相对较小的延迟获得足够的IO,目的是在应用进程启动阶段为它分配额外的IO带宽来尽快地将代码加载至内存。递增的权重会随着进程运行时间线性地减少。
  2. BFQ的预算计算以及允许的最大预算值,都基于底层设备的IO速率峰值。由于数据在磁盘上的位置以及设备本身的缓存等影响,IO速率峰值可能变动较大,因此对速率计算进行了一些微调来将这些因素考虑在内。例如,会考虑已经超时但是仍没有用完预算的进程,发生超时说明IO访问是随机的且磁盘没有跑出峰值,也可能说明最大预算值设置过高。除此之外还会过滤掉计算出的过大的速率值,因为可能是设备缓存的影响而不是真实的IO速率。
  3. 预算计算公式也有一些调整。如果一个进程在用完预算前处理完了请求,之前的做法是降低预算到实际发出的请求数目,而目前的做法是调度器会检查该进程是否有未完成的IO请求,如果有的话,速率值会被翻倍因为理论上当这些请求完成后会有更多的请求到来。当发生超时时,预算也会被翻倍,这是为了帮助进程度过较慢的一段,同时降低那些真正随机访问的进程被服务的频率。最后,如果当预算用完后仍然有未完成的请求,说明可能是IO密集型进程,因此预算会乘4。
  4. 写操作比读操作更耗资源,因为磁盘倾向于缓存写数据并立即返回请求,而过段时间才会发生向磁盘真正的写。这可能会对读请求造成饿死。BFQ通过对写请求更消耗预算来考虑这种代价,实际中一个写相当于十个读。
  5. 如果设备内部可以排队多个命令,那么让设备idle可能会使内部队列清空,从而造成吞吐量降低,因此BFQ会在能排队命令的SSD上关闭idle。旋转磁盘上也可以关闭idle,但是是在服务随机IO时才会这么做。
  6. 当多个进程访问磁盘的同一区域时,最好能将它们的队列合并而不是分开服务。一个很好的例子是QEMU,它会将IO分发给一组工作线程发送。BFQ包含一个叫“early queue merge”的算法会去探测这类进程并将它们的队列合并服务。
  7. BFQ会探测“软实时”程序,例如媒体播放器,并提高它们的权重来降低延迟。探测算法的原理是寻找特定的IO请求模式,并idle其一段时间。如果进程具有这种IO模式,它们的权重将会被提升。
    除了这些之外还有很多启发式算法,毕竟为所有负载类型优化系统模式是一个相当复杂的工作。从BFQ开发者Paolo Valente发出的测试数据来看,效果相当不错,但是离BFQ合进mainline仍然还有很多困难。

合并进mainline

BFQ目前的反馈还是很不错的,数字说明一切,同时大家也很高兴调度器和启发式算法被全面的描述和测试。CFQ包含很多启发式算法,但是懂的人很少,BFQ看起来是一个更干净清楚的版本。但是内核开发者不希望看到另一个像CFQ一样的小生态系统被合并,他们希望能逐步把CFQ改进为BFQ,同时系统中只有这一个调度器。Tejun Heo说这样合并起来更容易,也能让更多开发者了解一步步的过程,即使以后CFQ如果出现性能退化也可以通过bisect方式定位具体的修改。 BFQ已经使用了一段时间,一些发行版如Sabayon,OpenMandriva和CyanogenMod已经包含了进去。技术不错,但是需要一些时间来慢慢地推动合并进mainline。

The unified control group hierarchy in 3.16

重新实现内核中控制组的想法准确来说不是新的,参见2012上半年的一篇文章。然而,那篇文章所讲的还没有太多在内核实现。 这种情况在内核3.16中得到改变,它包括了新的统一的控制组分层代码。这篇文章将是统一分层在用户级别如何工作的一个概述。

虽然控制组系统从一开始就支持了多分层,每个能包含一组不同的进程,这种灵活性有其吸引力,但带来了开销。跟踪应用到 某个进程的所有控制器是昂贵的。在一些场景下,也需要更好的控制器间的协作来有效地控制资源的使用。最后这种特性在现实 世界里很少得到应用。因此有计划准备将多分层从内核中去掉。

统一控制组分层开发有一段时间了,许多准备工作也已经被合入了内核3.14和3.15。在3.16中,这个特性将是可用的,但仅仅 对于那些明确要求它的用户。为了使用统一分层,新的控制组虚拟文件系统应该被挂载像下面:

 mount -t cgroup -o __DEVEL__sane_behavior cgroup <mount-point>

很明显,__DEVEL__sane_behavior选项不是永久存在的,在统一分层变成缺省特性之前,它还会存在一段时间。 在统一分层中,所有的控制器连接到分层的根。基于一些规则,控制器能在分层子树中激活。出于解释这些规则的目的,想象 一个像右图的控制组分层。组A和组B直接位于跟控制组下,组C和组D是组B的孩子。

                         root
                         /  \
                        A    B
                            / \
                           C   D

规则1:控制组必须应用一个控制器在所有的孩子上或者谁也不被应用。一个控制器除非已经在它的父组里被激活了,否则不能 在该组中激活。

规则2:仅在关联的组没有包含进程的时候,cgroup.subtree_control文件能被用来改变控制器的设置。

规则3:当组内或者它的子孙组中有进程,读cgroup.populated文件将会返回非零。通过poll()这个文件,假如控制组变得完全空, 一个进程将会得到通知。

所有这些工作都讨论了好多年了;控制组用户的大多数都进行了评论,因此今天统一分层应该对大多数的用例是可用的。内核3.16 将给感兴趣的用户一个试用新模式,找出其中残留的问题的机会。寄希望在未来几个开发周期内互用能实际迁移到新模式是不切实际的。

The volatile volatile ranges patch set

“易失范围内存”(Volatile Ranges)的作用是指定一段用户空间内存范围,该范围内的内存在内存较为紧张的时候会被操作系统回收使用。常见的使用案例是浏览器图片缓存。浏览器喜欢将信息尽可能保存在内存中以加快页面的加载速度,但实际上这些内存更适合被使用在其他更需要内存的地方。这一想法的实现经历了许多波折,而且现在看对实现细节的修改仍然没有结束。

早期的版本使用的是posix_fadvise()系统调用,但是有些开发者认为使用这个系统调用并不合适,因为这个系统调用给用户的感觉更多的是与分配相关的工作。所以后来的版本改为使用fallocate()系统调用。随后在2013年,对用户的接口又改为了另外两个新的系统调用:fvrange()和mvrange()。到了2014年的第十一个版本使用的接口变味了vrange()。在经历了多轮迭代之后,开发者们又开始关注起用户空间的语义(比如:当一个进程访问一段已经被回收的内存时会发生什么)以及内部是实现细节。因此到目前为止,这个补丁仍然没有被合并。
到了第十四个版本用户态接口又换成了madvise(): <source lang="c"> madvise(address, length, MADV_VOLATILE); </source> 在调用该系统调用后,从address开始长度为length的内存随时可能被操作系统回收,而内存中的数据将会被丢弃。应用程序如果需要访问该段内存时,需要将其标记为不可散失: <source lang="c"> madvise(address, length, MADV_NONVOLATILE); </source> 返回值为0则表明调用成功(该段内存变为非易失,内存中的数据没有被丢弃);返回负值则说明有错误发生或者操作成功但内存中的数据已经被丢弃。

此前madvise()接口也曾经被考虑过,但当时的问题是当时的实现需要接口返回两个结果:1)有多少内存页已经被标记成功;2)是否有内存页的数据已经被丢弃。这次John终于找到了一种可以原子操作的实现方法来实现这一操作。由于不再需要第二个参数,所以madvise()系统调用就变成一个较为适合的接口了。

如果用户尝试访问一段已经被释放掉的内存空间时会发生什么呢?目前的实现中操作系统会向该进程发送SIGBUG信号。应用程序需要捕捉该信号从而向其他数据源获取数据。如果该进程没有捕捉SIGBUG信号,则会直接coredump。显然这样的做法不够友好。但是大家认为既然用户自己表明该段内存可以释放,就应当遵守规范,不再访问该段内存。

Minchan Kim不太喜欢这个这个解决方案。他认为应用程序在访问到那些已经标记为易失的内存空间时操作系统应担直接返回填0的内存页,这样做开销很小。同时也不需要应用程序显示调用MADV_NONVOLATILE并且捕捉SIGBUG信号。但是John认为这个工作应该是Minchan的MADV_FREE补丁来完成的。而Minchan不这么认为,他觉得MADV_FREE的操作无法经历反复的释放和重用。而MADV_VOLATILE则可以做到。但John表示很担心这样做带来意想不到的后果。
Johannes Weiner也比较倾向于返回填0内存页的方案。同时他认为John的实现可以基于Minchan的MADV_FREE来进行。至于填0还是SIGBUG,他认为可以考虑同时提供,让用户自己来选择。John认为可以一试。

John同时表示后面恐怕没有太多时间再来完善目前的工作了。确实,这个补丁经历的时间太长了。从第一版到第十四版,经历了不通开发者的审阅,知道现在仍然没有被合并。
同时,错也不在那些给出审阅意见的开发者身上。毕竟易失范围内存这个概念的引入是对用户可见的修改。如果实现、接口选择不正确,影响会持续很长时间。内存管理代码的改动似乎总是很难进入主线内核,而这一修改还会对用户课件,那事情就更糟了。这个补丁真是如此,因此开发者们格外谨慎也就可以理解了。

目前没有人能够回答这个补丁是否应该进入主线内核。同时使用这一补丁的用户(ashmem)早已开始使用类似的补丁了。所以除非有人能够继续推进这一补丁,否则主线内核恐怕很难能够用上这一特性。

RCU, cond_resched(), and performance regressions

性能退化是内核开发者经常遇到的问题。一个看似不相干的改动可能会引起一个显著的性能下降,有时候这种性能的退化会潜伏好几年,直到受影响的用户升级了的内核,并注意到运行速度变慢了。 好消息是开发社区为了发现性能退化,正在做更多的测试。这些测试发现了3.16内核里的一个典型的性能退化。 这个问题,作为一个例子来证明广泛的适用很多用户是多么的困难的,很值得一看。

The birth of a regression 一次性能退化的产生:

内核的read-copy-update (RCU)机制通过数据结构更改的免锁和集中的清理操作,极大的增加了内核的扩展性。 RCU的一个基本操作就是检测每个cpu的"quiescent states" ,"quiescent states" 是指一个状态, 在这个状态下,内核不持有任何的RCU保护的数据结构的引用。最初,"quiescent states"被定义为“当处理器 运行在用户态的状态”,但是之后事情变得更加的复杂(详情见LWN's lengthy list of RCU articles)。

内核的”full tickless mode”,已经比较正式的使用了,它会使得"quiescent states"的检测更加困难。 由于这种模式的限制,一个运行在tickless模式下的cpu会一直运行一个进程。如果那个进程占用内核运行很长时间, 就没有"quiescent states"被检测到。结果导致RCU不能够宣布一个"grace period" 结束,并运行(可能比较耗时) 一系列的累计的RCU回调函数。被拖延的"grace period"会导致过多的内核延迟,最坏情况下会导致内存耗尽。

Fixing the problem 修订这个问题

有人可能(确实有开发者这么做了)认为在内核里这样的循环会有严重的问题.确实有这样的情景发生了, Eric Dumazet提到了一个: 一个打开了几千个socket的进程调用exit(). 每个打开的socket都会通过RCU去释放结构体. 当同一个进程在关闭socket时, 这就产生了一个很长待做的工作的链, 这阻止了RCU在内核里的循环处理。

RCU开发者Paul McKenney在简单观察的基础上,给出了一个该问题的解决方法:在内核里已经有这么一套机制, 当一个很长的操作在运行时,它允许其他事情去运行。在已知一段要长时间运行的代码里,不时的调用cond_resched(), 从而给调度器一个运行更高优先级进程的机会。在tickless模式下,没有更高优先级的进程,因此,在当前内核里, cond_resched()在tickless模式下啥也不做。

但是内核只有在他可以被调度出cpu的时候,才可以调用cond_resched(),所以不可以运行在原子上下文中,不能够持有任何 的RCU保护的数据结构的引用。换句话说,调用cond_resched()一次,意味着一次quiescent state,所需要做的就是通知RCU。

如果真的这样的话,cond_resched()会在许多性能敏感的地方被调用(进而产生大量的负荷),而这是不可行的。 所以Paul没有在每个cond_resched()时,都通知RCU进入一个quiescent state。通过一个percpu的计数器, 每次更改时,计数器加一,每调用256次cond_resched(),才通知RCU一次。这以较小的代价修复了问题,因此patch 被合入到3.16内核里。

在这之后不久,Dave Hansen报告说,他有一个性能指标(一个除了打开并关闭大量文件外,基本不做其他事情的程序)下降了。 通过二分法,他定位到cond_resched()改动是罪魁祸首。有趣的是,没有cond_resched(),程序会跟预期运行的一样快。 相反,改动会让RCU grace periods出现的比以前更频繁。进而引起RCU 回调被以更小规模的批量执行,增加了在slab内存分配过程中 的竞争。通过把quiescent states的阈值从每256次cond_resched()调用调到更大,Dave更够恢复到3.15版本水平的性能。

Fixing problem(修订这个问题) 有人可能认为,简单的提高阈值就可以为所有的用户修复这个问题。但是,那样的话不仅会恢复性能,cond_resched()试图修复的问题又会出现了。 挑战就是要修复一个性能问题,而不会导致其他的问题。 还有一个附加的挑战,一些开发者喜欢把cond_resched()在全可抢占内核上,做成一个完全的空操作。毕竟,如果内核是可抢占的, 就不需要考虑需要调度的情景了。

Paul的第一个版修复方法是在几个地方做修改的一系列patch。cond_resched()仍然有检查,但是检查使用了另外的一种形式。RCU的核心被修改成了 注意当有一个特定的处理器保持grace period很长的时间。当这种情况发生时,一个percpu的标志位会被设置,然后cond_resched()只需要 检查那个标志,如果标志位被设置了,意味着经历了一个quiescent period。这个改变降低了grace periods的频率, 挽回了出大部分的性能损失。

另外,Paul提出了一个新的函数cond_resched_RCU_qs(),即所谓"慢版本的cond_resched()"。缺省情况下, 它跟普通的cond_resched()做同样的事情,但是它的目的是即使当cond_resched()被改为跳过检查,或者啥也不做时, 它仍然继续执行RCU grace period检查。这个patch在少量的战略位置上把cond_resched()调用改为cond_resched_RCU_qs(), 过去,问题在这些地方出现过。

这个解决方法能够工作,但是让一些开发者不太高兴。对于那些希望得到他们cpu最好性能的家伙,任何像在cond_resched()这样函数里的 计算都是太浪费的。所以, Paul提出了另外一个不同的方法,在cond_resched()不需要做任何的检查。相反,当RCU注意到一个cpu 已经持有grace period很长时间,它就会发送一个核间中断到那个处理器。核间中断会在那个cpu不运行在原子上下文的时候被投递到。 这样,也是通知一个quiescent state的恰当时机。

这个方案可能第一眼看起来很奇怪,IPI是很耗资源的,这就显得这个方法不是提高扩展性的一个正常方法。但是这个方法有两个优势: 它不再需要在性能敏感的cpu上进行监控,并且IPI只在有问题的时候才产生。所以,大多数时间,它应该对运行在tickless模式下的cpu 没有任何影响。这样看起来,这个方案是蛮不错的,并且解决了性能退化的问题。

How good is good enough? 怎样才算足够好?

尽管相比以前的,它变得更小了, 但是Dave仍然看到一些性能下降。这个方案不是完美的,但是Paul倾向于宣布无论如何这都是一个胜利。 考虑到短暂的grace periods帮助其他的负载,解决了实际问题的patch,大量的RCUtree.jiffies_till_sched_qs在3%内, 难道我们不应该认为是胜利吗?

对这种情况,Dave仍然不是100%的满意,他注意到相比缺省的设置,性能损失接近10%,并说“现有的改动抵消掉了我的系统通过RCU获得的收益” Paul回应说,“不是所有的有兴趣的微基准都能成为kernel的约束物”,并发起一个包含了第二版的解决方案的pull请求。

他建议,如果现实世界里真有被影响到的负载,通过各种方法优化系统,以缓解问题。

无论,这个问题是否真正彻底的解决掉,这次回退证明了在当前系统上的内核开发的危险性。可扩展性的压力使得复杂的代码尽量保证所有事情都 能用最小开销下在适当的时间发生。但是不可能让一个开发者测试所有可能的负载,所以经常会有一个改动导致惊人的性能下降。解决一个工作量可能会 对另外一个不利。不影响任何工作量的改动是不可能实现的。但是,充分的测试并关注测试中暴露出的问题,可以让绝大多数的问题在影响到 生产用户前有望被发现并解决掉。

Reworking kexec for signatures

kexec机制允许在已运行的内核中直接切换到另一个内核。这对快速启动很有意义,因为可以绕过firmware和bootloader。Kexec还可以与kdump一起产生crash dumps信息。不过,Matthew Garret在他的博客中提到,kexec可以被用以规避UEFI的安全启动限制,他给出了一种在安全启动时禁用kexec的方法。对大多数人来说,这并非很严重的问题。目前已经有一组patch使得kexec仅仅能启动那些被签名的内核。该方案在不需完全禁用kexec的情况下解决了Matthew Garret提到的问题。

Kexec子系统主要包括系统调用kexec_load()。该函数将新内核加载到内存,随后可以用系统调用reboot()引导系统。命令行工具kexec可以同时完成加载与重启工作,整个过程无需firmware与bootloader的干预。

UEFI引入了安全启动限制。Linux内核可以被用来引导未签名的(可能是恶意的)windows操作系统,因为kexec能够绕过UEFI的安全限制。微软可能会因此将Linux的bootloader列入黑名单,那么以后在通用机器上引导linux就会很难了。虽然微软可能不会那么快行动,kexec毕竟也可以影响到需要安全启动的linux系统。

不管怎么样,Garrett最终还是将禁用kexec的代码从他的patch中拿掉了,但是他依然建议支持安全启动的发行版最好禁用kexec。当然,这些patch还没有被合并。最近,Vivek Goyal提交了一组patch,用以解决上述的安全问题,但只能保护那些开启了模块签名机制的系统。Garrett在博客中解释了绕过安全限制的方法:在新内核中修改原有内核内存中的sig_enforce sysfs参数,再跳回原内核。   Goyal的patch对kexec做了限制,使得它只能执行带签名的代码。他实现了一个新的系统调用:

long kexec_file_load(int kernel_fd, int initrd_fd,  *const char *cmdline_ptr, unsigned long cmdline_len,    unsigned long flags);

Kernel_fd指向新内核的执行文件。Initrd_fd指向"initial ramdisk" (initrd)文件。新内核启动后将执行cmdline_prt制定的命令。现有系统调用的格式如下,大家可以对比下:

long kexec_load(unsigned long entry, unsigned long nr_segments,    struct kexec_segment *segments, unsigned long flags);

该系统调用要求在用户空间对内核的可执行文件进行解析,然后盲目地将它加载进内存。kexec_file_load()加载了整个内核文件,因此它知道当前加载与执行的到底是什么内容。

在加载进来的所有段中,有个较为独立,名为“purgatory”的段。它在两个内核之间运行。在重启时,现有内核首先跳到purgatory,它的主要功能是检查其它段的SHA-256 hash。检查没有问题,才可以继续启动。Purgatory将一些内存copy到备份区域,执行那些面向体系结构的安装代码,然后跳转到新内核。

Purgatory目前位于kexec-tools工具中,如果内核想要运行kernel binary与initrd中的段,就必须有它自己的Purgatory。Goyal的patch将内核的purgatory放在了arch/x86/purgatory/。

Goyal还将crypto/sha256_generic.c拷到了purgatory目录下。实际上他可以直接使用,但貌似没有成功。才会退而求其次,选择了copy。

目前,该patch发布了版本3,状态仍是“request for comment”。这组patch还有未尽的功能,首先就是签名验证。目前,还仅支持X86_64体系和bzimage的内核格式。后续还需要支持其它的体系结果和ELF格式的kernel image。此外还得完善文档,包括man。

Goyal解释了他在签名验证上的想法。它基于David Howells在加载内核模块时进行签名验证的工作。本质上,每次调用kexec_load_file()时都会验证签名。此时,还会计算每个segment的sha256 hash,存储在purgatory段中。purgatory必须验证这些hash,以确保被执行的是一个正确签名的内核。

该组patch的每一个版本都得到了很多评论,其中大多数都是技术性的改进意见。尚没有人反对这个想法本身。所以在明年的某个时候,我们就能看到kexec将执行带有加密签名的kernel。希望会更快一些。当Garrett的安全启动patch被merge时,Goyal的patch将会很有用。

Teaching the scheduler about power management

在移动领域中,高能效的CPU调度变得越来越重要。然而在降低大规模数据中心电费开销上,高能效CPU调度也变得同样重要。遗憾的是,内核CPU功耗控制部分与调度器的结合非常有限,使调度策略不够完善。本文总结CPU功耗控制机制的现状,关注正在进行的改进。

历史

进程调度器是操作系统的关键组件,它决定接下来哪一个进程执行。Linux调度器的实现经过了多年的演变,甚至是完全重写。由Ingo Molnar实现的完全公平调度器(Completely Fair Sheduler,CFS)在2.6.23内核中被引入,它替代了O(1)进程调度器。O(1)进程调度器同样也是由Ingo在2.5.2内核中引入替代了之前的调度器。不管这些调度算法有什么区别,它们的目标都是一样的:尽可能使用CPU资源。

在这段时间CPU资源也发生了变化。最初,调度器只负责在所有可运行的进程间管理处理器时间。随着SMP、SMT和NUMA的出现,硬件并行度的增加使问题变得更复杂。另外,调度器还需要面对不断增加的进程和处理器数目,其本身还不消耗过多的调度时间。这些变化解释了在过去半个多世纪多个调度器被设计开发,并且目前也被不断研究的原因。在其发展过程中,调度器复杂度不断增长,只有少数人成为了这方面的专家。

最初的进程调度只考虑吞吐量,完全不需要考虑能耗;调度器工作由企业驱动,这里的系统都使用固定电源。另一方面,嵌入式和移动领域使用电池的设备逐渐出现,能耗成为关键问题。解决能耗问题的子系统,如cpuidle和cpufreq被分别加入内核,它们的开发者并没有很多调度器经验。

至少在初期,这种分离的安排效果不错,分离的子系统降低了开发和维护的难度。随着移动设备性能的增长和大数据中心开销的增加,人们开始关心能耗的问题。这催生了很多关键的变化,包括可延时定时器(deferrable timers)、动态tick(dyntick)和运行时功耗控制(runtime power management)。多核移动设备的流行甚至导致出现了备受争议的CPU offlining技术。

这些变化显现出一种模式:调度器和功耗管理越来越复杂,它们也变得越来越分离开来。由于一方面不能了解另一方面的变化趋势,这种模式出现了适得其反的效果。尽管这样,芯片制造商还不断在操作系统外的硬件上实现DVFS(http://en.wikipedia.org/wiki/Voltage_and_frequency_scaling), 使问题加剧。ARM big.LITTLE(http://lwn.net/Articles/481055/) 问题的支持和调度器修改对功耗的影响越来越大,使功耗管理和调度器的合并无法避免。

调度器和cpuidle

当CPU空闲时,cpuidle子系统进入低功耗状态或者叫空闲状态(C态)以降低功耗。然而,使一个CPU空闲也是有代价的:进入低功耗状态程度越深,将CPU从该状态唤醒需要的时间越长。需要平衡实际降低的功耗和进入推出低功耗状态花费的时间。更进一步,进入某些状态时CPU的过渡就不可避免花费一定数量的电量,这意味着CPU必须处于空闲状态的时间足够长,进入空闲状态才是有意义的。大多数CPU有多个空闲状态,以方便多种情况下的功耗控制和唤醒延迟的平衡。

因此,cpuidle控制必须收集cpu使用信息,在CPU支持的空闲模式中进行选择。这些信息收集的工作使调度器变得更加复杂,即使通过不精确的启发式的算法。

选择深度不同的空闲状态由将CPU从空闲状态唤醒的事件决定。这些事件可以分为三类:

  • 可预测的事件:包括可以获得到期时间的定时器,可以设置空闲状态的时间。
  • 半可预测的事件:通常是重复事件,如IO请求完成,通常遵循一定的模式。
  • 随机事件:其他,如敲击键盘、触摸屏事件、网络包等。

把调度器加入到选择深度不同的空闲状态中,非常有利于对半可预测事件的处理。IO模式与发起的进程和设备密切相关。调度器可以记录每个进程的IO延迟,结合IO调度器的信息,根据某个CPU上等待的进程估计下次IO请求完成的时间。从调度器的角度出发更容易掌握CPU处于idle状态的时间。

因此使调度器和cpuidle结合更紧密,由调度器管理可选择的空闲模式最终接管cpuidle很有必要。把idle循环移到调度器中,也有助于统计空闲时CPU响应中断的时间和中断出现的次数。

此外,调度器了解当前的空闲状态也有助于负载均衡。例如对于/kernel/sched/fair.c中的函数find_idlest_cpu()来说,它通过比较CPU负载选择其中负载最小的CPU。如果多个CPU处于空闲状态,那么它们的负载都是0,这时选择空闲状态最早结束的CPU是最合适的。如果所有空闲CPU的结束时间是一样的,最晚进入空闲状态的CPU cache是最新的(假设空闲状态保留cache)。Daniel Lezcano已经提交了一些这样的补丁(https://lkml.org/lkml/2014/3/28/181) 。
这也说明了角度不同,一个事物的含义有区别。调度器中的find_idlest_cpu()函数仅仅实现了find_busiest_cpu()的相反功能,而在cpuidle上下文中它表示选择idle状态最深的CPU。处于idle状态越深,使CPU恢复工作的开销越大。同样对于“power”这个词来说,调度器中传统的含义是“消耗的能量”,在功耗控制里它的意思是“能量消耗速率”。最近的一些补丁(https://lkml.org/lkml/2014/5/26/614) 解释了这个问题。

调度器和cpufreq

调度器记录各CPU上的可调度程序工作量,公平的分配CPU资源给进程,决定负载均衡的时机。按需cpufreq控制器(Ondemand cpufreq governer)也做类似的记录,动态调整cpu频率以延长电池寿命。由于功率和电压的二次方成正比,比较合适的方法是在保证CPU足够快的基础上,尽量降低时钟频率,以降低CPU工作电压。

与cpuidle类似,cpufreq也是与调度器分别开发的,两个子系统之间也有一些问题:

  • cpufreq用大量代码以间接的方式获得CPU负载,而调度器已经记录了这些信息。
  • 调度器可以计算单个进程的负载,cpufreq则不能。在任务迁移和任务唤醒这样的情境下,调度器可以预先得到目标CPU上的负载估计,而cpufreq只能在事后作出反应。
  • 调度器记录进程执行时间保证所有进程间的公平。由于调度器无法获得cpufreq信息,对于相同的工作量来说,在时钟频率下降的CPU上执行的进程时间比在时钟频率正常CPU上执行的进程时间长。公平性收到了影响。
  • 由于cpu频率下降,任务负载的显著增加可能会导致任务负载迁移到其他处理器,尽管使任务负载增加是cpufreq的初衷。
  • 如果上述任务迁移的目标CPU的频率也被调低,它可能会过载。由于调度器和cpufreq缺少协同,它们中的任何一个(或者同时)都可能将任务回迁或者提升处理器频率。CPU频率恢复后,同样的过程可能会重复。

为了解决这些问题,cpufreq需要与调度器结合更紧密。底层平台相关的cpufreq驱动不会改变,然而控制部分(控制何时查询那个处理器)需要被完全绕过。实际上,调度器本身可以注册成为一个新的cpufreq控制器。

调度器和cpufreq更紧密的联系可以使调度器预先而不是时候对CPU频率变化做出反应,cpufreq也可以更好的与负载均衡协同。当fork()、exit()和进程休眠或者唤醒发生时,负载发生变化,CPU频率可能需要调整。频率调整的策略可以根据具体事件和进程活动记录而有区别。

然而,在CPU频率变化的情况下调度器还需要实现频率无关的负载统计。需要在负载统计中引入一个因子,使得进程在不同频率的CPU上迁移时,它的负载可以事先计算。随频率变化的CPU计算能力也需要变化以更好进行负载均衡。然而目前无法准确给出这个因子,进行大量访存操作的进程受CPU频率变化的影响少于计算型进程受的影响。即使这样,效果也要比当前完全没有这个因子要好。

顺便提一下,频率无关的负载统计也适用于big.LITTLE调度。“小”的CPU可以看做永久降低频率的处理器,比“大”的CPU处理器能力也小一些。用不同的因子处理器“大”和“小”的CPU,对进程工作量的统计正好合适,不需要再对调度器进行修改。

多位开发者,包括Mike Turquette和Tuukka Tikkanen正在处理cpufreq intergration的问题,早期的补丁马上就会出现。

调度器:功耗

在我们实现了cpuidle、cpufreq和调度器的集成之后就完美了吗?不,相比以前,新的机制调整后我们需要做更多重要的决定,这些决定与调度器如何使用这些机制完成负载均衡有关。例如:

  • 系统负载增加后,是将处于idle状态的CPU唤醒,还是提高正在运行CPU的频率,或是两者都有?
  • 相反的,当系统负载下降后,应该将所有处理器的频率降低,还是将任务集中到少数的处理器上?
  • 应该集中使用少数CPU还是将负载分散到所有处理器?
  • 执行active task packing使一个cpu集合(cluster, package)进入低功耗模式的时机是什么?

其他

我们非常需要测量工具验证解决方案。Linaro正在开发idlestat(https://wiki.linaro.org/WorkingGroups/PowerManagement/Resources/Tools/Idlestat) 以验证idle状态的使用和效果。传统的benchmark如sysbench(http://www.howtoforge.com/how-to-benchmark-your-system-cpu-file-io-mysql-with-sysbench) 可以和energy usage monitoring(https://lkml.org/lkml/2014/6/6/286)结合,展示系统的功耗特性。扩展cyclictest(https://rt.wiki.kernel.org/index.php/Cyclictest) 人为产生负载的方法也在进行探索。我们还是需要更高的集成度和自动化。
本文并没有涉及温控(http://en.wikipedia.org/wiki/Thermal_management_of_electronic_devices_and_systems)。 Linux内核设计了一个温控接口以允许用户态守护进程控制温度。然而,正如我们看到的,功耗问题不是独立的,不涉及调度器的温控方案也不能达到最优。调度器可以对功耗进行控制,它也可以控制平台温度(http://lwn.net/Articles/599598/)。这个问题可以以后讨论。
致谢 感谢Amit Kucheria, Daniel Lezcano, Kevin Hilman, Mike Turquette, and Vincent Guittot。

2038 is closer than it seems

大多数LWN读者可能都了解在2038年1月即将降临的厄运。在2038年1月,用来存放时间的time_t类型(自1970年1月1日起,单位秒)在32位系统上将要溢出。开发者担心未来24年的截止日期令人惊讶,然而他们的担心不无道理。这些担忧是否会导致在近期出现解决方案仍然不明朗;自从去年8月这个话题出现以来,进展并不大。但是最近的讨论至少向我们揭示了解决这个问题可能的方式。

有时,开发者希望这个问题可以自行解决。在64位系统中,time_t类型一直是一个64位变量,不会很快溢出。考虑到64位系统正在占据主导—即便是手机也将要在接下来的几年里完成转变—最好的解决方式是等32位系统消失吗?不需要改动的方案吸引力非常明显。

然而,这个推论有两个问题:(1) 32位系统将要继续被生产的时间比大多数人想象的更久,(2) 目前生产的32位系统可以存在24年或是更长的时间。32位系统会在相当长的时间内被用作廉价的微控制器。由于难以升级甚至无法升级,人们期望它们工作很长时间。几乎可以肯定已经存在的某些系统会在2038年给大家带来意想不到的惊喜。

内核解决方案

显而易见,尽快解决这个问题比推迟解决(比如说到2036年)更有意义。只有一个问题:这个问题不容易解决。至少如果考虑不危害现有系统这样的细节,解决这个问题并不容易。由于Linux开发者通常非常关心兼容性的问题,简单的解决方案(如BSD式的ABI分离)并不可行。在最近的讨论中,John Stultz概括了几种替代方案,这些方案都不简单。
第一种方案对32位ABI进行修改,使用64位的time_t类型(相关的数据结构如timespec和timeval也需要修改)。已经存在的二进制可执行文件可以通过兼容的接口支持,新编译的二进制文件则直接使用新的ABI。这种方案最大的优势是大量的应用重新编译后就可以升级。部分BSD变量已经采取这种修改方案,修正了一些严重的问题。嵌入式微控制器上的系统通常是完整由源代码编译而来;这种ABI修改方式能使这些系统适应2038,代价是近期的一些阵痛。
另一方面,内核则需要在相当长的时间内保留关键的兼容层。除此之外,开发者还担心很多应用在自身的数据结构中使用32位time_t变量,并保存在硬盘中。这些应用可能因此而崩溃,而这样的问题难以解决。64位time_t变量在32位系统上的运行开销也引起了担忧。通过在内核中定义另一种变量的方式可以缓和这些开销,但应用程序也可能变慢。
另一种方案是直接定义新的系统调用,新的系统调用从头使用更好的变量定义。新的变量定义也可以带走其他一些烦恼,比如不是所有人都喜欢timespec结构中分开的seconds和nanoseconds域。使用time_t变量的系统调用在2038年之前也许可以完全停止。
采用这种方案不会在近期带来生硬的ABI剥离,应用程序可以逐渐统一。同样的,嵌入式微控制器系统可以在近期由使用新的系统调用的代码完整编译而来,现有桌面系统可以继续使用十几年。这也是重新设计满足21世纪需求的系统调用提供了契机。
定义新的系统调用也有其避免,它使Linux离真正的POSIX系统越来越远,把我们带上一条与BSD界完全不同的道路。大量的系统调用需要被替换,time_t类型也在其他地方使用,最容易想到的是在大量ioctl()系统调用中。应用需要升级,包括运行在64位系统上的那些,而这些系统不会因为这次升级得到优化。并且毫无疑问,在2037年仍会有大量应用使用旧的系统调用。总之,这种解决方案也不简单。

修改glibc

对解决方案的讨论进行了很长时间,直到Christoph Hellwig给出了事后看来非常明显的建议:C库的开发者必将与任何解决这个2038-问题方案有关,也许现在他们就应该参与讨论。长久以来,内核社区和C库(包括GNU C库——glibc)开发者最多也只有零星的交流。glibc维护者的变化使建设性的对话更容易产生,然而要改变旧的习惯也很困难。无论如何,glibc开发者都应该参与到这个问题的解决。好消息是这似乎将要发生。

glibc开发者不喜欢ABI分离,也不喜欢非POSIX式的接口。glibc开发者Joseph Myers一参与进来,风向向着一个平滑的维持现有的POSIX系统调用同时保持应用程序兼容的过度方案逐渐转变。初步的方案大致是这样:

  • 为受影响的系统调用创建新的64位版本。例如gettimeofday64()返回timeval64类型的时间结果。这些系统调用的现有实现将会得到保留。
  • Glibc会包括一个新的特性检测宏,例如TIME_BITS。如果32位系统上的TIME_BITS等于64,gettimeofday()系统调用会在库内部被映射为gettimeofday64()。应用程序只需要定义合适的TIME_BITS值就能过度到新的世界。
  • 最后,也许在使用这个方案一段时间后,TIME_BITS==64会是默认的设置。即使在64位系统中,也需要兼容性符号使旧的二进制在新的库支持下工作。

这种解决方案可能会允许一个平稳的过渡期,然而还是存在一些头疼的问题。这个方案可能要用同样的方式映射ioctl()系统调用,考虑这些系统调用庞大的数目以及确定每一个这样系统调用的难度,这看起来像是自找麻烦。其他C库的开发者如果不想保持glibc中的兼容性扩展,可能希望采取其他的方案。

然而,即使存在挑战,有内核和glibc开发者讨论产生的初步方案已经给了我们希望。也许在2038问题变得真正紧要起来之前,会有合理而又健壮的解决方案,运气好的话希望在大量在2038年工作的系统开发之前。今天,我们有机会避免2038危机;如果我们利用好这些机会,未来我们会感到庆幸。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章