操作系统: 05 2011存档

捉虫记

| | Comments (0) | TrackBacks (0)
线上有应用使用了ext4文件系统的no journal模式(mkfs.ext4 -o ^has_journal /dev/sda),最近反馈有目录丢失,dmesg里报错:
EXT4-fs error (device sda7): ext4_lookup: deleted inode referenced: 26214657

上周五我们用e2image把线上的硬盘镜像出来,然后fsck sda.img -fy对镜像文件进行修复,发现这些corrupt的目录(54个子目录)在inode table对应的内容全为空(不仅是nlink为0,连 ctime/atime/mtime等都是空),这不是rmdir能造成的(rmdir会修改 dtime而不是清空inode table里对应的那一整个inode),所以此bug应该不是rmdir造成,这与线上的现象一致(从同事的反馈,线上应用只会在硬盘快满时才rmdir,而出现bug的时候硬盘空间还很宽裕)。

于是我们怀疑这种全空是由类似memset的内核代码操作造成的,便寻找 fs/ext4/ 下调用了memset的所有地方,其中 fs/ext4/inode.c 里 __ext4_get_inode_loc 函数里的一句尤其刺眼:

memset(bh->b_data, 0, bh->b_size);

这一句的调用时机是:当前inode如果是它所在inode table的buffer里唯一的一个 inode,则调用memset。线上corrupt的目录都是它父目录下的第255个目录,它们确实是满足这个调用时机的,故而此处嫌疑最大。问题很可能出现在mkdir上。

根据这个判断我们设计了一个重现方法:
首先,启动一个程序,重复的创建目录和文件(模拟线上squid创建的那个目录结 构),为了增加race,10个线程同时去mkdir,然后再删 除全部目录,再remount 硬盘,周而复始。
另外,还启动一个程序,重复的让系统drop_cache
最后,启动多个nbench(一个测试CPU和内存速度的测试工具),增加系统的CPU压力

使用这个重现方法,三管并发,在开发机上不到1小时这个bug便出现了。

于是我们在kernel source里插入褉子,最后追踪到是系统在writeback时会调用 ext4_write_inode ,而它里面错误的调用了 ext4_get_inode_loc (应该调 __ext4_get_inode_loc且第三个参数为0),memset掉了 inode table buffer_head的内容,回头再看upstream的code,这个bug原来已经被google的同学在一年前fix了:(commit 8b472d739b2ddd8ab7fb278874f696cd95b25a5e)。

ext4的no journal本来就是google贡献的,估计他们已经用开了,所以早就已经解决了这个问题,而我们的kernel tree上还没有这个fix。

有同事问到我:使用cp命令时,如果发现要被覆盖的文件(二进制可执行文件)正在运行,cp会报错,cp命令是怎么知道该文件正在执行的?

我第一个想到的是:可能ioctl可以检查文件。可惜我猜错了,看了一下cp的源码,原来如果一个文件正在运行,另一个进程再open以获得写权限的话,这个open本身就会失败,返回-1,errno为ETXTBSY。
那么cp -f 为什么又可以?因为cp -f会先把目标文件(要被覆盖的文件)删掉,然后将源文件rename为目标文件名。

从内核代码看,当运行一个二进制文件时
sys_execve()
  do_execve()
    open_exec()
      deny_write_access()
这里的deny_write_access会把文件对应inode的i_writecount成员减1,通常i_writecount的值就变成-1了(初始为0)
这时候再有进程想以写模式open:
do_sys_open()
  do_filp_open()
    path_openat()
      do_last()
        nameidata_to_filp()
          __dentry_open()
            __get_file_write_access()
              get_write_access()
get_write_access会发现inode的i_writecount成员为负数了,所以直接返回 -ETXTBSY