Open()函數的內核追蹤


Open()函數的內核追蹤

open函數相信大家都用過,這裏就不多說它的使用方法等事項,現直接進入正題...


用戶態程序調用open函數時,會產生一箇中斷號爲5的中斷請求,其值以該宏__NR__open進行標示.而後該進程上下文(process context)將會被切換到內核空間。待內核中的相關操作完成後,就會從內核返回,此時還需要一次進程上下文切換(process contextswitch)


待進程執行流進入內核後,會通過一系列轉換(這裏我們不關心),最終進入SYSCALL_DEFINE3(open,...)函數中。看起來該函數定義比較特殊,其實SYSCALL_DRFINE3是一個宏,它被定義成如下形式:

#defineSYSCALL_DEFINE3(name,...)SYSCALL_DEFINEx(3,_##name,__VA_ARGS__)


SYSCALL_DEFINEx宏具有如下形式:


#ifdefCONFIG_FTRACE_SYSCALLS
#defineSYSCALL_DEFINEx(x,sname,...) \
staticconstchar*types_##sname[]={ \
__SC_STR_TDECL##x(__VA_ARGS__) \
}; \
staticconstchar*args_##sname[]={ \
__SC_STR_ADECL##x(__VA_ARGS__) \
}; \
SYSCALL_METADATA(sname,x); \
__SYSCALL_DEFINEx(x,sname,__VA_ARGS__)
#else
#defineSYSCALL_DEFINEx(x,sname,...) \
__SYSCALL_DEFINEx(x,sname,__VA_ARGS__)
#endif


可以看到,不論是何種形式的宏定義,最終都會進入__SYSCALL_DEFINEx中,而__SYSCALL_DEFINEx的定義如下:


#ifdefCONFIG_HAVE_SYSCALL_WRAPPERS

#defineSYSCALL_DEFINE(name)staticinlinelongSYSC_##name

#define__SYSCALL_DEFINEx(x,name,...) \
asmlinkagelongsys##name(__SC_DECL##x(__VA_ARGS__)); \
staticinlinelongSYSC##name(__SC_DECL##x(__VA_ARGS__)); \
asmlinkagelongSyS##name(__SC_LONG##x(__VA_ARGS__)) \
{ \
__SC_TEST##x(__VA_ARGS__); \
return(long)SYSC##name(__SC_CAST##x(__VA_ARGS__)); \
} \
SYSCALL_ALIAS(sys##name,SyS##name); \
staticinlinelongSYSC##name(__SC_DECL##x(__VA_ARGS__))

#else/* CONFIG_HAVE_SYSCALL_WRAPPERS */

#defineSYSCALL_DEFINE(name)asmlinkagelongsys_##name
#define__SYSCALL_DEFINEx(x,name,...) \
asmlinkagelongsys##name(__SC_DECL##x(__VA_ARGS__))

#endif/* CONFIG_HAVE_SYSCALL_WRAPPERS */


經過該宏的替換作用以後,最終我們就會得到sys_openSYS_open所對應的函數原型。如下:


asmlinkage long sys_open(const char __user filename,intflags,intmode);


這也就是我們最常見到的open函數所對應的在內核中的實現部份。其實,對於linux下所有的系統調用函數,採用上述方法均可找到與其對應的內核函數sys_xxx().


接下來我們來看sys_open()函數。其實現如下:


SYSCALL_DEFINE3(open,const char __user*,filename,int,flags,int,mode)
{
longret;

if(force_o_largefile())
flags|=O_LARGEFILE;

ret=do_sys_open(AT_FDCWD,filename,flags,mode);
/*avoid REGPARM breakage on x86:*/
asmlinkage_protect(3,ret,filename,flags,mode);
returnret;
}


在函數中首先調用force_o_largefile()宏進行LARGEFILE?確認。若是LARGEFILE則將其在flags中置位。隨後調用主處理函數do_sys_open進行後續處理。其實,open的工作也就是在該函數中進行的。該函數原型如下:


longdo_sys_open(intdfd,constchar__user*filename,intflags,intmode);


在該函數中,如果通過getname()得到filename變量中文件名的指針沒有錯誤的話,接下來就會掉用get_unused_fd_flags()函數獲得一個沒被使用的文件描述符fd。注意,對於文件描述符fd來講,它只對本進程有效,也即它只在該進程中可見而在其它進程中代表着完全不同的文件。在32位系統中,一個進程最多打開32個文件,而在64位系統中可以打開64個文件。該函數就是用來獲得一個未被使用的文件描述符fd.至於它的獲取過程是很複雜的,這裏不進行講述。有興趣的話,可以到www.kernel.org下載最新的內核源碼進行研究。


在獲得了有效的fd之後,我們通過do_filp_open()函數打開或者創建相應的文件,並且返回與之對應的文件結構struct file *f。如果函數返回的結構地址有效的話,那麼就會調用fsnotify_open()函數(參數爲f)將該文件加入到文件監控的系統中。該系統是用來監控文件被打開,創建,讀寫,關閉,修改等操作的,具體工作原理見後面文章。本文中不做講述。隨後調用fd_install()函數將struct file *f加入到fd索引位置處的數組中。如果後續過程中,有對該文件描述符的操作的話,就會通過查找該數組得到對應的文件結構,而後在進行相關操作。完成這些工作之後,open函數就返回了。返回值也就是剛纔得到的fd.


那麼,在do_filp_open()函數中有具體做了哪些工作呢?文件是如何被創建的呢?以及文件若存在的話,又是怎樣被找到,而後被打開的呢?在中篇中我們將會回答這些問題.


接着上篇我們來回答文章最後提出的問題:在do_filp_open()函數中有具體做了哪些工作呢?文件是如何被創建的呢?以及文件若存在的話,又是怎樣被找到,而後被打開的呢?下面我們來回答這些問題。


可以推斷,在do_filp_open函數做了open函數的全部工作,包括創建,打開等等。該函數的原型是這樣的:


structfile*do_filp_open(intdfd,constchar*pathname,intopen_flag,intmode,intacc_mode);


需要說明的是該函數參數列表中的open_flag的低兩位的含義和該函數內部的flag變量中的是不同的。具體的區別如下:

open_flag參數(其實它就是sys_open函數中的flag參數)中的低兩位具有如下含義時:

00-read-only
01-write-only
10-read-only
11-special


它們將會通過選擇性的+1操作轉變成具有如下含義的值,並存入本地變量flag中:


00-no permissions needed
01-read-permission
10-write-permission
11-read-write

好了,現在我們來看do_filp_open()函數的實現。細心的朋友會發現,在函數的開始會進行一系列flagmode標誌位的檢查,這裏我們不關心。之後就會調用path_init()函數進行後續操作前的初始化工作。path_init()函數主要是爲了填充nd(nd是一個指向struct nameidata結構的指針)結構。


在函數path_init內部,會判斷*pathname是不是字符'/',若是則通過如下代碼設置nd->rootnd->path,該root即是指向current->fs->root的指針.


set_root(nd);
nd->path=nd->root;
path_get(&nd->root);


若上述字符不是'/',則在檢查dfd參數,如果該值爲AT_FDCWD,那麼我們就調用get_fs_pwd()函數將nd->path設置成current->fs->pwd指針所指向的當前工作目錄。這裏需要說明一下AT_FDCWD宏的含義。該宏的值是-100,它主要是用來指示openat應使用當前工作目錄。

如果以上判斷都不爲真的話,那麼,此時會調用fget_light()函數來獲得dfd所對應的file結構(struct file *)。這裏的dfdopen函數返回的fd,也就是上篇中分配的fd是不是具用相同的含義,這裏還不得而知。個人感覺好像是由VFS分配或查找以存在的文件時得到的。並且若不是create文件的話,它們應該具用相同的含義,否則不相同。這裏指示猜測,還沒深究。希望能有高人先來告訴鄙人一下,或是一起發貼來討論一下。我會盡快來澄清這個問題的。


上面通過fget_light函數得到的file需要通過S_ISDIR(file->f_path.dentry->d_inode->i_mode)來檢查這個已經存在的inode是不是一個目錄,若是則一切OK。否則fail。若是目錄的話,接下來調用file_permission(file,MAY_EXEC)來判斷該文件的執行權限。若是具有執行權限,那麼此時也應具有read權限,若全OK的話,審查就算是通過了。之後,通過如下語句設置nd->path,並將其引用計數加一。


nd->path=file->f_path;
path_get(&file->f_path);


記住,還沒完呢,一定要調用fput_light()fget_light()返回的file指針空間釋放掉。同時,實參中的fput_needed要和fget_light()函數返回的值相同。否則,後果...自己去體驗一下就知道了...


好了,到這裏我們的前期初始化工作就完成了。下一步通過調用link_path_walk(pathname,&nd)函數進行文件名解析。其實它是一個很基本的文件名字解析函數,用於將pathname轉換成最終的dentry,並存儲於nd->path.dentry中。注意這裏返回的dentry其實是其父結點相應信息。不理解的話,繼續往下看。待該函數返回時,如果沒有錯誤,那麼之後就可以通過get_enpty_filp()basename(pathname)申請filp(類型爲:struct file *)指針了。若申請成功,則將filp放入nd.intent.open.file域中,同時初始化filpnd.intent.open指針結構所指的實例中的f_flagsflagscreate_mode等域。


接下來就剩open的最實質性的工作了。它是通過do_last函數來完成的。


do_last函數的原型如下(位於文件fs/namei.c)

staticstructfile*do_last(structnameidata*nd,strcut path*path,intopen_flag,intacc_mode,intmode,const char*pathname)


函數中首先會根據open_flag中的標誌位判斷該文件是不是需要被創建,若否,那麼會進行最後階段的inode節點的查找,如果此時查找成功,這直接調用path_to_nameidate()函數通過如下語句設置nd->path.dentry的值:


nd->path.dentry=path->dentry


若沒有查找成功並且flag中有沒有將O_CREAT標誌爲置位。那麼,此時通過將error設置成-ENOTDIR並向用戶態返回NODIR的出錯提示。


但是,如果用戶在使用open函數時,使用了create選項的話,那麼我們只好進行下面的操作了:創建新的inode節點。這項工作是通過__open_namei_create()函數完成的。在該函數內部,會調用VFS層的vfs_create()函數將控制下發到不同的文件系統處理函數中。它是通過這樣的調用過成完成的:


error=dir->i_op->create(dir,dentry,mode,nd)


如果當前使用的是ext4文件系統的話,那麼create指針指向的是ext4_create,否則就有可能是ext3_create等等。具體的還要視具體情況而定。到了這裏我們也可以初步的瞭解VFS的作用了吧。就是將更低層的不同文件系統類型作統一管理,爲上層的使用提供統一的函數接口,這爲其作用之一。欲想知道create函數指針是如何被初始化的,請看或查閱內核模塊加載的過程。這裏不再詳述。inode被成功創建後,其相關的創建時間,訪問時間,修改時間,giduideuid以及訪問權限等一些屬性信息就會被正確的初始化。該過程結束後,我們還會調用fsnotifyhook函數fsnotify_create將新建文件節點的事件提交到監控系統。待完成後,操作流就會調用nameidata_to_filp(nd)進行nd->path.dentry的設置,並調用__denry_open()函數做打開文件的工作。其實在該函數仍然是VFS層函數,做實質工作的函數是通過f->f_op->open方式進行相應文件系統處理函數的調用的。open的初始化,請看create函數的過程實現。待這些操作完成後,控制將會返回到do_sys_open中。


另外,接前文如果文件已經存在,那好,餘下的就是一點點收尾工作,在函數finish_open()中調用nameidata_to_filp()函數將文件打開。之後同樣,控制返回到do_sys_open()中。


好了,現在我們已經回到了最初的地方,dentry找到了,inode生成了,filp確定了,接下來就是filpfd的綁定了。


它是如何完成的呢。說明這個之前,我們先看以下fsnotify_open()函數,它的作用是將filp的監控點打開,並將其添加到監控系統中。


完成filpfd綁定功能的函數是fd_install()。它的功能就是將filp添加到fdt->fd指定的數組空間,索引是fd的位置中去。其中struct fdtable *fdt是通過如下方式得到的:


structfiles_struct*file=current->files;
structfdtable*fdt;
spin_lock(&files->file_lock);
fdt=files_fdtable(files);
rcu_assign_pointer(fdt->fd[fd],filp);
spin_unlock(&files->file_lock)


從上面的程序片段中可以看到,每個運行的進程都有屬於自己的文件描述符表,由指針current->files指示。


上述操作都完成後,那麼空間也將會隨即返回到用戶空間了,返回值當然就是fd。這就是我們傳遞給readwritefcntlfioctl等函數的文件描述符。至此,文件的打開操作就全部完成了。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章