rpm是怎样安装/更新文件的?

这个问题的更准确表述是,在用rpm命令更新软件包时,这个软件包里的某个文件是逐渐改变的,还是原子的?

比如说,有一个软件包叫 fake-1.rpm,里面有A/B/C三个文件。这个软件包有一个更新版本 fake-2.rpm, 其中对B文件进行了变更。那么,在进行rpm的升级过程中,文件B会发生怎样的改变?在升级过程中,文件B有没有可能是不完整的?它的MD5值是yyy1234, yyy5678 还是其它值?

  fake-1.rpm (version 1)
  +-------+---------+
  | Files | MD5     |
  +-------+---------+
  | A     | xxx1234 |
  +-------+---------+
  | B     | yyy1234 |
  +-------+---------+
  | C     | zzz1234 |
  +-------+---------+

  fake-2.rpm (version 2)
  +-------+---------+
  | Files | MD5     |
  +-------+---------+
  | A     | xxx1234 |
  +-------+---------+
  | B     | yyy5678 |
  +-------+---------+
  | C     | zzz1234 |
  +-------+---------+

要解答这个问题,最简单的方法是对升级操作做一个strace. 以升级 zsh 为例:

[root@build tmp]# strace -Tttfv -s 8192 -o /tmp/update_zsh.out rpm -U zsh-5.0.2-28.el7.x86_64.rpm

31526 10:29:34.101189 umask(0777)       = 022 <0.000005>
31526 10:29:34.101206 open("/bin/zsh;59dd820c", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 23 <0.000053>
31526 10:29:34.101275 fcntl(23, F_SETFD, FD_CLOEXEC) = 0 <0.000004>
31526 10:29:34.101295 umask(022)        = 0777 <0.000004>

31526 10:29:34.103613 write(23, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\2\0>\0\1\0\..., 65536) = 65536 <0.000036>

31526 10:29:34.138335 rename("/bin/zsh;59dd820c", "/bin/zsh") = 0 <0.000159>  <<<----------

31526 10:29:34.138552 chown("/bin/zsh", 0, 0) = 0 <0.000011>
31526 10:29:34.138579 chmod("/bin/zsh", 0755) = 0 <0.000008>
31526 10:29:34.138605 utime("/bin/zsh", [2017/02/16-23:28:56, 2017/02/16-23:28:56]) = 0 <0.000008>

/bin/zsh 是这个rpm包的其中一个文件。可以看到,在升级过程中:
1. rpm命令首先创建一个临时文件,叫 /bin/zsh;59dd820c
2. 往该文件写入数据
3. 数据写完后,重命名该文件到 /bin/zsh
4. 修改这个文件的权限、owner和utime.

也就是说,在调用 rename() 之前,/bin/zsh 还是旧版本的 /bin/zsh,里面的内容没变;调用 rename(),系统会将原文件删除,替换成新的文件。关于 rename(),可以参考其 man page:

rename() renames a file, moving it between directories if required.
Any other hard links to the file (as created using link(2)) are
unaffected.  Open file descriptors for oldpath are also unaffected.

Various restrictions determine whether or not the rename operation
succeeds: see ERRORS below.

If newpath already exists, it will be atomically replaced, so that
there is no point at which another process attempting to access
newpath will find it missing.  However, there will probably be a
window in which both oldpath and newpath refer to the file being
renamed.

从文件系统的角度来看,rename()实际上是将 /bin/zsh 的 hardlink,从原 inode 指向了新的 inode, 所以对使用 /bin/zsh 的应用程序来说,/bin/zsh 只可能是旧版本或者新版本的文件,不存在“过渡状态”的文件。所以,在上面的例子中,在更新 fake 软件包的过程中,文件B的MD5值要么是yyy1234, 要么是yyy5678, 没有其他可能。

对于 rpm 更新的过程,通过 strace 已经有了一个大概的了解。那么 rpm 具体是怎样实现这样的操作呢? 从 rpm 的官方文档[1],可以知道 rpm 是通过状态机来完成 transcation 的。但是对于文件的状态机(File State Machine),文档里只写了RIP三个字... 上述的创建文件、覆盖文件的动作,应该是在文件状态机 lib/fsm.c 的 rpmPackageFilesInstall 里完成的。名义上它是一个状态机,但实际上只有一个叫做“安装”的状态。于是这个函数几乎完成了所有安装一个文件的动作。代码我现在看不下去,有空回来继续写。

参考文档

[1] http://rpm.org/devel_doc/state_machines.html