1. 程式人生 > >[apue] 一圖讀懂 unix 檔案控制代碼及檔案共享過程

[apue] 一圖讀懂 unix 檔案控制代碼及檔案共享過程

與檔案相關的一些概念

在開始上圖之前,先說明幾個和 unix 檔案密切相關的術語,方便後續討論使用

  • 檔案控制代碼 / 檔案描述符 (file descriptor 或 FD):描述一個開啟檔案相關屬性的型別;
  • 檔案描述符表 (file descriptor table 或 FDT):每個程序擁有一個 FDT,其中每個表項是一個 FD,使用 FDT 的下標表示各個 FD(從 0 開始的整數);
  • 全域性開啟檔案表 (open file table 或 OFT):系統只有一個 OFT,其中每個表項被 FD 所引用;
  • i 節點 (inode):描述檔案系統上的一個檔案,例如 所有者/大小/裝置/起始位置 等,它只包含和檔案系統相關的屬性;
  • v 節點 (vnode):描述檔案相關的操作,例如 讀 / 寫 / 移動相對偏移量 等,它只包含和檔案系統無關的屬性,用於統合各種不同型別的檔案系統;

其中前三項只有檔案被開啟後才有相應的結構,而後兩項只要檔案存在就存在了,與檔案是否開啟沒有關係。

檔案相關概念之間的關係

它們之間的關係是怎樣的呢,現在上圖

圖中左側展示了兩個程序,藍色的為 ProcessA (PA),紅色的為 ProcessB (PB),每個程序都有一個 FDT,其中包含若干個 FD,可以看到每個 FD 由兩部分組成:

  • pflag :在程序中的標誌位,目前只有一個標誌位 O_CLOEXEC,置位的話表示在程序執行 exec 函式族後自動關閉此檔案控制代碼,預設是不關閉的;
  • fileptr :指向 OFT 中相應的表項,來描述檔案剩餘的屬性。

再觀察 OFT 中表項的內容,可以看到它是由以下幾部分組成:

  • oflag :檔案開啟標誌位,除 O_CLOEXEC 之外的標誌位,如許可權位 O_RDONLY / O_WRONLY / O_RDWR,建立位 O_CREAT / O_EXCL,追加位 O_APPEND,截斷位 O_TRUNC,非同步位 O_NONBLOCK 等均由這個欄位指定。
  • offset :當前檔案偏移;
  • vnode :指向該檔案的 v 節點。

再觀察檔案屬性相關的節點,它一般由下面兩部分組成:

  • vnode :檔案的 v 節點資訊,通常是一些操作的抽象,用於構建檔案系統無關的 VFS;
  • inode :檔案的 i 節點資訊。

對於 vnode,你可以理解成是一組函式指標,例如在 Linux 上,它分別定義了 inode 與檔案的操作函式:

 1 struct inode_operations {
 2     struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
 3     void * (*follow_link) (struct dentry *, struct nameidata *);
 4     int (*permission) (struct inode *, int);
 5     struct posix_acl * (*get_acl)(struct inode *, int);
 6     int (*readlink) (struct dentry *, char __user *,int);
 7     void (*put_link) (struct dentry *, struct nameidata *, void *);
 8     int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
 9     int (*link) (struct dentry *,struct inode *,struct dentry *);
10     int (*unlink) (struct inode *,struct dentry *);
11     int (*symlink) (struct inode *,struct dentry *,const char *);
12     int (*mkdir) (struct inode *,struct dentry *,int);
13     int (*rmdir) (struct inode *,struct dentry *);
14     int (*mknod) (struct inode *,struct dentry *,int,dev_t);
15     int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *);
16     void (*truncate) (struct inode *);
17     int (*setattr) (struct dentry *, struct iattr *);
18     int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
19     int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
20     ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
21     ssize_t (*listxattr) (struct dentry *, char *, size_t);
22     int (*removexattr) (struct dentry *, const char *);
23     void (*truncate_range)(struct inode *, loff_t, loff_t);
24     int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start, u64 len);
25 } ____cacheline_aligned;
26 
27 struct file_operations { 
28   struct module *owner;//擁有該結構的模組的指標,一般為THIS_MODULES  
29     loff_t (*llseek) (struct file *, loff_t, int);//用來修改檔案當前的讀寫位置  
30     ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//從裝置中同步讀取資料
31     ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向裝置傳送資料  
32     ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一個非同步的讀取操作   
33     ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一個非同步的寫入操作   
34   int (*readdir) (struct file *, void *, filldir_t);//僅用於讀取目錄,對於裝置檔案,該欄位為NULL   
35     unsigned int (*poll) (struct file *, struct poll_table_struct *); //輪詢函式,判斷目前是否可以進行非阻塞的讀寫或寫入   
36   int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //執行裝置I/O控制命令   
37   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK檔案系統,將使用此種函式指標代替ioctl  
38   long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系統上,32位的ioctl呼叫將使用此函式指標代替   
39   int (*mmap) (struct file *, struct vm_area_struct *); //用於請求將裝置記憶體對映到程序地址空間  
40   int (*open) (struct inode *, struct file *); //開啟   
41   int (*flush) (struct file *, fl_owner_t id);   
42   int (*release) (struct inode *, struct file *); //關閉   
43   int (*fsync) (struct file *, struct dentry *, int datasync); //重新整理待處理的資料   
44   int (*aio_fsync) (struct kiocb *, int datasync); //非同步重新整理待處理的資料   
45   int (*fasync) (int, struct file *, int); //通知裝置FASYNC標誌發生變化   
46   int (*lock) (struct file *, int, struct file_lock *);   
47   ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);   
48   unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);  
49   int (*check_flags)(int);   
50   int (*flock) (struct file *, int, struct file_lock *);  
51   ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);  
52   ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);   
53   int (*setlease)(struct file *, long, struct file_lock **);   
54 };

 

ext2 上的 read 與 nfs 的 read 實現肯定不同,但是這裡通過函式指標來遮蔽了這種差異。注意:linux 上並沒有 vnode 的概念,它使用與檔案系統相關的 inode 和檔案系統無關的 inode,後者就是我們這裡說的 vnode。

上面的大圖是最普通的場景,就是兩個程序都開啟不同的檔案,相互之間沒有共享,下面我們分幾個場景來看一下共享檔案時這裡的關係是如何變化的。

一個程序多次開啟同一個檔案

使用 open 多次開啟同一個檔案(檔案路徑可能相同,也可能不同,考慮連結的情況)的場景如上圖,每個 FD 都有獨立的 OFT 對應項,雖然最後都是在操作同一個檔案,但一個 FD 的檔案偏移改變,不影響另外一個 FD 的檔案偏移;同理與檔案相關的 pflag、oflag 也是如此。

多個程序開啟同一個檔案

多個程序開啟同一個檔案的場景如上圖,除了跨程序外,其它與程序內並無任何不同。這裡著重考察一個具體場景,就是兩個程序同時開啟檔案進行追加(O_APPEND)寫。假設 PA 寫入一些資料完成後,它的 offset 會被更新,如果這個值大於 inode 中的檔案 size,則更新 inode.size 到 offset 表示檔案增長了;然後 PB 開始寫入資料,由於指定了 O_APPEND 標誌位,在寫入前,系統會先將它的 OFT 表項中的 offset 更新為當前 inode.size,這樣就可以得到 PA 寫入後的檔案末尾位置,接著在這個位置寫入 PB 的資料,寫入完成後的邏輯與 PA 相同,會更新 offset、inode.size 來表示檔案的最新增長。由於更新 offset 與 inode.size 是在一個 api 完成的,所以這個操作完全可以被某種鎖保護起來,從而實現原子性。相對的,如果沒有指定 O_APPEND 選項,而使用 lseek (fd, 0, SEEK_END) + write (fd, buf, size) 的方式,由於這個操作需要使用兩個 api 來完成,無法跨 api 加鎖使得這樣的操作沒有原子性保證,而可能產生的競爭會導致一個程序寫入的資料被另一個程序所覆蓋,從而丟失資料。

程序內檔案控制代碼 dup

程序內檔案控制代碼 dup 的場景如上圖,執行的是 fd2 = dup(fd1) 語句,複製成功後,fd2 與 fd1 都將指向同一個 OFT 表項。而 pflag 不在複製之列,也就是說,如果 fd1 指定了 O_CLOEXEC,則複製後的 fd2 預設是沒有設定這個標誌位的。除此之外,與檔案相關的其它屬性完全一樣,包括 oflag 的各種標誌位、offset 和檔案 inode 資訊。如果修改 fd1 的 oflag,例如 O_NONBLOCK,則 fd2 也將變成非阻塞的;如果讀寫 fd2,則 fd1 的 offset 也會隨之改變……

程序 fork

程序 PA 開啟一個檔案後 fork 產生子程序 PB 的場景如上圖,之前開啟的控制代碼將指向同樣的 OFT 表項,這樣的表現有點類似跨程序檔案控制代碼 dup,除了 fd0 分屬 PA 與 PB 兩個不同程序外,其它方面與上一個場景完全相同。所以如果希望通過 fork 來共享某些檔案資料,則在 PA 寫入資料後,PB 並不能讀到父程序剛剛寫入的資料,這是因為它的 fd0 對應的檔案偏移也被更新了的緣故。

程序間傳遞檔案控制代碼

說到程序間傳遞檔案控制代碼,很多人是不是第一反應是直接傳遞 FD 值啊?那就理解錯了。關於在程序間如何傳遞檔案控制代碼,請參考我之前寫過的一篇文章:記一次傳遞檔案控制代碼引發的血案 ,簡單說的話,可以引用 apue 書中的一句話來解釋:“在技術上,傳送程序實際上向接收程序傳送一個指向一開啟檔案表項的指標,該指標被分配存放在接收程序的第一個可用描述符項中”,其實非常類似 fork 所產生的效果,不同之處在於兩點:

  • 傳送與接收檔案控制代碼的程序不一定是父子程序關係;
  • 原程序與新程序中複製的檔案控制代碼值一般不同(fork 結果一般是相同)

上面的圖展示了這種細節的差異,PA 傳送的檔案控制代碼是 fd0,PB 由於已經打開了 fd0,所以接收後新的檔案控制代碼是 fd1,其它方面與 fork 場景的結論完全一致。

結語

其實判斷兩個控制代碼是在哪個級別共享的方法很簡單,就是改變一個控制代碼的檔案偏移,觀察另外一個控制代碼的檔案偏移是否變化。如果變了,則是在 OFT 層面共享的;如果沒變,則只是開啟同一個檔案而已。另外,有些東西會隨著時代而更新,有些原理則不會變,以本文開頭的這張結構圖來說,自 UNIX 的早期版本(1978)以來就沒有發生過根本性的變化,可見學知識還是要學原理性的東西,萬變不離其宗。

參考

[1]. inode_operations介紹

[2]. Linux字元裝置驅動file_operations

[3]. 驅動程式操作的三個核心資料結構(file_operations、file、inod