内核fuse实现中的一个隐患--基于centos 3.10.0-693.11

版权声明:欢迎转载,转载请注明出处~ https://blog.csdn.net/zancijun1666/article/details/83002753

一次项目中,我们用的基于fuse的文件系统进程出core挂掉了,排查发现,这并不是个BUG,算是个缺陷吧。出core表现是这样的,libfuse从/dev/fuse中调用read读取内容,读返回成功了,但缓冲区只更新了部分内容,还有部分内容是旧的,导致libfuse执行了旧的命令,而我们实现的fuse文件系统,某些命令重复执行是会出core的,也幸亏会出core才能暴露这个bug,不然某些命令重复执行,造成数据丢失就玩大发了
当排查到是read系统调用有问题的时候,我是不愿意相信的,堂堂centos 3.10.0 的内核还会有这种bug嘛?当然,事实这不算是bug,只是一个缺陷,用户层在多线程里fork,并使用非对齐到页的内存时,就会触发这个缺陷。下面我们来探下究竟。

fuse的原理还是比较简单易懂的,在用户层有个设备名为/dev/fuse,用户层(libfuse)就是通过这个来和内核的fuse通信。当用户层读写fuse实现的文件系统的时候,内核就会产生一个请求挂到队列里等待libfuse的读取,而libfuse读/dev/fuse来读取请求并执行相应操作后写回/dev/fuse。问题就在这个/dev/fuse的读实现里。

/dev/fuse是个混杂设备,其的最终的读函数是fuse_dev_read,我们分析下它的代码:

static ssize_t fuse_dev_read(struct kiocb *iocb, const struct iovec *iov,
			      unsigned long nr_segs, loff_t pos)
{
	struct fuse_copy_state cs;
	struct file *file = iocb->ki_filp;
	struct fuse_conn *fc = fuse_get_conn(file);
	if (!fc)
		return -EPERM;

	fuse_copy_init(&cs, fc, 1, iov, nr_segs);

	return fuse_dev_do_read(fc, file, &cs, iov_length(iov, nr_segs));
}

//fuse_copy_state 初始化, 这个结构体用于维护copy一个请求的状态。
static void fuse_copy_init(struct fuse_copy_state *cs, struct fuse_conn *fc,
			   int write,
			   const struct iovec *iov, unsigned long nr_segs)
{
	memset(cs, 0, sizeof(*cs));
	cs->fc = fc;	//fc连接
	cs->write = write;	//读写方向,1为读
	cs->iov = iov;	//用户地址iov,一般一个
	cs->nr_segs = nr_segs; //一般为1
}

fuse_copy_init初始化fuse_copy_state 这个结构体,这个结构体是维护fuse拷贝请求的,fuse_dev_do_read进行具体的读。

 //读取一个请求到用户空间的缓存里,这个调用会阻塞。 当一个请求不需要回复(FORGET)或者被abort或发生
 //错误,它会调用request_end来结束。其它情况下,这个请求被读取后会被挂载到processing 链表,然后设置 sent 标记
static ssize_t fuse_dev_do_read(struct fuse_conn *fc, struct file *file,
				struct fuse_copy_state *cs, size_t nbytes)
{
	int err;
	struct fuse_req *req;
	struct fuse_in *in;
	unsigned reqsize;

 restart:
	spin_lock(&fc->lock);
	err = -EAGAIN;
	//nonblock
	if ((file->f_flags & O_NONBLOCK) && fc->connected &&
	    !request_pending(fc))
		goto err_unlock;

	//等待队列有请求
	request_wait(fc);
	err = -ENODEV;
	if (!fc->connected)
		goto err_unlock;
	err = -ERESTARTSYS;
	if (!request_pending(fc))
		goto err_unlock;

	//如果有中断要发送到user space
	if (!list_empty(&fc->interrupts)) {
		req = list_entry(fc->interrupts.next, struct fuse_req,
				 intr_entry);
		return fuse_read_interrupt(fc, cs, nbytes, req);
	}

	if (forget_pending(fc)) {
		if (list_empty(&fc->pending) || fc->forget_batch-- > 0)
			return fuse_read_forget(fc, cs, nbytes);

		if (fc->forget_batch <= -8)
			fc->forget_batch = 16;
	}

	//获取请求
	req = list_entry(fc->pending.next, struct fuse_req, list);
	req->state = FUSE_REQ_READING;
	list_move(&req->list, &fc->io);

	in = &req->in;
	reqsize = in->h.len;
	/* If request is too large, reply with an error and restart the read */
	//如果read调用给的空间不足以装下这个request,则需要重启这个调用。
	if (nbytes < reqsize) {
		req->out.h.error = -EIO;
		/* SETXATTR is special, since it may contain too large data */
		if (in->h.opcode == FUSE_SETXATTR)
			req->out.h.error = -E2BIG;
		request_end(fc, req);
		goto restart;
	}
	spin_unlock(&fc->lock);
	cs->req = req;
	//拷贝一个请求,fuse通过将用户内存映射到内核空间,然后再进行拷贝,为啥不使用copy_to_user呢?因为copy_to_user效率太低了。然而这样的方式就会引入一个隐患。
	err = fuse_copy_one(cs, &in->h, sizeof(in->h));
	if (!err)
		err = fuse_copy_args(cs, in->numargs, in->argpages,
				     (struct fuse_arg *) in->args, 0);
	fuse_copy_finish(cs);
	spin_lock(&fc->lock);
	req->locked = 0;
	if (req->aborted) {
		request_end(fc, req);
		return -ENODEV;
	}
	if (err) {
		req->out.h.error = -EIO;
		request_end(fc, req);
		return err;
	}
	if (!req->isreply)
		request_end(fc, req);
	else {
		req->state = FUSE_REQ_SENT;
		list_move_tail(&req->list, &fc->processing); //将这个请求移到processing链表
		if (req->interrupted)
			queue_interrupt(fc, req);
		spin_unlock(&fc->lock);
	}
	return reqsize;

 err_unlock:
	spin_unlock(&fc->lock);
	return err;
}

可以看到fuse_dev_do_read主要就是阻塞到有一个请求到来,然后将请求拷贝到用户提供的空间中,整体没啥大毛病,问题出在它传输数据到用户空间的方式。这里没有使用copy_to_user之类的函数,而是用一种比较特殊的方式。我们看下fuse拷贝数据是怎么做的

//重点分析这个函数
static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size)
{
	while (size) {
		if (!cs->len) {	//第一次进入这里时,cs->len=0,会执行下面的分支
			int err = fuse_copy_fill(cs);	//这里会获取用户缓存的页并映射到内核
			if (err)
				return err;
		}
		fuse_copy_do(cs, &val, &size);	//这里就是尽全力去拷贝数据了
	}
	return 0;
}



/*
 * Get another pagefull of userspace buffer, and map it to kernel
 * address space, and lock request
 */
static int fuse_copy_fill(struct fuse_copy_state *cs)
{
	unsigned long offset;
	int err;

	unlock_request(cs->fc, cs->req);
	fuse_copy_finish(cs);	//当fuse read /dev/fuse时,cs->currbuf和cs->mapaddr都是0,所以这里不会有什么动作
	if (cs->pipebufs) {		//这里和本文主题没啥关系,跳过
		。。。
	} else {
		if (!cs->seglen) {
			BUG_ON(!cs->nr_segs);
			cs->seglen = cs->iov[0].iov_len;
			cs->addr = (unsigned long) cs->iov[0].iov_base;
			cs->iov++;
			cs->nr_segs--;
		}
		//获取cs->addr所在的用户页,这个调用是无锁的
		err = get_user_pages_fast(cs->addr, 1, cs->write, &cs->pg);
		if (err < 0)
			return err;
		BUG_ON(err != 1);
		//获取用户缓存在用户页的偏移
		offset = cs->addr % PAGE_SIZE;
		//映射到内核地址
		cs->mapaddr = kmap(cs->pg);
		//用户缓存映射到内核的地址
		cs->buf = cs->mapaddr + offset;
		cs->len = min(PAGE_SIZE - offset, cs->seglen);
		cs->seglen -= cs->len; //做相关计数,用于下一次调用
		cs->addr += cs->len;
	}

	return lock_request(cs->fc, cs->req);
}

可以看到,fuse是通过将用户缓存的页,映射到内核之后,再讲请求拷贝到该页,然后再解内核映射的。这是个比copy_to_user之类高效的方法,但是这会引入一个缺陷。总结来说是它会和copy on write这个机制一起触发一个bug。说到这了,各位看官能看出到底问题出在哪了吗?

问题在于,这里有两个通道去访问同一个页,一个是用户空间的映射,一个是内核的映射,这里并不是因为会产生竞争之类的造成写丢失,而是因为fork的cow机制,当进程处于cow状态,用户空间的映射进行写入是会产生页面复制、替换页表项的,而内核映射却感知不到这个变化。

我们考虑这样一个情景:

  1. fuse进程一个线程A申请了一块内存mem1,这个内存小于一个页,而且地址没有对齐到页。并把这个内存传递给read读取/dev/fuse的内容,内核映射了mem1所在的页,开始数据拷贝
  2. fuse进程的一个线程B申请了另一块内存mem2,这个内存和A所申请的内存在同一内存页里。然后存在某个线程调用了fork,此时这个进程处于cow状态,线程A、B申请的内存所在页也在cow状态。
  3. 线程B对mem2进行写访问,这时触发了cow,内核创建一个新页复制mem2所在的页内容,替换掉mem2所在的页
  4. fuse进程对/dev/fuse的读结束了,但数据所在的页到fork的新进程去了,mem1已经是新页了,还是老的内容

以上,就是我在fuse上发现的bug。是不是很神奇呢? 以前我知道的不能在多线程中进行fork,是因为除了调用fork的线程外新进程中的其他线程都蒸发了,这样可能会导致一些锁的问题,但没想到还有如此大一个坑在。除了fork,还有会调用fork的函数是不能在多线程用的,如popen、system之类的,我们这踩坑就是因为用system执行异步命令了,后来搞了个异步命令代理服务来执行异步命令。

悄咪咪告诉你,这个坑在direct io中也有~~稍后我会出篇文章分析direct io是怎样子的。

猜你喜欢

转载自blog.csdn.net/zancijun1666/article/details/83002753