linux 內核源碼:文件系統——虛擬文件系統 VFS

linux 的設計理念:萬物皆文件!換句話說:所有的設備,包括但不限於磁盤、串口、網卡、pipe 管道、打印機等統一看成是文件。對於用戶來說,所有操作都是通過 open、read、write、ioctl、close 等接口操作的,確實很方便;但是對於 linux,底層明明是不同的硬件設備,這些設備怎麼才能統一被上述接口識別和適配了?識別和適配這層接口的功能就是虛擬文件系統,簡稱 VFS,整體架構圖如下:

1、file_dev.c 定義了文件的讀寫函數;

(1)爲了更好地管理文件,linux 同樣也定義了 file 結構體:

struct file {
    unsigned short f_mode;/*FMODE_READ或FMODE_WRITE,標識標識文件是否可讀或可寫*/
    unsigned short f_flags;/*O_RDONLY/O_NONBLOCK/O_SYNC:O_NONBLOCK 打開文件是否阻塞*/
    unsigned short f_count;/*文件被多少進程引用?*/
    struct m_inode * f_inode;/*文件對應的inode節點,裏面存了很多文件的元信息;文件存放的描述:磁盤block->inode->struct file->file_table->file descriptor*/
    off_t f_pos;/*當前文件的讀寫位置偏移,lseek修改的*/
};

(2)這個 file_read 函數已經很接近用戶使用的 read 函數了,僅僅是多了第一個 inode 參數:

//// 文件讀函數 - 根據i節點和文件結構,讀取文件中數據。
// 由i節點我們可以知道設備號,由filp結構可以知道文件中當前讀寫指針位置。buf指定
// 用戶空間中緩衝區位置,count是需要讀取字節數。返回值是實際讀取的字節數,或出錯號(小於0).
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    int left,chars,nr;
    struct buffer_head * bh;

    // 首先判斷參數的有效性。若需要讀取的字節數count小於等於0,則返回0.若還需要讀
    // 取的字節數不等於0,就循環執行下面操作,直到數據全部讀出或遇到問題。在讀循環
    // 操作過程中,我們根據i節點和文件表結構信息,並利用bmap()得到包含文件當前讀寫
    // 位置的數據塊在設備上對應的邏輯塊號nr。若nr不爲0,則從i節點指定的設備上讀取該
    // 邏輯塊。如果讀操作是吧則退出循環。若nr爲0,表示指定的數據塊不存在,置緩衝塊
    // 指針爲NULL。(filp->f_pos)/BLOCK_SIZE用於計算出文件當前指針所在的數據塊號。
    if ((left=count)<=0)
        return 0;
    while (left) {
        if ((nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE))) {/*把f_pos位置之前數據所在磁盤的block號返回*/
            if (!(bh=bread(inode->i_dev,nr)))/*從對應的磁盤block號讀取數據,也就是從磁盤讀取(filp->f_pos)/BLOCK_SIZE塊的數據到內存的緩存區*/
                break;
        } else
            bh = NULL;
        // 接着我們計算文件讀寫指針在數據塊中的偏移值nr,則在該數據塊中我們希望讀取的
        // 字節數爲(BLOCK_SIZE-nr)。然後和現在還需讀取的字節數left做比較。其中小值
        // 即爲本次操作需讀取的字節數chars。如果(BLOCK_SIZE-nr) > left,則說明該塊
        // 是需要讀取的最後一塊數據。反之還需要讀取下一塊數據。之後調整讀寫文件指針。
        // 指針前移此次將讀取的字節數chars,剩餘字節計數left相應減去chars。
        nr = filp->f_pos % BLOCK_SIZE;/*f_pos在block內的offset*/
        chars = MIN( BLOCK_SIZE-nr , left );/*BLOCK_SIZE-nr:當前塊的剩餘區域;left:文件要讀取的剩餘size; 注意:要讀取的count不一定等於文件當前大小f_pos*/
        filp->f_pos += chars;
        left -= chars;
        // 若上面從設備上讀到了數據,則將p指向緩衝塊中開始讀取數據的位置,並且複製chars
        // 字節到用戶緩衝區buf中。否則往用戶緩衝區中填入chars個0值字節。
        if (bh) {
            char * p = nr + bh->b_data;
            while (chars-->0)
                put_fs_byte(*(p++),buf++);/*從內核緩存區copy到用戶指定的buf*/
            brelse(bh);
        } else {
            while (chars-->0)
                put_fs_byte(0,buf++);
        }
    }
    // 修改該i節點的訪問時間爲當前時間。返回讀取的字節數,若讀取字節數爲0,則返回
    // 出錯號。CURRENT_TIME是定義在include/linux/sched.h中的宏,用於計算UNIX時間。
    // 即從1970年1月1日0時0分0秒開始,到當前的時間,單位是秒。
    inode->i_atime = CURRENT_TIME;
    return (count-left)?(count-left):-ERROR;
}

與上面對應的是 file_write,把用戶指定的數據寫入緩存區(注意:此時並未調用 sys_sync 函數將數據從緩存區寫入磁盤):

//// 文件寫函數 - 根據i節點和文件結構信息,將用戶數據寫入文件中。
// 由i節點我們可以知道設備號,而由file結構可以知道文件中當前讀寫指針位置。buf指定
// 用戶態緩衝區的位置,count爲需要寫入的字節數。返回值是實際寫入的字節數,或出錯號。
int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    off_t pos;
    int block,c;
    struct buffer_head * bh;
    char * p;
    int i=0;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 */
    // 首先確定數據寫入文件的位置。如果是要向文件後添加數據,則將文件讀寫指針移到
    // 文件尾部。否則就將在文件當前讀寫指針處寫入。
    if (filp->f_flags & O_APPEND)
        pos = inode->i_size;
    else
        pos = filp->f_pos;
    // 然後在已寫入字節數i(剛開始爲0)小於指定寫入字節數count時,循環執行以下操作。
    // 在循環操作過程中,我們先取文件數據塊號(pos/BLOCK_SIZE)在設備上對應的邏輯
    // 塊號block。如果對應的邏輯塊不存在就創建一塊。如果得到的邏輯塊號=0,則表示
    // 創建失敗,於是退出循環。否則我們根據該邏輯塊號讀取設備上的相應邏輯塊,若出
    // 錯也退出循環。
    while (i<count) {
        if (!(block = create_block(inode,pos/BLOCK_SIZE)))
            break;
        if (!(bh=bread(inode->i_dev,block)))
            break;
        // 此時緩衝塊指針bh正指向剛讀入的文件數據庫。現在再求出文件當前讀寫指針在該
        // 數據塊中的偏移值c,並將指針p指向緩衝塊中開始寫入數據的位置,並置該緩衝塊已
        // 修改標誌。對於塊中當前指針,從開始讀寫位置到塊末共可寫入c=(BLOCK_SIZE - c)
        // 個字節。若c大於剩餘還需寫入的字節數(count - i),則此次只需再寫入c = (count - i)
        // 個字節即可。
        c = pos % BLOCK_SIZE;
        p = c + bh->b_data;
        bh->b_dirt = 1;
        c = BLOCK_SIZE-c;
        if (c > count-i) c = count-i;
        // 在寫入數據之前,我們先預先設置好下一次循環操作要讀寫文件中的位置。因此我們
        // 把pos指針前移此次需寫入的字節數。如果此時pos位置值超過了文件當前長度,則
        // 修改i節點中文件長度字段,並置i節點已修改標誌。然後把此次要寫入的字節數c累加到
        // 已寫入字節計數值i中,供循環判斷使用。接着從用戶緩衝區buf中複製c個字節到告訴緩
        // 衝塊中p指向的開始位置處。複製完後就釋放該緩衝塊。
        pos += c;
        if (pos > inode->i_size) {
            inode->i_size = pos;
            inode->i_dirt = 1;
        }
        i += c;
        while (c-->0)
            *(p++) = get_fs_byte(buf++);/*注意:此時數據只是寫入了緩存區,並未立即寫入磁盤*/
        brelse(bh);
    }
    // 當數據已全部寫入文件或者在寫操作工程中發生問題時就會退出循環。此時我們更改文件修改
    // 時間爲當前時間,並調整文件讀寫指針。如果此次操作不是在文件尾部添加數據,則把文件
    // 讀寫指針調整到當前讀寫位置pos處,並更改文件i節點的修改時間爲當前時間。最後返回寫入
    // 的字節數,若寫入字節數爲0,則返回出錯號-1.
    inode->i_mtime = CURRENT_TIME;
    if (!(filp->f_flags & O_APPEND)) {
        filp->f_pos = pos;
        inode->i_ctime = CURRENT_TIME;
    }
    return (i?i:-1);
}

2、和讀寫文件類似,linux 0.11 版本也提供了讀寫塊設備的 api,在 block_dev.c 文件中;代碼結構和讀寫文件沒本質區別:

注意:這裏求 block 號、block 內部偏移的代碼:int block = *pos >> BLOCK_SIZE_BITS;  int offset = *pos & (BLOCK_SIZE-1);  搞逆向時遇到這類代碼,需要第一時間知道代碼背後的邏輯意義!

//// 數據塊寫函數 - 向指定設備從給定偏移出寫入制定長度數據。
// 參數:dev - 設備號;pos - 設備文件中偏移量指針;buf - 用戶空間中緩衝區地址;
// count - 要傳送的字節數
// 返回已寫入字節數。若沒有寫入任何字節或出錯,則返回出錯號。
// 對於內核來說,寫操作是向高速緩衝區中寫入數據。什麼時候數據最終寫入設備是由高
// 速緩衝管理程序決定並處理的。另外,因爲塊設備是以塊爲單位進行讀寫,因此對於寫
// 開始位置不處於塊起始處時,需要先將開始字節所在的整個塊讀出,然後將需要寫的數
// 據從寫開始處填寫滿該塊,再將完整的一塊數據寫盤(即交由高速緩衝程序去處理)。
int block_write(int dev, long * pos, char * buf, int count)
{
    // 首先由文件中位置pos換算成開始讀寫盤快的塊序號block,並求出需寫第1字節在該
    // 塊中的偏移位置offset.
    int block = *pos >> BLOCK_SIZE_BITS;        // pos所在文件數據塊號,相當於除以1024
    int offset = *pos & (BLOCK_SIZE-1);         // pos在數據塊中偏移值,相當於模1024
    int chars;
    int written = 0;
    struct buffer_head * bh;
    register char * p;                          // 局部寄存器變量,被存放在寄存器中

    // 然後針對要寫入的字節數count,循環執行以下操作,知道數據全部寫入。在循環執行
    // 過程中,先計算在當前處理的數據塊中可寫入的字節數。如果寫入的字節數填不滿一塊,
    // 那麼就只需寫count字節。如果正要寫1塊數據內容,則直接申請1塊高速緩衝塊,並把
    // 用戶數據放入即可。否則就需要讀入將被寫入部分數據的數據塊,並預讀下兩塊數據。
    // 然後將塊號遞增1,爲下次操作做好準備。如果緩衝塊操作失敗,則返回已寫字節數,
    // 如果沒有寫入任何字節,則返回出錯號(負數).
    while (count>0) {
        chars = BLOCK_SIZE - offset;
        if (chars > count)
            chars=count;
        if (chars == BLOCK_SIZE)
            bh = getblk(dev,block);
        else
            bh = breada(dev,block,block+1,block+2,-1);
        block++;
        if (!bh)
            return written?written:-EIO;
        // 接着先把指針p指向讀出數據的緩衝塊中開始寫入數據的位置處。若最後一次循環寫入
        // 的數據不足一塊,則需從塊開始處填寫(修改)所需的字節,因此這裏需預先設置offset
        // 爲零。此後將文件中偏移指針pos前移此次將要寫的字節數chars,並累加這些要寫的
        // 字節數到統計值written中,再把還需要寫的計數值count減去此次要寫的字節數chars.
        // 然後我們從用戶緩衝區複製chars個字節到p指向的高速緩衝中開始寫入的位置處。複製
        // 完後就設置該緩衝區塊已修改標誌,並釋放該緩衝區(也即該緩衝區引用計數遞減1)p = offset + bh->b_data;
        offset = 0;
        *pos += chars;
        written += chars;           // 累計寫入字節數
        count -= chars;
        while (chars-->0)
            *(p++) = get_fs_byte(buf++);
        bh->b_dirt = 1;
        brelse(bh);
    }
    return written;
}

//// 數據塊讀函數 - 從指定設備和位置處讀入指定長度數據到用戶緩衝區中。
// 參數:dev - 設備號;pos - 設備文件中偏移量指針;buf - 用戶空間緩衝區地址;
// count - 要傳送的字節數。
// 返回已讀入字節數。若沒有讀入任何字節或出錯,則返回出錯號。
int block_read(int dev, unsigned long * pos, char * buf, int count)
{
    // 首先由文件中位置pos換算成開始讀寫盤塊的塊序號block,並求出需讀第1個字節在塊中
    // 的偏移位置offset.
    int block = *pos >> BLOCK_SIZE_BITS;
    int offset = *pos & (BLOCK_SIZE-1);
    int chars;
    int read = 0;
    struct buffer_head * bh;
    register char * p;

    // 然後針對要讀入的字節數count,循環執行以下操作,直到數據全部讀入。在循環執行
    // 過程中,先計算在當前處理的數據塊中需讀入的字節數。如果需要讀入的字節數還不滿
    // 一塊,那麼就只需要讀count字節。然後調用讀塊函數breada()讀如需要的數據塊,並
    // 預讀下兩塊數據,如果讀操作出錯,則返回已讀字節數,如果沒有讀入任何字節,則
    // 返回出錯號。然後將塊號遞增1.爲下次操作做好準備。如果緩衝塊操作失敗,則返回已
    // 寫字節數,如果沒有讀入任何字節,則返回出錯號(負數)。
    while (count>0) {
        chars = BLOCK_SIZE-offset;
        if (chars > count)
            chars = count;
        if (!(bh = breada(dev,block,block+1,block+2,-1)))
            return read?read:-EIO;
        block++;
        // 接着先把指針p指向讀出盤塊的緩衝中開始讀入數據的位置處。若最後一次循環讀
        // 操作的數據不足一塊,則需從塊起始處讀取所需字節,因此這裏需預先設置offset
        // 爲零。此後將文件中偏移指針pos前移此次將要讀的字節數chars,並且累加這些要讀
        // 的字節數到統計值read中。再把還需要讀的計數值count減去此次要讀的字節數chars。
        // 然後我們從高速緩衝塊中p指向的開始讀的位置處複製chars個字節到用戶緩衝區中,
        // 同時把用戶緩衝區指針前移。本次複製完後就釋放該緩衝塊。
        p = offset + bh->b_data;
        offset = 0;
        *pos += chars;
        read += chars;                      // 讀入累計字節數
        count -= chars;
        while (chars-->0)
            put_fs_byte(*(p++),buf++);
        brelse(bh);
    }
    return read;
}

3、除了常見的磁盤等塊設備,還有串口這類的字符型設備,讀寫接口定義在了 char_dev.c 文件中了:

extern int tty_read(unsigned minor,char * buf,int count);
extern int tty_write(unsigned minor,char * buf,int count);

// 定義字符設備讀寫函數指針類型
typedef int (*crw_ptr)(int rw,unsigned minor,char * buf,int count,off_t * pos);

//// 串口終端讀寫操作函數。
// 參數:rw - 讀寫命令;minor - 終端子設備號;buf - 緩衝區;count - 讀寫字節數
// pos - 讀寫操作當前指針,對於中斷操作,該指針無用
// 返回:實際讀寫的字節數。若失敗則返回出錯碼。
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
{
    return ((rw==READ)?tty_read(minor,buf,count):
        tty_write(minor,buf,count));
}

//// 終端讀寫操作函數。
// 同rw_ttyx,只是增加了對進程是否有控制終端的檢測。
static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)
{
    // 若進程沒有控制終端,則返回出錯號。否則調用終端讀寫函數rw_ttyx(),
    // 並返回實際讀寫字節數。
    if (current->tty<0)
        return -EPERM;
    return rw_ttyx(rw,current->tty,buf,count,pos);
}

// 內存數據讀寫,早期版本暫時沒實現
static int rw_ram(int rw,char * buf, int count, off_t *pos)
{
    return -EIO;
}

// 物理內存數據讀寫,早期版本暫時沒實現
static int rw_mem(int rw,char * buf, int count, off_t * pos)
{
    return -EIO;
}

// 內核虛擬內存數據讀寫,早期版本暫時沒實現
static int rw_kmem(int rw,char * buf, int count, off_t * pos)
{
    return -EIO;
}

//// 端口讀寫操作函數
// 參數:rw - 讀寫命令;buf - 緩衝區; cout - 讀寫字節數; pos - 端口地址。
// 返回:實際讀寫的字節數。
static int rw_port(int rw,char * buf, int count, off_t * pos)
{
    int i=*pos;

    // 對於所要求讀寫的字節數,並且端口地址小於64K時,循環執行單個字節的
    // 讀寫操作。若是讀命令,則從端口i中讀取一個字節內容並放到用戶緩衝區中。
    // 若是寫命令,則從用戶數據緩衝區中取一字節輸出到端口i。
    while (count-->0 && i<65536) {
        if (rw==READ)
            put_fs_byte(inb(i),buf++);
        else
            outb(get_fs_byte(buf++),i);
        i++;
    }
    // 然後計算讀/寫字節數,調整相應讀寫指針,並返回讀/寫的字節數。
    i -= *pos;
    *pos += i;
    return i;
}

//// 內存讀寫操作函數
static int rw_memory(int rw, unsigned minor, char * buf, int count, off_t * pos)
{
    // 根據內存設備子設備號,分別調用不同的內存讀寫函數。
    switch(minor) {
        case 0:
            return rw_ram(rw,buf,count,pos);
        case 1:
            return rw_mem(rw,buf,count,pos);
        case 2:
            return rw_kmem(rw,buf,count,pos);
        case 3:
            return (rw==READ)?0:count;    /* rw_null */
        case 4:
            return rw_port(rw,buf,count,pos);
        default:
            return -EIO;
    }
}

這裏的編碼方式非常巧妙:先定義一個數組,數組的每個元素都是函數入口;然後在 rw_char 函數中,根據 major(dev) 找到對應所需的函數入口,然後通過函數指針的方式調用:

// 字符設備讀寫函數指針表 file_operations
static crw_ptr crw_table[]={
    NULL,        /* nodev */
    rw_memory,    /* /dev/mem etc */
    NULL,        /* /dev/fd */
    NULL,        /* /dev/hd */
    rw_ttyx,    /* /dev/ttyx */
    rw_tty,        /* /dev/tty */
    NULL,        /* /dev/lp */
    NULL};        /* unnamed pipes */

// 字符設備讀寫操作函數
// 參數:rw - 讀寫命令;dev - 設備號;buf - 緩衝區; count - 讀寫字節數;pos - 讀寫指針。
// 返回:實際讀/寫字節數
int rw_char(int rw,int dev, char * buf, int count, off_t * pos)
{
    crw_ptr call_addr;

    // 如果設備號超出系統設備數,則返回出錯碼。如果該設備沒有對應的讀/寫函數,也
    // 返回出錯碼。否則調用對應設備的讀寫操作函數,並返回實際讀/寫的字節數。
    if (MAJOR(dev)>=NRDEVS)
        return -ENODEV;
    if (!(call_addr=crw_table[MAJOR(dev)]))
        return -ENODEV;
    return call_addr(rw,MINOR(dev),buf,count,pos);
}

4、前面說了,不同硬件設備有不同的讀寫接口,但是在 VFS 這一層確統一起來了,linux 0.11 版本是怎麼做的了?在 rea_write.c 函數中做了封裝:

(1)這裏先是導入不同類型的讀寫函數:

// 字符設備讀寫函數。
extern int rw_char(int rw,int dev, char * buf, int count, off_t * pos);
// 讀管道操作函數。
extern int read_pipe(struct m_inode * inode, char * buf, int count);
// 寫管道操作函數
extern int write_pipe(struct m_inode * inode, char * buf, int count);
// 塊設備讀操作函數
extern int block_read(int dev, off_t * pos, char * buf, int count);
// 塊設備寫操作函數
extern int block_write(int dev, off_t * pos, char * buf, int count);
// 讀文件操作函數
extern int file_read(struct m_inode * inode, struct file * filp,
        char * buf, int count);
// 寫文件操作函數
extern int file_write(struct m_inode * inode, struct file * filp,
        char * buf, int count);

(2)通過 sys_read 和 sys_write 徹底封裝了上述導入的不同類型設備的讀寫函數:這兩個函數內部都會通過 S_ISCHR(inode->i_mode)、S_ISBLK(inode->i_mode)、S_ISDIR(inode->i_mode) 、S_ISREG(inode->i_mode) 等方式判斷,根據不同的設備類型調用不同的讀寫函數!

//// 讀文件系統調用
// 參數fd是文件句柄,buf是緩衝區,count是預讀字節數
int sys_read(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    // 函數首先對參數有效性進行判斷。如果文件句柄值大於程序最多打開文件數NR_OPEN,
    // 或者需要讀取的字節計數值小於0,或者該句柄的文件結構指針爲空,則返回出錯碼並
    // 退出。若需讀取的字節數count等於0,則返回0退出。
    if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
        return -EINVAL;
    if (!count)
        return 0;
    // 然後驗證存放數據的緩衝區內存限制。並取文件的i節點。用於根據該i節點的屬性,分
    // 別調用相應的讀操作函數。若是管道文件,並且是讀管道文件模式,則進行讀管道操作,
    // 若成功則返回讀取的字節數,否則返回出錯碼,退出。如果是字符型文件,則進行讀
    // 字符設備操作,並返回讀取的字符數。如果是塊設備文件,則執行塊設備讀操作,並
    // 返回讀取的字節數。
    verify_area(buf,count);
    inode = file->f_inode;
    if (inode->i_pipe)
        return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode->i_mode))
        return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
    if (S_ISBLK(inode->i_mode))
        return block_read(inode->i_zone[0],&file->f_pos,buf,count);
    // 如果是目錄文件或者是常規文件,則首先驗證讀取字節數count的有效性並進行調整(若
    // 讀去字節數加上文件當前讀寫指針值大於文件長度,則重新設置讀取字節數爲文件長度
    // -當前讀寫指針值,若讀取數等於0,則返回0退出),然後執行文件讀操作,返回讀取的
    // 字節數並退出。
    if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
        if (count+file->f_pos > inode->i_size)
            count = inode->i_size - file->f_pos;
        if (count<=0)
            return 0;
        return file_read(inode,file,buf,count);
    }
    // 執行到這裏,說明我們無法判斷文件的屬性。則打印節點文件屬性,並返回出錯碼退出。
    printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
    return -EINVAL;
}

//// 寫文件系統調用
// 參數fd是文件句柄,buf是用戶緩衝區,count是欲寫字節數。
int sys_write(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    // 同樣地,我們首先判斷函數參數的有效性。若果進程文件句柄值大於程序最多打開文件數
    // NR_OPEN,或者需要寫入的字節數小於0,或者該句柄的文件結構指針爲空,則返回出錯碼
    // 並退出。如果需讀取字節數count等於0,則返回0退出。
    if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
        return -EINVAL;
    if (!count)
        return 0;
    // 然後驗證存放數據的緩衝區內存限制。並取文件的i節點。用於根據該i節點屬性,分別調
    // 用相應的讀操作函數。若是管道文件,並且是寫管道文件模式,則進行寫管道操作,若成
    // 功則返回寫入的字節數,否則返回出錯碼退出。如果是字符設備文件,則進行寫字符設備
    // 操作,返回寫入的字符數退出。如果是塊設備文件,則進行塊設備寫操作,並返回寫入的
    // 字節數退出。若是常規文件,則執行文件寫操作,並返回寫入的字節數,退出。
    inode=file->f_inode;
    if (inode->i_pipe)
        return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode->i_mode))
        return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
    if (S_ISBLK(inode->i_mode))
        return block_write(inode->i_zone[0],&file->f_pos,buf,count);
    if (S_ISREG(inode->i_mode))
        return file_write(inode,file,buf,count);
    // 執行到這裏,說明我們無法判斷文件的屬性。則打印節點文件屬性,並返回出錯碼退出。
    printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
    return -EINVAL;
}

(3)open.c 的 sys_open 函數,返回文件句柄(本質就是 file 結構體的指針):

//// 打開(或創建)文件系統調用。
// 參數filename是文件名,flag是打開文件標誌,它可取值:O_RDONLY(只讀)、O_WRONLY
// (只寫)或O_RDWR(讀寫),以及O_EXCL(被創建文件必須不存在)、O_APPEND(在文件
// 尾添加數據)等其他一些標誌的組合。如果本調用創建了一個新文件,則mode就用於指
// 定文件的許可屬性。這些屬性有S_IRWXU(文件宿主具有讀、寫和執行權限)、S_IRUSR
// (用戶具有讀文件權限)、S_IRWXG(組成員具有讀、寫和執行權限)等等。對於新創
// 建的文件,這些屬性只應用與將來對文件的訪問,創建了只讀文件的打開調用也將返回
// 一個可讀寫的文件句柄。如果調用操作成功,則返回文件句柄(文件描述符),否則返回出錯碼。
int sys_open(const char * filename,int flag,int mode)
{
    struct m_inode * inode;
    struct file * f;
    int i,fd;

    // 首先對參數進行處理。將用戶設置的文件模式和屏蔽碼相與,產生許可的文件模式。
    // 爲了爲打開文件建立一個文件句柄,需要搜索進程結構中文件結構指針數組,以查
    // 找一個空閒項。空閒項的索引號fd即是文件句柄值。若已經沒有空閒項,則返回出錯碼。
    mode &= 0777 & ~current->umask;
    for(fd=0 ; fd<NR_OPEN ; fd++)
        if (!current->filp[fd])
            break;
    if (fd>=NR_OPEN)
        return -EINVAL;
    // 然後我們設置當前進程的執行時關閉文件句柄(close_on_exec)位圖,復位對應的
    // bit位。close_on_exec是一個進程所有文件句柄的bit標誌。每個bit位代表一個打
    // 開着的文件描述符,用於確定在調用系統調用execve()時需要關閉的文件句柄。當
    // 程序使用fork()函數創建了一個子進程時,通常會在該子進程中調用execve()函數
    // 加載執行另一個新程序。此時子進程中開始執行新程序。若一個文件句柄在close_on_exec
    // 中的對應bit位被置位,那麼在執行execve()時應對應文件句柄將被關閉,否則該
    // 文件句柄將始終處於打開狀態。當打開一個文件時,默認情況下文件句柄在子進程
    // 中也處於打開狀態。因此這裏要復位對應bit位。
    current->close_on_exec &= ~(1<<fd);
    // 然後爲打開文件在文件表中尋找一個空閒結構項。我們令f指向文件表數組開始處。
    // 搜索空閒文件結構項(引用計數爲0的項),若已經沒有空閒文件表結構項,則返回
    // 出錯碼。
    f=0+file_table;
    for (i=0 ; i<NR_FILE ; i++,f++)
        if (!f->f_count) break;
    if (i>=NR_FILE)
        return -EINVAL;
    // 此時我們讓進程對應文件句柄fd的文件結構指針指向搜索到的文件結構,並令文件
    // 引用計數遞增1。然後調用函數open_namei()執行打開操作,若返回值小於0,則說
    // 明出錯,於是釋放剛申請到的文件結構,返回出錯碼i。若文件打開操作成功,則
    // inode是已打開文件的i節點指針。
    (current->filp[fd]=f)->f_count++;
    if ((i=open_namei(filename,flag,mode,&inode))<0) {
        current->filp[fd]=NULL;
        f->f_count=0;
        return i;
    }
    // 根據已打開文件的i節點的屬性字段,我們可以知道文件的具體類型。對於不同類
    // 型的文件,我們需要操作一些特別的處理。如果打開的是字符設備文件,那麼對於
    // 主設備號是4的字符文件(例如/dev/tty0),如果當前進程是組首領並且當前進程的
    // tty字段小於0(沒有終端),則設置當前進程的tty號爲該i節點的子設備號,並設置
    // 當前進程tty對應的tty表項的父進程組號等於當前進程的進程組號。表示爲該進程
    // 組(會話期)分配控制終端。對於主設備號是5的字符文件(/dev/tty),若當前進
    // 程沒有tty,則說明出錯,於是放回i節點和申請到的文件結構,返回出錯碼(無許可)。
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
    if (S_ISCHR(inode->i_mode)) {
        if (MAJOR(inode->i_zone[0])==4) {
            if (current->leader && current->tty<0) {
                current->tty = MINOR(inode->i_zone[0]);
                tty_table[current->tty].pgrp = current->pgrp;
            }
        } else if (MAJOR(inode->i_zone[0])==5)
            if (current->tty<0) {
                iput(inode);
                current->filp[fd]=NULL;
                f->f_count=0;
                return -EPERM;
            }
    }
/* Likewise with block-devices: check for floppy_change */
    // 如果打開的是塊設備文件,則檢查盤片是否更換過。若更換過則需要讓高速緩衝區
    // 中該設備的所有緩衝塊失敗。
    if (S_ISBLK(inode->i_mode))
        check_disk_change(inode->i_zone[0]);
    // 現在我們初始化打開文件的文件結構。設置文件結構屬性和標誌,置句柄引用計數
    // 爲1,並設置i節點字段爲打開文件的i節點,初始化文件讀寫指針爲0.最後返回文
    // 件句柄號。
    f->f_mode = inode->i_mode;
    f->f_flags = flag;
    f->f_count = 1;
    f->f_inode = inode;
    f->f_pos = 0;
    return (fd);
}

爲了方便直觀理解,圖示如下:

爲什麼 linux 系統的理念是 “萬物皆文件” 了?我們現在使用的這套軟硬件系統最原始的名稱叫 information technology,核心目的是存儲和讀取數據!IT 高速發展到現在,最核心的目的還是存儲和讀取數據,這一點幾十年都沒變!爲了方便上層 app 讀寫數據,linux 抽象出了 VFS:上層 app 所有的操作都統一了接口名稱,不同的設備實現這些接口就行了!對於 app 來說,只認接口就足夠了!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_DHU8HdU2dLWpf3Jciz9iA