深入解析Linux內核I/O剖析(open,write實現)


Linux內核將一切視為文件,那么Linux的文件是什么呢?其既可以是事實上的真正的物理文件,也可以是設備、管道,甚至還可以是一塊內存。狹義的文件是指文件系統中的物理文件,而廣義的文件則可以是Linux管理的所有對象。這些廣義的文件利用VFS機制,以文件系統的形式掛載在Linux內核中,對外提供一致的文件操作接口。
從數值上看,文件描述符是一個非負整數,其本質就是一個句柄,所以也可以認為文件描述符就是一個文件句柄。那么何為句柄呢?一切對於用戶透明的返回值,即可視為句柄。用戶空間利用文件描述符與內核進行交互;而內核拿到文件描述符后,可以通過它得到用於管理文件的真正的數據結構。
使用文件描述符即句柄,有兩個好處:一是增加了安全性,句柄類型對用戶完全透明,用戶無法通過任何hacking的方式,更改句柄對應的內部結果,比如Linux內核的文件描述符,只有內核才能通過該值得到對 應的文件結構;二是增加了可擴展性,用戶的代碼只依賴於句柄的值,這樣實際結構的類型就可以隨時發生變化,與句柄的映射關系也可以隨時改變,這些變化都不會影響任何現有的用戶代碼。
Linux的每個進程都會維護一個文件表,以便維護(並不是指包含,其中有指針指向file結構(偏移量,引用計數,文件信息))該進程打開文件的信息,包括打開的文件個數、每個打開文件的偏移量等信息,

內核中進程對應的結構是PCB(task_struct)pcb中的一個指針此進程獨有的文件表結構(包含文件描述符表)(struct files_struct)

   
   
   
           
  1. struct files_struct {
  2. /* count為文件表files_struct的引用計數 */
    atomic_t count;
    /* 文件描述符表 */
    /*
    為什么有兩個fdtable呢?這是內核的一種優化策略。fdt為指針, 而fdtab為普通變量。一般情況下,
    fdt是指向fdtab的, 當需要它的時候, 才會真正動態申請內存。因為默認大小的文件表足以應付大多數
    情況, 因此這樣就可以避免頻繁的內存申請。
    這也是內核的常用技巧之一。在創建時, 使用普通的變量或者數組, 然后讓指針指向它, 作為默認情況使
    用。只有當進程使用量超過默認值時, 才會動態申請內存。
    *//*
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
    * written part on a separate cache line in SMP
    */
    /* 使用____cacheline_aligned_in_smp可以保證file_lock是以cache
    line 對齊的, 避免了false sharing */
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    /* 用於查找下一個空閑的fd */
    int next_fd;
    /* 保存執行exec需要關閉的文件描述符的位圖 */
    struct embedded_fd_set close_on_exec_init;
    /* 保存打開的文件描述符的位圖 */
    struct embedded_fd_set open_fds_init;
    /* fd_array為一個固定大小的file結構數組。struct file是內核用於文
    件管理的結構。這里使用默認大小的數組, 就是為了可以涵蓋
    大多數情況, 避免動
  3. 態分配 */
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];
    };




每個文件都有一個32位的數字來表示下一個讀寫的字節位置,這個數字叫做文件位置。每次打開一個文件,除非明確要求,否則文件位置都被置為0,即文件的開始處,此后的讀或寫操作都將從文件的開始處執行,但你可以通過執行系統調用LSEEK(隨機存儲)對這個文件位置進行修改。Linux中專門用了一個數據 結構file來保存打開文件的文件位置,這個結構稱為 打開的文件描述(open file description)。這個數據結構的設置是煞費苦心的,因為它與進程的聯系非常緊密,可以說這是 VFS中一個比較難於理解的數據結構, file結構中主要保存了文件位置,此外,還把指向該文件索引節點的指針也放在其中。file結構形成一個雙鏈表,稱為系統打開文件表,其最大長度是NR_FILE,在fs.h中定義為8192。
 
  
  
  
          
  1. struct file 
  2. {
  3. struct list_head f_list; /*所有打開的文件形成一個鏈表*/
  4. struct dentry *f_dentry; /*指向相關目錄項的指針*/
  5. struct vfsmount *f_vfsmnt; /*指向VFS安裝點的指針*/
  6. struct file_operations *f_op; /*指向文件操作表的指針*/
  7. mode_t f_mode; /*文件的打開模式*/
  8. loff_t f_pos; /*文件的當前位置*/
  9. unsigned short f_flags; /*打開文件時所指定的標志*/
  10. unsigned short f_count; /*使用該結構的進程數*/
  11. unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
  12. /*預讀標志、要預讀的最多頁面數、上次預讀后的文件指針、預讀的字節數以及預讀的頁面數*/
  13. int f_owner; /* 通過信號進行異步I/O數據的傳送*/
  14. unsigned int f_uid, f_gid; /*用戶的UID和GID*/
  15. int f_error; /*網絡寫操作的錯誤碼*/
  16. unsigned long f_version; /*版本號*/
  17. void *private_data; /* tty驅動程序所需 */
  18. };

內核中,對應於每個進程都有一個文件描述符表,表示這個進程打開的所有文件。文件描述表中每一項都是一個指針,指向一個用於 描述打開的文件的數據塊———file對象,file對象中描述了文件的打開模式,讀寫位置等重要信息,當進程打開一個文件時,內核就會創建一個新的 file對象。需要注意的是,file對象不是專屬於某個進程的,不同進程的文件描述符表中的指針可以指向相同的file對象,從而共享這個打開的文件。 file對象有引用計數,記錄了引用這個對象的文件描述符個數,只有當引用計數為0時,內核才銷毀file對象,因此某個進程關閉文件,不影響與之共享同 一個file對象的進程.

file對象中包含一個指針,指向dentry對象。dentry對象代表一個獨立的文件路徑,如果一個文件路徑被打開多次,那么會建立多個file對象,但它們都指向同一個dentry對象。

dentry對象中又包含一個指向inode對象的指針。inode對象代表一個獨立文件。因為存在硬鏈接與符號鏈接,因此不同的dentry 對象可以指向相同的inode對象.inode 對象包含了最終對文件進行操作所需的所有信息,如文件系統類型、文件的操作方法、文件的權限、訪問日期等。

打開文件后,進程得到的文件描述符實質上就是文件描述符表的下標,內核根據這個下標值去訪問相應的文件對象,從而實現對文件的操作。

注意,同一個進程多次打開同一個文件時,內核會創建多個file對象。
當進程使用fork系統調用創建一個子進程后,子進程將繼承父進程的文件描述符表,因此在父進程中打開的文件可以在子進程中用同一個描述符訪問。
---------------------------------------------------------------open解析---------------------------------------------------
   
   
   
           
  1. int open(const char *pathname, int flags);
  2. int open(const char *pathname, int flags, mode_t mode);
前一個是glibc封裝的函數,后一個是系統調用
open源碼追蹤:
  
  
  
          
  1. long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
  2. {
  3. struct open_flags op;
  4. /* flags為用戶層傳遞的參數, 內核會對flags進行合法性檢查, 並根據mode生成新的flags值賦給 lookup */
  5. int lookup = build_open_flags(flags, mode, &op);
  6. /* 將用戶空間的文件名參數復制到內核空間 */
  7. char *tmp = getname(filename);
  8. int fd = PTR_ERR(tmp);
  9. if (!IS_ERR(tmp)) {
  10. /* 未出錯則申請 新的文件描述符 */
  11. fd = get_unused_fd_flags(flags);
  12. if (fd >= 0) {/* 申請新的文件管理結構file */
  13. struct file *f = do_filp_open(dfd, tmp, &op, lookup);
  14. if (IS_ERR(f)) {
  15. put_unused_fd(fd);
  16. fd = PTR_ERR(f);
  17. } else {
  18. /* 產生文件打開的通知事件 */
  19. fsnotify_open(f);
  20. /* 將文件描述符fd與文件管理結構file對應起來, 即安裝 */
  21. fd_install(fd, f);
  22. }
  23. }
  24. putname(tmp);
  25. }
  26. return fd;
  27. }
從上面來看,打開文件,內核消耗了2種資源:文件描述符跟內核管理文件結構file

根據POSIX標准,當獲取一個新的文件描述符時,要返回最低的未使用的文件描述符。Linux是如何實現這一標准的呢?
在Linux中,通過do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))來選擇文件描述符,代碼如下
    
    
    
            
  1. int alloc_fd(unsigned start, unsigned flags)
  2. {
  3. struct files_struct *files = current->files;//獲取當前進程的對應包含文件描述符表的結構
  4. unsigned int fd;
  5. int error;
  6. struct fdtable *fdt;
  7. /* files為進程的文件表, 下面需要更改文件表, 所以需要先鎖文件表 */
  8. spin_lock(&files->file_lock);
  9. repeat:
  10. /* 得到文件描述符表 */
  11. fdt = files_fdtable(files);
  12. /* 從start開始, 查找未用的文件描述符。在打開文件時, start為0 */
  13. fd = start;
  14. /* files->next_fd為上一次成功找到的fd的下一個描述符。使用next_fd, 可以快速找到未用的文件描述符;*/
  15. if (fd < files->next_fd)
  16. fd = files->next_fd;
  17. /*
  18. 當小於當前文件表支持的最大文件描述符個數時, 利用位圖找到未用的文件描述符。
  19. 如果大於max_fds怎么辦呢?如果大於當前支持的最大文件描述符, 那它肯定是未
  20. 用的, 就不需要用位圖來確認了。
  21. */
  22. if (fd < fdt->max_fds)
  23. fd = find_next_zero_bit(fdt->open_fds->fds_bits,
  24. fdt->max_fds, fd);
  25. /* expand_files用於在必要時擴展文件表。何時是必要的時候呢?比如當前文件描述符已經超過了當
  26. 前文件表支持的最大值的時候。 */
  27. error = expand_files(files, fd);
  28. if (error < 0)
  29. goto out;
  30. /*
  31. * If we needed to expand the fs array we
  32. * might have blocked - try again.
  33. */
  34. if (error)
  35. goto repeat;
  36. /* 只有在start小於next_fd時, 才需要更新next_fd, 以盡量保證文件描述符的連續性。*/
  37. if (start <= files->next_fd)
  38. files->next_fd = fd + 1;
  39. /* 將打開文件位圖open_fds對應fd的位置置位 */
  40. FD_SET(fd, fdt->open_fds);
  41. /* 根據flags是否設置了O_CLOEXEC, 設置或清除fdt->close_on_exec */
  42. if (flags & O_CLOEXEC)
  43. FD_SET(fd, fdt->close_on_exec);
  44. else
  45. FD_CLR(fd, fdt->close_on_exec);
  46. error = fd;
  47. #if 1
  48. /* Sanity check */
  49. if (rcu_dereference_raw(fdt->fd[fd]) != NULL) {
  50. printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
  51. rcu_assign_pointer(fdt->fd[fd], NULL);
  52. }
  53. #endif
  54. out:
  55. spin_unlock(&files->file_lock);
  56. return error;
  57. }
下面內核使用fd_install將文件管理結構file與fd組合起來,具體操作請看如下代碼:
    
    
    
            
  1. void fd_install(unsigned int fd, struct file *file)
  2. {
  3. struct files_struct *files = current->files;//獲得進程文件表(包含文件描述符表)
  4. struct fdtable *fdt;
  5. spin_lock(&files->file_lock);
  6. /* 得到文件描述符表 */
  7. fdt = files_fdtable(files);
  8. BUG_ON(fdt->fd[fd] != NULL);
  9. /*
  10. 將文件描述符表中的file類型的指針數組中對應fd的項指向file。
  11. 這樣文件描述符fd與file就建立了對應關系
  12. */
  13. rcu_assign_pointer(fdt->fd[fd], file);
  14. spin_unlock(&files->file_lock);
  15. }
當用戶使用fd與內核交互時,內核可以用fd從fdt->fd[fd]中得到內部管理文件的結構struct file。

-------------------------------------------close(關閉文件)------------------------------
close用於關閉文件描述符。而文件描述符可以是普通文件,也可以是設備,還可以是socket。在關閉時,VFS會根據不同的文件類型,執行不同的操作。
下面將通過跟蹤close的內核源碼來了解內核如何針對不同的文件類型執行不同的操作。
1. 分析close源碼跟蹤
首先,來看一下close的源碼實現,代碼如下
    
    
    
            
  1. SYSCALL_DEFINE1(close, unsigned int, fd)
  2. {
  3. struct file * filp;
  4. /* 得到當前進程的文件表 */
  5. struct files_struct *files = current->files;
  6. struct fdtable *fdt;
  7. int retval;
  8. spin_lock(&files->file_lock);
  9. /* 通過文件表, 取得文件描述符表 */
  10. fdt = files_fdtable(files);
  11. /* 參數fd大於文件描述符表記錄的最大描述符, 那么它一定是非法的描述符 */
  12. if (fd >= fdt->max_fds)
  13. goto out_unlock;
  14. /* 利用fd作為索引, 得到file結構指針 */
  15. filp = fdt->fd[fd];
  16. /*
  17. 檢查filp是否為NULL。正常情況下, filp一定不為NULL。
  18. */
  19. if (!filp)
  20. goto out_unlock;
  21. /* 將對應的filp置為0*/
  22. rcu_assign_pointer(fdt->fd[fd], NULL);
  23. /* 清除fd在close_on_exec位圖中的位 */
  24. FD_CLR(fd, fdt->close_on_exec);
  25. /* 釋放該fd, 或者說將其置為unused。*/
  26. __put_unused_fd(files, fd);
  27. spin_unlock(&files->file_lock);
  28. /* 關閉file結構 */
  29. retval = filp_close(filp, files); //這里將引用計數
  30. /* can't restart close syscall because file table entry was cleared */
  31. if (unlikely(retval == -ERESTARTSYS ||
  32. retval == -ERESTARTNOINTR ||
  33. retval == -ERESTARTNOHAND ||
  34. retval == -ERESTART_RESTARTBLOCK))
  35. retval = -EINTR;
  36. return retval;
  37. out_unlock:
  38. spin_unlock(&files->file_lock);
  39. return -EBADF;
  40. }
  41. EXPORT_SYMBOL(sys_close);
請注意26行的__put_unused_fd,源碼如下所示:
    
    
    
            
  1. static void __put_unused_fd(struct files_struct *files, unsigned int fd)
  2. {
  3. /* 取得文件描述符表 */
  4. struct fdtable *fdt = files_fdtable(files);
  5. /* 清除fd在open_fds位圖的位 */
  6. __FD_CLR(fd, fdt->open_fds);
  7. /* 如果fd小於next_fd, 重置next_fd為釋放的fd */
  8. if (fd < files->next_fd)
  9. files->next_fd = fd;
  10. }
看到這里,我們來回顧一下之前分析過的alloc_fd函數,就可以總結出完整的Linux文件描述符選擇計划:
·Linux選擇文件描述符是按從小到大的順序進行尋找的,文件表中next_fd用於記錄下一次開始尋找的起點。當有空閑的描述符時,即可分配。
·當某個文件描述符關閉時,如果其小於next_fd,則next_fd就重置為這個描述符,這樣下一次分配就會立刻重用這個文件描述符。
以上的策略,總結成一句話就是“Linux文件描述符策略永遠選擇最小的可用的文件描述符”。——這也是POSIX標准規定的。
          從__put_unused_fd退出后,close會接着調用filp_close,其調用路徑為filp_close->fput。在fput中,會對當前文件struct file的引用計數減一並檢查其值是否為0。當引用計數為0時,表示該struct file沒有被其他人使用,則可以調用__fput執行真正的文件釋放操作,然后調用要關閉文件所屬文件系統的release函數,從而實現針對不同的文件類型來執行不同的關閉操作。

下一節讓我們來看看Linux如何針對不同的文件類型,掛載不同的文件操作函數files_operations。
33

每個file結構體都指向一個file_operations結構體,這個結構體的成員都是函數指針,指向實現各種文件操作的內核函數。比如在用戶程序中read一個文件描述符,read通過系統調用進入內核,然后找到這個文件描述符所指向的file結構體,找到file結構體所指向的file_operations結構體,調用它的read成員所指向的內核函數以完成用戶請求。在用戶程序中調用lseekreadwriteioctlopen等函數,最終都由內核調用file_operations的各成員所指向的內核函數完成用戶請求。file_operations結構體中的release成員用於完成用戶程序的close請求,之所以叫release而不叫close是因為它不一定真的關閉文件,而是減少引用計數,只有引用計數減到0才關閉文件。對於同一個文件系統上打開的常規文件來說,readwrite等文件操作的步驟和方法應該是一樣的,調用的函數應該是相同的,所以圖中的三個打開文件的file結構體指向同一個file_operations結構體。如果打開一個字符設備文件,那么它的readwrite操作肯定和常規文件不一樣,不是讀寫磁盤的數據塊而是讀寫硬件設備,所以file結構體應該指向不同的file_operations結構體,其中的各種文件操作函數由該設備的驅動程序實現。

每個file結構體都有一個指向dentry結構體的指針,“dentry”是directory entry(目錄項)的縮寫。我們傳給openstat等函數的參數的是一個路徑,例如/home/akaedu/a,需要根據路徑找到文件的inode。為了減少讀盤次數,內核緩存了目錄的樹狀結構,稱為dentry cache,其中每個節點是一個dentry結構體,只要沿着路徑各部分的dentry搜索即可,從根目錄/找到home目錄,然后找到akaedu目錄,然后找到文件a。dentry cache只保存最近訪問過的目錄項,如果要找的目錄項在cache中沒有,就要從磁盤讀到內存中。

每個dentry結構體都有一個指針指向inode結構體。inode結構體保存着從磁盤inode讀上來的信息。在上圖的例子中,有兩個dentry,分別表示/home/akaedu/a/home/akaedu/b,它們都指向同一個inode,說明這兩個文件互為硬鏈接。inode結構體中保存着從磁盤分區的inode讀上來信息,例如所有者、文件大小、文件類型和權限位等。每個inode結構體都有一個指向inode_operations結構體的指針,后者也是一組函數指針指向一些完成文件目錄操作的內核函數。和file_operations不同,inode_operations所指向的不是針對某一個文件進行操作的函數,而是影響文件和目錄布局的函數,例如添加刪除文件和目錄、跟蹤符號鏈接等等,屬於同一文件系統的各inode結構體可以指向同一個inode_operations結構體

inode結構體有一個指向super_block結構體的指針。super_block結構體保存着從磁盤分區的超級塊讀上來的信息,例如文件系統類型、塊大小等。super_block結構體的s_root成員是一個指向dentry的指針,表示這個文件系統的根目錄被mount到哪里,在上圖的例子中這個分區被mount/home目錄下。

filedentryinodesuper_block這 幾個結構體組成了VFS的核心概念。對於ext2文件系統來說,在磁盤存儲布局上也有inode和超級塊的概念,所以很容易和VFS中的概念建立對應關 系。而另外一些文件系統格式來自非UNIX系統(例如Windows的FAT32、NTFS),可能沒有inode或超級塊這樣的概念,但為了能mount到Linux系統,也只好在驅動程序中硬湊一下,在Linux下看FAT32和NTFS分區會發現權限位是錯的,所有文件都是rwxrwxrwx,因為它們本來就沒有inode和權限位的概念,這是硬湊出來的

----------------------------------------------------以下來看自定義的files_operations,以socket舉例,有一個struct file_operations結構體定義了很多函數指針,對應不同的讀寫關之類的操作,socket的讀寫關閉等操作分別對應不同的內核函數
    
    
    
            
  1. static const struct file_operations socket_file_ops = {
  2. .owner = THIS_MODULE,
  3. .llseek = no_llseek,
  4. .aio_read = sock_aio_read,
  5. .aio_write = sock_aio_write,
  6. .poll = sock_poll,
  7. .unlocked_ioctl = sock_ioctl,
  8. #ifdef CONFIG_COMPAT
  9. .compat_ioctl = compat_sock_ioctl,
  10. #endif
  11. .mmap = sock_mmap,
  12. .open = sock_no_open, /* special open code to disallow open via /proc */
  13. .release = sock_close,
  14. .fasync = sock_fasync,
  15. .sendpage = sock_sendpage,
  16. .splice_write = generic_splice_sendpage,
  17. .splice_read = sock_splice_read,
  18. };

在socket中,底層的函數sock_alloc_file用於申請socket文件描述符及文件管理結構file結構。它調用alloc_file來申請管理結構file,並將socket_file_ops這個結構體作為參數,如下所示:
    
    
    
            
  1. file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
  2. &socket_file_ops);

    
    
    
            
  1. struct file *alloc_file(struct path *path, fmode_t mode,
  2. const struct file_operations *fop)
  3. {
  4. struct file *file;
  5. /* 申請一個file */
  6. file = get_empty_filp();
  7. if (!file)
  8. return NULL;
  9. file->f_path = *path;
  10. file->f_mapping = path->dentry->d_inode->i_mapping;
  11. file->f_mode = mode;
  12. /* 將自定義的文件操作函數指針結構體賦給file->f_op */
  13. file->f_op = fop;
  14. ……
  15. }
在初始化file結構的時候,socket文件系統將其自定義的文件操作賦給了file->f_op,從而實現了在VFS中可以調用socket文件系統自定義的操作。

----------------------------------遺忘close造成的后果---------------------------
文件描述符沒有被釋放。
用於文件管理的某些內存結構也沒有被釋放
對於普通進程來說,即使應用忘記了關閉文件,當進程退出時,Linux內核也會自動關閉文件,釋放內存(詳細過程見后文)。但是對於一個常駐進程來說,問題就變得嚴重了。
先看第一種情況,如果文件描述符沒有被釋放,那么再次申請新的描述符時,就不得不擴展當前的文件描述符表,如果文件描述發表始終不釋放,個數遲早會達到上限,返回EMFILE錯誤


-----------------------如何查看文件資源泄露--------------
使用lsof工具

---------------------------------讀取文件
Linux中讀取文件操作時,最常用的就是read函數,其原型如下
ssize_t read ( int fd , void * buf , size_t count );
read嘗試從fd中讀取count個字節到buf中,並返回成功讀取的字節數,同時將文件偏移向前移動相同的字節數。返回0的時候則表示已經到了“文件尾”。read還有可能讀取比count小的字節數。
使用read進行數據讀取時,要注意正確地處理錯誤,也是說read返回-1時,如果errno為EAGAIN、EWOULDBLOCK或EINTR,一般情況下都不能將其視為錯誤。因為前兩者是由於當前fd為非阻塞且沒有可讀數據時返回的,后者是由於read被信號中斷所造成的。這兩種情況基本上都可以視為正常情況。
    
    
    
            
  1. SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
  2. {
  3. struct file *file;
  4. ssize_t ret = -EBADF;
  5. int fput_needed;
  6. /* 通過文件描述符fd得到管理結構file */
  7. file = fget_light(fd, &fput_needed);
  8. if (file) {
  9. /* 得到文件的當前偏移量 */
  10. loff_t pos = file_pos_read(file);
  11. /* 利用vfs進行真正的read */
  12. ret = vfs_read(file, buf, count, &pos);
  13. /* 更新文件偏移量 */
  14. file_pos_write(file, pos);
  15. /* 歸還管理結構file, 如有必要, 就進行引用計數操作*/
  16. fput_light(file, fput_needed);
  17. }
  18. return ret;
  19. }

查看VFS_read代碼:
    
    
    
            
  1. ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
  2. {
  3. ssize_t ret;
  4. /* 檢查文件是否為讀取打開 */
  5. if (!(file->f_mode & FMODE_READ))
  6. return -EBADF;
  7. /* 檢查文件是否支持讀取操作 */
  8. if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
  9. return -EINVAL;
  10. /* 檢查用戶傳遞的參數buf的地址是否可寫 */
  11. if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
  12. return -EFAULT;
  13. /* 檢查要讀取的文件范圍實際可讀取的字節數 */
  14. ret = rw_verify_area(READ, file, pos, count);
  15. if (ret >= 0) {
  16. /* 根據上面的結構, 調整要讀取的字節數 */
  17. count = ret;
  18. /*
  19. 如果定義read操作, 則執行定義的read操作
  20. 如果沒有定義read操作, 則調用do_sync_read—其利用異步aio_read來完成同步的read操作。
  21. */
  22. if (file->f_op->read)
  23. ret = file->f_op->read(file, buf, count, pos);
  24. else
  25. ret = do_sync_read(file, buf, count, pos);
  26. if (ret > 0) {
  27. /* 讀取了一定的字節數, 進行通知操作 */
  28. fsnotify_access(file);
  29. /* 增加進程讀取字節的統計計數 */
  30. add_rchar(current, ret);
  31. }
  32. /* 增加進程系統調用的統計計數 */
  33. inc_syscr(current);
  34. }
  35. return ret;
  36. }
上面的代碼為read公共部分的源碼分析,具體的讀取動作是由實際的文件系統決定的。
1.6.2 部分讀取
前文中介紹read可以返回比指定count少的字節數,那么什么時候會發生這種情況呢?最直接的想法是在fd中沒有指定count大小的數據時。但這種情況下,系統是不是也可以阻塞到滿足count個字節的數據呢?那么內核到底采取的是哪種策略呢?
讓我們來看看socket文件系統中UDP協議的read實現:socket文件系統只定義了aio_read操作,沒有定義普通的read函數。根據前文,在這種情況下
do_sync_read會利用aio_read實現同步讀操作。
其調用鏈為sock_aio_read->do_sock_read->__sock_recvmsg->__sock_recvmsg_nose->udp_recvmsg,代碼如下所示:
    
    
    
            
  1. int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
  2. size_t len, int noblock, int flags, int *addr_len)
  3. ……
  4. ulen = skb->len - sizeof(struct udphdr);
  5. copied = len;
  6. if (copied > ulen)
  7. copied = ulen;
  8. ……
當UDP報文的數據長度小於參數len時,就會只復制真正的數據長度,那么對於read操作來說,返回的讀取字節數自然就小於參數count了。
看到這里,是否已經得到本小節開頭部分問題的答案了呢?當fd中的數據不夠count大小時,read會返回當前可以讀取的字節數?很可惜,答案是否定的。這種行為完全由具體實現來決定。即使同為socket文件系統,TCP套接字的讀取操作也會與UDP不同。當TCP的fd的數據不足時,read操作極可能會阻塞,而不是直接返回。注:TCP是否阻塞,取決於當前緩存區可用數據多少,要讀取的字節數,以及套接字設置的接收低水位大小。
因此在調用read的時候,只能根據read接口的說明,小心處理所有的情況,而不能主觀臆測內核的實現。比如本文中的部分讀取情況,阻塞和直接返回兩種策略同時存在。
------------------------------------write跟read的實現差不多,這里就不列出來了,主要討論多個文件同時寫-------------
前面說過,文件的讀寫操作都是從當前文件的偏移處開始的。這個文件偏移量保存在文件表中,而每個進程都有一個文件表。那么當多個進程同時寫一個文件時,即使對write進行了鎖保護,在進行串行寫操作時,文件依然不可避免地會被寫亂。根本原因就在於文件偏移量是進程級別的。
當使用O_APPEND以追加的形式來打開文件時,每次寫操作都會先定位到文件末尾,然后再執行寫操作。
Linux下大多數文件系統都是調用generic_file_aio_write來實現寫操作的。在generic_file_aio_write中,有如下代碼:
    
    
    
            
  1. mutex_lock(&inode->i_mutex);//加鎖
  2. blk_start_plug(&plug);
  3. ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);//發現文件是追加打開,直接從inode讀取最新文件大小作為偏移量
  4. mutex_unlock(&inode->i_mutex); //解鎖
這里有一個關鍵的語句,就是使用mutex_lock對該文件對應的inode進行保護,然后調用__generic_file_aio_write->generic_write_check。其部分代碼如下:
    
    
    
            
  1. if (file->f_flags & O_APPEND)
  2. *pos = i_size_read(inode);
上面的代碼中,如果發現文件是以追加方式打開的,則將從inode中讀取到的最新文件大小作為偏移量,然后通過__generic_file_aio_write再進行寫操作,這樣就能保證寫操作是在文件末尾追加的。

----------------------------------文件描述符的復制----------------------------
    
    
    
            
  1. int dup(int oldfd);
  2. int dup2(int oldfd, int newfd);
·dup會使用一個最小的未用文件描述符作為復制后的文件描述符。
·dup2是使用用戶指定的文件描述符newfd來復制oldfd的。如果newfd已經是打開的文件描述符,Linux會先關閉newfd,然后再復制oldfd。
dup的實現
      
      
      
              
  1. SYSCALL_DEFINE1(dup, unsigned int, fildes)
  2. {
  3. int ret = -EBADF;
  4. /* 必須先得到文件管理結構file, 同時也是對描述符fildes的檢查 */
  5. struct file *file = fget_raw(fildes);
  6. if (file) {
  7. /* 得到一個未使用的文件描述符 */
  8. ret = get_unused_fd();
  9. if (ret >= 0) {
  10. /* 將文件描述符與file指針關聯起來 */
  11. fd_install(ret, file);
  12. }
  13. else
  14. fput(file);
  15. }
  16. return ret;
  17. }
在dup中調用get_unused_fd,只是得到一個未用的文件描述符,那么如何實現在dup接口中使用最小的未用文件描述符呢?這就需要回顧1.4.2節中總結過的Linux文件描述符的選擇策略了。
Linux總是嘗試給用戶最小的未用文件描述符,所以get_unused_fd得到的文件描述符始終是最小的可用文件描述符。
查看dup代碼實現的第11行
      
      
      
              
  1. void fd_install(unsigned int fd, struct file *file)
  2. {
  3. struct files_struct *files = current->files;
  4. struct fdtable *fdt;
  5. /* 對文件表進行保護 */
  6. spin_lock(&files->file_lock);
  7. /* 得到文件表 */
  8. fdt = files_fdtable(files);
  9. BUG_ON(fdt->fd[fd] != NULL);
  10. /* 讓文件表中fd對應的指針等於該文件關聯結構file */
  11. rcu_assign_pointer(fdt->fd[fd], file);
  12. spin_unlock(&files->file_lock);
  13. }
在fd_install中,fd與file的關聯是利用fd來作為指針數組的索引的,從而讓對應的指針指向file。對於dup來說,這意味着數組中兩個指針都指向了同一個file。而file是進程中真正的管理文件的結構,文件偏移等信息都是保存在file中的。這就意味着,當使用oldfd進行讀寫操作時,無論是oldfd還是newfd的文件偏移都會發生變化。
---------------------看一下dup2的實現-------------------
      
      
      
              
  1. SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd)
  2. {
  3. /* 如果oldfd與newfd相等, 這是一種特殊的情況 */
  4. if (unlikely(newfd == oldfd)) { /* corner case */
  5. struct files_struct *files = current->files;
  6. int retval = oldfd;
  7. /*
  8. 檢查oldfd的合法性, 如果是合法的fd, 則直接返回oldfd的值;
  9. 如果是不合法的, 則返回EBADF
  10. */
  11. rcu_read_lock();
  12. if (!fcheck_files(files, oldfd))
  13. retval = -EBADF;
  14. rcu_read_unlock();
  15. return retval;
  16.     }
  17. /* 如果oldfd與newfd不同, 則利用sys_dup3來實現dup2 */
    return sys_dup3(oldfd, newfd, 0);
  18. }



------------------------------------文件的元數據獲取--------------
什么是文件的元數據呢?其包括文件的訪問權限、上次訪問的時間戳、所有者、所有組、文件大小等信息。
    
    
    
            
  1. int stat(const char *path, struct stat *buf);
  2. int fstat(int fd, struct stat *buf);
  3. int lstat(const char *path, struct stat *buf);
這三個函數都可用於得到文件的基本信息,區別在於stat得到路徑path所指定的文件基本信息,fstat得到文件描述符fd指定文件的基本信息,而lstat與stat則基本相同,只有當path是一個鏈接文件時,lstat得到的是鏈接文件自己本身的基本信息而不是其指向文件的信息。
所得到的文件基本信息的結果struct stat的結構如下:
    
    
    
            
  1. struct stat {
  2. dev_t st_dev; /* ID of device containing file */
  3. ino_t st_ino; /* inode number */
  4. mode_t st_mode; /* protection */
  5. nlink_t st_nlink; /* number of hard links */
  6. uid_t st_uid; /* user ID of owner */
  7. gid_t st_gid; /* group ID of owner */
  8. dev_t st_rdev; /* device ID (if special file) */
  9. off_t st_size; /* total size, in bytes */
  10. blksize_t st_blksize; /* blocksize for file system I/O */
  11. blkcnt_t st_blocks; /* number of 512B blocks allocated */
  12. time_t st_atime; /* time of last access */
  13. time_t st_atime; /* time of last access */
    time_t st_mtime; /* time of last modification */
    time_t st_ctime; /* time of last status change */
    };


st_mode要注意一點的是:st_mode,其注釋不僅僅是protection,同時也表示文件類型,比如是普通文件還是目錄
stat代碼實現:
    
    
    
            
  1. SYSCALL_DEFINE2(stat, const char __user *, filename,struct __old_kernel_stat __user *, statbuf){
  2. struct kstat stat;
  3. int error;
  4. /* vfs_stat用於讀取文件元數據至stat */
  5. error = vfs_stat(filename, &stat);
  6. if (error)
  7. return error;
  8. /* 這里僅是從內核的元數據結構stat復制到用戶層的數據結構statbuf中 */
  9. return cp_old_stat(&stat, statbuf);
  10. }
第5行,vfs_stat是關鍵。進入vfs_stat->vfs_fstatat->vfs_getattr,代碼如下:
    
    
    
            
  1. int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat)
  2. {
  3. struct inode *inode = dentry->d_inode;
  4. int retval;
  5. /* 對獲取inode屬性操作進行安全性檢查 */
  6. retval = security_inode_getattr(mnt, dentry);
  7. if (retval)
  8. return retval;
  9. /* 如果該文件系統定義了這個inode的自定義操作函數, 就執行它 */
  10. if (inode->i_op->getattr)
  11. return inode->i_op->getattr(mnt, dentry, stat);
  12. /* 如果文件系統沒有定義inode的操作函數, 則執行通用的函數 */
  13. generic_fillattr(inode, stat);
  14. return 0;
  15. }
不失一般性,也可以通過查看第13行的generic_fillattr來進一步了解,代碼如下:
    
    
    
            
  1. void generic_fillattr(struct inode *inode, struct kstat *stat)
  2. {
  3. stat->dev = inode->i_sb->s_dev;
  4. stat->ino = inode->i_ino;
  5. stat->mode = inode->i_mode;
  6. stat->nlink = inode->i_nlink;
  7. stat->uid = inode->i_uid;
  8. stat->gid = inode->i_gid;
  9. stat->rdev = inode->i_rdev;
  10. stat->size = i_size_read(inode);
  11. stat->atime = inode->i_atime;
  12. stat->mtime = inode->i_mtime;
  13. stat->ctime = inode->i_ctime;
  14. stat->blksize = (1 << inode->i_blkbits);
  15. stat->blocks = inode->i_blocks;
  16. }
從這里可以看出,所有的文件元數據均保存在inode中,而inode是Linux也是所有類Unix文件系統中的一個概念。這樣的文件系統一般將存儲區域分為兩類,一類是保存文件對象的元信息數據,即inode表;另一類是真正保存文件數據內容的塊,所有inode完全由文件系統來維護。但是Linux也可以掛載非類Unix的文件系統,這些文件系統本身沒有inode的概念,怎么辦?Linux為了讓VFS有統一的處理流程和方法,就必須要求那些沒有inode概念的文件系統,根據自己系統的特點——如何維護文件元數據,生成“虛擬的”inode以供Linux內核使用。

















免責聲明!

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



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