Live kernel patching on track for 3.20
运行时间敏感的用户一直以来都很希望有一种方法可以在不重启系统的情况下对运行的操作系统内核打补丁。目前有几个还没有进入主线内核的实现(比如:kpatch, Kgraft)。很明显,这些实现不可能全部进入upstream内核。从最近Jiri Kosina发出的一组补丁来看, 内核热升级补丁的开发似乎又向前迈进了一步,kpatch和kGraft的开发,并且内核热升级的核心功能已经被Reviewed/Acked过。并且双方都同意共同进行下一步的开发工作。目前核心功能的补丁计划在3.20版本进入到主线内核,我们即将在主线内核中看到内核热升级功能。

从此前的开发角度来看,kpatch还是具有一定的优势的,主要原因是kpatch使用stop_machine()的方式来对代码进行替换,这一技术已经在ksplice上经过验证并且确认是可以有效果工作的。而kGraft使用的类似RCU的机制则还缺少实际使用的验证。此外,kpatch拥有较为完善的用户太工具来帮助用户生成、管理热升级补丁。而kGraft在这一方面走的较慢,用户态工具的开发仍然较为缓慢。相信随着内核热升级补丁的核心功能进入主线内核以及两个项目组共同的努力,很快我们就可以使用上较为晚上的内核热升级功能。

对混合存储设备的内核支持
我们这里说的混合存储,就是指用基于Flash的设备(如SSD,FusionIO卡等等)给传统的旋转式硬盘做Cache,达到加速的目的。Linux内核最近已经加入了不少软件实现的混合存储方案,比如Bcache和dm-cache,还有没有进入内核主干的flashcache。同时,硬件厂商们也有很多自己的硬件解决方案–在一个黑盒子里装上硬盘和一些Flash存储设备,硬件做进去一些算法来自动判断哪些数据是热点,需要放到Flash设备上,让它们能自动升降级。然后对外仍然表现出好像只有一个设备的样子,这样不需要软件支持也能做加速。

然而,判断出某些数据比另一个数据更热,更值得放在Flash设备上并不是容易的事,由于硬件所在的层次太低,有用的信息在漫长的IO路径上已经被过滤掉不少,因此多数情况下上层应用和操作系统很可能比硬盘要更清楚哪些数据更有价值。如果能给软件提供一些手段把这些hint发给硬件,硬件混合存储设备就有希望工作得更有效率。Intel的Jason Akers最近提了一些patch来实现这类API。他建议的方法是复用已有的ionice系统调用(BTW,是不是很多人都不知道还有这样一个调用?),加入诸如IOPRIO_ADV_EVICT、IOPRIO_ADV_DONTNEED、IOPRIO_ADV_NORMAL、IOPRIO_ADV_WILLNEED这四个命令字,分别表示接下来要读写的数据如果在cache里则最好淘汰掉、接下来要读写的数据近期不会再次用到(但对是否淘汰不表态)、无建议、接下来要读写的数据近期要用到,一共四种建议。内核收到这些建议后再把它们转成ATA命令发给硬件,ATA standard 3.2 已经定义了混合存储设备的特性和接口,这样上位机就有办法把这些信息传下去了。

这套patch短期内不大可能合并到主干,社区的反对意见不少,但都是针对API的设计风格来的,并没有人从根本上质疑有没有必要这么做。很多开发者认为指定per-process的粒度太大了,用户可能想给不同的文件指定不同的建议,甚至给某文件的各个区域指定不同建议,使用上述API没法达到这种目的。另外,Dave Chinner指出实际提交IO请求的线程往往不是最初产生数据的那些用户线程,例如很多文件系统都有自己的work thread,由它们去做submit_bio(),这样用户进程给的建议就不起作用了。

总之,多数开发者认为应该实现一套可以按文件粒度来给出缓存建议的API,Jens Axboe甚至给出了一套按请求粒度来的参考实现,例如可以把这套API做到他最近提出的非阻塞buffer读的补丁中。

另外,这套API的另一个问题是它与目前的混合存储硬件设备耦合得过于紧密:它致力于精细地控制硬件的缓存策略,未来的硬件可能并不能很好地落到以上几种建议的适应范围内。需要软件给出类似的“缓存建议”的场景也不只是混合存储硬件这一种,像带持久化memory device和支持T10/T13的NFS 4.2也会需要类似建议。一般来说内核开发者倾向于设计出能适应多种场景的方案。针对这个问题,Martin Petersen提出了另一种风格的API:不强求软件给出具体的缓存操作指示,把这些“建议”改成“描述”,让软件描述清楚它们现在的IO流是什么风格的,再让硬件自己去把这些风格映射到不同的缓存策略上。举个例子:transaction(事务)类型的IO流,它要求IO操作要尽快完成、写下去的数据未来很可能再次用到、再次用到的时候延迟要尽可能地低;streaming(流式)类型的IO流,它也要求IO操作要尽快完成,但写下去的数据未来再次用到的可能性不大。其他的还有metadata类、Paging类、Background类等等。

目前看来上述问题短期内还不好解决,如果比较一下提出的这几种方案,Martin Petersen的适应性最广,对未来的新硬件也会适用,但他的方案的风格与Intel提的这套patch完全不同,采纳他的方案就意味着开发者们又要从头来过了。

open() flags: O_TMPFILE and O_BENEATH
open系统调用在Linux中扮演了重要的角色。除此之外,没有其他访问已经存在的文件的方法。不同的flag对open系统调用的行为影响很大,这里介绍两个flag,一个已经被加入到最新内核中,另一个还在讨论阶段。

O_TMPFILE
关于O_TMPFILE标识的讨论由来已久,它被迅速加入到内核中,review不很充分,合并之后还有些问题。O_TMPFILE要求创建文件时,不创建文件目录项,其他进程便无法访问这个临时文件。

Eric Rannaud最近提出,以如下的方式调用open会出现什么情况

int fd = open(“/tmp”, O_TMPFILE | O_RDWR, 0);
flag标识要求创建一个可写的文件,但是mode域确要求不能有任何读写权限。对于这种情况,POSIX明确规定mode参数在文件创建之后生效。也就是说,虽然这样创建的文件进程不能访问,在创建时还是可以获得一个文件描述符。 然而,实际情况却不是这样。mode参数在创建时就生效,因此open调用失败。这种行为被界定为bug,Eric的fix在3.18-rc3之后生效。

另一个比较好玩的情况是,

int fd = open(“/tmp”, O_TMPFILE | O_RDONLY, 0666);
带O_RDONLY的O_TMPFILE调用会失败。当引入O_TMPFILE的时候,大家觉得使用不能写的临时文件的情况不存在。但是后来有人找到了这样的情况,open()调用之后跟着fsetxattr()调用,随后用linkat()使文件可见。Linus最初决定支持这个的,后来又变卦了,所以这么使用还是会fail。

还有一个glibc的bug最有意思了啊,glibc编译带O_TMPFILE参数的时候,mode参数根本没有往内核里面传。巧合的是在x86-64机器上调用open进入内核态的时候mode域使用的寄存器正好和约定的是同一个。使用openat()调用的时候就不这么幸运了啊,mode的值就错了。这个应该很快会修掉,但是现在的glibc版本下,不要用open_at()和O_TMPFILE一起。

O_BENEATH
使用openat()调用的时候,需要打开的文件通常是明确的。但是,处理路径里面有符号链接的情况时候,再加上路径上的一些奇巧淫技,打开的文件会错掉啊。

David Drysdale给openat()系统调用增加了O_BENEATH flag来解决这个问题。当使用这个flag的时候,访问的文件必须在path指定的目录中或者更深的路径里面。采取的限制措施很简单,path不能以“/”开始或者包含“../”,解析符号链接时也是这样。

这个flag还有其他的应用,可以安全的给sandbox程序一个文件目录。

这个flag应该很快会被加入到主线中。

An introduction to compound pages
复合页是指物理上连续的两个或两个以上的页组成一个单元,而被当作一个单独的大页使用。hugetlbfs或透明大页系统经常用它来创建大页,但除此之外也有其他一些使用场景。复合页可以被内核用来当作匿名页或者buffer,但是不能用来做page cache,因为page cache只能处理单独的页。

分配复合页仍然使用内存分配函数alloc_pages()但需要设置上__GFP_COMP分配标志,且order至少为1。因为复合页实现方式的问题,不能使用order 0(单独一个页)来创建复合页(参数order是指分配以2为底以order为幂的页个数,0对应于一个页,1对应两个页等)。

需要注意的是一个复合页不同于正常的高阶(high-order)分配请求,下面的调用:

pages = alloc_pages(GFP_KERNEL, 2); /* no __GFP_COMP */
将返回四个物理上连续的页,但是它们不是一个复合页。区别在于创建复合页时需要创建一些元数据,虽然很多情况下这些元数据可能不需要另外生成。

大部分元数据都被放在了对应的struct page结构体中,首先来看一下struct page里的flag。复合页里的第一个页叫做“head page”,会设上PG_head标志,其余剩下的页叫做“tail pages”,会设上PG_tail标志。在64位系统下page flag位较多,是用这种方式存储的。但对于32位系统,没有多余的page flag使用,采用的是另一个不同的方案:复合页里的所有页都会设上PG_compound标志,tail pages同时也会设上PG_reclaim。PG_reclaim标志只为page cache部分的代码使用,但是因为复合页不能用做page cache,因此可以拿来复用。操作复合页的代码不需要关心这些细节,只需要调用PageCompound()就可查询传入的页是否是一个复合页。如果需要区分是head还是tail页,就调用PageHead()和PageTail()。

每个tail page都有一个指针指向head page,指针放在了struct page结构的first_page域。first_page域和private域、该页存储页表项时使用的spinlock域、该页属于一个slab时使用的slab_cache域 占用的是相同的存储空间。compound_head()函数用来找到一个tail page的head page。

表示复合页整体的有两个信息:order和一个将页释放回系统时的析构函数。head page的struct page结构已经塞满了没有地方再存这些信息,因此order被存在了第一个tail页的struct page的lru.prev域。struct page里很多域都是用union存的,因此复合页的order是被转换成了一个指针类型再存进去。类似地析构函数的指针被存在了第一个tail页struct page的lru.next域。正是因为需要用第二个页的struct page来存储元数据,所以复合页最少需要包含两个页。现在内核里只定义了两个复合页析构函数,默认使用的是free_compound_page(),它会将内存返还给页分配器。hugetlbfs使用另一个free_huge_page()来更新计数。

复合页的所有页是一个整体,当只访问其中的一个页时也需要遵守这个规定。透明大页就是一个例子,如果用户空间尝试修改其中一个页的保护权限时,需要定位出整个大页并拆分。有些驱动也使用复合页来管理大块buffer。

以上基本就是复合页和普通高阶分配的区别,大部分内核开发者都不会用到复合页,但是当确实需要将一组页作为一个整体时,复合页就会是一个很好的选择。

THP 引用计数
Caspar
Linux 下大多数架构都用4KB大小的页面(译者:有的例外,比如 powerpc 是64K的,hugepage 是 16MB的),大多数都支持更大的页面,从2MB的大页到1GB的超大页。这些大页在很多工作负载下对性能有显著提升,其中最大的收益在于减少了 TLB 的压力(2MB大页一次只要翻一个地址,4KB的页就得翻512次,显而易见)。内核的透明大页(THP, Transparent Huge Page)特性(理论上来说)能解放开发者和用户的劳力,使大页的使用透明化,只不过这玩意儿受到诸多限制,发挥不出理想状态下的效果。现在 Kirill A. Shutermov 改了一堆复杂的代码,提交了一系列 patch 来减少这些限制。

THP
THP 的工作原理是,当它判断一个进程的地址空间 (1)有空页,(2) 替换为大页之后进程可收益之时,就会静悄悄暗落落地把这部分地址空间替换为更大的页面。这是 Red Hat 的开发者 Andrea Arcangeli 在 2.6.38 内核引入的特性。结果留下了一个难对付的问题,内核的内存管理代码里有一大坨代码没法对付随进分布在进程地址空间里的大页。针对此问题,Andrea 的一个解决方法是,整个功能都不用大页,这就是为什么 page-cache 的页面用不了大页的缘故。
在另外一些情形下,Andrea 放了一个函数叫split_huge_page(), 当一段代码没法用于大页场景时,就用这个函数把大页切回小页。(原文接下来开始强烈吐槽了:很显然这样会有性能开销了,不过这么做还是值得的,起码不会出现一堆奇奇怪怪的内核问题。不过这函数就跟大内核锁(BKL)一样就跟个拐杖一样,不是个健全的解决方法但是至少能 work,把一个难题延后推迟来解决 blah blah。)

然后一直以来,陆陆续续有一些代码修改完之后可以用于大页了,这个函数就被替换了。不过在页面合并代码里,这函数还在。比如在同页归并(KSM, Kernel Samepage Merging)的实现中,在bad-memory poisoning代码中,mprotect(),mlock()这俩系统调用中,swap 代码中,以及其他一些地方都还得用这个函数。有一些估计是改不了的,比如 KSM, 不切割成小页,估计 KSM 永远你不可能成功合并重复大页;其他一些就是比较难改,比方说mprotect(),怎么保护半个大页之类的。针对后面这种情况,其实是可以优化的,就是使用引用计数。

PMD-level 和 PTE-level 映射
要理解 Kirill 的补丁集,就得记得大页在内核中是以复合页的形式存在的,这里有一些复合页的阅读材料。

Kirill 的终极目标是让 page cache 能用上透明大页。到目前为止,只有匿名页可以用大页来替换,这只占内存中的一小部分而已。所以说这个目标很宏大,而目前他的这个补丁集根本就尝试都没尝试一下这个方向(太毒舌了Orz),Kirill 只是简化了管理 THP 的方式,让它们变得更灵活一些。

他的这套补丁消除了正常的4K页和大页之间的隔阂。具体来说,当前的内核中,一个4K页要么是一个独立的4K页,要么成为一个大页的一部分,但是不能既是4K页又是大页的一部分。而 Kirill 的补丁让一个4K页在一个进程空间里是一个独立的页,在另外一个进程空间里成为一个大页的一部分。
先来回顾一下 Linux 页表结构(图片摘自这篇10年前的 LWN 文章):

从图上所见,大页在里面处于 PMD 这层,独立的页面在 PTE 这层。不过并非所有进程里的相同内存都得以相同方式映射,所以在一个进程里一个 2MB 的空间被映射为一个大页,另一个进程里相同的这段内存空间被映射为 512 个 4K 页,这是完全合法的。如果支持这种不同的映射,那么一个进程可以调用mprotect()来保护一个大页的一部分,其他进程里可以继续以大页方式调用,不受干扰。

换句话说,如果split_huge_page()函数可以被替换为诸如split_huge_pmd()这样的函数,这个函数就是只切分一个进程里大页的映射,其他进程里的大页收益继续不受影响。可是当前内核不支持不同的映射,所有进程必须以相同方式来映射,这个限制最终可以归结为大页里的引用计数该如何表达的问题。

大页引用计数
引用计数用于跟踪一个对象(比如内存里的一个页)有多少用户,内核因此决定这个对象是否空闲,是否能被删除。一个普通的页的引用计数有两种:第一种,放在 struct page 结构体里的 _count 字段,是这个页面被引用的总数,另一种在 _mapcount 字段,是指向这个页的页表项的数量。后者从属于前者,每一个页表项的映射引用都会在 _count 字段里同样增加一次引用,所以 _count 字段永远大于等于 _mapcount 字段。在 _count 字段而不在 _mapcount 字段的情况包括:映射到 DMA 的页,通过比如 get_user_pages() 这样的函数映射到内核地址空间的页;以及用了 mlock() 锁住的页。这两个值的差值很重要,如果 _count 值和 _mapcount 相等,这个页可以整个回收,对应的页表项可以删掉。如果前者大于后者,那么多出来那些引用被清理掉之前,这个页面不能动,就被“钉住”了。

而这个规则对于复合页来说完全不一样了。对复合页来说,所有 tail page 的 _count 都是0,引用计数放在 head page。不过这就没办法统计单独的小页了。举个例子,如果一个大页中的部分页面被用于 I/O 操作,那么就得用个小技巧:把每个小页的引用计数放到 _mapcount 里,然后根据当前页面是不是 tail page,来挑选对应的 helper 函数来访问正确的引用计数。

在这个小技巧里,因为大页的 mapping 和 unmapping 都是一起的,所以不需要每个 tail page 单独拿出来跟踪其 mapping 状况。而如果有人想要把一个大页中的几个页单独拿出来 mapping/unmapping,这方法就不靠谱了,得找一个既能跟踪整块大页的映射又能跟踪单独的小页的映射的方法。

对于跟踪整个大页来说,得用另外一个广为人知的小技巧,把整个大页的引用计数放到第一个 tail page 的 mapping 字段中。这个字段一般是用来跟踪一个文件是不是映射到内存的这个页面中的,不过既然整个大页不会用于 page cache,所以这个字段可以放心得挪作他用(仅用于大页情形),计数是个原子类型,mapping 是指向一个结构体 struct address_space的指针,所以得强制类型转换一次。有人说这里最好用个 union 类型,不过作者还没这么干。

对于那些非映射的引用计数,因为 _count 字段废掉了,只有 _mapcount 在用,事情就难办了,Kirill 的解决方法就是:不记录某个具体的单独的小页的非映射的计数,取而代之的是每当一个小页被非映射的引用(比如 get_page() 调用)时,只增加 head page 的引用计数。所以某个页面被非映射引用时,这个页面会被“钉住”(参见这一节的第一段),但是我们不知道具体是哪个页面被钉住了,只知道有小页并钉住了。

所以当一个大页被分割的时候,我们就没办法把某个小页给标记为“钉住”,Kirill 的解决方法是,一旦如此,就直接把 split_huge_page() 函数给 fail 掉。注意不是说让 split_huge_pmd() fail 掉,前者函数是对所有进程的地址空间里的这段内存分割,后者函数只是分割 pmd. 这么做的话,调用 split_huge_page() 函数的代码不用改,只是调用不成功而已。

移除 tail page 的引用计数的这种行为的好处就是让 _mapcount 字段回归其本源:跟踪页表中映射到这个页的计数。这样,一个进程映射一个单独的大页,另一个进程映射这块地址空间为一堆独立的 4K 小页就成为可能。

Kirill 说,这块代码的修改带来了性能提升,虽然他没提供详细的 benchmark 数据。允许大页映射和小页分割同时存在这种行为也可能让内存共享更快一些。展望未来,哪天 page cache 也能用 THP 了,生活就更幸福了。当然首先这块代码得想办法进 mainline,Kirill 说可能这段代码会导致一些意外的状况,不过他对自己代码还是有信心的,短期内不会出什么问题。

Introducing lazytime
引入lazytime
POSIX兼容的文件系统为每个文件维护了3个时间戳,分别对应于文件元数据或内容最后改变的时间(ctime),文件内容的修改(mtime),和文件的访问(atime).前2个时间戳通常被认为是有用的,但”atime”对于它能提供的好处长期以来是非常昂贵的.在当前系统中,有一个mount选项”relatime”,它能减缓atime造成的严重问题,但是它也有一些自身的问题.现在一个新的选项”lazytime”也许可以取代”relatime”并且工作得更好.

“atime”的问题是每当文件被访问,它将被更新.更新”atime”需要将文件inode写入磁盘,这就意味着”atime”跟踪本质上会将每个读操作转变成一个写操作.对于许多负载,这对性能的影响是非常严重的.在这之上,有很少的程序利用”atime”或者依赖于它的更新.因此,十年前,挂载文件系统的时候都会带有”noatime”选项,它彻底关闭了访问时间的跟踪.

问题是少数程序不是没有程序;最后确实有一些工具没有”atime”跟踪会出问题.一个经典的例子就是邮件客户端通常会用”atime”值来判断自从上次邮件被投递以来邮箱有没有被读.经过一些讨论之后,内核社区在2.6.20开发周期添加了”relatime”选项.”relatime”造成了大多数”atime”更新被抑制,但当当前记录的”atime”早于”ctime”或者”mtime”才会允许更新.后来,”relatime”被调整为每24小时更新一次.

“relatime”对于大多数系统来说工作得很好,但依然有一些系统需要更好的”atime”跟踪,不需要为它付出性能的代价.一些用户也不喜欢”relatime”,因为它会造成系统不与POSIX规范完全兼容.对于大部分,人们忍受”relatime”的小缺陷(或者忍受”atime”更新的代价),但现有有一个替代方法.

这个方法就是lazytime选项,它是Ted Ts’o发的一个ext4特定的补丁.当lazytime被使能,一个文件系统将保持”atime”更新在内存inode中.这个inode直到一些原因发生或者需要被从内存中清除的时候才被写入磁盘.这个好处是”atime”对于运行在系统的所有程序来说都是正确的.保存在磁盘上的”atime”可能是非常老的,但假如系统崩溃,当前的”atime”将会丢失.

Dave Chinner很快指出,这个选项是有用的,但不应该仅是在ext4中.假如它能在VFS实现,所有的文件系统都能用它,不仅仅是ext4,也许更重要的是,在所有的文件系统上能以相同的方式工作.Ted同意一个VFS实现实有意义的,这个补丁的下一版将会如此实现.

Dave也建议, 无规律的”atime”更新写入也许不是可取的. Ted也接受这个想法,因此下一版很可能将会以每24小时至少写入一次更新的”atime”.没有这个改变,像在数据库服务器上,”atime”更新可能呆在内存中几个月才更新一次.

最后,有一个问题是关于”lazytime”是否成为缺省的mount选项.它满足POSIX没有引入正常”atime”更新的开销,因此看起来是一个比”relatime”更好的选项.Ted似乎想不久改变当前的缺省选项,然而Dave担心会有回归问题,想先等等看.同时这将导致这个特性能否得到更多的测试的问题,但正如Dave提醒的,未来会有对这个特性更感兴趣的人帮着测试.

这个是否征程需要时间去检验,”relatime”对于大多数用户来说工作得很好,因此没有必要让大多数的用户去试这个新的选项.但最后一些富于尝试的发行版很可能会采用这个新选项.在这点上来看,经过长时间,任何潜在的问题都很可能会暴露出来.因此也许”lazytime”选项会在2016年变为缺省选项,也许真正被测试得很好,证明没有问题.

Control group namespaces
LinuxContainer(LXC)使用namespace来做名字空间上的隔离,使用cgroup来做性能或者说资源隔离。目前的这套方案中存在的是一个问题是想操作cgroup就必须得mount cgroupfs,而cgroupfs本身是没有隔离的,所以一个container里边的root用户只要mount上cgroupfs,就可以看到整个宿主机上所有container的情况了。在container里边cat /proc/self/cgroup,可以看到从根到自己的完整路径,这显然会造成明显的信息泄漏。这件事的影响还不光是泄漏,Linux下的容器方案折腾在线热迁移已经有一阵子了,OpenVZ其实很早之前就支持这个,它向内核主干贡献的CRIU(Checkpoint/Restore In Userspace)方案正趋于成熟,对于容器里的进程来说,很可能迁到别的地方上之后对端机器的cgroupfs目录层次与源端是不同的,这个进程再去看/proc/self/cgroup就发现它变了,而一般的应用进程都不会防备发生这种事情,因此这对热迁移也产生了间接的影响。怎么办呢? Google的Aditya Kali提了一个patch,给cgroupfs引入了一个新的namespace,就叫cgroup namespace,通过给unshare()加新一个名为CLONE_NEWCGROUP的新标志,指示当前线程要进入一个新的cgroup namespace,这个新namespace会让当前线程把自己所处的那一级cgroupfs目录做为虚拟的根目录,从它自己的角度看过去所见的其他目录都依据这个动作做相应的调整。

例如:

原先进程cat /proc/self/cgroup可见自己位于/batchjobs/c_job_1,unshare后就看见自己位于/
机器上有/batchjobs/ag和/batchjobs/bg两个组,一个位于ag组中的进程做了unshare,然后去cat /proc/{bg组中的线程}/cgroup,就会看见它位于/../bg
以此类推
社区对这个patch非常欢迎,namespace的维护者Eric W. Biederman说自打cgroup被合并到主干的那天起他就想要这个功能了。有个这个功能之后各种cgroup manager也可以跑在子组中,嵌套着加以部署。社区对这个patch主要的讨论焦点是权限相关的,即unshare了之后,新的“假”根组中的线程可以被允许移到哪些组中去?(注意这里讨论的移动是指namespace之间的移动,不是指对应的cgroup组的移动,后者需要做这个操作的人自己去决定要不要做)原始的patch第一版是只允许普通线程移动到自己的子组中去,不能平行移动或者向上移动。特权线程可以任意移动。开发者们觉得这样的限制有点过于苛刻。
由于总体上没什么反对意见,我们有望在后边的内核中很快见到这个特性。

ACCESS_ONCE() and compiler bugs
ACCESS_ONCE()与编译器Bug
宏ACCESS_ONCE()在内核中应用广泛,它确保了相应变量仅被编译器生成的代码访问一次。这篇文章(http://lwn.net/Articles/508991/) 说明了它的工作原理以及应用场景。该文写于2012年,那时内核中大概有200处地方使用了这个宏。现在大概有700处。像很多用于并发管理的底层技术一样,ACCESS_ONCE()使用了不太容易理解的小花招。而且与其它技术一样,当编译器发生改变时,可能在此处出现Bug。 14年11月份时, Christian Borntraeger报告了这个Bug。为了理解这个问题,我们来仔细分析下这个宏,它在当前内核中的定义很简单(

define ACCESS_ONCE(x) ((volatile typeof(x) )&(x))

ACCESS_ONCE()将变量声明为volatile类型。Christian的报告指出GCC4.6与4.7会把非标量变量前的volatile忽略掉。因此,对int是没有问题的。但是对于其它的复杂结构则存在问题。比如,ACCESS_ONCE()经常用于声明页表项: typedef struct { Unsigned long pet; }pte_t; 在此例中,volatile会被上述编译器忽略,而导致内核Bug。Christian尝试寻找了解决方案,不过看来只能避免使用有问题的GCC来编译内核。但是许多系统中都安装了4.6或4.7,拉黑他们会让很多用户不舒服。而且Linus指出: “虽然有时我们可以避免使用有问题的编译器。但是编译器Bug总是无休止地提醒我们:你们在做一件很脆弱的事。或许我们应该找种方法避免脆弱了。 一种方法是对复杂结构体中的标量型变量使用ACCESS_ONCE()而不是该结构体变量本身。例如: Pte_t p = ACCESS_ONCE(pte); 可以被改写为: Unsigned long p = ACCESS_ONCE(pte->pte); 这种方法需要去检查每一处ACCESS_ONCE()调用,找到使用非标量类型的地方。这个过程很费时,而且容易出错。 Christian指出的另一种方法是一些有问题的ACCESS_ONCE()调用。而用barrier()代替。在很多例子中,屏障很有效,但并非总是如此。我们需要更详细的统计,阻止新的代码使用ACCESS_ONCE()。 Christian修改了ACCESS_ONCE(),使得它不能作用于非标量变量。在他提交的最近一组Patch中,ACCESS_ONCE()长成了这样:

#define __ACCESS_ONCE(x) ({ \
__maybe_unused typeof(x) __var = 0; \ (volatile typeof(x) *)&(x); })
#define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
这个版本在使用于非标量时,会报出编译错误。但一个非标量需要使用这个功能该怎么办呢?Christian引入了两个新的宏,READ_ONCE()/ASSIGN_ONCE(). 前者的定义如下:

static __always_inline void __read_once_size(volatile void *p, void *res, int size)
{
switch (size) {
case 1: (u8 )res = (volatile u8 )p; break;
case 2: (u16 )res = (volatile u16 )p; break;
case 4: (u32 )res = (volatile u32 )p; break;
#ifdef CONFIG_64BIT
case 8: (u64 )res = (volatile u64 )p; break;
#endif
}
}

#define READ_ONCE(p) \
({ typeof(p) __val; __read_once_size(&p, &__val, sizeof(__val)); __val; })
可以看出,它强制使用标量类型,即使传入的变量不是此类型。 Christian的补丁集使用了READ_ONCE()与ASSIGN_ONCE()替换了ACCESS_ONCE()。代码中的评论建议为了将优先使用这些宏,但是大多数已有的ACCESS_ONCE()不会被替换。开发者使用ACCESS_ONCE()来访问非标量变量时,会收到编译器给出的警告。 这个版本的Patch收到的评论不多,看来会在不久的将来进入upstream.在此之前,最好避免使用有Bug的编译器。编译器的Bug同时也说明内核中的相关代码可以写的更好,更加健壮。

Attaching eBPF programs to sockets
最近的内核开发周期中已经看到添加了柏克莱封包过滤器(extend Berkeley Packet Filter,缩写 eBPF)子系统到内核。但是,对于3.18版本的内核,一个用户空间的程序可以加载一个eBPF程序,但是不能令其运行在有用的上下文环境中;程序虽然可以加载和验证,但也仅仅只是加载和验证。不用说,eBPF开发者Alexei Starovoitov想让这个子系统有更加广泛的作用。3.19版的内核应该包含将会第一次包含一系列新的体现Alexei想法的补丁。 将加入3.19内核的主要特性是将可以把eBPF程序附加到sockets上。操作的顺序将是首先在内存中建立eBPF程序,然后使用新的 bpf()系统调用(相对于3.18版内核而言)来将程序加载到内核并获取一个文件描述符来引用它。这样,程序便可以将新的SO_ATTACH_BPF选项负载到setsockopt()函数了。

setsockopt(socket, SOL_SOCKET, SO_ATTACH_BPF, &fd, sizeof(fd));
这里的参数socket表示网络的socket,fd表示加载eBPF程序的文件描述符。

一旦程序加载后,它将会在每次相应socket捕获到数据包的时候执行。目前,可用的功能仍然在以下两个方面受限: eBPF程序可以访问到存储在捕获到的数据包中的数据,但是不能访问到任何内核skb数据结构中的数据。未来计划将使一些元数据可用,但是目前还不清楚哪些数据将可以以及如何访问。 程序不能对数据包的传送产生任何影响。因此,尽管这些程序被成为“过滤器”,但是它们目前可以做的仅仅是存储在eBPF中的信息提供给用户空间程序使用。

最终的结果是,在3.19中,eBPF程序对于统计收集等功能有用,但用处不是很多。

不过,这是个开始。3.19内核中应该包含一些例子来说明如何使用该功能。它们中的两个是一个简单程序从数据包获取低级别的协议(如UDP,TCP,ICMP等)并在eBPF map中为每种协议维护一个计数器。如果某人想写这样一个直接使用eBPF虚拟机语言,他可以这样写:

struct bpf_insn prog[] = {
BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),
BPF_LD_ABS(BPF_B, 14 + 9 /* R0 = ip->proto */),
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */
BPF_LD_MAP_FD(BPF_REG_1, map_fd),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2),
BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */
BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0),
BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */
BPF_EXIT_INSN(),

};
不用说,这样的程序,对于大多数人而言,不是特别难懂。但是,正如例子所述,该程序也可以写成一个限制性的C语言版本。

int bpf_prog1(struct sk_buff *skb)
{
int index = load_byte(skb, 14 + 9);
long *value;
value = bpf_map_lookup_elem(&my_map, &index);
if (value)__sync_fetch_and_add(value, 1);return 0;
}

这个程序可以被送到一个特别版本的LLVM编译器,产生针对eBPF虚拟机的目标文件。但是现在,还只能使用Alexei那版的LLVM编译器,但Alexei表示它正在将这些更新提交到LLVM主线上。一个用户空间工具可以使用通常的方式从(LLVM编译器)产生的目标文件中读取这个程序并将它加载到内核空间。这样就不需要直接与eBPF语言打交道了。

当读到最后一个例子时,可使用高级语言的能力使其价值显现,这个例子编译了一个300条指令的eBPF程序,这个程序实现了流跟踪、根据IP地址计算数据包数量。这个程序本身可能只有这些功能,但是它向我们展示了一些熟悉的复杂的功能可以使用eBPF虚拟机在内核中实现。

未来的计划还包括将eBPF应用在其他一些地方,包括secure computing(“seccomp”)子系统以及filtering tracepoint hits。鉴于eBPF将成为内核中的一个通用设施,看来,内核开发者们将会在其他它可以使用的地方使用它。希望能在未来几年中见到一些与eBPF相关的有趣的事情。

The iov_iter interface
内核其中一项很常见的操作是处理用户空间传入的一个buffer,有的时候这个buffer会包含很多块。这个地方的处理是内核代码里面经常容易出错的点,有时会导致安全问题。为此内核开始考虑将之前用于内存管理和文件系统层的iov_iter扩展到内核的其他部分。

iov_iter主要用于遍历iovec结构,该结构在

阿里linux内核月报201412相关推荐

  1. 阿里linux内核月报201705

    A report from Netconf: Day 1 本文是2017年4月3日Netconf会议报告.主要的议题是:移除ndo_select_queue()函数,对于refcount_t类型引入开 ...

  2. 阿里linux内核月报201503

    Virtual filesystem layer changes, past and future LSF/MM 2015峰会上,虚拟文件系统也吸引了足够的目光.LSF/MM 2015峰会上,虚拟文件 ...

  3. 阿里数据库内核月报:2017年04月

    摘要: 阿里数据库内核月报:2017年04月 # 01 MySQL · 源码分析 · MySQL 半同步复制数据一致性分析 # 02 MYSQL · 新特性 · MySQL 8.0对Parser所做的 ...

  4. Linux阅码场 - Linux内核月报(2020年06月)

    关于Linux内核月报 Linux阅码场 Linux阅码场内核月报栏目,是汇总当月Linux内核社区最重要的一线开发动态,方便读者们更容易跟踪Linux内核的最前沿发展动向. 限于篇幅,只会对最新技术 ...

  5. 阿里数据库内核月报:2017年01月

    摘要: 阿里数据库内核月报:2017年01月 # 01 MySQL · 引擎特性 · InnoDB 同步机制 # 02 MySQL · myrocks · myrocks index conditio ...

  6. Linux阅码场 - Linux内核月报(2020年12月)

    关于Linux内核月报 Linux阅码场 Linux阅码场内核月报栏目,是汇总当月Linux内核社区最重要的一线开发动态,方便读者们更容易跟踪Linux内核的最前沿发展动向. 限于篇幅,只会对最新技术 ...

  7. Linux阅码场 - Linux内核月报(2020年09月)

    关于Linux内核月报 Linux阅码场 Linux阅码场内核月报栏目,是汇总当月Linux内核社区最重要的一线开发动态,方便读者们更容易跟踪Linux内核的最前沿发展动向. 限于篇幅,只会对最新技术 ...

  8. Linux阅码场 - Linux内核月报(2020年08月)

    关于Linux内核月报 Linux阅码场 Linux阅码场内核月报栏目,是汇总当月Linux内核社区最重要的一线开发动态,方便读者们更容易跟踪Linux内核的最前沿发展动向. 限于篇幅,只会对最新技术 ...

  9. 阿里数据库内核月报导航

    数据库内核月报 是阿里云RDS-数据库内核组维护的一个关于数据库的技术博客,上面的文章质量是有保证的.不过有一点不太友好的是,这个网站首页非常简洁,只是按照时间月份来归档文章,并没有按照类别来分,这样 ...

最新文章

  1. java自学语法_Java自学笔记(一):基础知识
  2. include的两种形式、CPP的搜索路径
  3. 牛客题霸 [数组中未出现的最小正整数] C++题解/答案
  4. 开发者论坛一周精粹(第五十七期) 阿里云免费套餐 个人备案备注
  5. 同步异步、阻塞非阻塞
  6. AndroidのTextView之CompoundDrawable那些坑
  7. 基于粒子滤波的定位算法 ——原理、理解与仿真
  8. 计算机管理员账户停用,win10系统提示“你的账户已被停用,请向系统管理员咨询”如何解决...
  9. 聊聊手机之--小米6
  10. 农产品商铺商城小程序(JavaSSM+微信小程序)
  11. 【U3D入门小白教程——案例篇】之一:球吃豆
  12. Kotlin 协程与flow
  13. 数据库服务的运行与登录
  14. 生兔子c语言递归的方法,经典的兔子生兔子问题(C#递归解法)
  15. 使用设计模式出任CEO迎娶白富美(5)--原型模式解决车间管理规范问题
  16. Android平台上基于OpenGl渲染yuv视频
  17. 从Siri到Mobile AI,华为人工智能突击苹果
  18. 【湍流】基于matlab模拟拉盖尔高斯光束传播的光强
  19. 比较不同版本Project所包含的应用与功能
  20. 【keras】python mnist_mlp.py下载数据集mnist.npz失败的解决

热门文章

  1. 笔试 | 数字IC设计之1bit的半加器、全加器实现
  2. “程序员”眼中的中秋节
  3. 【codevs4355】王的对决(简单数论) 莫比乌斯反演
  4. MT7628平台编程设计指南资料
  5. Excel文件解析性能对比(POI,easyexcel,xlsx-streamer)
  6. window7系统电脑,怎么调亮度?
  7. 如何衡量一个量化策略的好坏
  8. 最全Python绘制饼形图(饼状图)
  9. OneTab扩展:解决 Chrome 内存占用过多问题
  10. 对List集合属性进行模糊查找