<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          存儲基礎 — 文件描述符 fd 究竟是什么?

          共 12448字,需瀏覽 25分鐘

           ·

          2021-04-27 20:41

          堅持思考,就會很酷




          前情概要


          通過上篇 Go 存儲基礎 — 文件 IO 的姿勢, 我們看到有兩種文件讀寫的方式,一種是系統(tǒng)調(diào)用的方式,操作的對象是一個整數(shù) fd,另一種是 Go 標準庫自己封裝的標準庫 IO ,操作對象是 Go 封裝的 file 結(jié)構(gòu)體,但其內(nèi)部還是針對整數(shù) fd 的操作。所以一切的本源是通過 fd 來操作的,那么,這個 fd 究竟是什么?就這個點我們深入剖析。


          fd 是什么?


          fdFile descriptor 的縮寫,中文名叫做:文件描述符。文件描述符是一個非負整數(shù),本質(zhì)上是一個索引值(這句話非常重要)。

          什么時候拿到的 fd ?

          當打開一個文件時,內(nèi)核向進程返回一個文件描述符( open 系統(tǒng)調(diào)用得到 ),后續(xù) read、write 這個文件時,則只需要用這個文件描述符來標識該文件,將其作為參數(shù)傳入 read、write 。

          fd 的值范圍是什么?

          在 POSIX 語義中,0,1,2 這三個 fd 值已經(jīng)被賦予特殊含義,分別是標準輸入( STDIN_FILENO ),標準輸出( STDOUT_FILENO ),標準錯誤( STDERR_FILENO )。

          文件描述符是有一個范圍的:0 ~ OPEN_MAX-1 ,最早期的 UNIX 系統(tǒng)中范圍很小,現(xiàn)在的主流系統(tǒng)單就這個值來說,變化范圍是幾乎不受限制的,只受到系統(tǒng)硬件配置和系統(tǒng)管理員配置的約束。

          你可以通過 ulimit 命令查看當前系統(tǒng)的配置:

          ?  ulimit -n
          4864

          如上,我系統(tǒng)上進程默認最多打開 4864 文件。


          窺探 Linux 內(nèi)核


          fd 究竟是什么?必須去 Linux 內(nèi)核看一眼。

          用戶使用系統(tǒng)調(diào)用 open 或者 creat 來打開或創(chuàng)建一個文件,用戶態(tài)得到的結(jié)果值就是 fd ,后續(xù)的 IO 操作全都是用 fd 來標識這個文件,可想而知內(nèi)核做的操作并不簡單,我們接下來就是要揭開這層面紗。


          task_struct


          首先,我們知道進程的抽象是基于 struct task_struct 結(jié)構(gòu)體,這是 Linux 里面最復雜的結(jié)構(gòu)體之一 ,成員字段非常多,我們今天不需要詳解這個結(jié)構(gòu)體,我稍微簡化一下,只提取我們今天需要理解的字段如下:

          struct task_struct {
              // ...
              /* Open file information: */
              struct files_struct     *files;
              // ...
          }

          files; 這個字段就是今天的主角之一,files 是一個指針,指向一個為 struct files_struct 的結(jié)構(gòu)體。這個結(jié)構(gòu)體就是用來管理該進程打開的所有文件的管理結(jié)構(gòu)。

          重點理解一個概念:

          struct task_struct 是進程的抽象封裝,標識一個進程,在 Linux 里面的進程各種抽象視角,都是這個結(jié)構(gòu)體給到你的。當創(chuàng)建一個進程,其實也就是 new 一個 struct task_struct 出來;


          files_struct


          好,上面通過進程結(jié)構(gòu)體引出了 struct files_struct 這個結(jié)構(gòu)體。這個結(jié)構(gòu)體管理某進程打開的所有文件的管理結(jié)構(gòu),這個結(jié)構(gòu)體本身是比較簡單的:

          /*
           * Open file table structure
           */

          struct files_struct {
              // 讀相關字段
              atomic_t count;
              bool resize_in_progress;
              wait_queue_head_t resize_wait;

              // 打開的文件管理結(jié)構(gòu)
              struct fdtable __rcu *fdt;
              struct fdtable fdtab;

              // 寫相關字段
              unsigned int next_fd;
              unsigned long close_on_exec_init[1];
              unsigned long open_fds_init[1];
              unsigned long full_fds_bits_init[1];
              struct file * fd_array[NR_OPEN_DEFAULT];
          };

          files_struct 這個結(jié)構(gòu)體我們說是用來管理所有打開的文件的。怎么管理?本質(zhì)上就是數(shù)組管理的方式,所有打開的文件結(jié)構(gòu)都在一個數(shù)組里。這可能會讓你疑惑,數(shù)組在那里?有兩個地方:

          1. struct file * fd_array[NR_OPEN_DEFAULT] 是一個靜態(tài)數(shù)組,隨著 files_struct 結(jié)構(gòu)體分配出來的,在 64 位系統(tǒng)上,靜態(tài)數(shù)組大小為 64;
          2. struct fdtable 也是個數(shù)組管理結(jié)構(gòu),只不過這個是一個動態(tài)數(shù)組,數(shù)組邊界是用字段描述的;

          思考:為什么會有這種靜態(tài) + 動態(tài)的方式?

          性能和資源的權衡 !大部分進程只會打開少量的文件,所以靜態(tài)數(shù)組就夠了,這樣就不用另外分配內(nèi)存。如果超過了靜態(tài)數(shù)組的閾值,那么就動態(tài)擴展。

          可以回憶下,這個是不是跟 inode 的直接索引,一級索引的優(yōu)化思路類似。

          fdtable

          簡單介紹下 fdtable 結(jié)構(gòu)體,這個結(jié)構(gòu)體就是封裝用來管理 fd 的結(jié)構(gòu)體,fd 的秘密就在這個里面。簡化結(jié)構(gòu)體如下:

          struct fdtable {
              unsigned int max_fds;
              struct file __rcu **fd;      /* current fd array */
          };

          注意到 fdtable.fd 這個字段是一個二級指針,什么意思?

          就是指向 fdtable.fd 是一個指針字段,指向的內(nèi)存地址還是存儲指針的(元素指針類型為  struct file * )。換句話說,fdtable.fd 指向一個數(shù)組,數(shù)組元素為指針(指針類型為 struct file *)。

          其中 max_fds 指明數(shù)組邊界。

          files_struct 小結(jié)

          file_struct 本質(zhì)上是用來管理所有打開的文件的,內(nèi)部的核心是由一個靜態(tài)數(shù)組動態(tài)數(shù)組管理結(jié)構(gòu)實現(xiàn)。

          還記得上面我們說文件描述符 fd 本質(zhì)上就是索引嗎?這里就把概念接上了,fd 就是這個數(shù)組的索引,也就是數(shù)組的槽位編號而已。 通過非負數(shù) fd 就能拿到對應的 struct file 結(jié)構(gòu)體的地址。

          我們把概念串起來(注意,這里為了突出 fd 的本質(zhì),把 fdtable 管理簡化掉):



          • fd 真的就是 files 這個字段指向的指針數(shù)組的索引而已(僅此而已)。通過 fd 能夠找到對應文件的 struct file 結(jié)構(gòu)體;

          file


          現(xiàn)在我們知道了 fd 本質(zhì)是數(shù)組索引,數(shù)組元素是 struct file 結(jié)構(gòu)體的指針。那么這里就引出了一個 struct file 的結(jié)構(gòu)體。這個結(jié)構(gòu)體又是用來干什么的呢?

          這個結(jié)構(gòu)體是用來表征進程打開的文件的。簡化結(jié)構(gòu)如下:

          struct file {
              // ...
              struct path                     f_path;
              struct inode                    *f_inode;
              const struct file_operations    *f_op;

              atomic_long_t                    f_count;
              unsigned int                     f_flags;
              fmode_t                          f_mode;
              struct mutex                     f_pos_lock;
              loff_t                           f_pos;
              struct fown_struct               f_owner;
              // ...
          }

          這個結(jié)構(gòu)體非常重要,它標識一個進程打開的文件,下面解釋 IO 相關的幾個最重要的字段:

          • f_path :標識文件名
          • f_inode :非常重要的一個字段,inode 這個是 vfs 的 inode 類型,是基于具體文件系統(tǒng)之上的抽象封裝;
          • f_pos :這個字段非常重要,偏移,對,就是當前文件偏移。還記得上一篇 IO 基礎里也提過偏移對吧,指的就是這個,f_posopen 的時候會設置成默認值,seek 的時候可以更改,從而影響到 write/read 的位置;

          思考問題

          思考問題一:files_struct 結(jié)構(gòu)體只會屬于一個進程,那么struct file 這個結(jié)構(gòu)體呢,是只會屬于某一個進程?還是可能被多個進程共享?

          劃重點:struct file 是屬于系統(tǒng)級別的結(jié)構(gòu),換句話說是可以共享與多個不同的進程。

          思考問題二:什么時候會出現(xiàn)多個進程的  fd  指向同一個 file  結(jié)構(gòu)體?

          比如 fork  的時候,父進程打開了文件,后面 fork 出一個子進程。這種情況就會出現(xiàn)共享 file 的場景。如圖:



          思考問題三:在同一個進程中,多個 fd 可能指向同一個 file 結(jié)構(gòu)嗎?

          可以。dup  函數(shù)就是做這個的。

          #include <unistd.h>
          int dup(int oldfd);
          int dup2(int oldfd, int newfd);


          inode


          我們看到 struct file 結(jié)構(gòu)體里面有一個 inode 的指針,也就自然引出了 inode 的概念。這個指向的 inode 并沒有直接指向具體文件系統(tǒng)的 inode ,而是操作系統(tǒng)抽象出來的一層虛擬文件系統(tǒng),叫做 VFS ( Virtual File System ),然后在 VFS 之下才是真正的文件系統(tǒng),比如 ext4 之類的。

          完整架構(gòu)圖如下:



          思考:為什么會有這一層封裝呢?

          其實很容里理解,就是解耦。如果讓 struct file 直接和 struct ext4_inode 這樣的文件系統(tǒng)對接,那么會導致 struct file 的處理邏輯非常復雜,因為每對接一個具體的文件系統(tǒng),就要考慮一種實現(xiàn)。所以操作系統(tǒng)必須把底下文件系統(tǒng)屏蔽掉,對外提供統(tǒng)一的 inode 概念,對下定義好接口進行回調(diào)注冊。這樣讓 inode 的概念得以統(tǒng)一,Unix 一切皆文件的基礎就來源于此。

          再來看一樣 VFS 的 inode 的結(jié)構(gòu):

          struct inode {
              // 文件相關的基本信息(權限,模式,uid,gid等)
              umode_t             i_mode;
              unsigned short      i_opflags;
              kuid_t              i_uid;
              kgid_t              i_gid;
              unsigned int        i_flags;
              // 回調(diào)函數(shù)
              const struct inode_operations   *i_op;
              struct super_block              *i_sb;
              struct address_space            *i_mapping;
              // 文件大小,atime,ctime,mtime等
              loff_t              i_size;
              struct timespec64   i_atime;
              struct timespec64   i_mtime;
              struct timespec64   i_ctime;
              // 回調(diào)函數(shù)
              const struct file_operations    *i_fop;
              struct address_space            i_data;
              // 指向后端具體文件系統(tǒng)的特殊數(shù)據(jù)
              void    *i_private;     /* fs or device private pointer */
          };

          其中包括了一些基本的文件信息,包括 uid,gid,大小,模式,類型,時間等等。

          一個 vfs 和 后端具體文件系統(tǒng)的紐帶:i_private 字段。**用來傳遞一些具體文件系統(tǒng)使用的數(shù)據(jù)結(jié)構(gòu)。

          至于 i_op 回調(diào)函數(shù)在構(gòu)造 inode 的時候,就注冊成了后端的文件系統(tǒng)函數(shù),比如 ext4 等等。

          思考問題:通用的 VFS 層,定義了所有文件系統(tǒng)通用的 inode,叫做 vfs inode,而后端文件系統(tǒng)也有自身特殊的 inode 格式,該格式是在 vfs inode 之上進行擴展的,怎么通過 vfs inode 怎么得到具體文件系統(tǒng)的 inode 呢?

          下面以 ext4 文件系統(tǒng)舉例(因為所有的文件系統(tǒng)套路一樣),ext4 的 inode 類型是 struct ext4_inode_info

          劃重點:方法其實很簡單,這個是屬于 c 語言一種常見的(也是特有)編程手法:強轉(zhuǎn)類型。vfs inode 出生就和 ext4_inode_info 結(jié)構(gòu)體分配在一起的,直接通過 vfs inode 結(jié)構(gòu)體的地址強轉(zhuǎn)類型就能得到 ext4_inode_info 結(jié)構(gòu)體。

          struct ext4_inode_info {
              // ext4 inode 特色字段
              // ...
              
              // 重要!?。?/span>
              struct inode    vfs_inode;  
          };

          舉個例子,現(xiàn)已知 inode 地址和 vfs_inode 字段的內(nèi)偏移如下:

          • inode 的地址為 0xa89be0;
          • ext4_inode_info 里有個內(nèi)嵌字段 vfs_inode,類型為 struct inode ,該字段在結(jié)構(gòu)體內(nèi)偏移為 64 字節(jié);

          則可以得到:

          ext4_inode_info 的地址為

          (struct ext4_inode_info *)(0xa89be0 - 64)

          強轉(zhuǎn)方法使用了一個叫做 container_of 的宏,如下:

          // 強轉(zhuǎn)函數(shù)
          static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
          {
             return container_of(inode, struct ext4_inode_info, vfs_inode);
          }

          // 強轉(zhuǎn)實際封裝
          #define container_of(ptr, type, member) \
              (type *)((char *)(ptr) - (char *) &((type *)0)->member)

          #endif

          所以,你懂了嗎?

          分配 inode 的時候,其實分配的是 ext4_inode_info 結(jié)構(gòu)體,包含了 vfs inode,然后對外給出去 vfs_inode 字段的地址即可。VFS 層拿 inode 的地址使用,底下文件系統(tǒng)強轉(zhuǎn)類型后,取外層的 inode 地址使用。

          舉個 ext4 文件系統(tǒng)的例子:

          static struct inode *ext4_alloc_inode(struct super_block *sb)
          {
              struct ext4_inode_info *ei;

              // 內(nèi)存分配,分配 ext4_inode_info 的地址
              ei = kmem_cache_alloc(ext4_inode_cachep, GFP_NOFS);

              // ext4_inode_info 結(jié)構(gòu)體初始化

              // 返回 vfs_inode 字段的地址
              return &ei->vfs_inode;
          }

          vfs 拿到的就是這個 inode 地址。



          劃重點:inode 的內(nèi)存由后端文件系統(tǒng)分配,vfs inode 結(jié)構(gòu)體內(nèi)嵌在不同的文件系統(tǒng)的 inode 之中。不同的層次用不同的地址,ext4 文件系統(tǒng)用 ext4_inode_info 的結(jié)構(gòu)體的地址,vfs 層用 ext4_inode_info.vfs_inode 字段的地址。

          這種用法在 C 語言編程中很常見,算是 C 的特色了(仔細想想,這種用法和面向?qū)ο蟮亩鄳B(tài)的實現(xiàn)異曲同工)。

          思考問題:怎么理解 vfs inodeext2_inode_info,ext4_inode_info 等結(jié)構(gòu)體的區(qū)別?

          所有文件系統(tǒng)共性的東西抽象到 vfs inode ,不同文件系統(tǒng)差異的東西放在各自的 inode 結(jié)構(gòu)體中。


          小結(jié)梳理


          當用戶打開一個文件,用戶只得到了一個 fd 句柄,但內(nèi)核做了很多事情,梳理下來,我們得到幾個關鍵的數(shù)據(jù)結(jié)構(gòu),這幾個數(shù)據(jù)結(jié)構(gòu)是有層次遞進關系的,我們簡單梳理下:

          1. 進程結(jié)構(gòu) task_struct :表征進程實體,每一個進程都和一個 task_struct 結(jié)構(gòu)體對應,其中 task_struct.files 指向一個管理打開文件的結(jié)構(gòu)體 fiels_struct ;

          2. 文件表項管理結(jié)構(gòu) files_struct :用于管理進程打開的 open 文件列表,內(nèi)部以數(shù)組的方式實現(xiàn)(靜態(tài)數(shù)組和動態(tài)數(shù)組結(jié)合)。返回給用戶的 fd 就是這個數(shù)組的編號索引而已,索引元素為 file 結(jié)構(gòu);

            • files_struct 只從屬于某進程;
          3. 文件 file 結(jié)構(gòu):表征一個打開的文件,內(nèi)部包含關鍵的字段有:當前文件偏移,inode 結(jié)構(gòu)地址;

            • 該結(jié)構(gòu)雖然由進程觸發(fā)創(chuàng)建,但是 file  結(jié)構(gòu)可以在進程間共享;
          4. vfs inode 結(jié)構(gòu)體:文件 file 結(jié)構(gòu)指向 的是 vfs 的 inode ,這個是操作系統(tǒng)抽象出來的一層,用于屏蔽后端各種各樣的文件系統(tǒng)的 inode 差異;

            • inode 這個具體進程無關,是文件系統(tǒng)級別的資源;
          5. ext4 inode 結(jié)構(gòu)體(指代具體文件系統(tǒng) inode ):后端文件系統(tǒng)的 inode 結(jié)構(gòu),不同文件系統(tǒng)自定義的結(jié)構(gòu)體,ext2 有 ext2_inode_info,ext4 有ext4_inode_info,minix 有 minix_inode_info,這些結(jié)構(gòu)里都是內(nèi)嵌了一個 vfs inode 結(jié)構(gòu)體,原理相同;

          完整的架構(gòu)圖:



          思考實驗

          現(xiàn)在我們已經(jīng)徹底了解 fd 這個所謂的非負整數(shù)代表的深層含義了,我們可以準備一些 IO 的思考舉一反三。

          文件讀寫( IO )的時候會發(fā)生什么?

          • 在完成 write 操作后,在文件 file  中的當前文件偏移量會增加所寫入的字節(jié)數(shù),如果這導致當前文件偏移量超處了當前文件長度,則會把 inode 的當前長度設置為當前文件偏移量(也就是文件變長)
          • O_APPEND  標志打開一個文件,則相應的標識會被設置到文件 file  狀態(tài)的標識中,每次對這種具有追加寫標識的文件執(zhí)行 write 操作的時候,file 的當前文件偏移量首先會被設置成 inode 結(jié)構(gòu)體中的文件長度,這就使得每次寫入的數(shù)據(jù)都追加到文件的當前尾端處(該操作對用戶態(tài)提供原子語義);
          • 若一個文件 seek 定位到文件當前的尾端,則 file 中的當前文件偏移量設置成 inode 的當前文件長度;
          • seek 函數(shù)值修改 file 中的當前文件偏移量,不進行任何 I/O 操作;
          • 每個進程對有它自己的 file,其中包含了當前文件偏移,當多個進程寫同一個文件的時候,由于一個文件 IO 最終只會是落到全局的一個 inode 上,這種并發(fā)場景則可能產(chǎn)生用戶不可預期的結(jié)果;

          總結(jié)


          回到初心,理解 fd 的概念有什么用?

          一切 IO 的行為到系統(tǒng)層面都是以 fd 的形式進行。無論是 C/C++,Go,Python,JAVA 都是一樣,任何語言都是一樣,這才是最本源的東西,理解了 fd 關聯(lián)的一系列結(jié)構(gòu),你才能對 IO 游刃有余。

          簡要的總結(jié):

          1. 從姿勢上來講,用戶 open 文件得到一個非負數(shù)句柄 fd,之后針對該文件的 IO 操作都是基于這個 fd ;
          2. 文件描述符 fd 本質(zhì)上來講就是數(shù)組索引,fd 等于 5 ,那對應數(shù)組的第 5 個元素而已,該數(shù)組是進程打開的所有文件的數(shù)組,數(shù)組元素類型為 struct file;
          3. 結(jié)構(gòu)體 task_struct 對應一個抽象的進程,files_struct 是這個進程管理該進程打開的文件數(shù)組管理器。fd 則對應了這個數(shù)組的編號,每一個打開的文件用 file 結(jié)構(gòu)體表示,內(nèi)含當前偏移等信息;
          4. file 結(jié)構(gòu)體可以為進程間共享,屬于系統(tǒng)級資源,同一個文件可能對應多個 file 結(jié)構(gòu)體,file 內(nèi)部有個 inode 指針,指向文件系統(tǒng)的 inode;
          5. inode 是文件系統(tǒng)級別的概念,只由文件系統(tǒng)管理維護,不因進程改變( file 是進程出發(fā)創(chuàng)建的,進程 open 同一個文件會導致多個 file ,指向同一個 inode );

          回顧一眼架構(gòu)圖:

          ~完~


          后記



          內(nèi)核把最復雜的活干了,只暴露給您最簡單的一個非負整數(shù) fd 。所以,絕大部分場景會用fd 就行,倒不用想太多。當然如果能再深入看一眼知其所以然是最好不過。本文分享是基礎準備篇,希望能給你帶來不一樣的 IO 視角。










          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美男女日逼 | 天天做天天干天天爱麻豆 | 悠悠资源音影先锋在线观看 | 乱伦亚洲色國片 | 伊人大香蕉在线观看 |