[翻译] 实现的细节如何成为ABI:一个案例

实现的细节如何成为ABI:一个案例

译文作者:zhangzl2013
译文链接:http://www.cnblogs.com/zhangzl2013/p/How_implementation_details_become_ABI_a_case_study.html
原文作者:Jonathan Corbet
原文链接:How implementation details become ABI: a case study
本文有可能会被转载,从而导致评论留言的碎片化。想参与评论和探讨的同学,请找到原文或译文的原始地址,与原文或译文作者互动讨论。 

3.17-rc7发布之前,最后进入内核主线的补丁集之一是Mikhail Efremov提交的。它会影响虚拟文件系统的底层代码中管理dentry结构体中的文件名的部分——dentry结构体用于处理文件名到内核inode结构体之间的映射关系。这个补丁很重要,也告诉我们为什么一个无意的行为随着时间的推移会成为内核ABI。

问题

3.15开发周期中添加的ranemeat2()系统调用带来了一个细微的,意外的影响,我们将用一个例子来说明。在一个运行Bash的系统中,运行以下命令:

$cd /tmp
  $touh foo bar
  $exec 42<bar
  $ls -l /proc/self/fd/42
  lr-x------ 1 corbet lwn 64 Sep 29 13:01 /proc/self/fd/42 ->/tmp/bar

exec命令使shell打开一个名为bar的文件,并且文件描述符为42.ls命令的输出中能看到这一点。要是继续运行一下命令,会发生什么情况呢?

$mv foo bar
  $ls -l /proc/self/fd/42

在3.15之前,输出是这样的:

lr-x------ 1 corbet lwn 64 Sep 29 13:01 /proc/self/fd/42 -> /tmp/bar (deleted)

而在3.15之后,确是这样的:

lr-x------ 1 corbet lwn 64 Sep 29 15:00 /proc/self/fd/42 -> /tmp/foo (deleted)

当文件有打开的文件描述符而删除文件时,文件依然会存在,知道所有的文件描述符都关闭才真正删除。在3.15之前的内核中,文件对应的文件名还是原来的那个。而在新内核中,如果文件是通过rename命令删除的,它对应的文件名将会是新的名字。

这个变化看起来没什么大不了的;谁会在意一个文件系统中再也不会访问的已删除文件的名字呢?但是还真有在意的。Mikhail在他提交补丁时就提到了一个:ALT Linux中的软件包升级程序会用rename命令来用新的程序替换掉正在运行的服务程序,然后通过旧的程序名来找到这个正在运行中的进程。但是因为可执行程序的名字已经改成新的了,所以那个旧的进程通过名字看起来就不是在运行原来的程序了,从而导致升级程序失败。Piotr Karbowski应该是第一个报告这个bug的人,他说这导致了他的系统不可用。实际上这个行为确实会导致系统崩溃。

原因

要深入了解这个bug的原因,需要研究一个dentry结构体,和一个不易理解的函数switch_names(),这个函数用于处理rename操作。dentry结构体负责名字映射,它保存了文件名。但是文件名可以有两种保存方式。如果文件名的长度小于DNAME_INLINE_LEN(它是一个32到40之间的数值,由结构体的对齐字节数决定),名字会直接保存在此结构体中。否则的话,d_name域将包含一个指向外部的字符串空间的指针。

switch_names()函数声明如下:

void switch_names(struct dentry *dentry, struct dentry *target);

它交换dentry的名字和target的名字。它必须针对内部保存的名字和外部保存的名字两种情况做出相应的处理。这里有两个dentry结构体,所以实际有4种组合。如果两个参数都是外部保存的名字,就容易了:

void switch_names(struct dentry *dentry, struct dentry *target);

你会觉得这种方式交换名字有点奇怪,因为最终目的是修改dentry这个参数。这么交换是因为target参数马上就没用了,它的名字也就无所谓了。交换的方法可以使代码(1)不必分配内存,(2)不用关心释放旧名字的问题,因为target就要被释放掉了。

要是两个名字都是内部保存的,就不用考虑内存分配的问题。3.15之前相应的代码是这样的:

memcpy(dentry->d_iname, target->d_name.name, target->d_name.len + 1);
  dentry->d_name.len = target->d_name.len;

它会使两个dentry结构体保存相同的名字——这个行为和前面外部保存的那种情况就不一样了。跟上面一样,因为target马上就要销毁了,所以这点区别本不该有问题。但是,在3.15中,加入了一个“cross-rename”特性,它允许两个文件以原子操作交换名字。这时,两个名字是要像外部存储的一样被交换的。所以当前代码是这样的:

unsigned int i;
  for (i = 0; i < DNAME_INLINE_LEN / sizeof(long); i++) {
    swap(((long *) &dentry->d_iname)[i], ((long *) &target->d_iname)[i]);
  }

这个看起来比较奇怪的循环避免了申请临时变量和额外的拷贝。

谁都看得出来,上面的代码没问题;它使内部存储和外部存储的名字行为一致了。但是如果(1)一个文件改成另一个文件的名字,并且(2)两个文件都是短名字,那么用户可见的系统行为就不一样了,这就可能会导致程序崩溃。这样是有悖于内核开发的基本原则的,所以需要改。

修改

Mikhail最初的补丁给switch_names()加了一个“exchange”标记。如果此标记置位,那么就按照3.15的行为处理,否则代码回退到起初的行为。这个补丁一开始被拒绝了;Linus认为这个补丁太难看:

正确的解决方案应该是:只要没有明确请求交换两个名字,那就统一行为,都把名字拷贝给新的dentry结构体,即使过去不是这么做的,现在也要这样做。但是实现这个方案还是有个问题:当两个名字都是在外部存储的时候,就必须给目标文件分配内存。内存的分配还必须在原子的上下文中执行,因为会减慢代码的速度。所以目前还没有简单的办法。

所以即使开发者们觉得这个补丁不好,还是决定在3.17中合入Mikhail的补丁。Al Viro在整理代码时改对这个补丁做了一些修改,但是3.17以后的版本也不太可能有这个补丁了。

很有可能Al的补丁会替代它。Al的补丁对外部存储的名字添加了引用计数,这样只要增加计数就相当于拷贝了一份名字。释放名字时要减少计数并测试是否可以真的释放。这增加了一点复杂度;名字在访问时要遵循RCU规则,比如真正的删除操作要在RCU回调中执行。但这个想法是很简洁的,并且因为实际操作dentry名字的地方不多,改动也不大。

在当前这个开发阶段,它对于3.17来说,改动还是有点大了。所以要等3.18了。现在,Mikhail的补丁已经进入3.17,而且也会进入稳定版的升级计划中,所以以前的那种行为又要找回来了。这个行为是意外出现的,也从没有文档化,内核开发者认为依赖这个行为的代码都不好。但不管怎么说,这个行为已经成为内核ABI的一部分了,所以开发者即使不喜欢它,也得保留它。

--结束--

 

posted @ 2014-10-12 22:51 zhangzl2013 阅读(...) 评论(...) 编辑 收藏