對於普通用戶,平時使用操作系統是肯定涉及到創建、更改、刪除文件(比如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內核精講