深刻解析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 { /* 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是內核用於文
件管理的結構。這裏使用默認大小的數組, 就是爲了能夠涵蓋
/* 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是內核用於文
件管理的結構。這裏使用默認大小的數組, 就是爲了能夠涵蓋 態分配 */
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

態分配 */
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  struct list_head f_list; /*全部打開的文件造成一個鏈表*/ struct dentry *f_dentry; /*指向相關目錄項的指針*/ struct vfsmount *f_vfsmnt; /*指向VFS安裝點的指針*/ struct file_operations *f_op; /*指向文件操做表的指針*/ mode_t f_mode; /*文件的打開模式*/ loff_t f_pos; /*文件的當前位置*/ unsigned short f_flags; /*打開文件時所指定的標誌*/ unsigned short f_count; /*使用該結構的進程數*/ unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; /*預讀標誌、要預讀的最多頁面數、上次預讀後的文件指針、預讀的字節數以及預讀的頁面數*/ int f_owner; /* 經過信號進行異步I/O數據的傳送*/ unsigned int f_uid, f_gid; /*用戶的UID和GID*/ int f_error; /*網絡寫操做的錯誤碼*/ unsigned long f_version; /*版本號*/ void *private_data; /* tty驅動程序所需 */};

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

file對象中包含一個指針,指向dentry對象。dentry對象表明一個獨立的文件路徑,若是一個文件路徑被打開屢次,那麼會創建多個file對象,但它們都指向同一個dentry對象。

dentry對象中又包含一個指向inode對象的指針。inode對象表明一個獨立文件。由於存在硬連接與符號連接,所以不一樣的dentry 對象能夠指向相同的inode對象.inode 對象包含了最終對文件進行操做所需的全部信息,如文件系統類型、文件的操做方法、文件的權限、訪問日期等。

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

注意,同一個進程屢次打開同一個文件時,內核會建立多個file對象。
當進程使用fork系統調用建立一個子進程後,子進程將繼承父進程的文件描述符表,所以在父進程中打開的文件能夠在子進程中用同一個描述符訪問。
---------------------------------------------------------------open解析---------------------------------------------------
   
   
   
   
int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);
前一個是glibc封裝的函數,後一個是系統調用
open源碼追蹤:
  
  
  
  
  1. 新的文件描述符 */
  2. /* 申請新的文件管理結構file */
long do_sys_open(int dfd, const char __user *filename, int flags, int mode){ struct open_flags op; /* flags爲用戶層傳遞的參數, 內核會對flags進行合法性檢查, 並根據mode生成新的flags值賦給 lookup */ int lookup = build_open_flags(flags, mode, &op); /* 將用戶空間的文件名參數複製到內核空間 */ char *tmp = getname(filename); int fd = PTR_ERR(tmp); if (!IS_ERR(tmp)) { /* 未出錯則申請 fd = get_unused_fd_flags(flags); if (fd >= 0) { struct file *f = do_filp_open(dfd, tmp, &op, lookup); if (IS_ERR(f)) { put_unused_fd(fd); fd = PTR_ERR(f); } else { /* 產生文件打開的通知事件 */ fsnotify_open(f); /* 將文件描述符fd與文件管理結構file對應起來, 即安裝 */ fd_install(fd, f); } } putname(tmp); } return fd;}
從上面來看,打開文件,內核消耗了2種資源:文件描述符跟內核管理文件結構file

根據POSIX標準,當獲取一個新的文件描述符時,要返回最低的未使用的文件描述符。Linux是如何實現這一標準的呢?
在Linux中,經過do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))來選擇文件描述符,代碼以下
    
    
    
    
int alloc_fd(unsigned start, unsigned flags){ struct files_struct *files = current->files;//獲取當前進程的對應包含文件描述符表的結構 unsigned int fd; int error; struct fdtable *fdt; /* files爲進程的文件表, 下面須要更改文件表, 因此須要先鎖文件表 */ spin_lock(&files->file_lock);repeat: /* 獲得文件描述符表 */ fdt = files_fdtable(files); /* 從start開始, 查找未用的文件描述符。在打開文件時, start爲0 */ fd = start; /* files->next_fd爲上一次成功找到的fd的下一個描述符。使用next_fd, 能夠快速找到未用的文件描述符;*/ if (fd < files->next_fd) fd = files->next_fd; /* 當小於當前文件表支持的最大文件描述符個數時, 利用位圖找到未用的文件描述符。 若是大於max_fds怎麼辦呢?若是大於當前支持的最大文件描述符, 那它確定是未 用的, 就不須要用位圖來確認了。 */ if (fd < fdt->max_fds) fd = find_next_zero_bit(fdt->open_fds->fds_bits, fdt->max_fds, fd); /* expand_files用於在必要時擴展文件表。什麼時候是必要的時候呢?好比當前文件描述符已經超過了當 前文件表支持的最大值的時候。 */ error = expand_files(files, fd); if (error < 0) goto out; /* * If we needed to expand the fs array we * might have blocked - try again. */ if (error) goto repeat; /* 只有在start小於next_fd時, 才須要更新next_fd, 以儘可能保證文件描述符的連續性。*/ if (start <= files->next_fd) files->next_fd = fd + 1; /* 將打開文件位圖open_fds對應fd的位置置位 */ FD_SET(fd, fdt->open_fds);/* 根據flags是否設置了O_CLOEXEC, 設置或清除fdt->close_on_exec */ if (flags & O_CLOEXEC) FD_SET(fd, fdt->close_on_exec); else FD_CLR(fd, fdt->close_on_exec); error = fd;#if 1 /* Sanity check */ if (rcu_dereference_raw(fdt->fd[fd]) != NULL) { printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd); rcu_assign_pointer(fdt->fd[fd], NULL); }#endifout: spin_unlock(&files->file_lock); return error;}
下面內核使用fd_install將文件管理結構file與fd組合起來,具體操做請看以下代碼:
    
    
    
    
void fd_install(unsigned int fd, struct file *file){ struct files_struct *files = current->files;//得到進程文件表(包含文件描述符表) struct fdtable *fdt; spin_lock(&files->file_lock); /* 獲得文件描述符表 */ fdt = files_fdtable(files); BUG_ON(fdt->fd[fd] != NULL); /* 將文件描述符表中的file類型的指針數組中對應fd的項指向file。 這樣文件描述符fd與file就創建了對應關係 */ rcu_assign_pointer(fdt->fd[fd], file); spin_unlock(&files->file_lock);}
當用戶使用fd與內核交互時,內核能夠用fd從fdt->fd[fd]中獲得內部管理文件的結構struct file。

-------------------------------------------close(關閉文件)------------------------------
close用於關閉文件描述符。而文件描述符能夠是普通文件,也能夠是設備,還能夠是socket。在關閉時,VFS會根據不一樣的文件類型,執行不一樣的操做。
下面將經過跟蹤close的內核源碼來了解內核如何針對不一樣的文件類型執行不一樣的操做。
1. 分析close源碼跟蹤
首先,來看一下close的源碼實現,代碼以下
    
    
    
    
SYSCALL_DEFINE1(close, unsigned int, fd){ struct file * filp; /* 獲得當前進程的文件表 */ struct files_struct *files = current->files; struct fdtable *fdt; int retval; spin_lock(&files->file_lock); /* 經過文件表, 取得文件描述符表 */ fdt = files_fdtable(files); /* 參數fd大於文件描述符表記錄的最大描述符, 那麼它必定是非法的描述符 */ if (fd >= fdt->max_fds) goto out_unlock; /* 利用fd做爲索引, 獲得file結構指針 */ filp = fdt->fd[fd]; /* 檢查filp是否爲NULL。正常狀況下, filp必定不爲NULL。 */ if (!filp) goto out_unlock; /* 將對應的filp置爲0*/ rcu_assign_pointer(fdt->fd[fd], NULL); /* 清除fd在close_on_exec位圖中的位 */ FD_CLR(fd, fdt->close_on_exec); /* 釋放該fd, 或者說將其置爲unused。*/ __put_unused_fd(files, fd); spin_unlock(&files->file_lock); /* 關閉file結構 */ retval = filp_close(filp, files); //這裏將引用計數 /* can't restart close syscall because file table entry was cleared */ if (unlikely(retval == -ERESTARTSYS || retval == -ERESTARTNOINTR || retval == -ERESTARTNOHAND || retval == -ERESTART_RESTARTBLOCK)) retval = -EINTR; return retval;out_unlock: spin_unlock(&files->file_lock); return -EBADF;}EXPORT_SYMBOL(sys_close);
請注意26行的__put_unused_fd,源碼以下所示:
    
    
    
    
static void __put_unused_fd(struct files_struct *files, unsigned int fd){ /* 取得文件描述符表 */ struct fdtable *fdt = files_fdtable(files); /* 清除fd在open_fds位圖的位 */ __FD_CLR(fd, fdt->open_fds); /* 若是fd小於next_fd, 重置next_fd爲釋放的fd */ if (fd < files->next_fd) files->next_fd = fd;}
看到這裏,咱們來回顧一下以前分析過的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結構體,其中的各類文件操做函數由該設備的驅動程序實現。node

每一個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的FAT3二、NTFS),可能沒有inode或超級塊這樣的概念,但爲了能mount到Linux系統,也只好在驅動程序中硬湊一下,在Linux下看FAT32和NTFS分區會發現權限位是錯的,全部文件都是rwxrwxrwx,由於它們原本就沒有inode和權限位的概念,這是硬湊出來的網絡

----------------------------------------------------如下來看自定義的files_operations,以socket舉例,有一個struct file_operations結構體定義了不少函數指針,對應不一樣的讀寫關之類的操做,socket的讀寫關閉等操做分別對應不一樣的內核函數
    
    
    
    
static const struct file_operations socket_file_ops = { .owner = THIS_MODULE, .llseek = no_llseek, .aio_read = sock_aio_read, .aio_write = sock_aio_write, .poll = sock_poll, .unlocked_ioctl = sock_ioctl,#ifdef CONFIG_COMPAT .compat_ioctl = compat_sock_ioctl,#endif .mmap = sock_mmap, .open = sock_no_open, /* special open code to disallow open via /proc */ .release = sock_close, .fasync = sock_fasync, .sendpage = sock_sendpage, .splice_write = generic_splice_sendpage, .splice_read = sock_splice_read,};

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

    
    
    
    
struct file *alloc_file(struct path *path, fmode_t mode, const struct file_operations *fop){ struct file *file; /* 申請一個file */ file = get_empty_filp(); if (!file) return NULL; file->f_path = *path; file->f_mapping = path->dentry->d_inode->i_mapping; file->f_mode = mode; /* 將自定義的文件操做函數指針結構體賦給file->f_op */ file->f_op = fop; ……}
在初始化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被信號中斷所形成的。這兩種狀況基本上均可以視爲正常狀況。
    
    
    
    
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count){ struct file *file; ssize_t ret = -EBADF; int fput_needed; /* 經過文件描述符fd獲得管理結構file */ file = fget_light(fd, &fput_needed); if (file) { /* 獲得文件的當前偏移量 */ loff_t pos = file_pos_read(file); /* 利用vfs進行真正的read */ ret = vfs_read(file, buf, count, &pos); /* 更新文件偏移量 */ file_pos_write(file, pos); /* 歸還管理結構file, 若有必要, 就進行引用計數操做*/ fput_light(file, fput_needed); } return ret;}

查看VFS_read代碼:
    
    
    
    
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos){ ssize_t ret; /* 檢查文件是否爲讀取打開 */ if (!(file->f_mode & FMODE_READ)) return -EBADF; /* 檢查文件是否支持讀取操做 */ if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read)) return -EINVAL; /* 檢查用戶傳遞的參數buf的地址是否可寫 */ if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) return -EFAULT; /* 檢查要讀取的文件範圍實際可讀取的字節數 */ ret = rw_verify_area(READ, file, pos, count); if (ret >= 0) { /* 根據上面的結構, 調整要讀取的字節數 */ count = ret; /* 若是定義read操做, 則執行定義的read操做 若是沒有定義read操做, 則調用do_sync_read—其利用異步aio_read來完成同步的read操做。 */ if (file->f_op->read) ret = file->f_op->read(file, buf, count, pos); else ret = do_sync_read(file, buf, count, pos); if (ret > 0) { /* 讀取了必定的字節數, 進行通知操做 */ fsnotify_access(file); /* 增長進程讀取字節的統計計數 */ add_rchar(current, ret); } /* 增長進程系統調用的統計計數 */ inc_syscr(current); } return ret;}
上面的代碼爲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,代碼以下所示:
    
    
    
    
int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int noblock, int flags, int *addr_len) …… ulen = skb->len - sizeof(struct udphdr); copied = len; if (copied > ulen) copied = ulen; ……
當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中,有以下代碼:
    
    
    
    
mutex_lock(&inode->i_mutex);//加鎖 blk_start_plug(&plug); ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);//發現文件是追加打開,直接從inode讀取最新文件大小做爲偏移量mutex_unlock(&inode->i_mutex); //解鎖
這裏有一個關鍵的語句,就是使用mutex_lock對該文件對應的inode進行保護,而後調用__generic_file_aio_write->generic_write_check。其部分代碼以下:
    
    
    
    
if (file->f_flags & O_APPEND) *pos = i_size_read(inode);
上面的代碼中,若是發現文件是以追加方式打開的,則將從inode中讀取到的最新文件大小做爲偏移量,而後經過__generic_file_aio_write再進行寫操做,這樣就能保證寫操做是在文件末尾追加的。

----------------------------------文件描述符的複製----------------------------
    
    
    
    
int dup(int oldfd);int dup2(int oldfd, int newfd);
·dup會使用一個最小的未用文件描述符做爲複製後的文件描述符。
·dup2是使用用戶指定的文件描述符newfd來複制oldfd的。若是newfd已是打開的文件描述符,Linux會先關閉newfd,而後再複製oldfd。
dup的實現
      
      
      
      
SYSCALL_DEFINE1(dup, unsigned int, fildes){ int ret = -EBADF; /* 必須先獲得文件管理結構file, 同時也是對描述符fildes的檢查 */ struct file *file = fget_raw(fildes); if (file) { /* 獲得一個未使用的文件描述符 */ ret = get_unused_fd(); if (ret >= 0) { /* 將文件描述符與file指針關聯起來 */ fd_install(ret, file); } else fput(file); } return ret;}
在dup中調用get_unused_fd,只是獲得一個未用的文件描述符,那麼如何實如今dup接口中使用最小的未用文件描述符呢?這就須要回顧1.4.2節中總結過的Linux文件描述符的選擇策略了。
Linux老是嘗試給用戶最小的未用文件描述符,因此get_unused_fd獲得的文件描述符始終是最小的可用文件描述符。
查看dup代碼實現的第11行
      
      
      
      
void fd_install(unsigned int fd, struct file *file){ struct files_struct *files = current->files; struct fdtable *fdt; /* 對文件表進行保護 */ spin_lock(&files->file_lock); /* 獲得文件表 */ fdt = files_fdtable(files); BUG_ON(fdt->fd[fd] != NULL); /* 讓文件表中fd對應的指針等於該文件關聯結構file */ rcu_assign_pointer(fdt->fd[fd], file); spin_unlock(&files->file_lock);}
在fd_install中,fd與file的關聯是利用fd來做爲指針數組的索引的,從而讓對應的指針指向file。對於dup來講,這意味着數組中兩個指針都指向了同一個file。而file是進程中真正的管理文件的結構,文件偏移等信息都是保存在file中的。這就意味着,當使用oldfd進行讀寫操做時,不管是oldfd仍是newfd的文件偏移都會發生變化。
---------------------看一下dup2的實現-------------------
      
      
      
      
SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd){ /* 若是oldfd與newfd相等, 這是一種特殊的狀況 */ if (unlikely(newfd == oldfd)) { /* corner case */ struct files_struct *files = current->files; int retval = oldfd; /* 檢查oldfd的合法性, 若是是合法的fd, 則直接返回oldfd的值; 若是是不合法的, 則返回EBADF */ rcu_read_lock(); if (!fcheck_files(files, oldfd)) retval = -EBADF; rcu_read_unlock(); return retval;    } /* 若是oldfd與newfd不一樣, 則利用sys_dup3來實現dup2 */
return sys_dup3(oldfd, newfd, 0);}


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

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) struct kstat stat; int error; /* vfs_stat用於讀取文件元數據至stat */ error = vfs_stat(filename, &stat); if (error) return error; /* 這裏僅是從內核的元數據結構stat複製到用戶層的數據結構statbuf中 */ return cp_old_stat(&stat, statbuf);}
第5行,vfs_stat是關鍵。進入vfs_stat->vfs_fstatat->vfs_getattr,代碼以下:
    
    
    
    
int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat){ struct inode *inode = dentry->d_inode; int retval; /* 對獲取inode屬性操做進行安全性檢查 */ retval = security_inode_getattr(mnt, dentry); if (retval) return retval; /* 若是該文件系統定義了這個inode的自定義操做函數, 就執行它 */ if (inode->i_op->getattr) return inode->i_op->getattr(mnt, dentry, stat); /* 若是文件系統沒有定義inode的操做函數, 則執行通用的函數 */ generic_fillattr(inode, stat); return 0;}
不失通常性,也能夠經過查看第13行的generic_fillattr來進一步瞭解,代碼以下:
    
    
    
    
void generic_fillattr(struct inode *inode, struct kstat *stat){ stat->dev = inode->i_sb->s_dev; stat->ino = inode->i_ino; stat->mode = inode->i_mode; stat->nlink = inode->i_nlink; stat->uid = inode->i_uid; stat->gid = inode->i_gid; stat->rdev = inode->i_rdev; stat->size = i_size_read(inode); stat->atime = inode->i_atime; stat->mtime = inode->i_mtime; stat->ctime = inode->i_ctime; stat->blksize = (1 << inode->i_blkbits); stat->blocks = inode->i_blocks;}
從這裏能夠看出,全部的文件元數據均保存在inode中,而inode是Linux也是全部類Unix文件系統中的一個概念。這樣的文件系統通常將存儲區域分爲兩類,一類是保存文件對象的元信息數據,即inode表;另外一類是真正保存文件數據內容的塊,全部inode徹底由文件系統來維護。可是Linux也能夠掛載非類Unix的文件系統,這些文件系統自己沒有inode的概念,怎麼辦?Linux爲了讓VFS有統一的處理流程和方法,就必需要求那些沒有inode概念的文件系統,根據本身系統的特色——如何維護文件元數據,生成「虛擬的」inode以供Linux內核使用。














相關文章
相關標籤/搜索