阿里内核月报2015年03月

Virtual filesystem layer changes, past and future

LSF/MM 2015峰会上,虚拟文件系统也吸引了足够的目光。LSF/MM 2015峰会上,虚拟文件系统也吸引了足够的目光。

首先是一些需要继续期待的变化。一个是替代mount()系统调用的工作,Al做了一些但是还没能发出来review。另一个是revoke()系统调用,也只有框架。

进展比较大的是iov_iter接口的转变,Al计划到4.1版本完成aio_read()和aio_write()里面用iov_iter。还有其他一些地方也需要转变,不过大的问题已经没有了。网络协议栈的iov_iter的变化在过去一年已经完成了,sendpages()路径上还没改,不过应该也没什么障碍。 splice()系统调用改起来就麻烦一点,唯一的问题是用户态FUSE文件系统模块,希望做零拷贝IO,从splice() buffer直接到page cacha里。splice()最早被引入内核的时候,这种“page stealing”是为了优化的效果而设计的。不过后来发现有很多问题,比如一个也被直接塞到page cache里文件系统会出问题。于是Nick Piggin在07年把这个特性去掉了,后来一直没加回来。FUSE文件系统做零拷贝就没问题,也一直没去掉,所以现在只有FUSE没有转成用iov_iter了。Al的想法是全面恢复零拷贝,似乎有点麻烦。splice_read()今年就会用上iov_iter。 需要从iovec改成iov_iter的地方还有很多啊,一点一点来吧。

待续。。。

Overlayfs issues and experiences

在今年的LSF/MM峰会上David Howells和Mike Snitzer有一个session,主题是关于overlayfs目前存在的一些问题。Howells抱怨说overlayfs对一些安全技术目前支持不够(比如SELinux),由于overlayfs的设计原因,任何一个文件在overlayfs上有3个inode,只读层有一个,可写层有一个,overlayfs自己还有一个,这个带来的问题就是不知道到底应该使用哪个inode作为安全信息的来源,不过似乎这些问题目前都已经搞定了。

另外两个问题是关于文件锁file locking和fanotify。当一个在底层已经被锁住的文件在上层被写入的时候(这个时候上层要创建新文件),这个锁是否需要传递上去?如果刚好这个文件对应有两个上层文件,那是都应该上传么(fanotify也存在类似的问题)?。Al Viro同时指出,一个被只读打开的文件和一个被可写打开的文件在overlayfs中会有不同的inode号,这个可能会给很多应用造成困扰。对于这个问题,Bottomley认为我们需要先搞清楚用户到底关心什么,虽然一些posix语义在overlayfs中被破坏了,但是用户真的关心么?(个人感觉这个真的是一个好问题,如果没人关心,我们为啥还要去费力的优化呢)

由于docker的大热以及docker准备用overlayfs,这个话题也被引申了出来。docker尝试过btrfs,但是很失败,而一些基于dm的方案目前看也不适合,因为docker希望Go程序可以一次编译到处运行,因此需要静态连接很多库,但是udev不提供静态库(这个似乎不是一个技术问题)。。。所以docker真正希望切换到overlayfs的一个重要原因是在很多容器之间共享相同的page cache(这也是淘宝最初选择overlayfs的一个重要原因),有人提到KSM似乎可以解决,但是KSM目前的实现还要根据内存的内容hash来搞定,这个实现还是有很多overhead的,所以目前看overlayfs似乎是一个不错的方案。

带缓存的异步磁盘IO

带缓存的异步IO,或者说带page cache的异步IO,或者说buffer aio,真的已经是一个万年老话题了。各种竞争方案层出不穷,除了在preadv/pwritev上做文章的,还有通过syslets去解决的方案,但没有一个被mainline接受。

按照社区的传统,往往到最后我们会看见一个对现有体系改动最小,实现的功能也最少(所以引出的反对意见也最少)的方案先被接受,在buffer aio这个事情上这个规律再次应验了。

首先介绍一下背景,大家都知道Linux native aio必须是Direct IO,这给应用场景带来了很多不便。第一,Direct IO对对齐有额外要求(有几个使用Direct IO的人知道严格来说起始地址并不是按照512B对齐就万事大吉了,正确的要求是需要和底层块设备的logical block size对齐,而logical block size并不总是512B,比如cdrom就是2kb,其他一些虚拟块设备也有能力在代码里自己随意定义非512b的logical block size。因此最安全的做法是在发起IO前调用BLKSSZGET这个ioctl去查询一下才知道。把这些诡异的细节甩给用户去关心其实非常危险);第二,由于失去了page cache支持,一个严肃的应用往往需要自己实现用户态缓存层来保证性能,比如MySQL;第三,对于不想自己实现缓存层,一定需要page cache支持的应用来说,就只能用线程池来做并发IO,这样做不仅延迟高了,更大的问题是多线程之间的同步不是那么好写的,比起单线程做异步提交和异步完成,前者更容易有bug,对应用程序员的要求也更高。因此很久以来就有人呼吁应该支持带page cache的aio。

这次2015年的LSF上,Milosz Tanski带来的buffer aio解决方法只能勉强称为一个workaround。他加入了一个新的名为preadv2的系统调用 ,这个系统调用的形状很像preadv,只是增加了一个标志用来指示非阻塞,从语义上来说它其实就是标准的NON_BLOCKING标志而已— 进去后看看要的数据是不是都在page cache里,如果都在,就拷出来,完成;如果不全在,把相应的请求都发射给磁盘,但不用等这些请求返回,直接给用户返回EAGAIN — 只不过原先在Linux上NON_BLOCKING标志不能针对磁盘文件的fd使用(你可以用,但不会有任何效果)。这里算是又把这个语义实现完整了。

那么应用要如何使用这个“buffer aio”呢?Tanski建议的方法是这样,你还是要基于原先的多线程版本改,但在打算把任务扔到IO线程池里之前,先用一个单独的线程 — 不妨管这个线程叫查询者线程 — 去走preadv2尝试一下非阻塞读,如果运气好这些请求都被page cache满足了,那么你就不用再管你的IO线程池了,可以直接往下走拿着这些数据去做别的事情了。

那么这个东西和我想去读之前,直接用现有的接口readahead() + fincore()去查一下数据在不在page cache里又有什么区别呢?Tanski说区别在于fincore不能确保它告诉你存在的数据,当你后边用的时候,数据还能保证在那里。这是显然的,内核不可能允许一个系统调用带着对page cache里某些页的引用计数返回,不然如果你以后一直不来读,这个页要什么时候才能释放?因此有一定概率你后边去读时,仍然会触发同步IO,阻塞住查询者线程。老实说,我觉得这个说法很牵强,因为发生这种事情的概率太小了

Tanski说他已经在自己的一些内部项目上使用这个新系统调用做了改造,成效斐然。比如在一个架在ceph上边的列存储数据库上,延迟降低了23%等等。

上边说的都是读,那写的时候要怎么办呢?因为page cache的写一般情况下本来就是异步的,实际上要实现上边说的这种所谓“buffer aio”,对于写操作这一侧基本不需要修改。

参加这次讨论的人对这个patch没有太多反对意见,我们有望在4.1内核里见到它被合并进来。其实如果你觉得这么做会有用,并不需要等待新内核,完全可以在自己目前的环境中使用readahead() + fincore() 去达到类似的功能。注意使用老发行版的用户,比如rhel5,在man read ahead时会发现它说“readahead() blocks until the specified data has been read.”,这纯粹是因为老发行版带的man早已过时了,查看最新的man projects页面,你会发现这句话已经被删除掉了。

另一个需要注意的点是,不管是Linux native aio还是这里说的preadv2(),或者是readahead(),仍然都有可能因为需要读取文件系统元数据而发起同步IO并阻塞。

memcgroup相关问题

mem cgroup经常是LSF上讨论的热点,但这次已经不是了,这可能预示着memcg主要的问题都解决得差不多了。

早先memcg只能跟踪、限制、并且回收用户内存,也就是说page cache加匿名页。对内核自己占用的内存无能为力,就算是它代表用户去申请的,也没有办法限制它。后来加入了对内核内存的追踪,memcg就可以统计、限制这一类内存了,但仍然没有办法回收它们,只是说达到一定上限后就不再分配而已。再后来Vladimir Davydov在这上做了大量工作,对一些很常用又可以回收的结构,比如dentry和inode,实现了per-memcgroup的LRU,这样针对它们的回收也可以工作了。

现在讨论的主要问题集中在是否应该把“这个memcgroup的内核侧总共允许使用多少内存”这样一个开头暴露给用户,还是改变memory.limit_in_bytes的语义,让它表示“用户进程加它的内核侧一共允许使用多少内存”。讨论这个问题显然是有意义的,因为对于多数普通用户来说,对内核使用多少内存是合理的这个事情根本没有概念,也很难估算出来,而针对总量限制后用户就可以像之前使用系统虚拟化方案时指定一个虚拟机的内存那样指定一个mem cgroup的内存了。但暴露出来单独的内核内存使用上限也有它的好处,比如你可以通过这个接口间接限制最大可以创建的进程数量等等,PeterZ说很多用户都想要这个特性,但可能单独实现它更好些,通过memcg来实现太隐晦了。具体的行动需要开发者们再去更多地收集用户需求后决定。

另外,开发者们在LSF上也研究了一下memcg现在还有哪块大块的内核内存没有跟踪到,看起来页表本身占用的内存值得关注,下一轮的开发应该会解决这个问题

Filesystem support for SMR devices

今年的LSF/MM上,Hannes Reinecke和Adrian Palmer分别介绍了介绍了他们在SMR设备上的工作。前者主要是让块设备层支持SMR设备,后者的主要工作围绕让Ext4文件系统支持SMR设备展开。SMR设备主要的特点是在某些区域允许随机写入,而在另外一些区域仅允许顺序写入,Reinecke的工作就是让块设备能够遵循这一规范。由于SMR设备不允许跨区域IO,所以Reinecke使用红黑树来记录SMR设备上每个区域的状态,这些信息并不是在磁盘挂载上的时候就获取到的,而是在使用时进行查询。目前的规范中,并没有规定SMR设备上每个区域的大小一定要相等,尽管目前的设备厂商都还是使用相同大小的区域,但未来会怎样谁都说不好。Ted Ts'o建议对于区域大小不相等的设备的支持还是需要的,同时他也指出目前采用的延迟加载区域信息的做法可能会造成磁盘性能的下降。

Reinecke指出目前存在的另外一个问题是IO调度器的乱序问题。由于SMR设备在某些区域只能继续进行顺序写,否则就会返回IO错误。对于这种情况,使用nop IO调度器可以在一定程度上解决这个问题,因为nop调度器仅对连续的IO请求进行合并。对于乱序写入请求,Reinecke指出可以将这些写请求重新插入IO调度器的队尾来解决。但是Dave Chinner指出从文件系统层面看,文件系统是期望写请求按照发送顺序进行下发的,如果要保证这一行为,就需要将对SMR硬盘写入操作串行化。而Ted提出了另一个更宽泛的问题,是否需要让一个普通的文件系统针对SMR硬盘进行优化。Chinner的观点是SMR设备的问题还是让固件来解决的好(但是,LSF/MM刚结束,Dave Chinner就在xfs邮件列表中发了一封邮件来说明他对于xfs支持SMR硬盘的想法)。

Adrian Palmer随后介绍了他的工作。Adrian的想法是借助Ext4的block group来保存SMR设备上区域的相关信息,包括区域大小、写指针位置等等。他首先要解决的问题就是目前SMR设备的区域大小为256MB,而默认块大小为4K情况下,Ext4 block group只能表示128M的磁盘空间。因此,他需要增大块的大小,而且这个数值需要能够改变来支持以后更大的区域大小(解决方法很简单:bigalloc特性);此外他还需要解决O_DIRECT IO、写请求乱序等其他问题。Ted说他的一个实习生在修改Ext4的日志系统以使得Ext4的日志在写入时对SMR设备更加友好。Dave Chinner则更关心fsck的问题。当fsck过程中需要对一个已经写入的数据进行覆盖时,麻烦就来了。Ted说只能进行256MB的RMW(read-modify-write)来解决。Dave Chinner指出,最好的方法是硬盘厂商提供一个新的write allocate接口,写入数据的时候直接返回逻辑块号,这样磁盘就可以决定数据放到哪里了,而不用操作系统管理这件事了。当然这个想法基本属于YY,什么时候能出来完全没有时间点。

A rough patch for live patching

Linux Kernel 4.0中最重要的新特性应该就是内核热升级(live patching)了,即在不停机的情况下修复一个正在运行的Linux Kernel中的问题。当然,4.0版本中合并的代码仅仅是这一特性中最基本的一些代码,真正的核心代码还远没有达到能够合并进入主线的状态,并且现在看起来,想合并起来有点儿困难。

4.0代码中目前已经合并的代码是kaptch和kGraft项目的公共部分,即对于插入的热升级内核模块的添加、删除等管理接口。目前代码中缺少的最重要的组件是一致性模型的相关代码,即如何判断当前内核所处的状态是否可以安全的将补丁打上的代码。已有的两个项目:kpatch和kGraft在一致性模型上的分歧完全是不可调和的。

kpatch使用的一致性模型是通过调用stop_machine()做停机检查所有进程的栈信息,确保需要修复的函数没有被运行再打上补丁;kGraft则使用了一种类似RCU的模型,即每个进程都从“旧宇宙”进入到“新宇宙”之后,问题函数才会被替换掉。

上述两种一致性模型都各自有自己的优缺点,Josh Poimboeuf尝试将上面两种模型进行合并,他的做法是保留了kGraft中的新旧宇宙模型,同时通过检查所有进程的栈信息来加速切换过程。理论上讲这一方案利用了两种方案的有点,但是:

Peter Zijlstra提出了他对栈检查的反对,他认为:“目前检查栈的方法是用来进行debug的,用这个方法来确保内核的完整性是不可靠的”。Ingo Molnar则反驳道:“100%准确太难实现了。目前检查栈的代码中有很多问题,只有在你跑到的时候才暴露出来。”这就等于说用户为了避免宕机而进行热升级,结果在热升级的时候用户有可能真的宕机。但是这似乎就是目前的正式状况。Ingo随后也提出了自己对于一致性模型的想法,让所有进程跑到一个一致的沉默状态,这个状态不会影响打补丁,然后把补丁打上。

但是如何定义这个状态呢?内核线程又不能跑到内核外边运行,长期阻塞在内核中的进程也需要被唤醒,这样的改动量也很大。看起来似乎kGraft的新旧宇宙模型更好一些,或者最简单暴力的方法就是不检查,直接打补丁。当然这样就会限制可以打的补丁的范围。

Ingo随后又提出了另外一个观点,kpatch和kGraft整个路子就不对,他觉得不要在已有内核上打补丁,记录当前内核的状态,通过kexec启动一个新内核,将此前记录的状态在新内核上恢复就好了,完全不用担心一致性模型的问题。这个想法并不新,此前就曾经有人提出过相同的想法,并且在一些领域已经实现了内核热升级功能,并且速度很快,10s左右就能完成。但是目前对于内核热升级有需求的用户都希望热升级能够在秒级内完成,所以还是得回到目前kpatch和kGraft的路子上来。

总之,内核热升级项目还会继续,但是各位想在4.1内核中看到完整功能的内核热升级基本不太现实,所以让我们耐心等待吧。

Reservations for must-succeed memory allocations

LSFMM 2015大会上有一个议题是讨论如何在空闲内存较少的情况下必须成功地分配内存。Michal Hocko说内存管理子系统的开发者反对使用__GFP_NOFAIL标志,因为这个标志会试图满足内存申请而不管代价有多大。但是这样会使开发者转而在自己的代码中使用无限重试循环来申请内存,这显然不是解决问题之道。内核代码里到处是重试循环,出现bug时不仅不容易定位和解决,而且将“必须成功”的要求隐藏在了内存管理子系统之外。从内存管理开发者的角度来看,必须尽快去掉这些循环,因此Michal号召在座的开发者开始删掉。他建议当遇到这种循环时,可以先简单地替换成__GFP_NOFAIL标志的申请。当这些都替换完之后,下一步就是研究如何去掉“必须成功”的申请。Michal曾经尝试自动化地定位这些重试循环,但是使用Coccinelle后发现这个问题很难搞定。

Johannes Weiner提到他最近在尝试改进OOM机制,但是也比较难搞定。无论OOM工作的多么好,它始终是基于启发式算法且总会有一些错误决定,而且OOM有很多错误处理路径,修改后也比较难以验证。另外OOM路径也比较容易发生死锁,当一个进程拿到某个锁后再申请内存时,潜在地希望被OOM选中的进程不需要获取这个锁。现在就有一些跑在memcg里的负载,这些程序都严重依赖同一把锁。在这些系统上,当内存较少而发生OOM时就有可能导致整个系统死锁。他认为不应过分依赖OOM机制,内核最好在在开始一个transaction或进入某些不能回退的路径前保证可以申请到资源。最近有些讨论提到应该建立内存预留系统,但这也有弊端,比如会浪费内存。但是可以通过标记预留系统内的页可回收来减少浪费,内存可以被回收并重新分配出去。

James Bottomley说可以预留一个页左右的内存,但XFS maintainer Dave Chinner说并不是这样,比如在XFS里创建transaction来生成一个文件时,首先要申请内存来创建inode并更新目录,这个过程可能需要申请内存来保存和操作free-space bitmap,还有可能需要申请块来保存目录本身,总共下来可能需要1M的空间。这个操作一旦开始就无法回退,理论上是可以为XFS transaction设计出一套鲁棒的回退机制,但可能需要数年时间而且可能会使内存需求翻倍,使得问题更加糟糕。另外一个问题是VFS在调用文件系统代码前本身已经拿了一些锁,设计这么一套复杂的回退机制来避免一些corner case似乎不太值得。

在transaction执行前我们无法知道需要的确切内存量,也不可能提前将它们全部分配出来,但是我们可以预先估计最坏情况下的内存需求量并预留出来。对于一个XFS transaction来说,内存预留量是200-300KB左右,但文件系统也有可能根本就不用它们。当transaction运行时这些内存可以被拿去它用,但一旦需要就必须马上能得到。XFS现在有一套预留系统,但是预留的是transaction日志空间而不是内存,文件系统的并发度被可用日志空间的量所限制,在一个有大容量日志的繁忙系统上可以同时有7000-8000个transactions同时运行。预留系统工作的很好,可以预估出需要的日志空间,可以把这套机制扩展到内存系统。

一些开发者提出文件系统之下的各层IO栈怎么办,即使文件系统知道自己需要多少,但是它不知道底层IO的需求量。Dave说这些软件层几年前已经改成了使用mempool,mempool本身也在改进。另外如果是文件系统叠加文件系统的使用方式可能会有些复杂,但是可以通过在底层文件系统上增加一套机制来向上层反馈最坏情况下的内存使用量来解决。

预留系统应该由内存子系统管理,在进入transaction之前,文件系统或其他类似的模块申请最坏情况下的内存使用量,如果内存无法被满足,请求此时应被暂停,并对预留系统的使用者数量进行限制。对于按需缺页机制来说有一些复杂情况:当XFS读取所有目录块来为新文件寻找空间存放时,需要为它们分配内存来保存在page cache中。大多数时候这些块都不太常用且可以被马上回收,因此Dave认为预留系统不应把这些块计算在内,只应计算被pin在内存里的量。

Johannes认为所有的预留内存应该在一个大池子里统一管理,如果一个用户低估了需求并超额分配了内存,这可能会破坏对所有其他用户的保证。而Dave认为这种不确定性应该由预留计算机制来负责,计算部分可以在transaction超额使用预留内存后打印出警告信息。

slab的预留分配也有一些挑战,现在来看每分配一个slab对象应预留一个整页,这会大幅度增加内存需求,比如XFS的一个transaction可能需要分配多达50个slab。大部分transaction并不需要使用所有的预留内存,如果同时有大量transaction在运行,内核只需要维护一个少于所有预留总和的池就行,但是Dave认为这种变相的内存overcommit可能最终会带来一些问题。

Johannes担心预留系统会增加很多复杂性,而且可能根本就没有人想用这个特性,或者用户都想开启预留系统的overcommit功能来获取内存且不影响性能。Ted Ts'o也认为对这个特性的需求不是很强烈,在实际环境里因为低内存而引起的死锁情况很少,但是Dave说这些复杂性可以被降到很低,毕竟XFS已经有了例子。而Ted坚持认为这个工作是为了解决少数情况下才出现的问题,而99.9999%的情况下都是正常的,我们是否需要花这么大的代价和复杂性来解决它,Ric Wheeler也认为不应增加无关用户的负担,但Dave认为这些问题都是可以解决的。Ted认为即使现在有预留系统,系统管理员也很有可能将它关闭来减少对性能的影响(他预计会有5%的损失)。Dave质疑是否会有明显的性能影响,Chris Mason也赞成在还没有代码的前提下,不要假设对性能的影响。Dave说如果transaction最终会要进行限流,预留系统的实际作用是将限制从transaction的中间移到了开始。James也暂时还没有接受这个做法,他认为在低内存情况下我们总可以对付过去,但如果有了预留系统将会对请求进行限流,吞吐量应该会收到影响。关于预留系统对性能的影响,我们只能等代码出来以后才能有结论。Johannes在讨论最后总结说,预留系统还是要做的,但是需要可配置关闭,下一步就是等社区的patch了。

Inheriting capabilities

http://lwn.net/Articles/632520/
佳泽

Memory-management testing and debugging

内存管理的问题历来是一个难点,内存问题通常会影响正确行和性能。本文lsfmm期间相关的一些讨论。

Testing:

Davidlohr Bueso在做一些mmtests benchmark相关的工作,以能够检测出不同内核版本之间的差异。 同时他还研究了Mosbench和Parsec,他认为这些工具里还是有些用的测试。同时希望其他人也贡献一些 测试到mmtests里。Laura Abbott会提供一些针对移动系统的一组测试用例。同时,他说Scalability测试倾向于scaling up, 而移动开发者更关注scaling down的测试。 这个的讨论没有任何结论。Davidlohr 会继续这方面的工作。

Debugging:

Dave Jones, Sasha Levin, and Dave Hansen.发起了内存管理调试这个话题。 内存管理系统里有很多的调试特性,但是intel的 MPX机制还没有支持。 该机制是基于硬件的检查并确保指针不会访问到一个系列预定义的范围外。 MPX在系统运行时,几乎没有代价,因此可以在生产系统中部署。MPX要求gcc5,并且支持MPX的硬件 还没有真正准备就绪,因此还要可以再等一段时间。 Christoph问可否所有的对slab对象的访问都能够被MPX监控到。但事实上有些难度:一个内核里有成千上万个slab的对象, 但是mpx的硬件寄存器只有4组。那么当跟踪超过4组对象时,就需要将寄存器上的信息保存和恢复。 其他人还建议MPX可以使用在内核堆栈和atomic的上下文中,以及dma操作等场景下。 Sasha建议增加内核里的VM_BUG_ON,但是他担心会遇到以前类似的阻力,调试代码被排除在外。VM_BUG_ON 还有一些讨论,但是没有结论。Andrea Arcangeli, 因为他的开发时基于虚拟化系统, 质疑是否有必要增加那么多的内存管理的tracepoints。有人反对说有些问题只在bare-metal系统上才会出现。

KASan( kernel address sanitizer)

KASan 最近被被合并进入了内核主线。这个工具使用“shadow memory”数组纪录内核应该访问的合法的内存地址。当内核访问越界时,他会抛出一个 错误。 KASan的开发者 Andrey Ryabinin有一个关于关于这个工具和有待提高的地方的介绍。最初想法是使KASan能够正确的验证对vmalloc到的内存 的访问 。要达到这个要求,就需要在vmalloc里增加钩子并创建一个动态的shadown memory数组。整个工作的跟slab内存申请的跟踪很相似,除了 slab使用的shadow memory在系统启动的时候就被申请到了。不出意外,这个特性很快就能实现。 有一个小问题是,内存被释放后很快又被分配给另外一个用户。这片内存在KASan看起来很好,但是会掩盖如果之前用户有访问释放后内存的bug, 建议是将被释放的内存先暂时缓存一段时间再释放,不要立即让其他用户使用它。但延缓释放内存有可能会导致内存碎片增加。Andrey也不是很有 把握要不要做这个特性,并且组里人也没有其他新的想法。 另,通过编译器, 检测读未初始化的内存,但是有大量的问题需要解决。这些问题中的一个,内存初始化是汇编语言,必须人肉去改。Andrey试图 解决这个问题,但是发现很难实现。他担心开发者打开这些功能后发现这问题,会放弃使用所有的功能。 另外KASan还可以用来发现data race,就像现在我们用的其他的工具一样。但有个缺陷,就是shadown memory要使用4倍的被监视 内存. 最后一个问题是:现在我们有KASan了,还有必要维护kmemcheck工具吗?kmemcheck是个单核的工具,速度慢且使用很麻烦。好像没有人真正在使用它。 结论:干掉它 :)。

Investigating a performance bottleneck

http://lwn.net/Articles/637080/
承刚

Lazytime hits a snag

http://lwn.net/Articles/634803/
承刚

Progress on persistent memory

Matthew Wilcox 在 2015.03.09 波士顿的存储开发者大会(刚好今年也有同事参加过)的时候说,持久存储现在基本可以拿来当主存用了,所谓 battery-backed ,在内存上加块电池。也有说 400GB 的超大存储也有了,不过 who knows? 从内核角度,size 不同而导致的完全要当两种不同的设备来管理了。

Christoph Hellwig 问Wilcox ,英特尔啥时候 release 对应的驱动,说白了就是 open datasheet出来,答曰,要和 ACPI 兼容,等 ACPI 6 吧。James Bottomley 好像注意到 UEFI 里面有过 release出来部分的这种 spec 的流程 (译:透露出来很正常,拿开源当广告牌,只是没有 finalize,做做实验而已)

驱动角度,有很多原型产品,现在也没办法统一用一样的 driver cover, frustrating!

关于 struct page

和内存管理相关的,400GB 的设备来说,元数据估计要 6GB,太多了。(译:这个 size 到底是当内存还是当块设备啊?)

Wilcox 假设这种类型的设备不用 struct page 来管。另外一方面, Boaz Harrosh 推了一些 patch,小点的,仍然用 struct page。不过貌似 Wilcox 觉得这不应该是这种设备的目标。(译:估计以后这里会有很多争论)

对于大点的设备,很多特性就比较像 NAND flash 了,只是可擦写次数会很多 7 个0,8个0的数量级吧。而且访问时间可能甚至超过 DRAM。

Ted Ts,Dave Chinner 也都发言了,size 不同,用途不同,就这意思,不过也很难确定个标准啥的。Wilcox 说他有一些初级的 patch, 可以 get_user_sg() 替换掉 get_user_pages(),用的是 scatter/gather list 而不是 pages, 这样可以让这种设备变成类似个块设备,可以上文件系统,然后又可以用 mmap ,反正就是两边都粘点。然后开始讨论 truncate() 用在一个被 mmap 的文件的情况,Wilcox 认为 linux 这里处理有点问题,如果程序访问到的内存,由于 truncate 而不再是被映射到文件了,会收到 SIGSEGV,他认为 truncate 应该阻塞等 memory unmapped。
Peter Zijlstra 认为影响大,最好弄个 flag 给 mmap,在区分是不是等待。

现在回到驱动的话题上,现在已经有很多这种类型的设备了,最好驱动能跟得上啊,总之就是觉得赶紧把驱动弄到内核里,让大家都用起来,不过大体上,在那之前,还有很多工作要做。(译:就是现在没时间表了,等等吧)

新指令

Wilcox 开始说有三条指令要增加在新的处理器中了,clflushopt 可以保证 cache-line flush,而且要比 clflush 快,还有一个 clwb ,回写之后,仍然保持 cache line dirty。再一个就是 pcommit 也是保证 cacheline 被同步更新到内存的,而且是针对所有核心的,这点类似于于 某种 barrier,这条指令,也需要增加到我们上面提到的设备中来(译:毕竟可以当内存用的嘛)。

Ts'o 问其他处理器咋整,Wilcox 无语了都(英特尔是我老板),其他的自己搞定吧。
Pcommit 是全局的,讨论是否有必要针对每个 cpu 做,主要是有没有必要一下 flush 针对每个 core, 或者会不会慢,目前感觉不用担心,有需要再说吧。

错误处理

最后,他说到了错误处理,没有状态寄存器来表示各种错误类型,因为有可能当内存用啊。因为内存用的话,一旦出错,可能导致的结果就是需要重启了。但是如果问题存在,仍然会重启啦(可能是内部逻辑实现)。

系统启动的时候,也有些日志纪录这种错误,比如出错的块设备信息,然后文件系统可以试图恢复,XFS 很妖要增加这个功能了,Ts'o 认为 ext4 也可支持。

但是,crash 似乎不是发现问题的好办法,重启对很多公司的业务来说是难以接受的,mmap 处理之后,再处理可能发生的错误也很难。一些建议是,在 mmap 或者 page 创建的时候,增加出错信号,不过都需要用户态有能力感知,并处理。

Chris Mason 说用户期望 mmap 大文件的时候,即使里面有坏的页也可以工作,听起来不太合理,不过确实是用户角度期待的,关于错误处理的话,讨论了半天,没啥结论。

Ftrace and histograms: a fork in the road

ftrace是获取内核运行时信息的一个重要工具,但目前它输出的数据非常原始,用户通常都得执行额外的脚本将这些数据转换成其它可读的格式/形式。那么,为什么不把这项几乎是必须要做的事情放到内核里呢?所以,Tom Zanussi最近提交一个名为“hist triggers”的补丁,它生成的数据可以用于直接构造绘制直方图。想法还是挺直观的,例如,通过以下命令

   # echo 'hist:key=call_site:val=bytes_req' > \
           /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

就可以得到如下结果:

    # cat /sys/kernel/debug/tracing/events/kmem/kmalloc/hist
    trigger info: hist:keys=call_site:vals=bytes_req:sort=hitcount:size=2048 [active]

    call_site: 18446744071581750326 hitcount:          1  bytes_req:         24
    call_site: 18446744071583151255 hitcount:          1  bytes_req:         32
    call_site: 18446744071582443167 hitcount:          1  bytes_req:        264
    call_site: 18446744072104099935 hitcount:          2  bytes_req:        464
    call_site: 18446744071579323550 hitcount:          3  bytes_req:        168
    [...]

可以看到key和val其实对应直方图上的两个坐标轴。当然,一个明显的改进点是用符号化的方法显示调用点。

基本上没有人对这个功能表示反对,但是ftrace的维护者Steve Rostedt指出用tracepoint生成这些数据需要额外的内存分配,但tracepoint是有可能在任何地方执行的,因此这里有死锁的风险。

条条大路通罗马,另一个观点来自于Alexei Starovoitov。他是eBPF的开发者。他觉得计算直方图数据完全可以使用eBPF完成,这可以充分复用现有基础设施,但同时他也指出这个方法并不是dtrace的替代品,只是像ftrace这种基于read/write接口的输入风格才适用(赶脚这个理由站不住脚......)。eBPF方法的确可以让计算直方图的逻辑脱离内核,但这也意味着ftrace的使用者原来只需要读写几个“文本文件”,而现在需要将这些指令先编译成eBPF再输入给内核,这个变化是否可接受还很难说。

向左转?向右转?再想想看:)

When real validation begins

http://lwn.net/Articles/629742/

木名

Epoll evolving

http://lwn.net/Articles/633422/

夷则

Epoll 是 Linux 下用于快速轮询一大坨fd的一系列系统调用,虽说早在2.5开发版本中就已经有了,但还是有改进空间。现下便有两批补丁对它们做了功能上的改进。

1. epoll 概览

首先是 epoll 和 select()/poll() 的区别。最大的区别就是效率,epoll 在处理大量fd的时候更有性能上的优势。简单来说,select()/poll() 在每次轮询间隔都会遍历一遍要轮训的fd,而事实上在两次轮询间隔中,fd其实是不怎么变的,用不着每个fd都去过一遍。epoll 把 setup 和 waiting 两个步骤分开了,这样就能提高效率。使用的时候,先 epoll_create() 创建一个特定的efd,然后通过epoll_ctl() 把要操作的fd和要轮询的事件关联到这个efd上,然后调用epoll_wait()或者epoll_pwait()开始真正轮询等待。

2. epoll_ctl_batch() 和 epoll_pwait1()

Fam Zheng 的 patch 引入了上面这两个新的系统调用。

第一个系统调用的背景是这样的,epoll_ctl() 一次只能增加、修改、删除__一个__fd,所以当需要操作的fd太多的时候频繁调用epoll_ctl()效率也是很差的。epoll_ctl_batch()从名字上就能看出是用来解决这个问题的。它的参数如下:
int epoll_ctl_batch(int epfd, int flags, int ncmds, struct epoll_ctl_cmd *cmds);
这个很容易理解,请读者自行理会 :-)
Fam 的另一个系统调用,用于更精细化地控制epoll_wait的过程。当前的epoll_wait是毫秒级别的轮询精度,这个新的系统调用epoll_wait1()提供了纳秒级别的精度。它还多了个没有特定值(设为0)的flag字段,估计是为了扩展性,当前似乎没啥用。
这个系统调用的参数如下:


   struct epoll_wait_params {
int clockid; struct timespec timeout; sigset_t *sigmask; size_t sigsetsize;
   };
   int epoll_pwait1(int epfd, int flags,
                    struct epoll_event *events, int maxevents,
                    struct epoll_wait_params *params);

3. 多线程优化

Jason Baron 的场景是个多线程的轮询场景。多个线程在监控同一批fd的时候,只要里面有一个fd变化了,因为所有关联线程里都有epoll,所以都会被唤醒,哪怕只有其中一个线程要去处理事件。

他的解决方案就是在epoll_ctl()里加flag参数。第一个flag是:EPOLLEXCLUSIVE,加了这个标记位之后,就只有一个进程/线程会被唤醒了。不过这还是有问题,在Jason的场景里,每次都是同一个进程被唤醒,这是因为在每个等待队列头都是同一个进程。所以Jason又加了一个flag: EPOLLROUNDROBIN,顾名思义,就是把轮询进程改为RR轮转方式,队列头的进程处理完轮询请求之后就轮转到队尾,下一次就换另一个进程上来轮询。

Jason 还自己跑了个 benchmark,在他的多进程场景里,使用上面的补丁,程序的执行时间降了一半。

How programs get run

http://lwn.net/Articles/630727/

无牙

Heterogeneous memory management

在2015 LSFMM内存管理的会议上,Jérôme Glisse提出异构存储管理(HMM)。他提到现阶段CPU的存储带宽增加缓慢,而且大部分workload并没有达到极限带宽,因此对带宽的需求不是那么迫切,而延迟才是性能的决定因素。但是,将目光投向GPU时,事情变得不一样了。现在的GPU可支持同时运行多达10000个线程,性能不错,运行时最大带宽可达CPU带宽的十倍。其中一些设计得好的GPU可以达到饱和带宽,处理速度非常快。Jérôme Glisse则正在寻找CPU与GPU在同一块die上的系统,两者都可以访问同一片内存区域。其中GPU更侧重于游戏,UI渲染等,并且占用了大部分的带宽。

HMM则允许CPU跟GPU共享物理内存以及地址空间;更进一步地,也许可以实现一些通用设备也可访问这些共享内存。GPU与CPU类似,具有自己的页表,可以触发apge faults等。整个系统的关键在于指定区域内存的所有权归属,如何避免竞争条件。为此,HMM提供了一套机制,可以在在CPU与GPU间迁移内存,保证在任一时刻只有CPU或者GPU访问内存。比如,某一时刻CPU需要访问正被GPU使用的内存区域,将会触发page fault,fault handler则会将内存控制权由GPU转交给CPU,CPU则继续运行。实现该功能需要保证两侧的页表同步,通过CPU侧的mmu notifier回调机制来实现。只要任意一块内存状态发生改变,则执行相应的page-table invalidations操作。为了能有效运行,mmu notifier还必须支持可睡眠(该功能目前尚未支持),这也是这个patch能否被接受的关键点。

Andrew Morton则表示对整个系统的通用性感到担忧。GPU发展如此迅速,也许五年以后没有人再使用HMM功能,但是仍需要维护这份代码。Jérôme Glisse则回应到,他相信HMM子系统可以更具有通用性,包括GPU、数字信号处理器等等。他说HMM在于提供一份完整的、对应用程序透明的GPU解决方案,而complier project可以使一些循环执行操作由GPU并行执行,这个功能实现后,GPU的使用对应用程序来说完全透明。

最后,大家讨论了一下HMM一些细节上的实现。如何将匿名页迁移给设备?将设备看成一种特殊的swap文件,fork()时,所有相关的内存首先迁移给CPU,并设置只读属性。设备后续在访问内存时触发写异常(类似copy-on-write),保证了原子访问。如果能够处理file-backed页则更完善,不过这需要在页缓存中创建新类型。同时这也带来了与mmu notifier同样的问题:文件系统部分的代码认为页缓存查找是原子的,而在这种情况下,是可能导致睡眠的。目前还没有明确的解决思路...

你可能感兴趣的:(阿里内核月报2015年03月)