linux源碼解讀(五):文件系統——文件和目錄的操作


  對於普通用戶,平時使用操作系統是肯定涉及到創建、更改、刪除文件(比如mkdir、rmdir、rm、chmod、ln等);有些文件是高權限用戶建的,低權限用戶甚至都打不開,也刪不掉;為了方便管理不同業務類型的文件,還需要在不同的邏輯分區建文件夾,分門別類各種文件;linux下用ls -l命令還可以查看文件的詳細屬性,這一系列的功能構師怎么實現的了?功能都在fs/namei.c文件中

  1、(1)權限檢查,核心就是依靠inode結構體中的i_mode成員變量了!這個變量是unsigned short類型,一共2byte=16bit長;linux用低9位表示當前用戶權限、用戶組權限、其他用戶權限,用戶平時用ls -l查到的權限就是靠這個字段得到的!舉個例子:rwx------表示當前用戶有讀寫執行權限,用戶組沒有任何權限,其他用戶也沒有任何權限,所有權限表示剛好使用9bit;

  •   i_mode節點右移3位,與上0007后得到用戶組權限
  •        i_mode節點右移6位,與上0007后得到當前用戶權限
  •        chmod改的就是i_mode這個字段
/*
 *    permission()
 *
 * is used to check for read/write/execute permissions on a file.
 * I don't know if we should look at just the euid or both euid and
 * uid, but that should be easily changed.
 */
//// 檢測文件訪問權限
// 參數:inode - 文件的i節點指針;mask - 訪問屬性屏蔽碼。
// 返回:訪問許可返回1,否則返回0.
static int permission(struct m_inode * inode,int mask)
{
    int mode = inode->i_mode;

/* special case: not even root can read/write a deleted file */
    // 如果i節點有對應的設備,但該i節點的連接計數值等於0,表示該文件
    // 已被刪除,則返回。否則,如果進程的有效用戶ID(euid)與i節點的
    // 用戶id相同,則取文件宿主的訪問權限。否則如果與組id相同,
    // 則取組用戶的訪問權限。
    if (inode->i_dev && !inode->i_nlinks)
        return 0;
    else if (current->euid==inode->i_uid)
        mode >>= 6;
    else if (current->egid==inode->i_gid)
        mode >>= 3;
    /* &0007:取最后3位
       &mask:取傳入參數的位
    */
    if (((mode & mask & 0007) == mask) 
            || suser())/*要么是管理員,是超級用戶*/
        return 1;
    return 0;
}

   (2)因為是涉及到設備名、文件名、目錄路徑的比對,自然少不了字符串相關的操作。平時在3環做應用開發,碼農都習慣於使用操作系統提供的庫函數,比如strcmp、strcat等,但是現在還在內核,哪來的庫函數直接調用了,只能自己動手重新寫字符串的比較函數,如下:

*
 * ok, we cannot use strncmp, as the name is not in our data space.
 * Thus we'll have to use match. No big problem. Match also makes
 * some sanity tests.
 *
 * NOTE! unlike strncmp, match returns 1 for success, 0 for failure.
 */
//// 指定長度字符串比較函數
// 參數:len - 比較的字符串長度;name - 文件名指針;de - 目錄項結構
// 返回:相同返回1,不同返回0. 
// 下面函數中的寄存器變了same被保存在eax寄存器中,以便高效訪問。
static int match(int len,const char * name,struct dir_entry * de)
{
    register int same ;

    // 首先判斷函數參數的有效性。如果目錄項指針空,或者目錄項i節點等於0,或者
    // 要比較的字符串長度超過文件名長度,則返回0.如果要比較的長度len小於NAME_LEN,
    // 但是目錄項中文件名長度超過len,也返回0.
    if (!de || !de->inode || len > NAME_LEN)
        return 0;
    if (len < NAME_LEN && de->name[len])
        return 0;
    // 然后使用嵌套匯編語句進行快速比較操作。他會在用戶數據空間(fs段)執行字符串的比較
    // 操作。%0 - eax(比較結果same);%1 - eax (eax初值0);%2 - esi(名字指針);
    // %3 - edi(目錄項名指針);%4 - ecx(比較的字節長度值len).
    __asm__("cld\n\t"
        "fs ; repe ; cmpsb\n\t"
        "setz %%al"
        :"=a" (same)
        :"0" (0),"S" ((long) name),"D" ((long) de->name),"c" (len)
        );
    return same;
}

  (3)還有在某個目錄下查找名為xxx的文件,比如:"find /home -name test"命令,就是在home目錄下查找名為test的文件,實現如下:

  注意:函數的參數有兩個雙重指針,第二個雙重指針明顯是用來保存返回值的!

/*
 *    find_entry()
 *
 * finds an entry in the specified directory with the wanted name. It
 * returns the cache buffer in which the entry was found, and the entry
 * itself (as a parameter - res_dir). It does NOT read the inode of the
 * entry - you'll have to do that yourself if you want to.
 *
 * This also takes care of the few special cases due to '..'-traversal
 * over a pseudo-root and a mount point.
 */
//// 查找指定目錄和文件名的目錄項。 find -name "xxx" /xxx/xxx
// 參數:*dir - 指定目錄i節點的指針;name - 文件名;namelen - 文件名長度;
// 該函數在指定目錄的數據(文件)中搜索指定文件名的目錄項。並對指定文件名
// 是'..'的情況根據當前進行的相關設置進行特殊處理。關於函數參數傳遞指針的指針
// 作用,請參見seched.c中的注釋。
// 返回:成功則函數高速緩沖區指針,並在*res_dir處返回的目錄項結構指針。失敗則
// 返回空指針NULL。
static struct buffer_head * find_entry(struct m_inode ** dir,
    const char * name, int namelen, struct dir_entry ** res_dir)
{
    int entries;
    int block,i;
    struct buffer_head * bh;
    struct dir_entry * de;
    struct super_block * sb;

    // 同樣,本函數一上來也需要對函數參數的有效性進行判斷和驗證。如果我們在前面
    // 定義了符號常數NO_TRUNCATE,那么如果文件名長度超過最大長度NAME_LEN,則不予
    // 處理。如果沒有定義過NO_TRUNCATE,那么在文件名長度超過最大長度NAME_LEN時截短之。
#ifdef NO_TRUNCATE
    if (namelen > NAME_LEN)
        return NULL;
#else
    if (namelen > NAME_LEN)
        namelen = NAME_LEN;
#endif
    // 首先計算本目錄中目錄項項數entries(也即是當前目錄中能存放的最大目錄個數)。目錄i節點i_size字段中含有本目錄包含的數據
    // 長度,因此其除以一個目錄項的長度(16字節)即課得到該目錄中目錄項數。然后置空 
    // 返回目錄項結構指針。如果長度等於0,則返回NULL,退出。
    entries = (*dir)->i_size / (sizeof (struct dir_entry));
    *res_dir = NULL;
    if (!namelen)
        return NULL;
    // 接下來我們對目錄項文件名是'..'的情況進行特殊處理。如果當前進程指定的根i節點就是
    // 函數參數指定的目錄,則說明對於本進程來說,這個目錄就是它偽根目錄,即進程只能訪問
    // 該目錄中的項而不能后退到其父目錄中去。也即對於該進程本目錄就如同是文件系統的根目錄,
    // 因此我們需要將文件名修改為‘.’。
    // 否則,如果該目錄的i節點號等於ROOT_INO(1號)的話,說明確實是文件系統的根i節點。
    // 則取文件系統的超級塊。如果被安裝到的i節點存在,則先放回原i節點,然后對被安裝到
    // 的i節點進行處理。於是我們讓*dir指向該被安裝到的i節點;並且該i節點的引用數加1.
    // 即針對這種情況,我們悄悄的進行了“偷梁換柱”工程。:-)
/* check for '..', as we might have to do some "magic" for it */
    if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') {
/* '..' in a pseudo-root results in a faked '.' (just change namelen) */
        if ((*dir) == current->root)
            namelen=1;
        else if ((*dir)->i_num == ROOT_INO) {
/* '..' over a mount-point results in 'dir' being exchanged for the mounted
   directory-inode. NOTE! We set mounted, so that we can iput the new dir */
            sb=get_super((*dir)->i_dev);
            if (sb->s_imount) {
                iput(*dir);
                (*dir)=sb->s_imount;
                (*dir)->i_count++;
            }
        }
    }
    // 現在我們開始正常操作,查找指定文件名的目錄項在什么地方。因此我們需要讀取目錄的
    // 數據,即取出目錄i節點對應塊設備數據區中的數據塊(邏輯塊)信息。這些邏輯塊的塊號
    // 保存在i節點結構的i_zone[9]數組中。我們先取其中第一個塊號。如果目錄i節點指向的
    // 第一個直接磁盤塊好為0,則說明該目錄竟然不含數據,這不正常。於是返回NULL退出,
    // 否則我們就從節點所在設備讀取指定的目錄項數據塊。當然,如果不成功,則也返回NULL 退出。
    if (!(block = (*dir)->i_zone[0]))
        return NULL;
    if (!(bh = bread((*dir)->i_dev,block)))
        return NULL;
    // 此時我們就在這個讀取的目錄i節點數據塊中搜索匹配指定文件名的目錄項。首先讓de指向
    // 緩沖塊中的數據塊部分。並在不超過目錄中目錄項數的條件下,循環執行搜索。其中i是目錄中
    // 的目錄項索引號。在循環開始時初始化為0.
    i = 0;
    de = (struct dir_entry *) bh->b_data;
    while (i < entries) {
        // 如果當前目錄項數據塊已經搜索完,還沒有找到匹配的目錄項,則釋放當前目錄項數據塊。
        // 再讀入目錄的下一個邏輯塊。若這塊為空。則只要還沒有搜索完目錄中的所有目錄項,就
        // 跳過該塊,繼續讀目錄的下一邏輯塊。若該塊不空,就讓de指向該數據塊,然后在其中繼續
        // 搜索。其中DIR_ENTRIES_PER_BLOCK可得到當前搜索的目錄項所在目錄文件中的塊號,而bmap()
        // 函數則課計算出在設備上對應的邏輯塊號.
        if ((char *)de >= BLOCK_SIZE+bh->b_data) {
            brelse(bh);
            bh = NULL;
            if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
                !(bh = bread((*dir)->i_dev,block))) {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }
            de = (struct dir_entry *) bh->b_data;
        }
        // 如果找到匹配的目錄項的話,則返回該目錄項結構指針de和該目錄項i節點指針*dir以及該目錄項
        // 數據塊指針bh,並退出函數。否則繼續在目錄項數據塊中比較下一個目錄項。
        if (match(namelen,name,de)) {
            *res_dir = de;
            return bh;
        }
        de++;
        i++;
    }
    // 如果指定目錄中的所有目錄項都搜索完后,還沒有找到相應的目錄項,則釋放目錄的數據塊,
    // 最后返回NULL(失敗)。
    brelse(bh);
    return NULL;
}

   這個函數的開頭就出現了一個新的結構體dir_entry,是這樣定義的:結構體很簡單,只有2個字段,分別是當前文件或目錄中包含的inode個數,以及自己的名字;最后一個參數也是用這個結構體保存找到的文件名稱和inode節點號數,通過inode節點號數從inode位圖查看該inode是否被使用,也可以查找到該文件的inode節點在磁盤的block位置,進而找到文件元信息

struct dir_entry {
    unsigned short inode;
    char name[NAME_LEN];
};

  (4)既然能夠查找文件,也就能新建文件或目錄,linux的實現方式如下:

/*
 *    add_entry()
 *
 * adds a file entry to the specified directory, using the same
 * semantics as find_entry(). It returns NULL if it failed.
 *
 * NOTE!! The inode part of 'de' is left at 0 - which means you
 * may not sleep between calling this and putting something into
 * the entry, as someone else might have used it while you slept.
 */
//// 根據指定的目錄和文件名添加目錄項
// 參數:dir - 指定目錄的i節點;name - 文件名;namelen - 文件名長度;
// 返回:高速緩沖區指針;res_dir - 返回的目錄項結構指針。
static struct buffer_head * add_entry(struct m_inode * dir,
    const char * name, int namelen, struct dir_entry ** res_dir)
{
    int block,i;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 同樣,本函數一上來也需要對函數參數的有效性進行判斷和驗證。
    // 如果我們在前面定義了符號常數NO_TRUNCATE,那么如果文件名長
    // 度超過最大長度NAME_LEN,則不予處理。如果沒有定義過NO_TRUNCATE,
    // 那么在文件名長度超過最大長度NAME_LEN時截短之。
    *res_dir = NULL;
#ifdef NO_TRUNCATE
    if (namelen > NAME_LEN)
        return NULL;
#else
    if (namelen > NAME_LEN)
        namelen = NAME_LEN;
#endif
    // 現在我們開始操作,向指定目錄中添加一個指定文件名的目錄項。因此
    // 我們需要先讀取目錄的數據,即取出目錄i節點對應塊數據區中的數據塊
    // 信息。這些邏輯塊的塊號保存在i節點結構i_zone[9]數組中。我們先取
    // 其中第1個塊號,如果目錄i節點指向的第一個直接磁盤塊號為0,則說明
    // 該目錄竟然不含數據,這不正常。於是返回NULL退出。否則我們就從節點
    // 所在設備讀取指定目錄項數據塊。當然,如果不成功,則也返回NULL退出。
    // 如果參數提供的文件名長度等於0,則也返回NULL退出。
    if (!namelen)
        return NULL;
    if (!(block = dir->i_zone[0]))/*目錄文件存儲的第一個磁盤邏輯塊號,肯定不會是0(0是引導塊)*/
        return NULL;
    //目錄數據必須存磁盤,不能只存內存,否則關機后就全丟了
    if (!(bh = bread(dir->i_dev,block)))/*讀取目錄文件第一個邏輯塊的數據到緩存區,里面存放的都是dir_entry,所以下面把b_data強轉成dir_entry*/
        return NULL;
    // 此時我們就在這個目錄i節點數據塊中循環查找最后未使用的空目錄項。
    // 首先讓目錄項結構指針de指向緩沖塊中的數據塊部分,即第一個目錄項處。
    // 其中i是目錄中的目錄項索引號,在循環開始時初始化為0.
    i = 0;
    de = (struct dir_entry *) bh->b_data;
    while (1) {
        // 如果當前目錄項數據塊已經搜索完畢,但還沒有找到需要的空目錄項,
        // 則釋放當前目錄項數據塊,再讀入目錄的下一個邏輯塊。如果對應的邏輯塊。
        // 如果對應的邏輯塊不存在就創建一塊。如果讀取或創建操作失敗則返回空。
        // 如果此次讀取的磁盤邏輯塊數據返回的緩沖塊數據為空,說明這塊邏輯塊
        // 可能是因為不存在而新創建的空塊,則把目錄項索引值加上一塊邏輯塊所
        // 能容納的目錄項數DIR_ENTRIES_PER_BLOCK,用以跳過該塊並繼續搜索。
        // 否則說明新讀入的塊上有目錄項數據,於是讓目錄項結構指針de指向該塊
        // 的緩沖塊數據部分,然后在其中繼續搜索。其中i/DIR_ENTRIES_PER_BLOCK可
        // 計算得到當前搜索的目錄項i所在目錄文件中的塊號,而create_block函數則可
        // 讀取或創建出在設備上對應的邏輯塊。
        if ((char *)de >= BLOCK_SIZE+bh->b_data) {
            brelse(bh);
            bh = NULL;
            block = create_block(dir,i/DIR_ENTRIES_PER_BLOCK);
            if (!block)
                return NULL;
            if (!(bh = bread(dir->i_dev,block))) {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }
            de = (struct dir_entry *) bh->b_data;
        }
        // 如果當前所操作的目錄項序號i乘上目錄結構大小所在長度值已經超過了該目錄
        // i節點信息所指出的目錄數據長度值i_size,則說明整個目錄文件數據中沒有
        // 由於刪除文件留下的空目錄項,因此我們只能把需要添加的新目錄項附加到
        // 目錄文件數據的末端處。於是對該處目錄項進行設置(置該目錄項的i節點指針
        // 為空),並更新該目錄文件的長度值(加上一個目錄項的長度),然后設置目錄
        // 的i節點已修改標志,再更新該目錄的改變時間為當前時間。
        if (i*sizeof(struct dir_entry) >= dir->i_size) {
            de->inode=0;
            dir->i_size = (i+1)*sizeof(struct dir_entry);
            dir->i_dirt = 1;
            dir->i_ctime = CURRENT_TIME;
        }
        // 若當前搜索的目錄項de的i節點為空,則表示找到一個還未使用的空閑目錄項
        // 或是添加的新目錄項。於是更新目錄的修改時間為當前時間,並從用戶數據區
        // 復制文件名到該目錄項的文件名字段,置含有本目錄項的相應高速緩沖塊已修改
        // 標志。返回該目錄項的指針以及該高速緩沖塊的指針,退出。
        if (!de->inode) {
            dir->i_mtime = CURRENT_TIME;
            for (i=0; i < NAME_LEN ; i++)
                de->name[i]=(i<namelen)?get_fs_byte(name+i):0;
            bh->b_dirt = 1;
            *res_dir = de;
            return bh;
        }
        de++;
        i++;
    }
    // 本函數執行不到這里。這也許是Linus在寫這段代碼時,先復制了上面的find_entry()
    // 函數的代碼,而后修改成本函數的。:-)
    brelse(bh);
    return NULL;
}

  (5)截至目前,linux文件系統涉及到好多的結構體、緩存區、磁盤(主要是inode、buffer_head、dir_entry、block等),不熟悉的初學者看到這里估計都開始暈菜了,這里有個現成的圖示(參考1),展示了各個結構體的關系:

        

   整個磁盤文件數據讀取流程如下:

  •   先根據文件路徑找到文件對應的inode節點。假設是個絕對路徑,文件路徑是/a/b/c.txt;系統初始化的時候我們已經拿到了根目錄對應的inode(磁盤上第一個inode節點就是根目錄所在的節點,從這里也可以看出,文件目錄也必須保存在磁盤,而不僅僅是保存在內存,避免斷電后丟失,把根目錄文件的block內容讀進來,是一系列的dir_entry結構體。然后逐個遍歷,比較文件名是不是等於a,最后得到一個目錄a對應的dir_entry;
  •        dir_entry結構體不僅保存了文件名,還保存了對應的inode號;根據inode號把a目錄文件的內容也讀取進來;以此類推,得到c對應的dir_entry
  •        再根據c對應的dir_entry的inode號,從磁盤把inode的內容讀進來,發現就是個普通文件;至此,找到了這個文件對應的inode節點,完成fd->file結構體->inode結構體的賦值
  •        最后根據fd找到對應的inode節點,根據file結構體的pos字段;根據數據在文件中的偏移,可以算出應該取i_zone[9]字段的哪個索引,文件的前7塊對應索引0-6,前7到7+512對應索引7等。得到索引后,讀取i_zone數組在該索引的值,即我們要讀取的數據在硬盤的數據塊。然后把這個數據塊從硬盤讀取進來。返回給用戶
  •         整個流程總結:磁盤inode首節點->dir_entry->根據pathname查找目錄或文件inode編號->從磁盤讀取inode內容->分析i_zone得到文件內容的block編號->從磁盤讀數據;整個思路流程和內存管理的CR3分頁檢索沒有任何本質區別;

      (6)linux常用的命令還有“cat  /home/test.c”、“cd /home/jdk/java” 等目錄相關的操作。通過前面的分析得知,操作文件或目錄,本質就是讀寫其元信息,這些都存放在inode里面,所以想想辦法得到inode,代碼如下:

/*
 *    get_dir()
 *
 * Getdir traverses the pathname until it hits the topmost directory.
 * It returns NULL on failure.
 */
//// 搜尋指定路徑的目錄(或文件名)的i節點。
// 參數:pathname - 路徑名
// 返回:目錄或文件的i節點指針。
static struct m_inode * get_dir(const char * pathname)
{
    char c;
    const char * thisname;
    struct m_inode * inode;
    struct buffer_head * bh;
    int namelen,inr,idev;
    struct dir_entry * de;

    // 搜索操作會從當前任務結構中設置的根(或偽根)i節點或當前工作目錄i節點
    // 開始,因此首先需要判斷進程的根i節點指針和當前工作目錄i節點指針是否有效。
    // 如果當前進程沒有設定根i節點,或者該進程根i節點指向是一個空閑i節點(引用為0),
    // 則系統出錯停機。如果進程的當前工作目錄i節點指針為空,或者該當前工作目錄
    // 指向的i節點是一個空閑i節點,這也是系統有問題,停機。
    if (!current->root || !current->root->i_count)
        panic("No root inode");
    if (!current->pwd || !current->pwd->i_count)
        panic("No cwd inode");
    // 如果用戶指定的路徑名的第1個字符是'/',則說明路徑名是絕對路徑名。則從
    // 根i節點開始操作,否則第一個字符是其他字符,則表示給定的相對路徑名。
    // 應從進程的當前工作目錄開始操作。則取進程當前工作目錄的i節點。如果路徑
    // 名為空,則出錯返回NULL退出。此時變量inode指向了正確的i節點 -- 進程的
    // 根i節點或當前工作目錄i節點之一。
    if ((c=get_fs_byte(pathname))=='/') {
        inode = current->root;
        pathname++;
    } else if (c)
        inode = current->pwd;
    else
        return NULL;    /* empty name is bad */
    // 然后針對路徑名中的各個目錄名部分和文件名進行循環出路,首先把得到的i節點
    // 引用計數增1,表示我們正在使用。在循環處理過程中,我們先要對當前正在處理
    // 的目錄名部分(或文件名)的i節點進行有效性判斷,並且把變量thisname指向
    // 當前正在處理的目錄名部分(或文件名)。如果該i節點不是目錄類型的i節點,
    // 或者沒有可進入該目錄的訪問許可,則放回該i節點,並返回NULL退出。當然,剛
    // 進入循環時,當前的i節點就是進程根i節點或者是當前工作目錄的i節點。
    inode->i_count++;
    while (1) {
        thisname = pathname;
        if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
            iput(inode);
            return NULL;
        }
        // 每次循環我們處理路徑名中一個目錄名(或文件名)部分。因此在每次循環中
        // 我們都要從路徑名字符串中分離出一個目錄名(或文件名)。方法是從當前路徑名
        // 指針pathname開始處搜索檢測字符,知道字符是一個結尾符(NULL)或者是一
        // 個'/'字符。此時變量namelen正好是當前處理目錄名部分的長度,而變量thisname
        // 正指向該目錄名部分的開始處。此時如果字符是結尾符NULL,則表明以及你敢搜索
        // 到路徑名末尾,並已到達最后指定目錄名或文件名,則返回該i節點指針退出。
        // 注意!如果路徑名中最后一個名稱也是一個目錄名,但其后面沒有加上'/'字符,
        // 則函數不會返回該最后目錄的i節點!例如:對於路徑名/usr/src/linux,該函數
        // 將只返回src/目錄名的i節點。
        for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
            /* nothing */ ;
        if (!c)
            return inode;
        // 在得到當前目錄名部分(或文件名)后,我們調用查找目錄項函數find_entry()在
        // 當前處理的目錄中尋找指定名稱的目錄項。如果沒有找到,則返回該i節點,並返回
        // NULL退出。然后在找到的目錄項中取出其i節點號inr和設備號idev,釋放包含該目錄
        // 項的高速緩沖塊並放回該i節點。然后去節點號inr的i節點inode,並以該目錄項為
        // 當前目錄繼續循環處理路徑名中的下一目錄名部分(或文件名)。
        if (!(bh = find_entry(&inode,thisname,namelen,&de))) {
            iput(inode);
            return NULL;
        }
        inr = de->inode;                        // 當前目錄名部分的i節點號
        idev = inode->i_dev;
        brelse(bh);
        iput(inode);
        if (!(inode = iget(idev,inr)))          // 取i節點內容。
            return NULL;
    }
}

   (7)查找目錄的最高路徑,比如:

  •  cd  /home/test的最高路徑是test(最后一個反斜杠后面是test):basename是test,namelen=4;
  •  cd  /home/test/的最高路徑是空的(最后一個反斜杠后面是空的):basename是null,namelen=0;
/*
 *    dir_namei()
 *
 * dir_namei() returns the inode of the directory of the
 * specified name, and the name within that directory.
 */
// 參數:pathname - 目錄路徑名;namelen - 路徑名長度;name - 返回的最頂層目錄名。
// 返回:指定目錄名最頂層目錄的i節點指針和最頂層目錄名稱及長度。出錯時返回NULL。
// 注意!!這里"最頂層目錄"是指路徑名中最靠近末端的目錄。
static struct m_inode * dir_namei(const char * pathname,
    int * namelen, const char ** name)
{
    char c;
    const char * basename;
    struct m_inode * dir;

    // 首先取得指定路徑名最頂層目錄的i節點。然后對路徑名Pathname 進行搜索檢測,查出
    // 最后一個'/'字符后面的名字字符串,計算其長度,並且返回最頂層目錄的i節點指針。
    // 注意!如果路徑名最后一個字符是斜杠字符'/',那么返回的目錄名為空,並且長度為0.
    // 但返回的i節點指針仍然指向最后一個'/'字符錢目錄名的i節點。
    if (!(dir = get_dir(pathname)))
        return NULL;
    basename = pathname;
    while ((c=get_fs_byte(pathname++)))
        if (c=='/')
            basename=pathname;
    *namelen = pathname-basename-1;
    *name = basename;
    return dir;
}

   (8)這個可能是最有用的函數之一了:namei函數,用戶傳入路徑,返回對應的inode節點

/*
 *    namei()
 *
 * is used by most simple commands to get the inode of a specified name.
 * Open, link etc use their own routines, but this is enough for things
 * like 'chmod' etc.
 */
//// 取指定路徑名的i節點。
// 參數:pathname - 路徑名。
// 返回:對應的i節點。
struct m_inode * namei(const char * pathname)
{
    const char * basename;
    int inr,dev,namelen;
    struct m_inode * dir;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 首先查找指定路徑的最頂層目錄的目錄名並得到其i節點,若不存在,則返回NULL退出。
    // 如果返回的最頂層名字長度是0,則表示該路徑名以一個目錄名為最后一項。因此我們
    // 已經找到對應目錄的i節點,可以直接返回該i節點退出。
    if (!(dir = dir_namei(pathname,&namelen,&basename)))
        return NULL;
    if (!namelen)            /* special case: '/usr/' etc */
        return dir;
    // 然后在返回的頂層目錄中尋找指定文件名目錄項的i節點。注意!因為如果最后也是一個
    // 目錄名,但其后沒有加'/',則不會返回該目錄的i節點!例如:/usr/src/linux,將只返回
    // src/目錄名的i節點。因為函數dir_namei()把不以'/'結束的最后一個名字當作一個文件名
    // 來看待,所以這里需要單獨對這種情況使用尋找目錄項i節點函數find_entry()進行處理。
    // 此時de中含有尋找到的目錄項指針,而dir是包含該目錄項的目錄的i節點指針。
    bh = find_entry(&dir,basename,namelen,&de);
    if (!bh) {
        iput(dir);
        return NULL;
    }
    // 接着取該目錄項的i節點號和設備號,並釋放包含該目錄項的高速緩沖塊並返回目錄i節點。
    // 然后取對應節點號的i節點,修改其被訪問時間為當前時間,並置已修改標志。最后返回
    // 該i節點指針。
    inr = de->inode;
    dev = dir->i_dev;/*子目錄設備號要和父目錄一致*/
    brelse(bh);
    iput(dir);
    dir=iget(dev,inr);
    if (dir) {
        dir->i_atime=CURRENT_TIME;
        dir->i_dirt=1;
    }
    return dir;
}

   namei沒有做任何權限的判斷,也只是查找現成的dir_entry,如果沒有就返回null了,所以只能用在find -name這種命令;但實際用戶使用時,還涉及到文件的權限校驗,文件打開方式判斷(只讀?可讀可寫?)等,情況比find -name這種命令復雜很多,需要單獨重新寫個接口來實現這些需求,如下:相比namei,

  •   檢查了權限和打開模式;
  •        如果沒找到對飲的inode就新建inode,而不是直接返回null;“宏觀”層面感受:用戶調用open函數想打開一個文件,如果該文件不存在,就新建文件,並返回文件的handler
/*
 *    open_namei()
 *
 * namei for open - this is in fact almost the whole open-routine.
 */
//// 文件打開namei函數。
// 參數filename是文件名,flag是打開文件標志,他可取值:O_RDONLY(只讀)、O_WRONLY(只寫)
// 或O_RDWR(讀寫),以及O_CREAT(創建)、O_EXCL(被創建文件必須不存在)、O_APPEND(在文件尾
// 添加數據)等其他一些標志的組合。如果本調用創建了一個新文件,則mode就用於指定文件的
// 許可屬性。這些屬性有S_IRWXU(文件宿主具有讀、寫和執行權限)、S_IRUSR(用戶具有讀文件
// 權限)、S_IRWXG(組成員具有讀、寫和執行權限)等等。對於新創建的文件,這些屬性只應用於
// 將來對文件的訪問,創建了只讀文件的打開調用也將返回一個可讀寫的文件句柄。
// 返回:成功返回0,否則返回出錯碼;res_inode - 返回對應文件路徑名的i節點指針。
int open_namei(const char * pathname, int flag, int mode,
    struct m_inode ** res_inode)
{
    const char * basename;
    int inr,dev,namelen;
    struct m_inode * dir, *inode;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 首先對函數參數進行合理的處理。如果文件訪問模式標志是只讀(0),但是文件截零標志
    // O_TRUNC卻置位了,則在文件打開標志中添加只寫O_WRONLY。這樣做的原因是由於截零標志
    // O_TRUNC必須在文件可寫情況下才有效。然后使用當前進程的文件訪問許可屏蔽碼,屏蔽掉
    // 給定模式中的相應位,並添上對普通文件標志I_REGULAR。該標志將用於打開的文件不存在
    // 而需要創建文件時,作為新文件的默認屬性。
    if ((flag & O_TRUNC) && !(flag & O_ACCMODE))
        flag |= O_WRONLY;
    mode &= 0777 & ~current->umask;
    mode |= I_REGULAR;
    // 然后根據指定的路徑名尋找對應的i節點,以及最頂端目錄名及其長度。此時如果最頂端目錄
    // 名長度為0(例如'/usr/'這種路徑名的情況),那么若操作不是讀寫、創建和文件長度截0,
    // 則表示是在打開一個目錄名文件操作。於是直接返回該目錄的i節點並返回0退出。否則說明
    // 進程操作非法,於是放回該i節點,返回出錯碼。
    if (!(dir = dir_namei(pathname,&namelen,&basename)))
        return -ENOENT;
    if (!namelen) {            /* special case: '/usr/' etc */
        if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) {
            *res_inode=dir;
            return 0;
        }
        iput(dir);
        return -EISDIR;
    }
    // 接着根據上面得到的最頂層目錄名的i節點dir,在其中查找取得路徑名字符串中最后的文件名
    // 對應的目錄項結構de,並同時得到該目錄項所在的高速緩沖區指針。如果該高速緩沖指針為NULL,
    // 則表示沒有找到對應文件名的目錄項,因此只可能是創建文件操作。此時如果不是創建文件,則
    // 放回該目錄的i節點,返回出錯號退出。如果用戶在該目錄沒有寫的權力,則放回該目錄的i節點,
    // 返回出錯號退出。
    bh = find_entry(&dir,basename,namelen,&de);
    if (!bh) {
        if (!(flag & O_CREAT)) {
            iput(dir);
            return -ENOENT;
        }
        if (!permission(dir,MAY_WRITE)) {
            iput(dir);
            return -EACCES;
        }
        // 現在我們確定了是創建操作並且有寫操作許可。因此我們就在目錄i節點對設備上申請一個
        // 新的i節點給路徑名上指定的文件使用。若失敗則放回目錄的i節點,並返回沒有空間出錯碼。
        // 否則使用該新i節點,對其進行初始設置:置節點的用戶id;對應節點訪問模式;置已修改
        // 標志。然后並在指定目錄dir中添加一個新目錄項。
        inode = new_inode(dir->i_dev);
        if (!inode) {
            iput(dir);
            return -ENOSPC;
        }
        inode->i_uid = current->euid;
        inode->i_mode = mode;
        inode->i_dirt = 1;
        bh = add_entry(dir,basename,namelen,&de);
        // 如果返回的應該含有新目錄項的高速緩沖區指針為NULL,則表示添加目錄項操作失敗。於是
        // 將該新i節點的引用計數減1,放回該i節點與目錄的i節點並返回出錯碼退出。否則說明添加
        // 目錄項操作成功。於是我們來設置該新目錄的一些初始值:置i節點號為新申請的i節點的號
        // 碼;並置高速緩沖區已修改標志。然后釋放該高速緩沖區,放回目錄的i節點。返回新目錄
        // 項的i節點指針,並成功退出。
        if (!bh) {
            inode->i_nlinks--;
            iput(inode);
            iput(dir);
            return -ENOSPC;
        }
        de->inode = inode->i_num;
        bh->b_dirt = 1;
        brelse(bh);
        iput(dir);
        *res_inode = inode;
        return 0;
    }
    // 若上面在目錄中取文件名對應目錄項結構的操作成功(即bh不為NULL),則說明指定打開的文件已
    // 經存在。於是取出該目錄項的i節點號和其所在設備號,並釋放該高速緩沖區以及放回目錄的i節點
    // 如果此時堵在操作標志O_EXCL置位,但現在文件已經存在,則返回文件已存在出錯碼退出。
    inr = de->inode;
    dev = dir->i_dev;
    brelse(bh);
    iput(dir);
    if (flag & O_EXCL)
        return -EEXIST;
    // 然后我們讀取該目錄項的i節點內容。若該i節點是一個目錄i節點並且訪問模式是只寫或讀寫,或者
    // 沒有訪問的許可權限,則放回該i節點,返回訪問權限出錯碼退出。
    if (!(inode=iget(dev,inr)))
        return -EACCES;
    if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||
        !permission(inode,ACC_MODE(flag))) {
        iput(inode);
        return -EPERM;
    }
    // 接着我們更新該i節點的訪問時間字段值為當前時間。如果設立了截0標志,則將該i節點的文件長度
    // 截0.最后返回該目錄項i節點的指針,並返回0(成功)。
    inode->i_atime = CURRENT_TIME;
    if (flag & O_TRUNC)
        truncate(inode);
    *res_inode = inode;
    return 0;
}

   至此,不知道各讀者有沒有發現文件和目錄相關操作的共性:全都圍繞inode和dir_entry兩個結構體各種操作

  •   dir_entry有字符串數組,存放了目錄或文件的字符,可以先根據字符串找到目標dir_entry
  •        取出目標dir_entry的inode字段,這個字段標時了inode節點的偏移位置(或者說在磁盤上block的位置)
  •        利用inode偏移從磁盤讀取inode節點,這里面包含了文件的元信息,尤其時i_zone字段,根據這個進一步從磁盤讀取文件數據
  •        磁盤中的inode通過inode位圖標記是否使用;dir_entry在inode根節點;內存中inode存放在inode_table數組!不論是在磁盤,還是在內存,本質上都是把inode或dir_entry結構體的實例集合起來統一管理(檢索查找)

  兩個結構體本質上都是用來做索引,dir_entry字段少,相當於簡版的索引!inode字段多,相當於完整的索引

 (9)依次類推,mknod也是類似的操作(這居然還是個系統調用,級別相當的高):

//// 創建一個設備特殊文件或普通文件節點(node)
// 該函數創建名稱為filename,由mode和dev指定的文件系統節點(普通文件、設備特殊文件或命名管道)
// 參數:filename - 路徑名;mode - 指定使用許可以及所創建節點的類型;dev - 設備號。
// 返回:成功則返回0,否則返回出錯碼。
int sys_mknod(const char * filename, int mode, int dev)
{
    const char * basename;
    int namelen;
    struct m_inode * dir, * inode;
    struct buffer_head * bh;
    struct dir_entry * de;

    // 首先檢查操作許可和參數的有效性並取路徑名中頂層目錄的i節點。如果不是超級用戶,則返回
    // 訪問許可出錯碼。如果找不到對應路徑名中頂層目錄的i節點,則返回出錯碼。如果最頂端的
    // 文件名長度為0,則說明給出的路徑名最后沒有指定文件名,放回該目錄i節點,返回出錯碼退出。
    // 如果在該目錄中沒有寫的權限,則放回該目錄的i節點,返回訪問許可出錯碼退出。如果不是超級
    // 用戶,則返回訪問許可出錯碼。
    if (!suser())
        return -EPERM;
    if (!(dir = dir_namei(filename,&namelen,&basename)))
        return -ENOENT;
    if (!namelen) {
        iput(dir);
        return -ENOENT;
    }
    if (!permission(dir,MAY_WRITE)) {
        iput(dir);
        return -EPERM;
    }
    // 然后我們搜索一下路徑名指定的文件是否已經存在。若已經存在則不能創建同名文件節點。
    // 如果對應路徑名上最后的文件名的目錄項已經存在,則釋放包含該目錄項的緩沖區塊並放回
    // 目錄的i節點,放回文件已存在的出錯碼退出。
    bh = find_entry(&dir,basename,namelen,&de);
    if (bh) {
        brelse(bh);
        iput(dir);
        return -EEXIST;
    }
    // 否則我們就申請一個新的i節點,並設置該i節點的屬性模式。如果要創建的是塊設備文件或者是
    // 字符設備文件,則令i節點的直接邏輯塊指針0等於設備號。即對於設備文件來說,其i節點的
    // i_zone[0]中存放的是該設備文件所定義設備的設備號。然后設置該i節點的修改時間、訪問
    // 時間為當前時間,並設置i節點已修改標志。
    inode = new_inode(dir->i_dev);
    if (!inode) {
        iput(dir);
        return -ENOSPC;
    }
    inode->i_mode = mode;
    if (S_ISBLK(mode) || S_ISCHR(mode))
        inode->i_zone[0] = dev;
    inode->i_mtime = inode->i_atime = CURRENT_TIME;
    inode->i_dirt = 1;
    // 接着為這個新的i節點在目錄中新添加一個目錄項。如果失敗(包含該目錄項的高速緩沖塊指針為
    // NULL),則放回目錄的i節點,吧所申請的i節點引用連接計數復位,並放回該i節點,返回出錯碼退出。
    bh = add_entry(dir,basename,namelen,&de);
    if (!bh) {
        iput(dir);
        inode->i_nlinks=0;
        iput(inode);
        return -ENOSPC;
    }
    // 現在添加目錄項操作也成功了,於是我們來設置這個目錄項內容。令該目錄項的i節點字段於新i節點
    // 號,並置高速緩沖區已修改標志,放回目錄和新的i節點,釋放高速緩沖區,最后返回0(成功)。
    de->inode = inode->i_num;/*剛創建的inode和dir_entry做映射*/
    bh->b_dirt = 1;
    iput(dir);
    iput(inode);
    brelse(bh);
    return 0;
}

   早期連創建目錄都是系統調用,只能系統管理員創建的:

//// 創建一個目錄
// 參數:pathname - 路徑名;mode - 目錄使用的權限屬性。
// 返回:成功則返回0,否則返回出錯碼。
int sys_mkdir(const char * pathname, int mode)
{
    const char * basename;
    int namelen;
    struct m_inode * dir, * inode;
    struct buffer_head * bh, *dir_block;
    struct dir_entry * de;

    // 首先檢查操作許可和參數的有效性並取路徑名中頂層目錄的i節點。如果不是超級用戶,則
    // 放回訪問許可出錯碼。如果找不到對應路徑名中頂層目錄的i節點,則返回出錯碼。如果最
    // 頂端的文件名長度為0,則說明給出的路徑名最后沒有指定文件名,放回該目錄i節點,返回
    // 出錯碼退出。如果在該目錄中沒有寫權限,則放回該目錄的i節點,返回訪問許可出錯碼退出。
    // 如果不是超級用戶,則返回訪問許可出錯碼。
    if (!suser())
        return -EPERM;
    if (!(dir = dir_namei(pathname,&namelen,&basename)))
        return -ENOENT;
    if (!namelen) {
        iput(dir);
        return -ENOENT;
    }
    if (!permission(dir,MAY_WRITE)) {
        iput(dir);
        return -EPERM;
    }
    // 然后我們搜索一下路徑名指定的目錄名是否已經存在。若已經存在則不能創建同名目錄節點。
    // 如果對應路徑名上最后的目錄名的目錄項已經存在,則釋放包含該目錄項的緩沖區塊並放回
    // 目錄的i節點,返回文件已經存在的出錯碼退出。否則我們就申請一個新的i節點,並設置該i
    // 節點的屬性模式:置該新i節點對應的文件長度為32字節(2個目錄項的大小),置節點已修改
    // 標志,以及節點的修改時間和訪問時間,2個目錄項分別用於‘.’和'..'目錄。
    bh = find_entry(&dir,basename,namelen,&de);
    if (bh) {
        brelse(bh);
        iput(dir);
        return -EEXIST;
    }
    inode = new_inode(dir->i_dev);
    if (!inode) {
        iput(dir);
        return -ENOSPC;
    }
    inode->i_size = 32;/*目錄的inode節點size是32,這個是固定的*/
    inode->i_dirt = 1;
    inode->i_mtime = inode->i_atime = CURRENT_TIME;
    // 接着為該新i節點申請一用於保存目錄項數據的磁盤塊,用於保存目錄項結構信息。並令i節
    // 點的第一個直接塊指針等於該塊號。如果申請失敗則放回對應目錄的i節點;復位新申請的i
    // 節點連接計數;放回該新的i節點,返回沒有空間出錯碼退出。否則置該新的i節點已修改標志。
    if (!(inode->i_zone[0]=new_block(inode->i_dev))) {
        iput(dir);
        inode->i_nlinks--;
        iput(inode);
        return -ENOSPC;
    }
    inode->i_dirt = 1;
    // 從設備上讀取新申請的磁盤塊(目的是吧對應塊放到高速緩沖區中)。若出錯,則放回對應
    // 目錄的i節點;釋放申請的磁盤塊;復位新申請的i節點連接計數;放回該新的i節點,返回沒有
    // 空間出錯碼退出。
    if (!(dir_block=bread(inode->i_dev,inode->i_zone[0]))) {
        iput(dir);
        free_block(inode->i_dev,inode->i_zone[0]);
        inode->i_nlinks--;
        iput(inode);
        return -ERROR;
    }
    // 然后我們在緩沖塊中建立起所創建目錄文件中的2個默認的新目錄項('.'和'..')結構數據。
    // 首先令de指向存放目錄項的數據塊,然后置該目錄項的i節點號字段等於新申請的i節點號,
    // 名字字段等於'.'。然后de指向下一個目錄項結構,並在該結構中存放上級目錄的i節點號
    // 和名字'..'。然后設置該高速緩沖塊 已修改標志,並釋放該緩沖塊。再初始化設置新i節點
    // 的模式字段,並置該i節點已修改標志。
    de = (struct dir_entry *) dir_block->b_data;
    de->inode=inode->i_num;
    strcpy(de->name,".");/*新創建的目錄,用ls -al查詢會發現有.和..這兩個目錄*/
    de++;
    de->inode = dir->i_num;
    strcpy(de->name,"..");
    inode->i_nlinks = 2;
    dir_block->b_dirt = 1;
    brelse(dir_block);
    inode->i_mode = I_DIRECTORY | (mode & 0777 & ~current->umask);
    inode->i_dirt = 1;
    // 現在我們在指定目錄中新添加一個目錄項,用於存放新建目錄的i節點號和目錄名。如果
    // 失敗(包含該目錄項的高速緩沖區指針為NULL),則放回目錄的i節點;所申請的i節點引用
    // 連接計數復位,並放回該i節點。返回出錯碼退出。
    bh = add_entry(dir,basename,namelen,&de);
    if (!bh) {
        iput(dir);
        free_block(inode->i_dev,inode->i_zone[0]);
        inode->i_nlinks=0;
        iput(inode);
        return -ENOSPC;
    }
    // 最后令該新目錄項的i節點字段等於新i節點號,並置高速緩沖塊已修改標志,放回目錄和
    // 新的i節點,是否高速緩沖塊,最后返回0(成功).
    de->inode = inode->i_num;
    bh->b_dirt = 1;
    dir->i_nlinks++;
    dir->i_dirt = 1;
    iput(dir);
    iput(inode);
    brelse(bh);
    return 0;
}

   (10)創建硬鏈接:本質就是新建該路徑的dir_entry,然后和文件原inode映射綁定!

  • 找到指定文件的inode
  • 再指定文件的路徑中創建新的dir_entry
  • 新創建的dir_entry映射到原inode:de->inode = oldinode->i_num
//// 為文件建立一個文件名目錄項
// 為一個已存在的文件創建一個新鏈接(也稱為硬鏈接 - hard link)
// 參數:oldname - 原路徑名;newname - 新的路徑名
// 返回:若成功則返回0,否則返回出錯號。
int sys_link(const char * oldname, const char * newname)
{
    struct dir_entry * de;
    struct m_inode * oldinode, * dir;
    struct buffer_head * bh;
    const char * basename;
    int namelen;

    // 首先對原文件名進行有效性驗證,它應該存在並且不是一個目錄名。所以我們先取得原文件
    // 路徑名對應的i節點oldname.若果為0,則表示出錯,返回出錯號。若果原路徑名對應的是
    // 一個目錄名,則放回該i節點,也返回出錯號。
    oldinode=namei(oldname);
    if (!oldinode)
        return -ENOENT;
    if (S_ISDIR(oldinode->i_mode)) {
        iput(oldinode);
        return -EPERM;
    }
    // 然后查找新路徑名的最頂層目錄的i節點dir,並返回最后的文件名及其長度。如果目錄的
    // i節點沒有找到,則放回原路徑名的i節點,返回出錯號。如果新路徑名中不包括文件名,
    // 則放回原路徑名i節點和新路徑名目錄的i節點,返回出錯號。
    dir = dir_namei(newname,&namelen,&basename);
    if (!dir) {
        iput(oldinode);
        return -EACCES;
    }
    if (!namelen) {//以反斜杠結尾,后面啥都沒了,導致namelen=0;
        iput(oldinode);
        iput(dir);
        return -EPERM;
    }
    // 我們不能跨設備建立硬鏈接。因此如果新路徑名頂層目錄的設備號與原路徑名的設備號不
    // 一樣,則放回新路徑名目錄的i節點和原路徑名的i節點,返回出錯號。另外,如果用戶沒
    // 有在新目錄中寫的權限,則也不能建立連接,於是放回新路徑名目錄的i節點和原路徑名
    // 的i節點,返回出錯號。
    if (dir->i_dev != oldinode->i_dev) {
        iput(dir);
        iput(oldinode);
        return -EXDEV;
    }
    if (!permission(dir,MAY_WRITE)) {
        iput(dir);
        iput(oldinode);
        return -EACCES;
    }
    // 現在查詢該新路徑名是否已經存在,如果存在則也不能建立鏈接。於是釋放包含該已存在
    // 目錄項的高速緩沖塊,放回新路徑名目錄的i節點和原路徑名的i節點,返回出錯號。
    bh = find_entry(&dir,basename,namelen,&de);
    if (bh) {
        brelse(bh);
        iput(dir);
        iput(oldinode);
        return -EEXIST;
    }
    // 現在所有條件都滿足了,於是我們在新目錄中添加一個目錄項。若失敗則放回該目錄的
    // i節點和原路徑名的i節點,返回出錯號。否則初始設置該目錄項的i節點號等於原路徑名的
    // i節點號,並置包含該新添加目錄項的緩沖塊已修改標志,釋放該緩沖塊,放回目錄的i節點。
    bh = add_entry(dir,basename,namelen,&de);
    if (!bh) {
        iput(dir);
        iput(oldinode);
        return -ENOSPC;
    }
    de->inode = oldinode->i_num;/*老的inode對一個的block號給新建的dir_entry,借此建立映射*/
    bh->b_dirt = 1;
    brelse(bh);
    iput(dir);
    // 再將原節點的硬鏈接計數加1,修改其改變時間為當前時間,並設置i節點已修改標志。最后
    // 放回原路徑名的i節點,並返回0(成功)。
    oldinode->i_nlinks++;
    oldinode->i_ctime = CURRENT_TIME;
    oldinode->i_dirt = 1;
    iput(oldinode);
    return 0;
}

  總結:

  •   目錄本質上就是一系列dir_entry的集合!創建/修改目錄就是創建/修改dir_entry,創建/修改文件就是創建/修改inode;   
  •        逆向或破解時掌握dir_entry和inode,就相當於掌握了所有的目錄和文件

 

 

參考:

1、https://zhuanlan.zhihu.com/p/76595175    深入淺出文件系統原理之文件讀取(基於linux0.11)

2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=27  linux內核精講


免責聲明!

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



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