Chinaunix首页 | 论坛 | 博客
  • 博客访问: 63143
  • 博文数量: 6
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 131
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-04 10:04
文章分类
文章存档

2014年(6)

我的朋友

分类: LINUX

2014-03-18 15:12:18

  

某企业Linux服务器执行df –h卡住问题的深入分析

     某企业用户在使用Linux服务器进行业务测试时,发现执行df -h命令一直返回不了,把进程:gvfsdgvfs-fuse-deamon杀掉,可以返回。但每次重启机器,这些进程又都起来了。而且过一会儿执行df,发现又挂住不返回了,不知道是什么原因?

 广东省Linux公共服务技术支持中心(咨询热线:400-033-0108)在收到用户帮助请求后,工程师们对这个问题进行了深入分析,并彻底解决该问题。现总结经验方法供同行参考。

 

 

复现方法

和用户联系,交流了出现故障前后的所有操作步骤,经过一些过滤后故障也很快的就复现了,步骤如下:

1、安装启动vnc

2、启动vsftpd

3、直接登陆入gnome图形界面

4、执行df

初步分析

    vnc远程连接或者直接物理登陆Linux的图形界面,会触发gdm执行/usr/libexec/gvfs-fuse-daemon /root/.gvfs,

其次,执行df –h它会根据/etc/matab中挂载的文件系统列表,对所有文件系统调用statfs查询该文件系统的使用信息,并将该信息输出到标准输出。故障复现后查看了两个关键信息:

1、   df后面跟上文件系统参数对/etc/mtab中的每个文件系统列表进行查询:发现df是挂在查询gvfs/root/.gvfs这个目录上

2、   df挂住不返回时刻的堆栈回溯如下:

[] fuse_get_req+0x95/0x170 [fuse]

[] fuse_getxattr+0x74/0x180 [fuse]

[] get_vfs_caps_from_disk+0x65/0xe0

[] audit_copy_inode+0x83/0xc0

[] __audit_inode+0xf6/0x220

[] audit_inode+0x35/0x40

[] do_path_lookup+0x94/0xa0

[] user_path_at+0x57/0xb0

[] sys_statfs+0x3c/0xb0

[] system_call_fastpath+0x16/0x1b

[] 0xffffffffffffffff

从堆栈回溯上看,调用链中比较可能和故障相关的模块包括auditfuse,这个也是后面重点分析的模块。

规避方法

   既然和audit有关,首先想到的是是否用户修改过审计规则,查看了系统的审计规则文件,如下:

# The rules are simply the parameters that would be passed

# to auditctl.

 

# First rule - delete all

-D

 

# Increase the buffers to survive stress events.

# Make this bigger for busy systems

-b 320

 

# Feel free to add below this line. See auditctl man page

 

-w /etc/bashrc -p wa -k ETC_BASHRC

-w /etc/exports -p wa -k ETC_EXPORTS

-w /etc/filesystems -p wa -k ETC_FILESYSTEMS

-w /etc/fstab -p wa -k ETC_FSTAB

-w /etc/group -p wa -k ETC_GROUP

-w /etc/gshadow -p wa -k ETC_GSHADOW

-w /etc/hosts.allow -p wa -k ETC_HOSTS_ALLOW

-w /etc/hosts.deny -p wa -k ETC_HOSTS_DENY

-w /etc/init.d -p wa -k ETC_INIT_D

-w /etc/inputrc -p wa -k ETC_INPUTRC

-w /etc/kdump.conf -p wa -k ETC_KDUMP_CONF

-w /etc/klinux-release -p wa -k ETC_KLINUX_RELEASE

-w /etc/login.defs -p wa -k ETC_LOGIN_DEFS

-w /etc/passwd -p wa -k ETC_PASSWD

-w /etc/profile -p wa -k ETC_PROFILE

-w /etc/rc.d -p wa -k ETC_RC_D

-w /etc/security -p wa -k ETC_SECURITY

-w /etc/selinux -p wa -k ETC_SELINUX

-w /etc/shadow -p wa -k ETC_SHADOW

-w /etc/sudoers -p wa -k ETC_SUDOERS

-w /etc/sysconfig -p wa -k ETC_SYSCONFIG

-w /etc/sysctl.conf -p wa -k ETC_SYSCTL_CONF

-w /etc/rsyslog.conf -p wa -k ETC_RSYSLOG_CONF

-w /proc/sys -p wa -k PROC_SYS

-w /boot -p wa

该文件用户没有进行过修改,但红体字部分为该Linux系统默认添加的一些审计规则。起初怀疑可能是某个或某几个规则触发的故障,但通过一系列的对比测试后发现添加任何一条规则故障都会复现,没有添加规则故障不复现。

于是和用户沟通,暂且先删掉审计规则,规避一下该问题。

 

深入分析

首先,在加和不加audit规则的情况分别用strace查看了/usr/libexec/gvfs-fuse-daemon /root/.gvfs的执行结果,截取片段。

不加audit规则,每次都正常返回,但是加了audit规则后,第二次执行结果:

……

rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

--- SIGCHLD (Child exited) @ 0 (0) ---

umount("/root/.gvfs", MNT_DETACH)     

 

在添加audit规则的情况下,第二次调用了umount,但是该库函数没有收到系统调用umount的返回,在此挂住了。

 

于是对内核umount流程打点,对比分析加和不加审计规则,发现内核进行了不同的流程处理,在开启审计的情况下,是否进行文件的审核关键是在这句:

static int do_path_lookup(int dfd, const char *name,

              unsigned int flags, struct nameidata *nd)

{

……

    if (unlikely(!retval && !audit_dummy_context() && nd->path.dentry &&

              nd->path.dentry->d_inode))

       audit_inode(name, nd->path.dentry);

……

}

 

if判断中,其它三个条件都为真,那么关键就在!audit_dummy_context()

static inline int audit_dummy_context(void)

{

       void *p = current->audit_context;

       return !p || *(int *)p;

}

   要进入audit_inode,那么条件就需要current->audit_context不为空,且该结构的第一个对象(int dummy)为非零。

对代码进一步分析流程,梳理如下图所示:

从该流程也解释了规避方法没有复现故障的原因。没有audit规则的情况下,访问没有挂住是因为它跳过了audit_inode步骤。

但是故障的深层次原因,仍需要进一步分析。

查看了最后卡住的fuse_get_req函数,最终是开在了这句:

struct fuse_req *fuse_get_req(struct fuse_conn *fc)

{

……

       intr = wait_event_interruptible(fc->blocked_waitq, !fc->blocked);

……

}

   从代码中可以看出,等待条件fc->blocked=0时,该句话返回。

   对内核中可能修改到fc->blocked的所有地方打点,并同时调试上层相关的进程,对比对流程进行了梳理,前半部分的执行流程图如下:

 

1)        前部分的关键流程是从调用库函数mount开始,该步骤会触发系统调用mount,除了对超级块进行初始化等操作,进入内核后最关键的一个步骤是对fuse_conn结构对象fc进程初始化,该结构对象是一个处理内核fuse和应用层fuse的关键数据结构,挂载fuse时,会在fuse_fill_super中调用fuse_conn_init,关键的一句话是fc->blocked = 1 ,该字段的意思是fuse文件系统虽然被内核挂载了,但是它还是不可以使用的,因为上层fuse守护进程还没有完成初始化,内核需要等待上层fuse也完成初始化并通知它以后,才能给fc->blocked字段清零

2)        内核完成mount后,流程又回到上层,fuse在此fork子进程/bin/mount,并给予一些参数。以挂载目录为/root/.gvfs为例执行的命令为:“/bin/mount -i -f -t fuse.gvfs-fuse-daemon -o rw,nosuid,nodev gvfs-fuse-daemon /root/.gvfs”,这条命令的主要作用是不进入mount系统调用(参数 -f),但是对/etc/mtab文件进行更新

3)        /bin/mount执行中,会对文件系统是否挂载进行判断,在try_mount_one中,实际挂载文件系统并更新/etc/mtab之前有句很关键的判断:

static int

try_mount_one (const char *spec0, const char *node0, const char *types0,

              const char *opts0, int freq, int pass, int ro) {

……

  if (!(flags & MS_REMOUNT) && fake && mounted (spec, node))

      die(EX_USAGE, _("mount: according to mtab, "

                      "%s is already mounted on %s\n"),

                    spec, node);

……

}

    该句话的作用是检查老的/etc/mtab文件系统是否已经在相同的目录挂载过了,如果挂载过了,则直接退出,并返回错误

4)        从处理流程上来看,如果更新/etc/mtab文件成功,那么一切按正常流程处理;如果挂载失败,那么调用库函数umount,再次触发系统调用进入内核,卸载文件系统清除超级块等对象,以及/proc/mounts中的挂载文件系统列表。但是,程序并没有按照程序设计者预想的流程设计进行,从代码上可以较为容易的看出程序设计者的初衷:如果调用/bin/mount更新mtab失败,那么返回错误给/usr/libexec/gvfs-fuse-daemon进程,后者则调用umount,并退出。但是在步骤1)中分析过,fuse内核fc->blocked = 1,也就是说umount同样在访问挂载目录/root/.gvfs目录上阻塞,这样也导致gvfs-fuse-daemon进程也挂住,受害的当然也包括后面访问挂载目录的进程df

内核fuse阻塞的状态是在上层守护进程gvfs-fuse-daemon完成初始化后,通知内核设置完成清除的,后面的流程如下图:

上层完成初始化后,fuse_kern_chan_send通过写入/dev/fuse接口,通知内核,内核最终在process_init_reply中设置fc->blocked = 0,调用链如下:

[] ? security_capable+0x2a/0x30

 [] ? process_init_reply+0x3a/0x270 [fuse]

 [] ? request_end+0xc5/0x1a0 [fuse]

 [] ? fuse_dev_write+0x2d8/0x3e0 [fuse]

 [] ? fuse_dev_write+0x0/0x3e0 [fuse]

 [] ? do_sync_readv_writev+0xfb/0x140

 [] ? autoremove_wake_function+0x0/0x40

 [] ? selinux_file_permission+0xfb/0x150

 [] ? security_file_permission+0x16/0x20

 [] ? do_readv_writev+0xcf/0x1f0

 [] ? vfs_writev+0x46/0x60

 [] ? sys_writev+0x51/0xb0

 [] ? system_call_fastpath+0x16/0x1b

解决方法

    在此再简述一下本次故障的原因。gvfs-fuse-daemon进程的正常执行流程为:

1)        调用库函数mountfuse_fill_super设置fc->blocked=1

2)        fork子进程/bin/mount  -f -i,更新/etc/mtab文件,将执行结果返回给父进程

3)        变守护进程,并初始化初始化上层数据结构

4)        完成初始化并通知内核,内核fuse设置fc->blocked=0

在添加audit规则的情况下,访问挂载目录挂住,是由于执行过两次/usr/libexec/gvfs-fuse-daemon /root/.gvfs,而第二次执行到步骤2时,由于/bin/mount发现文件系统挂载过了,返回了错误给守护进程,导致执行了错误的执行流程,没有执行后面的步骤4,进而造成后面访问挂载目录的所有操作都会被挂住。

解决方案是修改上层fuse的代码,在步骤1)之前就检查/proc/mounts,看文件系统是否挂载过,如果挂载过,就跳过挂载操作并错误返回,避免后面未按预期进行处理,访问挂载目录挂住。

修改补丁如下:

diff -auNr fuse-2.8.3/lib/mount.c fuse-2.8.3_new/lib/mount.c

--- fuse-2.8.3/lib/mount.c      2010-01-27 02:10:24.000000000 +0800

+++ fuse-2.8.3_new/lib/mount.c   2012-07-23 20:52:37.574272981 +0800

@@ -385,6 +385,36 @@

      return fuse_mount_fusermount(mountpoint, opts, 0);

 }

 

+#define PATH_MAX 255

+static int check_gvfs_if_mount(const char *mnt_path)

+{

+        FILE *file;

+        char line[PATH_MAX];

+        char device[PATH_MAX];

+        char path[PATH_MAX];

+        char type[PATH_MAX];

+        int count = 0;

+

+        file = fopen("/proc/mounts", "r");

+        if (!file){

+                fprintf(stderr, "fuse: open /proc/mounts failed,errno:%d!\n",errno);

+                return -1;

+        }

+

+        while (fgets(line, PATH_MAX, file)) {

+                if (sscanf(line, "%s %s %s", device, path, type) != 3)

+                        continue;

+                if (!strcmp(type, "fuse.gvfs-fuse-daemon") && !strcmp(device, "gvfs-fuse-daemon")

+                        && !strcmp(path,mnt_path)) {

+                        fprintf(stderr, "fuse:according to /proc/mounts gvfs-fuse-daemon is mount on %s ,skip mount it!\n",mnt_path);

+                       

+                        count++;

+                }

+        }

+        fclose(file);

+        return count;

+}

+

 static int fuse_mount_sys(const char *mnt, struct mount_opts *mo,

                      const char *mnt_opts)

 {

@@ -450,6 +480,11 @@

      strcpy(source,

             mo->fsname ? mo->fsname : (mo->subtype ? mo->subtype : devname));

 

+        if (check_gvfs_if_mount(mnt)){

+               res = -1;

+               goto out_close;

+        }

+

      res = mount(source, mnt, type, mo->flags, mo->kernel_opts);

      if (res == -1 && errno == ENODEV && mo->subtype) {

             /* Probably missing subtype support */

测试了在添加audit规则的情况:首次挂载;二次挂载;多次挂载;卸载文件系统后再次挂载等情况。

测试结果:检查了/proc/mounts条目,/etc/mtab条目,ps进程信息,访问挂载目录,都按照预期返回了,并且没有挂住。

 

 

阅读(18061) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~