本筆記對 linux kernel 的文件系統操作以及設備操作進行了分析,主要是針 對 ext3 文件系統的 open 流程的分析,目的是為了解答心中的幾個疑問: 1、一個文件的操作流程,系統是如何把 struct file 與 struct dentry 以及 struct inode 結合起來的? 2、文件與設備驅動都是對 VFS(Virtual File System) 抽象出來的 struct file 進行操作的,那麼系統是如何區分的?在哪裡開始區分的? 3、linux 內核中沒有類 UNIX VFS(Virtual File System) 提供的 struct vnode 結構,那麼具體的文件操作是如何與實際文件系統的操作掛鉤的? 4、超級塊(super block)在文件與設備驅動操作中起到的作用? 5、在以前的嘗試中對 struct file 做手腳為什麼影響不到全局? 6、在文件系統內核有幾個函數操作集?有何不同?分別是在什麼時候賦值? 注:此文檔是根據當時的分析過程記錄的,分析順序也就沒有再更改過, 每個人讀內核源碼的思路不同,或者說目的不同,流程自然也就不同。 所以在別人看來我所記錄的可能比較凌亂。如果真是這樣,那我只能 說句抱歉,因為我並不打算再修改記錄順序。最後還是那句話,如果 您在閱讀本文時發現了錯誤,還望得到您的指正。 我們知道在 linux kernel 中,如果想操作一個文件,首先要通過 filp_open() 這個 kernel api 來打開這個文件,那麼我們就從這裡入手分析。可以看到 filp_open() 函數只是個簡單封狀,具體實現是 do_filp_open() 函數,函數 本身先通過 open_namei() 函數得到一個 fd 對應的 struct nameidata 結構。 最後使用 nameidata_to_filp() 函數返回一個 struct file 結構。 static struct file *do_filp_open(int dfd, const char *filename, int flags, int mode) { int namei_flags, error; struct nameidata nd; namei_flags = flags; if ((namei_flags+1) & O_ACCMODE) namei_flags++; // // 這個函數調用 path_lookup_xxx() 等函數根據路徑名稱 // 返回一個 struct nameidata 結構。這個函數完成了很多 // 工作,後面會隨著疑問詳細分析這個函數。這裡只需要知 // 道它返回了一個 nameidata 結構。 // error = open_namei(dfd, filename, namei_flags, mode, &nd); if (!error) // // 這裡返回的 struct file 結構已經創建並填充完畢了。 // 直接返回給調用者。 // return nameidata_to_filp(&nd, flags); return ERR_PTR(error); } 這個函數根據 struct nameidata 結構返回一個 struct file。可以看到 struct file 是在使用了 __dentry_open() 函數後被填充的,且使用的第 一個參數是 nameidata->dentry,這也是為什麼我們要獲得 struct nameidata 的一個主要原因,其目的就是為了得到 struct dentry 結構。 struct file *nameidata_to_filp(struct nameidata *nd, int flags) { struct file *filp; /* Pick up the filp from the open intent */ filp = nd->intent.open.file; /* Has the filesystem initialised the file for us? */ if (filp->f_dentry == NULL) // // 這個函數主要就是填充一個 struct file 結構,通過這段 // 代碼也可以看到,一個 struct file 是動態分配的。 // filp = __dentry_open(nd->dentry, nd->mnt, flags, filp, NULL); else path_release(nd); return filp; } 此函數分配並填充一個 struct file 結構。從這個函數中很明顯可以看到, 一個 struct file 結構是使用 struct dentry,struct inode,struct vfsmount 結構中的相關信息填充的。在 struct dentry 中有一個區域指向了 struct inode 結構,這也就是為什麼我們要獲得 struct dentry 原因之一。有了 struct inode 結構我們就可以得到一個文件的相關信息和實際文件系統所提供的函數,如 ext3 文件系統。或者是一個設備驅動所提供的方法,如字符設備驅動。為什麼這麼說? 看下面的詳細記錄。 static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags, struct file *f, int (*open)(struct inode *, struct file *)) { struct inode *inode; int error; // // 得到訪問標志 // f->f_flags = flags; f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE; // // 通過 struct dentry 得到 struct inode 結構 // inode = dentry->d_inode; // // 判斷這個文件(inode) 是否有寫權限,沒有則 // 跳轉到 cleanup_file 處退出 // if (f->f_mode & FMODE_WRITE) { error = get_write_access(inode); if (error) goto cleanup_file; } // // 使用 vfsmount,dentry,inode 結構 // 填充 struct file 中相關域。 // f->f_mapping = inode->i_mapping; f->f_dentry = dentry; f->f_vfsmnt = mnt; f->f_pos = 0; // // 注意:這裡使用的是 struct inode 中的 struct file_operations // 回調函數來填充的 struct file->f_op。也就是說 struct file 中的 // 函數其實是 inode->file_operations 的一份復制品。而這個 struct // file 很明顯是動態創建的,也就是說 open 一個文件則會動態生成一個 // struct file 結構,並把 inode->i_fop 函數給它,struct file 並不是 // 全局唯一的,而是與進程相關的,在 task_struct 中的 files_struct // 結構則是 struct file 的一個集合。這也就是為什麼在 struct file // 裡做了手腳,影響的僅是當前進程,而不是全局的原因。;) // f->f_op = fops_get(inode->i_fop); file_move(f, &inode->i_sb->s_files); // // 注意:這裡調用了 struct file->f_op->open 函數,也就是說調用了 // struct inode->i_fop->open 函數。這裡有必要注解一下,在 struct // inode 結構中,有兩套回調函數的方法集,一個是 struct // file_operations 一個是 struct inode_operations。而對於 open 函數 // 只是存在 file_operations 當中,另一個則不存在。那麼在 struct inode // 這個 i_fop 函數集中有可能使用的是實際文件系統的函數,如 // ext3_file_operations 函數集。也有可能是一個設備驅動所提供的函數 // 方法如 def_chr_fops 函數集。 // if (!open && f->f_op) open = f->f_op->open; if (open) { error = open(inode, f); if (error) goto cleanup_all; } // // 去掉相關標志位。 // f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC); file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping); /* NB: we're sure to have correct a_ops only after f_op->open */ if (f->f_flags & O_DIRECT) { if (!f->f_mapping->a_ops || ((!f->f_mapping->a_ops->direct_IO) && (!f->f_mapping->a_ops->get_xip_page))) { fput(f); f = ERR_PTR(-EINVAL); } } return f; // // 以下兩個流程,只有失敗時才會走到。釋放 struct file 中 // 所有相關信息,並返回錯誤。 // cleanup_all: fops_put(f->f_op); if (f->f_mode & FMODE_WRITE) put_write_access(inode); file_kill(f); f->f_dentry = NULL; f->f_vfsmnt = NULL; cleanup_file: put_filp(f); dput(dentry); mntput(mnt); return ERR_PTR(error); } 在上面詳細分析中介紹的 struct file 中使用的 f_op 其實是 struct inode->i_fop 中的一個副本。寫過設備驅動的人都知道,在使用 register_xxx 注冊一個“字符” 或 “塊” 設備驅動時,都要填充一個 struct file 結構以便與應用層交互。那麼 這樣就存在一個問題,大家都知道在 *nix 系統下文件與設備都是以文件形式存在的, 即都有 inode,而訪問 file system 與 device driver 所使用的函數操作集 都是通過 struct inode 提供的,且都是一個 file_operations 函數集,那麼系統 是如何區分所訪問的是 file system 上的文件還是 device driver 呢?如果是 device driver 那麼又是在什麼地方初始化連接你所注冊的回調函數呢?下面我們 以 ext3 文件系統為例,來看一下 ext3_read_inode() 函數的實現。至於這個函數 什麼時候被調用,在哪裡被調用的?以及下面注釋中提到的 ext3 文件系統的 open 操作為什麼為空操作等疑問會在後面章節中介紹,這裡為了結合上下文,保持連貫 性,還是先講一下這個函數。 void ext3_read_inode(struct inode * inode) { struct ext3_iloc iloc; struct ext3_inode *raw_inode; struct ext3_inode_info *ei = EXT3_I(inode); struct buffer_head *bh; int block; // // 篇幅所限,在這個函數中我們只列出相關代碼。 // #ifdef CONFIG_EXT3_FS_POSIX_ACL ei->i_acl = EXT3_ACL_NOT_CACHED; ei->i_default_acl = EXT3_ACL_NOT_CACHED; #endif ei->i_block_alloc_info = NULL; // // 注意:這裡的 __ext3_get_inode_loc 是產生 // 一個磁盤 I/O 從磁盤讀取真正的 struct inode // 來填充 in core 類型的。注意這個函數使用的 // 第三個參數,為 0 的情況下產生 I/O 從磁盤 // 讀取,否則從 buffer_head 磁盤緩存中查找。 // if (__ext3_get_inode_loc(inode, &iloc, 0)) // // 如果從磁盤獲取 inode 失敗則直接跳到退出處理, // 不會進行下面的任何操作。 // goto bad_inode; ...... ...... // // 可以看到,目錄/文件/連接分別賦予了不同的函數集。 // if (S_ISREG(inode->i_mode)) { // // 如果是普通文件的話,則使用 ext3_file_xxx 函數集 // 注意:在使用 ext3_file_operations 函數集時,它的 // open 函數對應的是 generic_file_open() 函數,而這個函數 // 除了判斷大文件是否合法外,幾乎就是一個空函數,也就是說 // 如果是在一個 ext3 文件系統上,open 操作其實沒有任何具體 // 動作,是無意義的。為什麼會這樣呢?在後面介紹文件系統時 // 會講到。 // inode->i_op = &ext3_file_inode_operations; inode->i_fop = &ext3_file_operations; ext3_set_aops(inode); } else if (S_ISDIR(inode->i_mode)) { // // 如果是目錄的話,則要區別對待,使用 ext3_dir_xxx 函數集 // inode->i_op = &ext3_dir_inode_operations; inode->i_fop = &ext3_dir_operations; } else if (S_ISLNK(inode->i_mode)) { // // 如果是連接的話,也要區別對待,使用 ext3_symlink_xxx 函數集 // if (ext3_inode_is_fast_symlink(inode)) inode->i_op = &ext3_fast_symlink_inode_operations; else { inode->i_op = &ext3_symlink_inode_operations; ext3_set_aops(inode); } } else { // // 如果以上三種情況都排除了,那麼我們則認為他是一個設備驅動 // 注意:這裡的僅對 inode->i_op 函數集進行了直接賦值。對於 // inode->i_fop 函數集使用的是 init_special_inode() 函數 // 進行的賦值 // inode->i_op = &ext3_special_inode_operations; if (raw_inode->i_block[0]) init_special_inode(inode, inode->i_mode, old_decode_dev(le32_to_cpu(raw_inode->i_block[0]))); else init_special_inode(inode, inode->i_mode, new_decode_dev(le32_to_cpu(raw_inode->i_block[1]))); } ...... ...... } 流程走到這個函數已經可以確定用戶操作打開的是一個設備驅動,那麼這裡就要 繼續判斷打開的是哪種類型設備驅動和需要賦什麼樣的函數操作集。通過下面的 代碼我們可以看到,系統只支持了四種設備驅動類型,也就是說系統注冊設備驅 動類型只可能是 “字符”,“塊”,“FIFO”,“SOCKET” 設備,其中的 “FIFO”,“SOCK” 還不是真實設備,這裡我們稱其為“偽” 設備,可能用詞 不大准確,姑且在這裡這麼叫。 void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) { inode->i_mode = mode; // // 如果是字符設備,則使用 def_chr_fops 函數集 // 只有真實設備才有會設置 inode->i_rdev 字段 // if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; // // 如果是塊設備,則使用 def_blk_fops 函數集 // 只有真實設備才有會設置 inode->i_rdev 字段 // } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; // // 如果是 FIFO,則使用 def_fifo_fops 函數集 // } else if (S_ISFIFO(mode)) inode->i_fop = &def_fifo_fops; // // 如果是 SOCKET,則使用 def_sock_fops 函數集 // else if (S_ISSOCK(mode)) inode->i_fop = &bad_sock_fops; // // 如果不是以上四種類型則忽略,並打印提示信息。 // else printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n", mode); } 以上四種類型設備驅動的函數集都大同小異,這裡我們僅以“字符”設備的函數 集為例,可以看到 file_operations 結構只設置了 open 方法,把它指向了 chrdev_open() 函數。那麼我們的在設備驅動裡指定的 struct file->f_op 函 數怎麼被調用的?繼續看 chrdev_open() 函數實現。 const struct file_operations def_chr_fops = { .open = chrdev_open, }; 此函數主要完成的工作就是填充並調用用戶給出的 struct file->f_op 結構中的 函數集。它首先嘗試得到正確的字符設備結構,判斷如果注冊了相應的函數集則 調用。 int chrdev_open(struct inode * inode, struct file * filp) { struct cdev *p; struct cdev *new = NULL; int ret = 0; spin_lock(&cdev_lock); // // 得到相應的字符設備結構 // p = inode->i_cdev; if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); // // 如果此字符設備結構無效,則從設備對象管理中查找 // kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -ENXIO; new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); // // 再次嘗試獲得正確的字符設備結構 // p = inode->i_cdev; if (!p) { inode->i_cdev = p = new; inode->i_cindex = idx; list_add(&inode->i_devices, &p->list); new = NULL; // // 使用 cdev_get() 函數判斷相應設備結構的內核設備對象是否 // 有效 // } else if (!cdev_get(p)) ret = -ENXIO; // // 如果有效,則調用 cdev_get() 函數繼續判斷相應設備結構的內核 // 設備對象是否有效,如果無效則表明此設備仍不可用。 // } else if (!cdev_get(p)) ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); // // 如果到此字符設備還無效的話,則返回錯誤。 // if (ret) return ret; // // 注意:這裡使用 cdev->file_operations 函數操作集來 // 填充的 struct file->f_op 這也是我們注冊字符設備驅動 // 時所給出的函數集。 // filp->f_op = fops_get(p->ops); // // 如果 struct file->f_op 無效,那麼它所指向的函數集 // 肯定也無效,這樣的話直接返回錯誤。注意:這裡有一 // 種可能,那就是調用者雖注冊了一個字符設備驅動,但是 // 並沒有提供相應的操作集,或許調用者認為沒有必要。 // if (!filp->f_op) { cdev_put(p); return -ENXIO; } // // 如果 open 函數有效那麼則先鎖定內核,調用此方法後 // 再解鎖內核 // if (filp->f_op->open) { lock_kernel(); ret = filp->f_op->open(inode,filp); unlock_kernel(); } if (ret) cdev_put(p); return ret; } 到這裡我們可以知道,對文件或設備驅動的判斷與函數集的賦值都是在文件 系統這一級區分的,也就是說在有 open 操作時是到具體的文件系統,如 ext3,並在 ext3 上再次區分出是否為設備驅動,這點很好理解,因為設備 驅動也是以文件形式存在的。分析到這裡可以算是把對設備驅動的操作流程 弄清晰了。但這僅是對設備驅動的操作,別忘了上面還存在一大堆的疑問, 我們知道只有在觸發調用了 ext3_read_inode() 時才會區分,那麼它何時 被調用的?什麼情況下調用的?為什麼 ext3 的文件操作集中的 open 是 空操作呢?要解答這些問題,我們仍要從 open_namei() 函數開始進行分析。 在這個函數的實現過程中會根據標志的不同將路徑名轉換成 struct nameidata 結構,當得到此結構後還會根據目錄,連接等做不同處理。這裡我們只關心操作 流程,所以只對 path_look_xxx() 函數做跟蹤分析。 int open_namei(int dfd, const char *pathname, int flag, int mode, struct nameidata *nd) { int acc_mode, error; struct path path; struct dentry *dir; int count = 0; // // 篇幅所限,在這個函數中我們只列出相關代碼。 // ...... // // 判斷是否是建立標志,如果不是則使用 path_lookup_open() // if (!(flag & O_CREAT)) { // // 通過路徑名查詢 inode, dentry 並返回 nameidata 結構。 // error = path_lookup_open(dfd, pathname, lookup_flags(flag), nd, flag); if (error) return error; goto ok; } /* * Create - we need to know the parent. */ // // 如果是建立標志則使用 path_lookup_create() // error = path_lookup_create(dfd,pathname,LOOKUP_PARENT,nd,flag,mode); if (error) return error; ...... } 上面的 path_lookup_open() 與 path_lookup_create() 都是一個很簡單的封狀 無條件的調用了 __path_lookup_intent_open() 函數,只不過是傳輸標志不同 而已。此函數在預先填充一些 struct nameidata 結構後繼續調用 do_path_lookup() 完成查找。 static int __path_lookup_intent_open(int dfd, const char *name, unsigned int lookup_flags, struct nameidata *nd, int open_flags, int create_mode) { // // 獲得一個空的 struct file 結構 // struct file *filp = get_empty_filp(); int err; if (filp == NULL) return -ENFILE; // // 先填充要返回的 struct nameidata 結構中的相關字段 // nd->intent.open.file = filp; nd->intent.open.flags = open_flags; // // 填充建立標志位,這個也就是 path_lookup_open() // 與 path_lookup_create() 函數調用的區別 // nd->intent.open.create_mode = create_mode; // // 根據路徑調用 do_path_lookup() 得到一個 struct nameidata 結構 // err = do_path_lookup(dfd, name, lookup_flags|LOOKUP_OPEN, nd); if (IS_ERR(nd->intent.open.file)) { if (err == 0) { err = PTR_ERR(nd->intent.open.file); path_release(nd); } } else if (err != 0) release_open_intent(nd); return err; } 此函數根據 "/" 根路徑與 AT_FDCWD 標志從不同位置得到 struct vfsmount 與 struct dentry 結構來填充 struct nameidata 中的相關字段,這裡應該僅是占 位用。最終路徑分解工作與查找由 link_path_walk() 函數來完成。 static int fastcall do_path_lookup(int dfd, const char *name, unsigned int flags, struct nameidata *nd) { int retval = 0; int fput_needed; struct file *file; // // 當前進程的 struct file 集 // struct fs_struct *fs = current->fs; nd->last_type = LAST_ROOT; /* if there are only slashes... */ nd->flags = flags; nd->depth = 0; // // 如果路徑是根目錄則從 fs_struct->altrootmnt 與 fs_struct->altroot // 中得到 struct vfsmount 與 struct dentry 結構 // if (*name=='/') { read_lock(&fs->lock); if (fs->altroot && !(nd->flags & LOOKUP_NOALT)) { nd->mnt = mntget(fs->altrootmnt); nd->dentry = dget(fs->altroot); read_unlock(&fs->lock); if (__emul_lookup_dentry(name,nd)) goto out; /* found in altroot */ read_lock(&fs->lock); } nd->mnt = mntget(fs->rootmnt); nd->dentry = dget(fs->root); read_unlock(&fs->lock); // // 如果路徑不是根目錄且有 AT_FDCWD 標志則從 fs_struct->pwdmnt // 與 fs_struct->pwd 中得到 struct vfsmount 與 struct dentry 結構 // 這裡應該表示是當前目錄? FIXME // } else if (dfd == AT_FDCWD) { read_lock(&fs->lock); nd->mnt = mntget(fs->pwdmnt); nd->dentry = dget(fs->pwd); read_unlock(&fs->lock); // // 如果以上都不是的話則使用 fget_light() 得到一個 struct file // 並從 struct file->f_vfsmnt 中得到 struct vfsmount 結構,而 // struct dentry 則使用 struct file->f_dentry 中的 // } else { // // 注意:這裡聲明了一個 struct dentry 結構 // struct dentry *dentry; file = fget_light(dfd, &fput_needed); retval = -EBADF; if (!file) goto out_fail; // // 使用 struct file 中的來填充 // dentry = file->f_dentry; retval = -ENOTDIR; if (!S_ISDIR(dentry->d_inode->i_mode)) goto fput_fail; retval = file_permission(file, MAY_EXEC); if (retval) goto fput_fail; nd->mnt = mntget(file->f_vfsmnt); nd->dentry = dget(dentry); fput_light(file, fput_needed); } current->total_link_count = 0; // // 注意:這個函數才真正的分解路徑,調用實際文件系統的操作。 // 它本身也是個簡單封狀,實際是使用 __link_path_walk() 函數 // 完成操作。 // retval = link_path_walk(name, nd); out: if (likely(retval == 0)) { if (unlikely(!audit_dummy_context() && nd && nd->dentry && nd->dentry->d_inode)) audit_inode(name, nd->dentry->d_inode); } out_fail: return retval; fput_fail: fput_light(file, fput_needed); goto out_fail; } 在 link_path_walk() 函數中實際使用的函數為 __link_path_walk(),在這個函數 中分解路徑,並依次調用 do_lookup() 函數完成實際的轉換工作,do_lookup() 才有可能去調用實際文件系統的讀磁盤 inode 操作。結合上下文,我們只關心讀 取流程,不對路徑分解算法做分析,所以只提取相關代碼。 static fastcall int __link_path_walk(const char * name, struct nameidata *nd) { struct path next; struct inode *inode; int err; unsigned int lookup_flags = nd->flags; // // 篇幅所限,在這個函數中我們只列出相關代碼。 // // // 這裡是一個大循環,目的是用來分解路徑並在 // 分解的中間過程使用 do_lookup() 得到相關的 // inode 一直到最後指定的文件或路徑。也就是說對 // 於象 /dir/temp/readme.txt 這種路徑會首先從 // 根一直分解並調用 do_lookup() 得到其 inode // 一直到得到最後的 readme.txt 為止。 // for(;;) { ....... // // 從緩存或調用實際文件系統函數獲取 inode 信息 // err = do_lookup(nd, &this, &next); if (err) break; ...... last_with_slashes: lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY; last_component: /* Clear LOOKUP_CONTINUE iff it was previously unset */ nd->flags &= lookup_flags | ~LOOKUP_CONTINUE; ....... // // 這裡是去掉了 LOOKUP_CONTINUE 標志後,又調用了一次。 // err = do_lookup(nd, &this, &next); if (err) break; ...... } path_release(nd); return_err: return err; } 到這裡才是查找對應 struct dentry 的具體操作,此函數首先從緩存中嘗試獲取 struct dentry 結構。如果獲取失敗,則調用 real_lookup() 函數使用實際文件 系統方法來讀取 inode 信息。這裡要明確 struct dentry 中包含了 struct inode 信息。 static int do_lookup(struct nameidata *nd, struct qstr *name, struct path *path) { struct vfsmount *mnt = nd->mnt; // // 從 hlist 中獲取 struct dentry 結構,hlist 代表的是 // 一個 inode 的緩存即是一個 HASH 表。 // struct dentry *dentry = __d_lookup(nd->dentry, name); // // 如果沒有找到則會調用 real_lookup() 實際文件系統方法 // 從磁盤中獲取 // if (!dentry) goto need_lookup; if (dentry->d_op && dentry->d_op->d_revalidate) goto need_revalidate; done: // // 如果從緩存中找到,則設置 struct path 並返回 // path->mnt = mnt; path->dentry = dentry; __follow_mount(path); return 0; need_lookup: // // 使用實際文件系統方法,從磁盤中獲得 inode 信息 // dentry = real_lookup(nd->dentry, name, nd); if (IS_ERR(dentry)) goto fail; goto done; need_revalidate: dentry = do_revalidate(dentry, nd); // // 這裡是緩存的分之。如果 struct dentry 無效還是需要調 // 用 real_lookup() 讀取 // if (!dentry) goto need_lookup; if (IS_ERR(dentry)) goto fail; goto done; fail: return PTR_ERR(dentry); } 在分析 real_lookup() 函數前,我們先來看一下 ext3 文件系統的 inode 結構。很明顯可以看出 lookup 指向了 ext3_lookup() 函數。 struct inode_operations ext3_dir_inode_operations = { // // 為了更清晰,在這個結構中只列出我們感興趣的字段 // ...... .lookup = ext3_lookup, ...... }; 此函數先從緩存中查找對應的 inode,如果沒有則新分配一個 struct dentry 結構,然後調用 parent->d_inode->i_op->lookup 即調用了 ext3_lookup() 函數來查找 inode。 static struct dentry * real_lookup(struct dentry * parent, struct qstr * name, struct nameidata *nd) { struct dentry * result; // // 篇幅所限,在這個函數中我們只列出相關代碼。 // // // 獲得上一層目錄的 inode。別忘了我們是分解路徑依次 // 調用的,所以上一層的 inode 肯定是存在的。 // struct inode *dir = parent->d_inode; ...... // // 先從緩存裡查找。 // result = d_lookup(parent, name); if (!result) { // // 沒找到的話,新分配一個 struct dentry 結構 // 注意:我們這裡新分配了一個 struct dentry, // 也就是說每一個目錄或文件都需要一個 dentry 結構。 // struct dentry * dentry = d_alloc(parent, name); result = ERR_PTR(-ENOMEM); if (dentry) { // // 這裡也就是調用了 ext3_lookup() 函數,可以 // 看下上面介紹的 ext3_dir_inode_operations // 結構 // result = dir->i_op->lookup(dir, dentry, nd); if (result) dput(dentry); else result = dentry; } mutex_unlock(&dir->i_mutex); return result; } ....... } 這裡到了實際文件系統的查找函數。首先根據第一個參數,也就是上級的 dentry 從 ext3_dir_entry_2 中得到新的 dentry 結構,並從其中得到相關的 inode number, 再調用 iget() 函數去獲取相應的 struct inode 結構,最後將此 inode 與 dentry 進行關聯。 static struct dentry *ext3_lookup(struct inode * dir, struct dentry *dentry, struct nameidata *nd) { struct inode * inode; struct ext3_dir_entry_2 * de; struct buffer_head * bh; if (dentry->d_name.len > EXT3_NAME_LEN) return ERR_PTR(-ENAMETOOLONG); // // 得到新的 dentry 並返回一個磁盤緩存 buffer_head 結構 // 注意:這個 dentry 雖然是新分配的,但它所指向的 d_parent // 與 d_inode 是有效的,也就是說上級目錄相關信息是有效的。 // 返回的 de 裡包含了 inode number。 // bh = ext3_find_entry(dentry, &de); // // 注意:這裡的 inode 默認置為 NULL // inode = NULL; if (bh) { unsigned long ino = le32_to_cpu(de->inode); brelse (bh); // // 如果對應的超級塊(super block)無效則直接返回錯誤 // if (!ext3_valid_inum(dir->i_sb, ino)) { ext3_error(dir->i_sb, "ext3_lookup", "bad inode number: %lu", ino); inode = NULL; } else // // 有效則調用 iget() 函數得到正確的 struct inode // 其實也就是根據超級塊(super block)的函數集獲取 // inode = iget(dir->i_sb, ino); if (!inode) return ERR_PTR(-EACCES); } // // 關鍵此 inode 對應的 dentry 結構並返回。 // return d_splice_alias(inode, dentry); } 在分析 iget() 函數之前,有必要先了解下超級塊(super block)中的 相關字段與函數。 struct super_block { // // 為了更清晰,在這個結構中只列出我們感興趣的字段 // ...... // // 文件系統結構。在下面介紹 mount 掛載文件系統時 // 會有詳細介紹。 // struct file_system_type *s_type; // // 超級塊(super block)函數集 // struct super_operations *s_op; ...... }; 下面是 ext3 文件系統的超級塊(super block)函數集結構 static struct super_operations ext3_sops = { // // 為了更清晰,在這個結構中只列出我們感興趣的字段 // ...... // // 注意:這裡的 ext3_read_inode() 是不是很眼熟 // .read_inode = ext3_read_inode, ...... }; 終於走到了最終的讀取函數!這個函數非常簡單,在判斷一些有效性後,直接調用 超級塊(super block)函數集中的 read_inode 方法,也就是我們前面介紹的 ext3_sops 函數集中的 ext3_read_inode() 函數。 static inline struct inode *iget(struct super_block *sb, unsigned long ino) { struct inode *inode = iget_locked(sb, ino); if (inode && (inode->i_state & I_NEW)) { // // 這裡調用的就是 ext3_read_inode() 函數 // sb->s_op->read_inode(inode); unlock_new_inode(inode); } return inode; } 到這裡我們可以解釋 ext3_read_inode() 函數是何時調用的了,可以說是 open_namei() 函數在路徑轉換時間接的調用了 iget() 函數,而 iget() 函 數則是調用了已經注冊好的超級塊(super block)函數集 ext3_sops 中的 ext3_read_inode() 函數來獲取相應的 inode。其實這也就可以解釋為什麼 在 struct inode->i_fop 中(也就是 ext3_file_operations 函數集中) open 操作函數 generic_file_open() 是個空操作。因為其對應的 inode 已經在 open_namei()->iget() 中得到了,得到了一個 inode 其實在實際 文件系統中就是一個打開操作,得到了 inode 當然就可以對它進行讀/寫 操作了。只所以提供了一個 generic_file_open() 應該是占位用的,占位 的目的應該是為了可以使用用戶提供的操作方法。也就是說,如果你自己 寫了一個 open 操作並賦值給 struct inode->i_fop->open 的話,系統會 調用你所提供的這個 open 操作。我們在上面分析 __dentry_open() 函數時 已經指出了這個調用點。以上的疑問都得到了解答,但這裡又再次引出了一 個疑問,那就是這個已經注冊好了的 超級塊(super block)函數集 ext3_sops 是什麼時候注冊的?要解答這個疑問我們只能從頭,也就是 mount 文件系統 時進行分析。 在分析 mount 前我們首先來了解下如下結構,這個結構是在注冊新的文件 系統時被作為參數傳遞的,注冊文件系統的函數為 register_filesyste()。 struct file_system_type { // // 文件系統名稱,如:ext3 // const char *name; int fs_flags; // // 實際文件系統的超級塊(super block)函數。在 mount 時通 // 過它來得到超級塊的信息,包含 inode 等。 // int (*get_sb) (struct file_system_type *, int, const char *, void *, struct vfsmount *); void (*kill_sb) (struct super_block *); // // 當前模塊 // struct module *owner; // // 指向下一個文件系統地址 // struct file_system_type * next; struct list_head fs_supers; struct lock_class_key s_lock_key; struct lock_class_key s_umount_key; }; 我們再來看下 ext3 文件系統是如何填充這個結構的。 static struct file_system_type ext3_fs_type = { .owner = THIS_MODULE, .name = "ext3", // // 注意這裡的回調函數指向了 ext3_get_sb() // .get_sb = ext3_get_sb, .kill_sb = kill_block_super, .fs_flags = FS_REQUIRES_DEV, }; 最終使用 register_filesystem( &ext3_fs_type ); 完成文件系統的注冊。 這裡僅是注冊了文件系統,我們知道要使用一個文件系統首先要 mount 才可 使用。我們清楚了以上結構後,接著來看 vfs_kern_mount() 函數,這個函數 是內核最終實現 mount 的函數,這個函數的第一個參數即是上面提到的 file_system_type 結構,在 ext3 文件系統下傳遞的是 ext3_fs_type。函數 中調用的 type->get_sb 即觸發了 ext3_get_sb() 函數。 struct vfsmount * vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data) { struct vfsmount *mnt; char *secdata = NULL; int error; if (!type) return ERR_PTR(-ENODEV); error = -ENOMEM; // // 根據名稱分配一個新的 vfsmount 掛接點。 // mnt = alloc_vfsmnt(name); if (!mnt) goto out; if (data) { secdata = alloc_secdata(); if (!secdata) goto out_mnt; error = security_sb_copy_data(type, data, secdata); if (error) goto out_free_secdata; } // // 注意:這裡調用了已注冊文件系統的超級塊(super block)函數 // 對於 ext3 文件系統來說,就是調用了 ext3_get_sb,可參考 // 以上對 file_system_type 的說明。 // error = type->get_sb(type, flags, name, data, mnt); if (error < 0) goto out_free_secdata; error = security_sb_kern_mount(mnt->mnt_sb, secdata); if (error) goto out_sb; // // 這裡的掛接點是一個 dentry 結構 // mnt->mnt_mountpoint = mnt->mnt_root; // // 把新的 vfsmount 結構賦給自身的 parent 這樣可以 // 通過 parent 遍歷出所有 mount 的文件系統 // mnt->mnt_parent = mnt; up_write(&mnt->mnt_sb->s_umount); free_secdata(secdata); return mnt; // // 以下流程只有出錯時才會走到 // out_sb: dput(mnt->mnt_root); up_write(&mnt->mnt_sb->s_umount); deactivate_super(mnt->mnt_sb); out_free_secdata: free_secdata(secdata); out_mnt: free_vfsmnt(mnt); out: return ERR_PTR(error); } 下面的 ext3_get_sb() 函數僅是個簡單的封狀,直接調用的 get_sb_bdev() 函數,但這裡要注意 get_sb_bdev() 函數不是嚴格按照 ext3_get_sb() 函數 進行傳遞的,它本身多出了一個 ext3_fill_super 參數,而這個參數是以一個 回調函數形式提供的。 static int ext3_get_sb(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, struct vfsmount *mnt) { // // 注意:這裡多了一個 ext3_fill_super() 的回調函數。 // return get_sb_bdev(fs_type, flags, dev_name, data, ext3_fill_super, mnt); } 了解了以上結構我們再來看 ext3_fill_super() 函數的具體實現,這個函數的第 一個參數即是一個超級塊(super block)結構。在此函數中將上面提到的 ext3 超級 塊(super block) 函數集 ext3_sops 賦給了此結構。然後調用 iget() 函數觸發 超級塊(super block) 函數集。 static int ext3_fill_super (struct super_block *sb, void *data, int silent) { // // 篇幅所限,在這個函數中我們只列出相關代碼。 // // // 設置超級塊的函數集 // sb->s_op = &ext3_sops; sb->s_export_op = &ext3_export_ops; sb->s_xattr = ext3_xattr_handlers; #ifdef CONFIG_QUOTA sb->s_qcop = &ext3_qctl_operations; sb->dq_op = &ext3_quota_operations; #endif INIT_LIST_HEAD(&sbi->s_orphan); /* unlinked but open files */ sb->s_root = NULL; // // 調用 iget() 函數得到相應的 inode。 // root = iget(sb, EXT3_ROOT_INO); // // 根據得到的根 inode 分配超級塊(super block)中的 // s_root 此字段是一個 struct dentry 結構。 // sb->s_root = d_alloc_root(root); // // 如果根 dentry 無效則提示錯誤跳到失敗處。 // if (!sb->s_root) { printk(KERN_ERR "EXT3-fs: get root inode failed\n"); iput(root); goto failed_mount4; } // // 如果根 inode 不是目錄或者大小與塊無效則提示錯誤 // 跳到失敗處。 // if (!S_ISDIR(root->i_mode) || !root->i_blocks || !root->i_size) { dput(sb->s_root); sb->s_root = NULL; printk(KERN_ERR "EXT3-fs: corrupt root inode, run e2fsck\n"); goto failed_mount4; } } 至此所有流程都走到了,疑問也被一個個打破。我們在整體的梳理下流程。在內核 sys_open 被調用打開一個文件或者設備驅動時,調用 filp_open()->do_filp_open() 函數,在 do_filp_open() 函數中,首先利用 open_namei() 函數得到一個 struct nameidata 結構,那麼在這個過程中 __path_lookup_intent_open() 函數設置了 struct nameidata->intent.open 相關字段,然後調用 do_path_lookup() 函數,在這 個函數中設置了 struct nameidata->mnt 與 struct nameidata->dentry 相關字段後 調用了 _link_path_walk() 函數開始分解路徑,並依次調用 do_lookup() 函數來 獲得路徑中個目錄與最終文件的 struct inode。do_lookup() 函數先從 inode 緩存 即 hlist 中查找 inode,如果沒有找到則調用 real_lookup() 函數,此函數分配 了一個 struct dentry 結構,然後使用上層目錄的 struct inode->i_op->lookup() 方法來繼續查找,這樣就觸發了 ext3_lookup() 函數,而此函數得到 struct dentry 與 inode number 後調用 iget() 函數來返回 struct inode。(這裡有必要強調一點, 那就是不僅目錄才有 struct dentry 結構,一個文件也擁有一個 struct dentry 結 構,這個從上面具體代碼分析中可以看到)而 iget() 函數是使用 struct inode 超 級塊(super block)中的函數 ext3_read_inode() 來最終完成從磁盤讀取 inode 操 作,讀到一個 in core 類型的 struct inode 後為了提供文件與設備讀/寫等操作設 置了 struct inode->i_op 與 struct inode->i_fop 函數集。其實以上步驟按照提供 的系統調用以及內核操作流程來理解等於是打開了一個文件或目錄。這也就是為什麼 在 ext3_file_operations 函數集中只有讀/寫等操作,而打開是空操作的原因。至於 為什麼提供一個空操作函數,在上面分析時已經給出了,這裡不在闡述。到此 struct inode,struct dentry, struct nameidata 結構都已完全填充好。在 open_namei() 調用返回後將得到的 nameidata 結構作為參數調用 nameidata_to_filp() 函數,在此函數當中使用 struct dentry 作參數調用了 __dentry_open() 函數,在這個函數中會動態初始化一個 struct file 結構,並使用 struct inode->i_fop 函數集來填充 struct file->f_op (別忘了,我們前面的 inode 結構中相關域都已經准備好了,這裡直接拿來使用即可)。那麼不管是文件還是設備驅 動,可以看出來是走到具體文件系統這裡才開始區分的。如果是目錄/文件/連接則直接 使用 ext3_file_xxx 或 ext3_dir_xxx 等函數集。如果操作對象是一個設備驅動的話 則使用 init_special_inode 來初始化不同的設備驅動,如果是一個字符設備驅動的 話則調用 chrdev_open() 函數來對應 struct file 操作集。而上面提到的超級塊 (super block) 函數是在注冊文件系統注冊時由 register_filesystem() 函數注冊 , 在 mount 時由 vfs_kern_mount() 函數間接調用 ext3_fill_super() 函數時進行關聯 的。具體可以看上面的代碼分析,這裡不在詳述。所有流程清晰後我們再說一下 struct inode 中的幾個函數集的區別與作用。我們這裡僅以文件/目錄為例進行解釋,struct inode_operations 操作是對文件(inode)的建立/查找(打開)/刪除/重命名操作,struct file_operations 操作是對已經存在的文件的讀/寫/刷新/列目錄(readdir)/發送控制字 操作。