做內核開發的朋友,可能對下面的代碼都很眼熟。
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文章
11292瀏覽量
209333
原文標題:關于Linux文件系統的幾點注意事項
文章出處:【微信號:gh_c472c2199c88,微信公眾號:嵌入式微處理器】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論