做内核开发的朋友,可能对下面的代码都很眼熟。
1.staticconststructfile_operationsxxx_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 plaincopy
1.file->private_data=kmalloc(sizeof(structxxx),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 plaincopy
1.void__fd_install(structfiles_struct*files,unsignedintfd,
2.structfile*file)
3.{
4.structfdtable*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 plaincopy
1.SYSCALL_DEFINE3(write,unsignedint,fd,constchar__user*,buf,
2.size_t,count)
3.{
4.structfdf=fdget(fd);
5.ssize_tret=-EBADF;
6.
7.if(f.file){
8.loff_tpos=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.returnret;
15.}
[cpp]view plaincopy
1.ssize_tvfs_write(structfile*file,constchar__user*buf,size_tcount,loff_t*pos)
2.{
3.ssize_tret;
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.returnret;
29.}
[cpp]view plaincopy
1.ssize_tdo_sync_write(structfile*filp,constchar__user*buf,size_tlen,loff_t*ppos)
2.{
3.structioveciov={.iov_base=(void__user*)buf,.iov_len=len};
4.structkiocbkiocb;
5.ssize_tret;
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.returnret;
17.}
可以看出,读写操作是无锁的。也不好加锁,因为读写操作,还有ioctl,有可能阻塞。如果需要锁,用户自己可以使用文件锁,《UNIX环境高级编程》中有关于文件锁的描述。
不过fdget与fdput中包含了一些rcu方面的操作,那是为了能够与close fd的操作迸发进行。
另外,可以看出,如果只实现一个f_op->aio_write,也是可以支持C库函数write的。
再来看看ioctl的实现。
[cpp]view plaincopy
1.SYSCALL_DEFINE3(ioctl,unsignedint,fd,unsignedint,cmd,unsignedlong,arg)
2.{
3.interror;
4.structfdf=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.returnerror;
13.}
对于非常规文件,或者常规文件中文件系统特有的命令,最终都会走到
filp->f_op->unlocked_ioctl
另外,ioctl也是无锁的。同时,流程中包含了fdget与fdput,这一点与read/write一样。
再来看看关闭文件的操作。系统调用sys_close的实现如下(fs/open.c)
[cpp]view plaincopy
1.SYSCALL_DEFINE1(close,unsignedint,fd)
2.{
3.intretval=__close_fd(current->files,fd);
4.
5./*can'trestartclosesyscallbecausefiletableentrywascleared*/
6.if(unlikely(retval==-ERESTARTSYS||
7.retval==-ERESTARTNOINTR||
8.retval==-ERESTARTNOHAND||
9.retval==-ERESTART_RESTARTBLOCK))
10.retval=-EINTR;
11.
12.returnretval;
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 plaincopy
1.int__close_fd(structfiles_struct*files,unsignedfd)
2.{
3.structfile*file;
4.structfdtable*fdt;
5.
6.spin_lock(&files->file_lock);
7.fdt=files_fdtable(files);
8.if(fd>=fdt->max_fds)
9.gotoout_unlock;
10.file=fdt->fd[fd];
11.if(!file)
12.gotoout_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.returnfilp_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 plaincopy
1.staticvoid____fput(structcallback_head*work)
2.{
3.__fput(container_of(work,structfile,f_u.fu_rcuhead));
4.}
5.
6.
7.voidflush_delayed_fput(void)
8.{
9.delayed_fput(NULL);
10.}
11.
12.staticDECLARE_WORK(delayed_fput_work,delayed_fput);
13.
14.voidfput(structfile*file)
15.{
16.if(atomic_long_dec_and_test(&file->f_count)){
17.structtask_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指向的内存,而是标记一下他的状态为已关闭。然后异步流程再访问此数据时,先检查一下状态。
若为已关闭,则妥善处理并释放即可。
-
Linux
+关注
关注
87文章
11298浏览量
209391
原文标题:关于Linux文件系统的几点注意事项
文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论