linux字符設備文件的打開操作


2.7  字符設備文件的打開操作(1)

作為例子,這里假定前面對應於/dev/demodev設備節點的驅動程序在自己的代碼里實現了如下的struct file_operations對象fops:

  1. static struct file_operations fops = {  
  2.     .open = demoopen,  
  3.     .read = demoread,  
  4.     .write = demowrite,  
  5.     .ioctl = demoioctl,  
  6. };  

 

用戶空間open函數的原型為:

  1. int open(const char *filename, int flags, mode_t mode); 

這個函數如果成功,將返回一個文件描述符,否則返回-1。函數的第一個參數filename表示要打開的文件名,第二個參數flags用於指定文件的打開或者創建模式,本書在后續"字符設備的高級操作"一章中會討論其中一些常見取值對驅動程序的影響,最后一個參數mode只在創建一個新文件時才使用,用於指定新建文件的訪問權限,比如可讀、可寫及可執行等權限。

位於內核空間的驅動程序中open函數的原型為:

  1. <include/linux/fs.h
  2. struct file_operations {  
  3.     …  
  4.     int (*open) (struct inode *, struct file *);  
  5.     …  
  6. };  

兩者相比差異很大。接下來我們將描述從用戶態的open是如何一步一步調用到驅動程序提供的open函數(在我們的例子中,它的具體實現是demoopen)的。如同設備文件節點的生成一樣,透徹了解這里的每一個步驟也需要掌握全面的Linux下文件系統的技術細節。從設備驅動程序員的角度,我們依然將重點放在兩者如何建立聯系的關鍵點上。

用戶程序調用open函數返回的文件描述符,本文用fd表示,這是個int型的變量,會被用戶程序后續的read、write和ioctl等函數所使用。同時可以看到,在驅動程序中的demodev_read、demodev_write和demodev_ioctl等函數其第一個參數都是struct file *filp。顯然內核需要在打開設備文件時為fd與filp建立某種聯系,其次是為filp與驅動程序中的fops建立關聯。

用戶空間程序調用open函數,將發起一個系統調用,通過sys_open函數進入內核空間,其中一系列關鍵的函數調用關系如圖2-8所示:

 
圖2-8  sys_open到chrdev_open調用流程

do_sys_open函數首先通過get_unused_fd_flags為本次的open操作分配一個未使用過的文件描述符fd :

  1. <fs/open.c
  2. long do_sys_open(int dfd, const char __user *filename, int flags, int mode)  
  3. {  
  4.     …  
  5.     fd = get_unused_fd_flags(flags);  
  6.     …  
  7. }  

get_unused_fd_flags實際上是封裝了alloc_fd的一個宏,真正分配fd的操作發生在alloc_fd函數中,后者會涉及大量文件系統方面的細節,這不是本書的主題。讀者這里只需知道alloc_fd將會為本次的open操作分配一個新的fd。

do_sys_open隨后調用do_filp_open函數,后者會首先查找"/dev/demodev"設備文件所對應的inode。在Linux文件系統中,每個文件都有一個inode與之對應。從文件名查找對應的inode這一過程,同樣會涉及大量文件系統方面的細節。

do_filp_open在成功查找到"/dev/demodev"設備文件對應的inode之后,接着會調用函數get_empty_filp,后者會為每個打開的文件分配一個新的struct file類型的內存空間(本書將把指向該結構體對象的內存指針簡寫為filp):

  1. <fs/namei.c
  2. struct file *do_filp_open(int dfd, const char *pathname,  
  3.         const struct open_flags *op, int flags)  
  4. {  
  5.     struct nameidata nd;  
  6.     struct file *filp;  
  7.  
  8.     filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);  
  9.     …  
  10.     return filp;  
  11. }  

 

內核用struct file對象來描述進程打開的每一個文件的視圖,即使是打開同一文件,內核也會為之生成一個新的struct file對象,用來表示當前操作的文件的相關信息,其定義為:

  1. <include/linux/fs.h
  2. struct file {  
  3.     union {  
  4.         struct list_head    fu_list;  
  5.         struct rcu_head     fu_rcuhead;  
  6.     } f_u;  
  7.     struct path     f_path;  
  8. #define f_dentry    f_path.dentry  
  9. #define f_vfsmnt    f_path.mnt  
  10.     const struct file_operations    *f_op;  
  11.     spinlock_t      f_lock;  
  12.     atomic_long_t       f_count;  
  13.     unsigned int        f_flags;  
  14.     fmode_t         f_mode;  
  15.     loff_t          f_pos;  
  16.     struct fown_struct  f_owner;  
  17.     const struct cred   *f_cred;  
  18.     struct file_ra_state    f_ra;  
  19.  
  20.     u64         f_version;  
  21. #ifdef CONFIG_SECURITY  
  22.     void            *f_security;  
  23. #endif  
  24.     /* needed for tty driver, and maybe others */  
  25.     void            *private_data;  
  26.  
  27. #ifdef CONFIG_EPOLL  
  28.     /* Used by fs/eventpoll.c to link all the hooks to this file */  
  29.     struct list_head    f_ep_links;  
  30. #endif /* #ifdef CONFIG_EPOLL */  
  31.     struct address_space    *f_mapping;  
  32. };  
2.7  字符設備文件的打開操作(2)
這個結構中與設備驅動程序關系最密切的是f_op、f_flags、f_count和private_data成員。f_op指針的類型是struct file_operations,恰好我們的字符設備驅動程序中也需要實現一個該類型的對象,馬上我們將看到這兩者之間是如何建立聯系的。f_flags用於記錄當前文件被open時所指定的打開模式,這個成員將會影響后續的read/write等函數的行為模式。成員f_count用於對struct file對象的使用計數,當close一個文件時,只有struct file對象中f_count成員為0才真正執行關閉操作。private_data常被用來記錄設備驅動程序自身定義的數據,因為filp指針會在驅動程序實現的file_operations對象其他成員函數之間傳遞,所以可以通過filp中的private_data成員在某一個特定文件視圖的基礎上共享數據。
進程為文件操作維護一個文件描述符表(current->files->fdt),正如在本節開始部分看到的那樣,對設備文件的打開,最終會得到一個文件描述符fd,然后用該描述符fd作為進程維護的文件描述符表(指向struct file *類型數組)的索引值,將之前新分配的struct file空間地址賦值給它:
  1. current->files->fdt->pfd[fd] = filp; 
 
這樣,用戶空間程序在后續的read、write、ioctl等函數調用中利用fd就可以找到對應的filp,如圖2-9所示: 
 
(點擊查看大圖)圖2-9  fd與filp的關聯
在do_sys_open的后半部分,會調用__dentry_open函數將"/dev/demodev"對應節點的inode中的i_fop賦值給filp->f_op,然后調用i_fop中的open函數:
  1. <fs/open.c
  2. static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,  
  3.                     struct file *f,  
  4.                     int (*open)(struct inode *, struct file *),  
  5.                     const struct cred *cred)  
  6. {  
  7.     struct inode *inode;  
  8.     …  
  9.     f->f_op = fops_get(inode->i_fop);  
  10.     …  
  11.     if (!open && f->f_op)  
  12.         open = f->f_op->open;  
  13.     if (open) {  
  14.         error = open(inode, f);  
  15.         …  
  16.     }  
  17.     …  
  18. }  
 
__dentry_open函數當初在nameidata_to_filp中被調用時,第四個實參是NULL,所以在__dentry_open中,open = f->f_op->open。在上節設備文件節點的生成中,我們知道inode->i_fop = &def_chr_fops,這樣filp->f_op = &def_chr_fops。接下來會利用filp中的這個新的f_op作調用:filp->f_op->open(inode, filp),於是chrdev_open函數將被調用到。該函數非常重要,為了突出其主線,下面先將它改寫成以下簡單幾行:
  1. <fs/char_dev.c
  2. static int chrdev_open(struct inode *inode, struct file *filp)  
  3. {  
  4.     int ret = 0, idx;  
  5.       
  6.     struct kobject *kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);  
  7.     struct cdev *new = container_of(kobj, struct cdev, kobj);  
  8.     inode->i_cdev = new;  
  9.     list_add(&inode->i_devices, &new->list);  
  10.     filp->f_op = new->ops;  
  11.     if (filp->f_op->open) {  
  12.         ret = filp->f_op->open(inode,filp);  
  13.     }  
  14.     return ret;  
  15. }  
函數首先通過kobj_lookup在cdev_map中用inode->i_rdev來查找設備號所對應的設備new,這里展示了設備號的作用。成功查找到設備后,通過filp->f_op = new->ops這行代碼將設備對象new中的ops指針(前面曾討論過,驅動程序通過調用cdev_init將其實現的file_operations對象的指針賦值給設備對象cdev的ops成員)賦值給filp對象中的f_op成員,此處展示了如何將驅動程序中實現的struct file_operations與filp關聯起來,從此圖2-9中的filp->f_op將指向驅動程序中實現的struct file_operations對象。
接下來函數會檢查驅動程序中是否實現了open函數(if (filp->f_op->open)),如果實現了,就調用設備驅動程序中實現的open函數。打開一個字符設備節點的大體流程如圖2-10所示:
 
圖2-10  開一個字符設備節點的功能流程
 
 
2.7  字符設備文件的打開操作(3)
圖中,當應用程序打開一個設備文件時,將通過系統調用sys_open進入內核空間。在內核空間將主要由do_sys_open函數負責發起整個設備文件打開操作,它首先要獲得該設備文件所對應的inode,然后調用其中的i_fop函數,對字符設備節點的inode而言,i_fop函數就是chrdev_open(圖中標號1的線段),后者通過inode中的i_rdev成員在cdev_map中查找該設備文件所對應的設備對象cdev(圖中標號2的線段),在成功找到了該設備對象之后,將inode的i_cdev成員指向該字符設備對象(圖中標號3的線段),這樣下次再對該設備文件節點進行打開操作時,就可以直接通過i_cdev成員得到設備節點所對應的字符設備對象,而無須再通過cdev_map進行查找。內核在每次打開一個設備文件時,都會產生一個整型的文件描述符fd和一個新的struct file對象filp來跟蹤對該文件的這一次操作,在打開設備文件時,內核會將filp和fd關聯起來,同時會將cdev中的ops賦值給filp->f_op(圖中標號4的線段)。最后,sys_open系統調用將設備文件描述符fd返回到用戶空間,如此在用戶空間對后續的文件操作read、write和ioctl等函數的調用,將會通過該fd獲得文件所對應的filp,根據filp中的f_op就可以調用到該文件所對應的設備驅動上實現的函數。
通過以上過程,我們看到了設備號在其中的重要作用。當設備驅動程序通過cdev_add把一個字符設備對象加入到系統時,需要一個設備號來標記該對象在cdev_map中的位置信息。當我們在用戶空間通過mknod來生成一個設備文件節點時,也需要在命令行中提供設備號的信息,內核會將該設備號信息記錄到設備文件節點所對應inode的i_rdev成員中。當我們的應用程序打開一個設備文件時,系統將會根據設備文件對應的inode->i_rdev信息在cdev_map中尋找設備。所以在這個過程中務必要保證設備文件節點的inode->i_rdev數據和設備驅動程序使用的設備號完全一致,否則就會發生嚴重問題。對應到現實世界的操作,那就是在用mknod生成設備節點時所提供的設備號信息一定要與設備驅動程序中分配使用的設備號一致。
在上述open一個設備文件的基礎上,接下來不妨看看它的相反操作close。有了前面對open操作技術細節討論所打下的良好基礎,現在理解起close並不困難,在此讀者也正好可以看看用戶空間open函數返回的文件描述符fd如何被close等函數使用。
用戶空間close函數的原型為:
 
  1. int close(unsigned int fd); 
 
針對close的系統調用函數為sys_close,這里將其核心代碼重新整理如下:
  1. <fs/open.c
  2. int sys_close(unsigned int fd)  
  3. {  
  4.     struct file * filp;  
  5.     struct files_struct *files = current->files;  
  6.     struct fdtable *fdt;  
  7.     int retval;  
  8.     …  
  9.     fdt = files_fdtable(files);  
  10.     …  
  11.     filp = fdt->fd[fd];  
  12.     …  
  13.     retval = filp_close(filp, files);  
  14.     …  
  15.     return retval;  
  16. }  
 
從fd得到filp這段代碼,請讀者參考本章2-9。接下來調用filp_close函數,close函數的大部分秘密都隱藏在其中,有必要看看其主要代碼片段:
  1. <fs/open.c
  2. int filp_close(struct file *filp, fl_owner_t id)  
  3. {  
  4.     int retval = 0;  
  5.  
  6.     if (!file_count(filp)) {  
  7.         printk(KERN_ERR "VFS: Close: file count is 0\n");  
  8.         return 0;  
  9.     }  
  10.  
  11.     if (filp->f_op && filp->f_op->flush)  
  12.         retval = filp->f_op->flush(filp, id);  
  13.         …  
  14.     fput(filp);  
  15.     return retval;  
  16. }  
if (!file_count(filp))用來判斷filp中的f_count成員是否為0,如果針對同一個設備文件close的次數多於open次數,就會出現這種情況,此時函數直接返回0,因為實質性的工作都被前面的close做完了。接下來的情況有點意思,如果設備驅動程序定義了flush函數,那么在release函數被調用前,會首先調用flush,這是為了確保在把文件關閉前緩存在系統中的數據被真正寫回到硬件中。字符設備很少會出現這種情況,因為這種設備的慢速I/O特性決定了它無須使用這種緩沖機制來提升系統性能,但是塊設備就不一樣了,比如SCSI硬盤會和系統進行大量數據的傳輸,為此內核為塊設備驅動程序設計了高速緩存機制,這種情況下為了保證文件數據的完整性,必須在文件關閉前將高速緩存中的數據寫回到磁盤中。不過這是后話了,塊設備驅動程序的這種機制將在"塊設備驅動程序"一章中討論。
函數的最后調用fput,貌似很簡單的一個函數,其實內涵卻很豐富:
  1. <fs/file_table.c
  2. void fput(struct file *file)  
  3. {  
  4.     if (atomic_long_dec_and_test(&file->f_count))  
  5.         __fput(file);  
  6. }  
 
函數中的那個atomic_long_dec_and_test是個體系架構相關的原子測試操作,就是說,如果file->f_count的值為1,那么它將返回true,這意味着可以真正關閉當前的文件了,所以__fput將被調用,並最終完成文件關閉的任務,它的一些關鍵調用節點如下所示:
  1. <fs/file_table.c
  2. static void __fput(struct file *file)  
  3. {  
  4.     …  
  5.     if (unlikely(file->f_flags & FASYNC)) {  
  6.         if (file->f_op && file->f_op->fasync)  
  7.             file->f_op->fasync(-1, file, 0);  
  8.     }  
  9.     if (file->f_op && file->f_op->release)  
  10.         file->f_op->release(inode, file);  
  11.     …  
  12.     fops_put(file->f_op);  
  13.     file_free(file);  
  14. }  
 
注意上面的FASYNC標志位,在本書后面的章節會討論到file_operations中的一些常用的函數實現。然后函數調用到了設備驅動程序中提供的release函數,接下來是一些系統資源的釋放。可見,對於應用程序的一個close調用,並非必然對應着release函數的調用,只有在當前文件的所有副本都關閉之后,release函數才會被調用。
 

2.8  本章小結

本章描述了字符設備驅動程序內核框架的技術細節。基本上可以看到,字符設備驅動內核框架的展開是按照兩條線進行的:一條是設備與系統的關系,一個字符設備對象cdev通過cdev_add加入到系統中(由cdev_map所管理的哈希鏈表),此時設備號作為哈希索引值;另一條是設備與文件系統的關系,設備通過設備號以設備文件的形式向用戶空間宣示其存在。這兩條線間的聯系通過文件系統接口去打開一個字符設備文件而建立:

mknod命令將為字符設備創建一個設備節點,mknod的系統調用將會為此設備節點產生一個inode,mknod命令行中給出的設備號將被記錄到inode->i_rdev中,同時inode的i_fop會將open成員指向chrdev_open函數。

當用戶空間open一個設備文件時,open函數通過系統進入內核空間。在內核空間,首先找到該設備節點所對應的inode,然后調用inode->i_fop->open(),我們知道這將導致chrdev_open函數被調用。同時,open的系統調用還將產生一個(fd, filp)二元組來標識本次的文件打開操作,這個二元組是一一對應的關系。

chrdev_open通過inode->i_rdev在cdev_map中查找inode對應的字符設備,cdev_map中記錄着所有通過cdev_add加入系統的字符設備。

當在cdev_map中成功查找到該字符設備時,chrdev_open將inode->i_cdev指向找到的字符設備對象,同時將cdev->ops賦值給filp->f_op。

字符設備驅動程序負責實現struct file_operations對象,在字符設備對象初始化時cdev_init函數負責將字符設備對象cdev->ops指向該file_operations對象。

用戶空間對字符設備的后續操作,比如read、write和ioctl等,將通過open函數返回的fd找到對應的filp,然后調用filp->f_op中實現的各類字符設備操作函數。

以上就是內核為字符設備驅動程序設計的大體框架,從中可以看到設備號在溝通用戶空間的設備文件與內核中的設備對象之間所起的重要作用。

另外,對於字符設備驅動程序本身而言,核心的工作是實現struct file_operations對象中的各類函數,file_operations結構中雖然定義了眾多的函數指針,但是現實中設備驅動程序並不需要為它的每一個函數指針都提供相應的實現。本書后面的"字符設備的高級操作"一章會詳細討論其中一些重要函數的作用和實現原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM