The future of the page cache
持久化内存用得越来越多, 促使了内核的一系列变更, 内核是否还真的需要页面缓存呢? 在2017 linux.conf.au会上, Matthew Wilcox先是纠正了数年前的一个错误,然后表示, 我们不仅需要页面缓存,还要将他的作用将进一步得到提升。
他从他作为微软员工的时候开始讲起,以前他以为不会提及这个。 然后进入主题,内容如下, 计算机就是缓存的世界。只要缓存都命中,他的新电脑每秒可以执行100亿条指令。但是内存每秒只能跑5亿3千万条cache line,因此缓存未命中就会严重影响性能。如果数据没有缓存到主存,需要从存储设备读, 即使是快速的SSD,也会变得很慢。
计算机就是这样,PDP-11也会因缓存未命中而显著变慢。更严重的是,CPU的发展速度比内存快,而同时内存的发展速度相比存储也更快。缓存未命中的带来的性能损失也会越来越严重。
页面缓存
很久以来,Unix系统都有缓冲区缓存,位于文件系统与磁盘之间,目的是为了缓存磁盘块到内存中。为了准备这次演讲,他回过头查阅了1975年发行的Unix第六版,并在那里找到了使用缓冲区缓存的例子。Linux从一开始就有个缓冲区缓存。在1995年发行的1.3.50版本中,Linus Torval做了一个牛逼创新, 页面缓存。页面缓存与缓冲区缓存的区别在于,它是位于虚拟文件系统(VFS)与文件系统本身之间。有了页面缓存,如果所需的页面已经存在,则根本无需调用文件系统的代码。起初,页面缓存和缓冲区缓存是完全独立的,在1999年,Ingo Molnar统一了它们。现在,缓冲区缓存仍然存在,但是内容是指向页面缓存。
页面缓存有非常多的内置功能。明显的如通过给定的索引查找页面;如果页面不存在,则创建并选择从磁盘填充。脏页可以刷回磁盘,页面可以被锁定,解锁,以及从缓存中删除。线程可以等待页面状态的变更,同时有以给定状态搜索页面的接口。页面缓存也能够追踪与持久化内存相关的错误。
页面缓存内部处理锁机制。在内核社区,应该在哪层之上处理锁存在分歧,但是内部实现锁是肯定的。当页面缓存被修改时,有个自旋锁以控制其访问;而查找则通过无锁的读-拷贝-更新(RCU)机制来处理。
缓存是预测未来的艺术,他说。当缓存增长太大时,各种启发算法开始决策哪些页面应当被移除。仅使用了一次的页面很可能不会被再使用,因此它们将保留在“不活跃”链表并相对较快的换出。第二次使用将会把页面从不活跃链表提升到活跃链表。活跃链表的页也会因为超时而被移到不活跃列表。 有一个例外,“影子”条目用于追踪已脱离不活跃链表并已回收的页面,这些条目可以延长相对遥远的过去使用过的页的生命周期。
一段时间以来,大页一直作为页面缓存的一个挑战。内核的透明大页(THP)特性最初只用于匿名(非文件后端)内存,尽管在页面缓存中使用大页有很多优点。该领域的最开始的工作就是简单地往页面缓存中增加了大量单页条目,以对应于单个大页。Wilcox认为这种方法是“愚蠢的”,他增强了用于追踪页面缓存中页面的基数树代码,以能够直接处理大页条目。待提交的patch将使得页面缓存可使用单个条目对应大页。
我们是否仍然需要页面缓存?
近期,Dave Chinner预测将不再需要页面缓存。他指出,最初由Wilcox创建的DAX系统,支持直接访问持久化内存,完全绕过页面缓存。Wilcox说,“没有什么比你的同事怀疑你的整个动机更糟糕”。但也有其他人不同意Chinner,包括Torvalds。Torvalds在一个单独的论坛指出页面缓存很重要,因为在数据访问的关键路径上,好的东西不是来自低层的文件系统代码。
在最后,Wilcox深入讲了IO请求在DAX系统上如何工作。他在设计DAX原始代码的时候, 没有使用缓存。但是这个决定是错误的。
在当前的内核中,当一个应用使用类似于read()的系统调用从存储在持久化内存中的文件读取数据时,DAX介入。由于请求的数据不存在于页面缓存中,VFS层调用文件系统特定的read_iter()函数。这反过来调用到DAX代码,它将回调到文件系统将文件偏移转换为块号,然后查询块层以获取持久化内存块的位置(如果需要,将其映射到内核地址空间),最终使得块内容可以被拷贝回应用。
这“不可怕”,但理应以另外一种方式工作,他说。初始步骤应当相同,因为read_iter()函数仍将被调用,同时它将调用到DAX代码。但是,DAX不是回调到文件系统,而是应当调用到页面缓存以获取文件中所需偏移关联的物理地址,然后数据从该地址拷贝到用户空间。虽然这一切都假定信息已经存在页面缓存中,但在这种情况下,低层文件系统代码完全无需介入。文件系统已经完成了这项工作,同时页面缓存也缓存了结果。
当Torvalds写了上述关于页面缓存的帖子时,他说:
从锁定角度来看,这也是一个重大的灾难:相信我,如果你认为你的文件系统可以进行细粒度的锁定,当诸如并发路径查找的事情到来时,你会生活在一个梦幻世界。
这是“如此正确”,Wilcox说。DAX中的锁定的确是灾难性的。他最初认为可能用相对简单的锁定来解决,但复杂性在每个新发现的边缘场景蔓延。DAX锁定现在“实在丑陋”,他很抱歉他犯了一个错误,认为可以绕过页面缓存。现在,他说他必须去解决。
未来的工作
他想重新考虑文件系统块大小大于系统页面大小的想法,这是人们多年想要的东西。现在页面缓存可以处理多个页面大小,应该是可行的。“一个简单的编码问题”,他说。他正在找寻其他感兴趣的开发人员一起来做这个项目。
巨大的交换条目也是感兴趣的领域。我们在内存中有大量的匿名页面,但当换出时,他们被分解成正常页面。“这可能是错误的答案”。目前有提升交换性能的工作,但需要重新调整以保持大页在一起。这可能有助于交换到持久化内存的关联想法。持久化内存交换空间中的数据仍然可以被访问,因此将其留在那可能是有意义的,尤其是没有被大量修改。
The Machine: Controlling storage with a filesystem
HPE的新机器:The Machine HPE即HP Enterprise。之前的LinuxConf上,来自HPE的Keith Packard向大家展示过他们正在开发中的名为The Machine的新架构机器。而本次2017的北美LinuxConf上,Keith Packard着重展示了这个新机器中存储系统管理方面的一些特点,和The Machine本身一样,这一类的系统往往是几个新主意加上若干早已存在的成熟技术拼接而成。
简单介绍一下The Machine(详细的介绍可以直接看https://lwn.net/Articles/655437/, A look at The Machine)。The Machine差不多是一种针对传统服务器的Rethink。基础想法是传统服务器是以cpu为中心的,把数据从各种外存零散地传送到分散在处的cpu上加以处理,而The Machine则是要以数据为中心----在拓扑结构的中央放置一个非常高速的庞大持久化内存(计划中一台机器最终会达到300TB内存,目前的实现全是DRAM,未来要切换到真的persistent memory上)。庞大内存的内部靠光通信同步,因此严格从物理意义上看它显然还是NUMA的,但不同node之间的速度差异已经足够小,程序员可以认为主存速度是均一的,即UMA。庞大的主存区域外围环绕着大量cpu,目前用的是ARM64,一台The Machine计划装80个ARM64。
由于每一个cpu都具有通过Load/Store指令直接访问持久化主存的能力,传统的文件系统和磁盘、块设备抽象就不再需要了,每一个cpu都直接面对自己要处理的数据的唯一拷贝。Packard把这种设计叫做”memory-drivencomputing“,内存驱动计算。
The Machine上的众多cpu们对于这片大家共享的主存区域有各种管理需求,其中比较重要的一点是The Machine的各个cpu不被认为是可信的,因此它们每个人能访问到的主存区域都各有限制。Packard承认最初他完全从头设计了一套API,但很快就意识到这个API和传统的POSIX式文件接口非常相似。索性就直接用文件系统抽象来管理这个庞大的主存区域了。
于是Packard用FUSE实现了一个使用POSIX文件式风格管理内存的系统,称为LFS(LIbrary Filesystem),它把主存分成若干8GB片来管理,目前每台The Machine有300TB内存,所以一共也就是四万左右的分片需要照顾,元数据量 并不大。谈到分片,Packard也承认可以用LVM2来管理它们,但是从头设计一个LFS有额外的好处。比如各种接口比起用lvm tools来要简洁得多:touch文件意味着建一个新的volume set、fallocate意味着要做实际的空间分配了,等等。
LFS直接面对主存,下边不再有块设备等等的抽象。这使得它不能使用一些传统的组件,比如软RAID。Packard认为这可以接受:软RAID在直接读写持久化内存的场景下没有需求。使用文件系统抽象的一大挑战是:你要如何表示那些传统上通过NUMA API来控制的东西,例如程序员知道这个文件是放在主存上的,也知道主存是NUMA的,他想指定这个文件一定要放在某几个结点上。POSIX API没有这样的语义,Packard选择了使用XATTR来表达这些需求。
https://github.com/FabricAttachedMemory 提供了一个The Machine模拟器供有兴趣的读者进一步深入研究。
kvmalloc()
内核提供有两种基础的内存分配机制,一种是slab分配器,用于在内核自己的地址空间分配物理地址连续的内存,使用这种分配器的典型代表是kmalloc。另一种是vmalloc,用于在一个独立的地址空间分配虚拟地址连续但物理地址可能不连续的内存。
slab分配器几乎是大部分内存分配首选,在没有内存压力的时slab分配器不用修改地址空间从而更加快速。slab分配器最适合的是小于一个物理页大小内存的分配;当内存碎片化之后,物理地址连续的内存页会变得很难查找,系统性能也会由于分配器要不断的合并出物理地址连续的页而变差。
vmalloc分配的内存不要求物理地址连续,因此在内存紧张时更容易成功。但多方面原因也导致过度使用vmalloc会有很多阻碍:1)每次vmalloc分配完成需要更新页表,并刷新tlb; 2)vmalloc只能够分配整个物理页,因此也不适合小内存分配;3)在32位系统上,vmalloc分配的地址范围是有限的(但是在64位系统上目前已经没有这个限制)。
内核里面有很多地方必须要求分配物理地址连续的大内存,但更大部分其实并不没有这个要求。对于不要求物理地址连续的分配请求,其实并不关心是使用kmalloc或者vmalloc分配,只要能够分配就ok。对这一类的请求,可以先尝试从slab分配器分配,如果失败再回退到使用vmalloc。内核里面也确实有很多不一样的代码在做这同样一件事。
然而,就像Hocko指出的一样,这其中一些代码很有"创意",但很多代码并不如他们期望的一样工作。考虑如下代码:
memory = kmalloc(allocation_size, GFP_KERNEL);
if (!memory)
memory = vmalloc(allocation_size);
这段代码的问题在于,对于小内存(小于等于8个物理页)的分配,kmalloc会一直重试,而不是返回失败。这种情况下,回退到vmalloc的这段代码并不会执行。更糟糕的是,kmalloc为了满足分配请求甚至可能调用oom killer杀死一些未预料的线程。kmalloc的这种机制在某些情况下确实是有必要的,但是上面这段代码,这个并不在这些情况里面。
这就需要有一组接口能够使用这种回退机制,又同时能够最小化这种回退机制带来的影响。最终Hocko的一组patch增加了以下几个新接口函数:
void *kvmalloc(size_t size, gfp_t flags);
void *kvzalloc(size_t size, gfp_t flags);
void *kvmalloc_node(size_t size, gfp_t flags, int node);
void *kvzalloc_node(size_t size, gfp_t flags, int node);
正如大家期望的,kvmalloc首先尝试从slab分配器分配内存,通过使用__GFP_NOWARN和__GFP_NORETRY标志尽量减小在不能立即分配到内存情况下的影响(也避免调用oom killer)。如果从slab分配器尝试分配内存失败,kvmalloc会回退到用vmalloc分配。kvzalloc会在分配到的内存返回之前进行清零操作;_node结尾的接口用于从指定NUMA节点分配内存。和其他内存分配函数一样,这些函数也依旧可能失败。
这组函数使用上有以下两点需要注意:1) 使用这组函数分配小于一个页的内存没有任何意义,回退路径里面的vmalloc并不支持小于一个页内存的分配,因此回退路径在这种情况下会失效;2) 这组函数在原子上下文中工作会有问 题,原因是由于vmalloc不能在原子上下文中调用。
历史上Changli Gao在2010年的时候也提交过一个版本的patch尝试在内核里面增加kvmalloc,但是由于没有考虑到这些不可预期的负面影响,并没有被合并。Hocko似乎找到了让这组patch进入主线的方法,最终让这组实现进入了内核
Last-minute control-group BPF ABI concerns
主线4.10中一个称之为cgroup-BPF的特性被合并,这个新的特性可以将一个BPF(Berkeley Packet Filter)程序附加(attach)到cgroups中,该BPF程序可以通过cgroups中的进程对其接受和发送的包进行过滤。该特性就本身而言并没有太大的争议,并且直到最近,其接口和语义仍然没有较大争议。但自从这个特性被合并,出现了一些不同的声音。开发社区可能不得不决定是否对其进行调整,或者在4.10发布之前临时关闭这个功能。
相关问题的讨论最早是由Andy Lutomirski发起的,第一个问题是:bpf()系统调用被用于附加(attach)一个程序到cgroups,他认为这个系统调用从根本上来说是个cgroups操作而非BPF操作,所以应该通过cgroups的接口进行处理。如果将来其它开发者引入其它类型的程序(非BPF程序),那么仍然沿用bpf()接口将会失去其本来的意义。不管怎么说,他认为bpf()并不是一个足够灵活的系统调用。
这个异议并没有引起广泛关注,看起来并没有多少开发者对添加其它的包过滤机制感兴趣。BPF的开发者Alexei Starovoitov认为其他的机制也可以很容易的基于BPF实现。网络的maintainer David Miller在这个问题上完全同意Starovoitov,所以对于这一点来说,不会有太大的改变。
下一个问题牵扯的更深一些。Cgroups是天然的层级结构的,对于version 2的cgroups接口,用户期待控制器的行为是层级管理的。控制器规则一般来说会下沉到层级的下一级,比如:如果某个cgroup被配置为CPU占用率为10%,那么其子组被配置为占用50%,这意味着其50%是基于其父亲组的10%的一半,也就是说系统绝对资源的5%。BPF过滤器机制并非一个完全的控制器,但他的层级管理行为也会受到关注。
如果程序运行在一个两层cgroups层级中,并且上下两层均有过滤程序被附加(attach),通常认为这两个过滤器都是可以运行的,即两个过滤器的约束条件都是有效的。但事实并非如此,取而代之的是只有低层级过滤器程序在运行,而高层级对应的过滤器则被忽略。上层过滤器打算阻止某些特定类型的通信,但底层的过滤器却重载了这些限制,使其上层的限制无法生效。如果所有层级的过滤器设置都是一个管理员完成的,这个语义可能并不会带来问题,但如果系统管理员希望设置整个容器和用户名字空间,而容器可以添加自己的过滤程序,那么这个行为将使得系统级的设置被旁路。
Starovoitov承认这个问题,最差情况也就是针对给定的层级结构,采用一个组合所有过滤规则的程序。但是他同时认为“当前的语义与设计是一致的”,并且说不同的行为可以在未来实现。问题是如果将来更改语义,会引入ABI的更改,这类更改会打破目前在4.10上的语义,是的系统出现问题,这种更改是不允许的。如何以兼容的方式添加新的语义,目前没有相应的计划,因此我们不得不假设:如果4.10按照当前的行为发布,未来将没有人能够改变它。
其他的开发者(Peter Zijlstra and Michal Hocko)也表达了对这个行为的顾虑。Zijlstra询问cgroups的maintainer Tejun Heo对这个问题的想法,但并没有获得更多信息。Starovoitov确信当前语义没有任何问题,并且可以在未来不打破兼容的情况下进行调整。
Lutomirski的顾虑则更加模糊。直到现在为止,cgroups都是用于资源控制,附带的BPF过滤器的引入则改变了这个规则。这些程序可以作为攻击者运行一些恶意代码。例如:他们可以在输入的协助下介入到一个setUID程序,产生潜在的权限问题。有些程序隐藏的有用信息也会被攻击者发现。
对于现在来说,捆绑一个网络过滤器程序是一个特权操作,所以暂时并不是个大问题。但一些人试图使过滤程序工作在用户命名空间,这将带来更多问题。Lutomirski提出了“未完成提案”,该提案可以阻止创建“危险的”cgroups,除非将来相应的问题得到解决。
再次重申,降低未来系统的风险,需要在最开始就要施加相应限制,这将暗示这个特性需要在4.10发布的时候被关闭。但是Starovoitov之前同意在安全领域展开工作,他再次重申这些问题会在未来某个时间点完成。
这是到目前为止的所有讨论。如果这些讨论没有结论,4.10将要如期发布这个新特性,即便关于ABI和安全的顾虑仍然存在。对于新的API发布时仍然有类似的未回答的问题,历史上有一些教训的。这里给出的结论,仅仅是希望BPF的开发者可以发现和定位语义和安全问题并且不会产生ABI兼容问题。
Making sense of GFP_TEMPORARY
本文主要试图描述 linux 内核内存分配标记 GFP_TEMPORARY 语义的合理性,以及现状。
本文翻译到此也许可以结束了,因为,几乎没有人看到 GFP_TEMPORARY 会联想到背后一丝实际的语义。老实说我以前没关注过这个 flag,也从来没用过,居然还被 mainline 了,不过这就是 linux 的世界。
我们都知道,linux 内核在分配内存的时候一般会通过指定 flags (linux/gfp.h) 来告诉分配器如何处理不同情况(比如:是否分配器可以阻塞),那么像之前的文章里提到的有些 flags 是 common 的有些是 by design 的。
相比 GFP_KERNEL,GFP_TEMPORARY 仅仅增加了__GFP_RECLAIMABLE,也就是说增加了内存页可以回收的标记 (顾名思义了),而这之前__GFP_RECLAIMABLE 主要是被 slab/slub 间接标记的 (SLAB_RECLAIM_ACCOUNT)。
为啥仅仅增加 __GFP_RECLAIMABLE 就变成了 GFP_TEMPORARY?Who knows?
讨论中有些声音是赞成 GFP_TEMPORARY 的,我觉得其实不是赞成 GFP_TEMPORARY 而是觉得 __GFP_RECLAIMABLE 会产生更多潜在的可回收的内存分配,在系统整体上有利于满足高阶内存分配。即使这样,起码换个名?
最后,GFP_TEMPORARY 即将被 Michal Hocko 在后续的 patch 中拿掉,这下真的呼应了 “TEMPORARY” 。
BTW,大家在实际的开发中,本着对代码负责,定义变量也是最好能合理的体现它的语义,关于变量名定义的语义表达,也许够给很多程序员开个专题了。
A pair of GCC plugins
这些年来,很多gresecurity/PaX内核的加固特性由于内核自保护项目的贡献进入了内核主线中。其中之一是4.8内核引入的GCC插件基础架构,该特性在内核代码编译时引入各种类型的保护。还有其他很多特性引入到4.9内核代码中,比较重要的是latent_entropy插件。此外最近两个引入的插件是kernexec来组织内核执行用户空间代码和清理从用户空间拷贝数据结构的structleak插件。
kernexec
攻击者经常会通过引诱内核执行一些用户态代码来攻击系统。通过这一方式,攻击者可以以内核权限来执行他自己的代码。为了防止这个问题的发生,英特尔和ARM的CPU上分别实现SMEP和PAN技术来保护系统。
但是对于那些没有SMEP的英特尔CPU来说,kernexec可以提供相同的保护功能。一月中旬,Kees Cook提交了第一版kernexec插件的代码。该插件通过将内核代码所在地址的最高比特位置位来实现保护功能。所有内核地址空间中的函数地址的最高比特位被置位后,系统在调用函数前会检查最高比特位是否置位。因为用户态函数内存地址的最高比特位没有置位,因此系统就可以发现内核将要执行用户态代码,并生成一个通用保护错误。
添加内核加固特性后的性能开销总是被特别的关注。为了优化性能,插件会尝试优化函数调用和返回指令。在函数调用过程中,使用一个寄存器并进行或运算来实现最高比特位的检查。在函数返回时,使用btsq指令来检查返回地址的最高比特位。
Cook特别注明当前的插件还不能支持内核汇编代码的部分。也就是说汇编代码仍然可以调用和返回到用户态内存地址上。
structleak
内核结构(或包含在其中的字段)经常被复制到用户空间。如果这些结构不进行初始化,它们可能包含一些“interesting”的值,而这些值是存在于内核内存中。如果攻击者可以安排这些值与内核结构对齐,并将它们复制到用户空间,最终就会导致内核信息泄漏。cve-2013-2141就是这种类型的信息泄露;这也就促使“PaX Team”(Pax补丁集的作者)创建structleak插件。
Cook还在1月13日发布了该插件的端口到内核邮件列表。它会在函数里的局部变量结构中查找__ser属性(这是一个注释,用来表示用户空间指针)。如果这些变量没有被初始化(因此也可能会包含堆栈“垃圾”),插件就会清理它们。这样的话,如果这些值在某个时候被复制到用户空间,也不会有内核内存内容的暴露。
PaX Team在补丁发布中也进行了评论,不过大多是建议调整一些插件的文本说明。特别是,Cook已经改变了在Kconfig描述的插件描述。然而,Cook对于那些变化有合理的理由。
此外,一个Kconfig选项用来打开structleak详细模式的(gcc_plugin_structleak_verbose)不符合PaX Team的标准。需要指出的是,可能会发生误报,这是因为“不是所有现有的初始化由插件检测“,但PaX Team对此表示反对:“一个变量要么有一个构造函数要么没有”。但Cook不这么认为:
正如指出的那样,在[插件]报告需要初始化变量时有大量的误报。它并没有报告说,缺少一个构造函数。 这是对正在发生的事情进行一个务实的描述,因为插件有时在不需要的地方确实没有必要初始化,那对我来说真的是一个假阳性。
除了选项的问题,正如Mark Rutland指出的,这__user注释并不是一个真正的迹象用于表明有问题:
对我来说,似乎__user注释只能是发生偶然问题的一个指标。我们有__user指针结构,而这些结构永远将不会被复制到用户空间,相反我们有这样的结构,它们不含__user注释,但将被复制到用户空间。
他建议,分析 copy_to_user() 的调用可能会有更好的检测。PaX Team也表示同意,但同时也说,最初的想法是要找到一个简单的模式匹配来消除cve-2013-2141和其他类似的错误。既然错误已经消除了,插件是否还有问题还不清楚,但没有理由不保持它,PaX Team说:“我把这个插件放在这里是因为维护无需成本,并且替代它(更好的)解决方案还不存在。”
这些都是相当简单的功能,可以防止内核错误被攻击者使用。从这点来讲,structleak可能并不真正被需要,但新的代码可能引进一个相似的问题,而没有特定的插件用于解决这些问题。另一方面,Kernexec有潜力去阻止那些依赖于内核执行用户空间代码的攻击。现在这两个插件已经存在了一段时间,让它们进入upstream,这样就能使发布者开始建立他们的内核,从而使他们手中有更多Linux用户,这会是一件好事。希望我们会看到有些人使它们很快进入主线。
Unscheduled maintenance for sched.h
在内核的发展过程中,对头文件的维护远没有像C文件那般重视。4.10内核包含18,407个头文件,只有不到10,000个头文件会在特定子系统外部被引用。然而,在内核0.01版本,总共才31个头文件。以为例,4.10中该文件达到3,674行,还直接引用50个头文件,而这其中的许多头文件又会进一步引用其它头文件。
臃肿的头文件会降低内核编译的速度。假如sched.h冗余1000行代码,被2500个文件引用,则内核编译阶段需要多处理250万行代码,严重影响编译速度。同时,臃肿的头文件也难以维护。
Ingo Molnar, CPU调度器的主要维护者,决定以sched.h为切入点,开启精简头文件的工作。主要方法是将sched.h中的数据结构和函数接口分类,拆分成多个更小的头文件。这是一个繁琐的工作,许多引用sched.h的文件都需要重新引用新的更细粒度的头文件。整个工作下来,涉及将近1200个文件的修改。
工作虽辛苦,但效果显著,重新整理sched.h后, all-yes-config kernel build节省了30秒。目前这部分工作由于patch数量多,review不便,还没有决定在哪个窗口期进入主干分支。可以肯定的是,将来内核头文件将会变得更清爽,后续还会对其他臃肿的头文件进行改造,如<linux/mm.h>等。
Understanding the new control groups API
过去几年,Linux的cgroup部分经历了一次重写,修改了很多API。理解这些变化对于开发者来说非常重要,特别是那些做容器相关的项目的开发人员。 这篇文章将会过一遍cgroup v2的一些新加的特性,这些特性在4.5的内核中已经宣称达到生产级。这篇文章主要基于作者在西班牙Seville开的Netdev1.1会议上做的一次talk, talk的链接见: https://www.youtube.com/watch?v=zMJD8PJKoYQ。
background
cgroup子系统已经与其关联的控制器计量并管理机器上的各种资源,包括CPU、内存、I/O等等。cgroup子系统以及namespace子系统(出现得比cgroup早且相对更成熟一些)是构成Linux容器的基础。当前,绝大多数涉及到Linux容器的项目,像Docker、LXC、OpenVZ、Kubernetes等,都是同时基于两者。
Linux cgroup子系统的开发始于2006,最早在Google开始,由Rohit Seth和Paul Menage主导。最开始,项目的名字叫"Process Containers",但是后来为了避免跟Linux containers造成混乱改名为"Control Groups",现在,所有人都直接简称"Cgroups"。
当前cgroups v1有12个cgroup控制器,除了PID控制器之外的其他11个控制器都已经存在了数年之久,PID控制器是由Aditya Kali开发并合并到4.3的内核之中。它允许限制一个control group中创建的进程数量,因此它可以用作反"fork炸弹"(anti-fork-bomb,指一直fork新进程的程序)的解决方案。Linux的最大的PID空间数量大概也就4百万个(PID_MAX_LIMIT)。按照今天的RAM容量,这个限制很容易并且可以很快被单个容器中的一个“fork炸弹"耗尽。cgroups v1和cgroups v2都支持PID控制器。
这些年来,对于cgroups的实现有很多批判的声音,因为它有不少不一致性以及混乱的地方。例如,当创建一个subgroup(在一个cgroup中再创建一个cgroup),有好几个cgroup控制器的实现都会把参数传递给他们的直接subgroup,而另一些cgroup控制器却不传递。又如,一些cgroup控制器使用的接口文件(如cpuset控制器的clone_children)出现在所有的cgroup控制器中,却仅仅只有一个控制器会用到它。
作为cgroup的maintainer,Tejun Heo自己也承认,cgroups v1有不少缺点:”实现先于设计“,”不同的控制器使用不同的方法“,“有时候太灵活反而成了拦路虎”。在一篇2012年的LWN文章中,这么说到“cgroup是内核开发者又爱又恨、相爱相杀的一个功能”。
Migration
cgroups v2 在kernel 4.5版本中宣称已达到生产级别;但cgroup v1的代码仍然在内核树中,且v1跟v2都是默认打开的。目前v1与v2允许混合使用,但是对于某一特定类型的cgroup无法同时使用。值得一提的是,在kernel4.6中,已经添加了一个命令行启动参数(cgroups_no_v1)来强制关闭v1的逻辑,相应patch见: https://lkml.org/lkml/2016/2/11/603.
只要存在用户态应用程序继续使用v1接口,那么内核里v1代码将继续存在,这可能持续好几年。好消息是,一些用户态程序(比如systemd与CGManager)已经开始往v2接口升级。Systemed使用cgroups来管理服务,每个服务都被映射到了一个单独的控制组,对v2部分支持。同样,CGManager也是部分支持v2。
用户通过挂载文件系统来使用这两个版本的cgroups。过去几年,v1有一个挂载选项(__DEVEL__sane_behavior)允许用户使用一些实验性质的特性,其中的部分特性是v2的基础功能,该选项在kernel 4.5中已经删除。比如,v1中使用该选项后会强制使用统一分层模型,这在v2中是默认支持的。__DEVEL__sane_behavior选项与 noprefix, clone_children, release_agent等互斥,后者在v2中已删除。
v2已经支持了三种cgroup:I/O,memory,PID。CPU cgroup已有相关patch以及邮件列表讨论。改进的同时也带了一些问题,比如v1支持配置同一进程的不同线程至不同的cgroup,v2不支持。这导致了一个问题:如何对同一进程的不同线程分配CPU资源,v2中是无法支持的,因为所有线程只能属于同一控制组。好在Heo提交了一个resource cgroup的patch来解决这个问题,可以看做是setpriority()系统调用的一个扩展。
Details of the cgroups v2 interface
这一段主要介绍了 cgroup v2 的接口细节,原文比较啰嗦,翻个大概意思:
首先是挂载方式,大致命令如下:
mount -t cgroup2 none $MOUNT_POINT
重点是 -o 选项没了,所以你无法指定这个挂载点是什么类型的 controller,事实上 cgroup v2 是单一层次结构的(single hierarchy),所以无需 -o 选项来指定了。挂载上去之后根组多了三个接口文件:
- cgroup.controllers - 显示支持的 cgroup controller;
- cgoup.procs - 显示 attach 进来的进程,这一点和 v1 差不多;
- cgroup.subtree_control - 这大概是和v2的精华接口,因为 v2 不支持 -o 指定 controller 了,需要用这个文件来控制 controller 的开关。只要运行下面的命令:
echo "+memory" > /sys/fs/cgroup2/cgroup.subtree_control
echo "-memory" > /sys/fs/cgroup2/cgroup.subtree_control
就可以启用或者停用对应的controller,而且可以反复操作。
这三个文件既存在于根组也会出现在子组中;另外子组专属的还有一个新的接口文件,叫 cgroup.events.
这个文件可以看有多少个进程 attach 到了这个子组。文件里有一个 “populated: value” 项,value 是个 0/1 值,表示这个子组和它的子孙有没有进程 attach 上去(就是看空闲不空闲)。
子组的创建和删除和 v1 差不多,都可以通过 mkdir/rmdir 来完成。不通的是由于 cgroup v2 的单一层次结构限制,你只能创建到唯一的 cgroup v2 挂载点下。
你可以用一些诸如 poll() inotify() dnotify() 等系统调用来监控子组的事件活动,比如第一个或者最后一个进程 attach 的时间等等。这种机制比 cgroup v1 的 release_agent 机制更高效。
每个不同的子组也可以有 controller 独有的接口文件,比如 v2 memory controller 有一个 memory.events 文件,可以监控OOM事件等。子组创建的时候,这种独有的接口文件会在启用了对应controller的子组中出现,比如一个子组启用了PIDs controller,这个子组就多了两个文件, pids.max 和 pids.current; 前者用于设定这个子组可以 fork 的进程数上限,后者用于统计当前子组有多少个进程。
下面就是举栗子了。
比如说我们跑完下面几个命令之后,会出现什么呢:
mount -t cgroup2 nodev /cgroup2 // 出现一个名为 /cgroup2 的根组
mkdir /cgroup2/group1 // 出现一个子组
mkdir /cgroup2/group1/nested1
mkdir /cgroup2/group1/nested2 // 子组下面产生两个新的孙子组
echo +pids > /cgroup2/cgroup.subtree_control
最后一条命令执行完毕之后, /cgroup2/group1 下面出现了 pids.max, pids.current 两个文件,因为 cgroup2 启用了 PIDs controller 支持。
然后我们运行:
echo +pids > /cgroup2/group1/cgroup.subtree_control
就会在 netsed1 nested2 这两个目录下出现 pids.max, pids.current 文件。这个故事告诉我们,subtree_control 只管儿子,不管孙子和其他后代。
The no-internal-process rule
和 v1 不同的另外一点是,v2 只能在叶子节点上 attach 进程。还是拿上面的举栗子,如果你 echo 0 到 /cgroup2/group1/cgroup.procs 会失败。关于这个规则,更多的讨论在 Documentation 里面有。
还有一点和 v1 不同的是,一个进程只能 attach 到一个子组去了,就不会像 v1 一样结构混乱。接下来文章举了一个网络 controller 的例子说明其意义。
Summary
cgroup v2 的开发还在继续,比如接下来有 RDMA 相关的 controller patch,这个 patch 允许 per-RDMA-device 的资源隔离,这个特性大概很快就能合并了。
文末最后总结性地夸了一下 cgroup v2。
Reliably generating good passwords
Title: 如何生成一个靠谱的密码
如今生活中,密码在电子邮件,银行卡以及其他一些你认为需要安全加密的地方随处可见。但是对于如何生成一个靠谱的密码,当前业界还没有一个标准以及一个被认可的工具。这篇文章将会为你讲述什么是一个靠谱的密码以及哪些工具能够生产一个靠谱的密码。
我们开始逐步意识到当今的密码有一定缺陷。例如:通常人们使用的密码会暴露一定的个人机密信息。甚至一些密码能被很容易推导至另一密码:假如你的密码被盗取,其他人也能够轻易的伪装成你去获取/盗用你的重要资产。因此,一些比较大的公司开始逐步意识到单一密码认证是不够的。比如Google,现在逐步要求员工(访问内网)使用手机客户端进行双因子认证,或者完全使用双因子认证方式代替传统的密码认证方式。当然上述方式还存在一定的争议。
传统的密码认证方式依旧会存在相当长的一段时间直到另一种更好的认证方式被大家所公认。请大家注意下,英文单词中的"password","PIN","passphrase"是同一个意思,广义上都指代使用一段文字信息对用户进行唯一性校验。
什么才是一个靠谱的密码?
对于不同的人来说,一个靠谱的密码有不同的定义。我坚持认为一个靠谱的密码需要同时具备如下几种属性:
- 高信息含量:不容易被暴力破解
- 易传递:方便使用不同协议在不同人/计算机传递
- 易记忆:方便被使用者记住
高信息含量意味着密码不容易被黑客推导。当你选择一个密码时,不管你觉得他有多么的生僻,然而对黑客来说,却可能比较容易找到。当黑客想去做这个事情是,你的生日信息,你的初恋信息,你母亲的名字,你最近一个暑假在的地方或者其他你能想到的信息,对于黑客来说,根本不是事情。
唯一的解决办法就是利用足够的随机手段去生成一个无法被暴力破解的密码。考虑到如今已有现成的软件(hashcat)能够利用GPU每秒能够进行数百万次密码推算,现在8位数的密码已经不再安全。使用合适的硬件,离线状态下一天就能够破解这样的密码。即使最近NIST起草了一份文档依旧推荐密码至少8位数,但是我们如今已经被推荐使用12位或者14位数的密码。
同时,密码也必须具备易传递的功能。一些字符,例如 & 或者 !, 对web系统或者shell系统有特殊的意义,在传输的过程中,很容易被破坏。另有一些软件则明确拒绝这些特殊字符。同样,含有特殊字符字符的密码,在人们口语化的传递中也极为困难。举一个极端的例子:一些流行的软件中,依旧使用数字作为签名信息的传递。
但是,对于"易记忆",那些离散随机的字符对人们的记忆来说,有太大的成本。正如xkcd上说的, "通过20年的努力,我们成功的训练了每一个人去使用人类难以记忆的密码,但这些密码却很容易被计算机所推测"。同时他也指出一系列(无关联)的单词集合作为密码比单个单词同时替换掉其中几个字符作为密码要安全的多。
当然,你不需要记住任何密码。你只需要把密码放在一个安全的密码管理器中(这个点我们将会在另一篇文章中阐述)或者把他们写下来放在你的钱包中。在有些情况下,你需要的不是一个密码,而是一个我称为"token"的东西。或者像Daniel Kahn Gillmor(Debian开发者)在其私人邮件中说的那样,你需要一个“高信息含量,小巧的,易交换的字符”。一些API使用那些被精心设计的token. 比如有些API使用OAuth认证体系。OAuth会生成由随机字符串组成的"access toekn",用于API访问前的鉴权。
请注意,在“token”的设计上,对其中一项属性我们为何使用“小巧”去代替“易记忆”是为了应对部分系统的密码策略(比如对位数要求,字符要求),我们需要高效的将高信息含量的种子转换为合适的长度. 比如,有些银行只允许5位数字的密码,以及绝大部分web网站对密码的长度有上限要求。"小巧"只适用于"token", 而不是"password",是因为我假设你仅仅在秘钥管理,SSH登陆,电脑登陆,秘钥加密使用"password",因为"password"在上述地方,其长度限制等都在你的控制范围内。
生成一个安全的密码
现在,我们来看如何生成一个不易被攻击的,易于交换以及易于记忆的密码。"password" 与 "token"一样,很多时候不能在屏幕上直接显示出来。这里描述的密码生成器都是通过命令行来工作的。密码管理器通常内置了密码生成器,但是这些密码生成器比较难以使用。
我经常使用xkcd漫画来跟大家解释什么才是一个好的密码,最后,有人把这些来自Randall Munroe建议做成了一个叫xkcdpass的工具:
$ xkcdpass
estop mixing edelweiss conduct rejoin flexitime
在详情模式,你会看到xkcdpass所采用的编码量:
$ xkcdpass -V
The supplied word list is located at /usr/lib/python3/dist-packages/xkcdpass/static/default.txt.
Your word list contains 38271 words, or 2^15.22 words.
A 6 word password from this list will have roughly 91 (15.22 * 6) bits of entropy,
assuming truly random word selection.
estop mixing edelweiss conduct rejoin flexitime
请注意,用上述工具生成的15字符密码含有91位元的信息资讯,如果使用大小写,数字,符号随机的方式,那么则是:
log2((26 + 26 + 10 + 10)^15) = approx. 92.548875
有意思的是,这个很接近15字母base 64编码的信息含量:因为在base64中,每个字符包含6位编码,所以最终你会得到90位元的信息资讯。xkcdpass是一款脚本化以及非常容易使用的工具。甚至,你可以自定义词组列表,分隔符,以及不同的命令行参数。默认的,xkcdpass使用12个字典中2^12个单词列表,这个设计并不是为了优化密码生成的速度,而是为了对不同长度的词元做重新组织。
关于xkcdpass的另外一方是是其“骰子系统”。“骰子系统”利用类似骰子的投掷从一份词元列表中拿出相因的词元。例如,你投掷5次骰子“1 4 2 1 4”,你可能会拿到“bilge”这个单词。通过投掷骰子,你将会得到得到一个既随机又便于记忆的密码。
$ diceware
AbateStripDummy16thThanBrock
diceware能够明显的改变xkcdpass的输出,当有人对内置的骰子系统有质疑时,其也可以使用自定义的骰子程序用作与编码元:
$ diceware -d ' ' -r realdice -w en_orig
Please roll 5 dice (or a single dice 5 times).
What number shows dice number 1? 4
What number shows dice number 2? 2
What number shows dice number 3? 6
[...]
Aspire O's Ester Court Born Pk
diceware基于词源列表运行,默认的词元列表用于密码的生成。默认的词元列表来自于SecureDrop project。diceware同样的可以运行在优化重组的EFF词元列表上,但是此列表默认情况下未作开启,因为EFF列表是后面才被加入的。当前,项目开发者们正在考虑把EFF列表作为默认词源列表使用。
diceware有一个明显的缺点是在其生产密码是无法告知使用了多少位元的信息资讯,使用者必须自己去计算。计算结果依赖于输入的词源列表:默认的词源列表每个词元有13位元的信息资讯,这也就意味着默认的6个词元拥有78位元的信息资讯。
log2(8192) * 6 = 78
diceware和xcdpass都是比较新的程序。比如在Debian上,最新的发行版才会集成它们。另外,骰子系统需要一些列骰子算法以及词元列表。假如你需要安装diceware和xkcdpass,你可以用过pip来安装他们。如果说上诉对你来说还是太复杂,你可以看下OpenWall的passwdqc,这款程序相对来说比diceware和xcdpass比较老。但是你同样的可以利用他们来生成易于记忆的密码以及允许你来设定信息含量的等级。
$ pwqgen
vest5Lyric8wake
$ pwqgen random=78
Theme9accord=milan8ninety9few
由于某种原因,passwdqc限制其生成密码的信息含量在24至85位之间。而且pwqgen的工具链明显少xcdpass和diceware。以及其4096个词源列表是固定的(来自sci.crypt, 1997)。
xkcdpass和diceware比较关键的优势是你可以自定义资源列表,这个能够让那些基于词典攻击的黑客增加一定的成本。事实上,针对这些基于词组的密码生成器的攻击,最有效的方式就是使用字典攻击。因为密码足够长的情况下,按照单词来猜测会让攻击变得不可行。而且对默认词源列表的更改会给攻击者带来成本。讲到这里,其实有一个关于“隐藏式安全”说法。事实上“隐藏式安全”是不安全的。当你使用你母语词典作为词源列表,这只会阻止一部分攻击者,但是却阻止不了对你有一定了解的攻击者。
另外有一点必须注意,一个大的词源列表只是扩大了索引空间,所以一个密码的信息含量只跟其长度有关,与你选择用哪个词源列表无关。换句话说,使用多一份的词源列表还不如对密码多增加几位长度。
Generating security tokens ‘’‘生成安全Tokens’‘’
正如之前提到的那样,很多密码管理器支持使用不同的策略生成不易被攻击的安全tokens。总的来说,你需要使用你的密码管理器中的密码生成工具去为你生成token用于访问你的应用。但是如果密码管理器不支持秘钥生成,那该怎么办呢?
"pass",标准unix密码管理工具,使用pwgen来作为密码生成工具。但是它在安全问题上有着糟糕的表现。虽然"pass"支持所谓的安全模式(-s),但是我也要指出去除这个选项作为默认参数是有意义的。我已经为"pass"做了一个小patch,使其生成的密码更加安全。如果不使用"pass",我建议使用如下的管道方式来生成密码:
head -c $entropy /dev/random | base64 | tr -d '\n='
以上命令从内核读取一个固定长度随机字节,并做base64编码。上述结果就是Gillmor描述的"高信息含量,可打印,易于交换的字符串"。重要的是,在这个例子中,我们可以获取到一个小巧的,有含有高信息量的“token”。虽然在同一时刻,你只能使用一个字符合集,这个可能导致一些小问题。例如部分网站会对字符做一些限制。Gillmor是"Assword password manager"其中一个维护者,Assword password manager使用base64,因为其能够被更加广泛的接受,以及只比原先的8位二进制编码多33%空间。经过一段漫长的讨论后,"pass"的维护者Jason A. Donenfeld最终采用如下的管道命令:
read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom)
这条命令与之前的命令比较类似,除了它使用tr命令直接从内核读取字符,以及选择一个基于一个用户早些配置的固定的字符集。之后,read命令从输出中抽取多个字符,最后把结果存储与"pass"变量中。在mail list的讨论中,Brian Candler认为:相比base64方式,使用tr命令从/dev/urandom会丢失一定的信息编码量,但是最后放弃了这个异议点。
另外的密码管理器,例如KeePass使用自己的方式去生成tokens,但是大致都过程都是类似的:从内核读取信息编码源,最终把这些数据转换为一个能够被传输的字符串。
结论
虽然目前有很多方式做密码管理,但是我们的关注点在能够为用户和开发者生成即安全又有用的密码的技术上。虽然pwgen软件暴露了一些安全上的弱点,但是生成一个不易被攻击且易于记忆的密码依旧不是一个问题。一旦不使用用户自己的设备,用户自己生成的密码很容易遭受黑客攻击,特别是那些对用户背景有相当研究的黑客。所以,向用户提供能够生成足够健壮密码的工具以及鼓励他们使用密码管理器则变得尤为重要。