2014年(6)
分类: LINUX
2014-03-18 15:12:18
某企业Linux服务器执行df –h卡住问题的深入分析
某企业用户在使用Linux服务器进行业务测试时,发现执行df -h命令一直返回不了,把进程:gvfsd、gvfs-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挂住不返回时刻的堆栈回溯如下:
[
[
[
[
[
[
[
[
[
[
[
从堆栈回溯上看,调用链中比较可能和故障相关的模块包括audit和fuse,这个也是后面重点分析的模块。
既然和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,调用链如下:
[
[
[
[
[
[
[
[
[
[
[
[
[
在此再简述一下本次故障的原因。gvfs-fuse-daemon进程的正常执行流程为:
1) 调用库函数mount(fuse_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进程信息,访问挂载目录,都按照预期返回了,并且没有挂住。
复现方法
初步分析
规避方法
深入分析
解决方法