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

嵌入式ARM 发表于 2018-03-17 11:14:19 收藏 已收藏
赞(0) •  评论(0

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

嵌入式ARM 发表于 2018-03-17 11:14:19

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

1. static const struct file_operations xxx_fops = {  

2.     .owner      = THIS_MODULE,  

3.     .llseek     = no_llseek,  

4.     .write      = xxx_write,  

5.     .unlocked_ioctl = xxx_ioctl,  

6.     .open       = xxx_open,  

7.     .release    = xxx_release,  

8. };  

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

[cpp] view plain copy

1. 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的操作如下。可见这里是对文件表加了锁的,并且不是针对单个文件,是整体性的加锁。

[cpp] view plain copy

1. void __fd_install(struct files_struct *files, unsigned int fd,  

2.         struct file *file)  

3. {  

4.     struct fdtable *fdt;  

5.     spin_lock(&files->file_lock);  

6.     fdt = files_fdtable(files);  

7.     BUG_ON(fdt->fd[fd] != NULL);  

8.     rcu_assign_pointer(fdt->fd[fd], file);  

9.     spin_unlock(&files->file_lock);  

10. }  

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

[cpp] view plain copy

1. SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,  

2.         size_t, count)  

3. {  

4.     struct fd f = fdget(fd);  

5.     ssize_t ret = -EBADF;  

6.   

7.     if (f.file) {  

8.         loff_t pos = file_pos_read(f.file);  

9.         ret = vfs_write(f.file, buf, count, &pos);  

10.         file_pos_write(f.file, pos);  

11.         fdput(f);  

12.     }  

13.   

14.     return ret;  

15. }  

[cpp] view plain copy

1. ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)  

2. {  

3.     ssize_t ret;  

4.   

5.     if (!(file->f_mode & FMODE_WRITE))  

6.         return -EBADF;  

7.     if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))  

8.         return -EINVAL;  

9.     if (unlikely(!access_ok(VERIFY_READ, buf, count)))  

10.         return -EFAULT;  

11.   

12.     ret = rw_verify_area(WRITE, file, pos, count);  

13.     if (ret >= 0) {  

14.         count = ret;  

15.         file_start_write(file);  

16.         if (file->f_op->write)  

17.             ret = file->f_op->write(file, buf, count, pos);  

18.         else  

19.             ret = do_sync_write(file, buf, count, pos);  

20.         if (ret > 0) {  

21.             fsnotify_modify(file);  

22.             add_wchar(current, ret);  

23.         }  

24.         inc_syscw(current);  

25.         file_end_write(file);  

26.     }  

27.   

28.     return ret;  

29. }  

[cpp] view plain copy

1. ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)  

2. {  

3.     struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };  

4.     struct kiocb kiocb;  

5.     ssize_t ret;  

6.   

7.     init_sync_kiocb(&kiocb, filp);  

8.     kiocb.ki_pos = *ppos;  

9.     kiocb.ki_left = len;  

10.     kiocb.ki_nbytes = len;  

11.   

12.     ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);  

13.     if (-EIOCBQUEUED == ret)  

14.         ret = wait_on_sync_kiocb(&kiocb);  

15.     *ppos = kiocb.ki_pos;  

16.     return ret;  

17. }  

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

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

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

再来看看ioctl的实现。

[cpp] view plain copy

1. SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)  

2. {  

3.     int error;  

4.     struct fd f = fdget(fd);  

5.   

6.     if (!f.file)  

7.         return -EBADF;  

8.     error = security_file_ioctl(f.file, cmd, arg);  

9.     if (!error)  

10.         error = do_vfs_ioctl(f.file, fd, cmd, arg);  

11.     fdput(f);  

12.     return error;  

13. }  

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

filp->f_op->unlocked_ioctl

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

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

[cpp] view plain copy

1. SYSCALL_DEFINE1(close, unsigned int, fd)  

2. {  

3.     int retval = __close_fd(current->files, fd);  

4.   

5.     /* can't restart close syscall because file table entry was cleared */  

6.     if (unlikely(retval == -ERESTARTSYS ||  

7.              retval == -ERESTARTNOINTR ||  

8.              retval == -ERESTARTNOHAND ||  

9.              retval == -ERESTART_RESTARTBLOCK))  

10.         retval = -EINTR;  

11.   

12.     return retval;  

13. }  

可见主要工作是__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中处理。

[cpp] view plain copy

1. int __close_fd(struct files_struct *files, unsigned fd)  

2. {  

3.     struct file *file;  

4.     struct fdtable *fdt;  

5.   

6.     spin_lock(&files->file_lock);  

7.     fdt = files_fdtable(files);  

8.     if (fd >= fdt->max_fds)  

9.         goto out_unlock;  

10.     file = fdt->fd[fd];  

11.     if (!file)  

12.         goto out_unlock;  

13.     rcu_assign_pointer(fdt->fd[fd], NULL);  

14.     __clear_close_on_exec(fd, fdt);  

15.     __put_unused_fd(files, fd);  

16.     spin_unlock(&files->file_lock);  

17.     return filp_close(file, files);  

18.   

19. out_unlock:  

20.     spin_unlock(&files->file_lock);  

21.     return -EBADF;  

22. }  

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

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

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

[cpp] view plain copy

1. static void ____fput(struct callback_head *work)  

2. {  

3.     __fput(container_of(work, struct file, f_u.fu_rcuhead));  

4. }  

5.   

6.   

7. void flush_delayed_fput(void)  

8. {  

9.     delayed_fput(NULL);  

10. }  

11.   

12. static DECLARE_WORK(delayed_fput_work, delayed_fput);  

13.   

14. void fput(struct file *file)  

15. {  

16.     if (atomic_long_dec_and_test(&file->f_count)) {  

17.         struct task_struct *task = current;  

18.   

19.         if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {  

20.             init_task_work(&file->f_u.fu_rcuhead, ____fput);  

21.             if (!task_work_add(task, &file->f_u.fu_rcuhead, true))  

22.                 return;  

23.         }  

24.   

25.         if (llist_add(&file->f_u.fu_llist, &delayed_fput_list))  

26.             schedule_work(&delayed_fput_work);  

27.     }  

28. }  

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

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

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

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

收藏

相关话题
文章来源专栏
+关注

评论(0)

加载更多评论

参与评论

分享到

QQ空间 QQ好友 微博
取消