关于Linux文件系统的几点注意事项

本文探讨了在Linux内核开发中,文件系统处理的同步与异步访问问题,尤其是file->private_data的潜在风险。文章指出,虽然内核提供了文件系统框架保护,但在异步流程中仍需注意并发访问可能导致的问题。解决方案建议在xxx_release中不立即释放private_data,而是设置关闭状态,以避免后续异步访问引发错误。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本人水平相当有限,不当之处还望大家多多指教。

做内核开发的朋友,可能对下面的代码都很眼熟。

static const struct file_operations xxx_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.write		= xxx_write,
	.unlocked_ioctl	= xxx_ioctl,
	.open		= xxx_open,
	.release	= xxx_release,
};

一般我们在xxx_open中会用类似如下的代码分配一块内存。

file->private_data = kmalloc(sizeof(struct xxx), GFP_KERNEL);

然后在接下来的read/write/ioctl中,我们就可以通过file->private_data取到与此文件关联的数据。

最后,在xxx_release中,我们会释放file->private_data指向的内存。

如果只是上面这几种流程访问file->private_data所指向的数据,基本上不会出问题。

因为内核的文件系统框架已经做了很完善的处理。

对于迸发访问,我们自己也可以通过锁等机制来解决。

然而,我们通常还会在一些异步的流程中访问file->private_data所指向的数据,这些异步流程可能由定时器,中断,进程间通信等因素触发。

并且,这些流程访问数据时,没有经过内核的文件系统框架。

那么这就有可能导致出现问题了。


下面我们先来看看内核文件系统框架的部分实现代码,再来考虑如何规避可能出现的问题。我们的分析基于linux-3.10.102的内核源码。

首先,要得到一个fd,必须先有一次调用C库函数open的行为。而在C库函数open返回之前,其他线程得不到fd,当然也就不会对此fd进行操作。等拿到fd时,open操作都已经完成了。

实际上,更夸张的情况还是有可能存在的。例如,可能由于程序的错误甚至是程序员故意构造特殊代码,导致在open返回之前,其他线程就使用即将返回的fd进行文件操作了。这种情况,这里就不讨论了。有兴趣的朋友,可以自己钻研内核代码,看看会产生什么效果。

先看看文件打开操作的主要函数调用:

sys_open, do_sys_open, do_filp_open, fd_install, __fd_install。

安装fd的操作如下。可见这里是对文件表加了锁的,并且不是针对单个文件,是整体性的加锁。

void __fd_install(struct files_struct *files, unsigned int fd,
		struct file *file)
{
	struct fdtable *fdt;
	spin_lock(&files->file_lock);
	fdt = files_fdtable(files);
	BUG_ON(fdt->fd[fd] != NULL);
	rcu_assign_pointer(fdt->fd[fd], file);
	spin_unlock(&files->file_lock);
}

读写操作,代码结构非常相似。这里只看写操作吧。其实现如下:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	struct fd f = fdget(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos = file_pos_read(f.file);
		ret = vfs_write(f.file, buf, count, &pos);
		file_pos_write(f.file, pos);
		fdput(f);
	}

	return ret;
}

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	if (!(file->f_mode & FMODE_WRITE))
		return -EBADF;
	if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
		return -EINVAL;
	if (unlikely(!access_ok(VERIFY_READ, buf, count)))
		return -EFAULT;

	ret = rw_verify_area(WRITE, file, pos, count);
	if (ret >= 0) {
		count = ret;
		file_start_write(file);
		if (file->f_op->write)
			ret = file->f_op->write(file, buf, count, pos);
		else
			ret = do_sync_write(file, buf, count, pos);
		if (ret > 0) {
			fsnotify_modify(file);
			add_wchar(current, ret);
		}
		inc_syscw(current);
		file_end_write(file);
	}

	return ret;
}


ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
	struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
	struct kiocb kiocb;
	ssize_t ret;

	init_sync_kiocb(&kiocb, filp);
	kiocb.ki_pos = *ppos;
	kiocb.ki_left = len;
	kiocb.ki_nbytes = len;

	ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
	if (-EIOCBQUEUED == ret)
		ret = wait_on_sync_kiocb(&kiocb);
	*ppos = kiocb.ki_pos;
	return ret;
}

可以看出,读写操作是无锁的。也不好加锁,因为读写操作,还有ioctl,有可能阻塞。如果需要锁,用户自己可以使用文件锁,《UNIX环境高级编程》中有关于文件锁的描述。

不过fdget与fdput中包含了一些rcu方面的操作,那是为了能够与close fd的操作迸发进行。

另外,可以看出,如果只实现一个f_op->aio_write,也是可以支持C库函数write的。

再来看看ioctl的实现。

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
	int error;
	struct fd f = fdget(fd);

	if (!f.file)
		return -EBADF;
	error = security_file_ioctl(f.file, cmd, arg);
	if (!error)
		error = do_vfs_ioctl(f.file, fd, cmd, arg);
	fdput(f);
	return error;
}

对于非常规文件,或者常规文件中文件系统特有的命令,最终都会走到

filp->f_op->unlocked_ioctl

另外,ioctl也是无锁的。同时,流程中包含了fdget与fdput,这一点与read/write一样。


再来看看关闭文件的操作。系统调用sys_close的实现如下(fs/open.c)

SYSCALL_DEFINE1(close, unsigned int, fd)
{
	int retval = __close_fd(current->files, fd);

	/* can't restart close syscall because file table entry was cleared */
	if (unlikely(retval == -ERESTARTSYS ||
		     retval == -ERESTARTNOINTR ||
		     retval == -ERESTARTNOHAND ||
		     retval == -ERESTART_RESTARTBLOCK))
		retval = -EINTR;

	return retval;
}


可见主要工作是__close_fd函数(fs/file.c)完成的,其代码如下。可见他是对进程的文件表加了锁的。因此,open、close操作是有互斥的,并且不是针对某一文件的互斥,而是整体的互斥。

对于close一个fd时,其他cpu上的线程若正要或正在读写此fd怎么办?可以看出,close操作并不会为此等待,而是直接继续操作。

其中的rcu_assign_pointer(fdt->fd[fd], NULL);清除了此fd与file结构的关联,因此在此之后通过此fd已经访问不到相应的file结构了。至于在此之前就发起了的且尚未结束的访问怎么处理,答案是在filp_close中处理。

int __close_fd(struct files_struct *files, unsigned fd)
{
	struct file *file;
	struct fdtable *fdt;

	spin_lock(&files->file_lock);
	fdt = files_fdtable(files);
	if (fd >= fdt->max_fds)
		goto out_unlock;
	file = fdt->fd[fd];
	if (!file)
		goto out_unlock;
	rcu_assign_pointer(fdt->fd[fd], NULL);
	__clear_close_on_exec(fd, fdt);
	__put_unused_fd(files, fd);
	spin_unlock(&files->file_lock);
	return filp_close(file, files);

out_unlock:
	spin_unlock(&files->file_lock);
	return -EBADF;
}

filp_close又调用了fput, 后者的相关代码如下。可见当前任务若非内核线程,接下来就是走____fput,否则就是走delayed_fput。

但是最终都是走__fput,__fput中会调用file->f_op->release,即我们的xxx_release。

不过,从fput代码可以看出,____fput会由rcu相关的work触发。因此,可以预见当____fput被调用时,已经没有已经发生且尚未结束的针对此文件的访问流程了。

static void ____fput(struct callback_head *work)
{
	__fput(container_of(work, struct file, f_u.fu_rcuhead));
}


void flush_delayed_fput(void)
{
	delayed_fput(NULL);
}

static DECLARE_WORK(delayed_fput_work, delayed_fput);

void fput(struct file *file)
{
	if (atomic_long_dec_and_test(&file->f_count)) {
		struct task_struct *task = current;

		if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
			init_task_work(&file->f_u.fu_rcuhead, ____fput);
			if (!task_work_add(task, &file->f_u.fu_rcuhead, true))
				return;
		}

		if (llist_add(&file->f_u.fu_llist, &delayed_fput_list))
			schedule_work(&delayed_fput_work);
	}
}


现在再来想想,我们上面提到的那些访问file->private_data所指向的数据的异步流程,这些流程并没有走文件系统框架。

会不会出现这种情况,xxx_release已经执行过了,可是异步流程却还来访问file->private_data所指向的数据呢?

其实xxx_release不妨不要释放file->private_data指向的内存,而是标记一下他的状态为已关闭。然后异步流程再访问此数据时,先检查一下状态。

若为已关闭,则妥善处理并释放即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值