【梳理】簡明操作系統原理 第十三章 文件和目錄(內附文檔高清截圖)

參考教材:
Operating Systems: Three Easy Pieces
Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
在線閱讀:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 認爲課本應該是免費的
————————————————————————————————————————
這是專業必修課《操作系統原理》的複習指引。
在本文的最後附有複習指導的高清截圖。需要掌握的概念在文檔截圖中以藍色標識,並用可讀性更好的字體顯示 Linux 命令和代碼。代碼部分語法高亮。
操作系統原理不是語言課,本複習指導對用到的編程語言的語法的講解也不會很細緻。如果不知道代碼中的一些關鍵字或函數的具體用法,你應該自行查找相關資料。

十三 文件和目錄

1、文件(file)的本質是一個一維的字節數組,可以進行讀寫。每個文件都有一個低級名稱(low-level name),通常是一個數字,用戶一般不會注意到這個名稱。由於歷史原因,低級名稱常被叫做索引節點號(inode number),這個將在未來的章節中學習。現在只需知道:每個文件都有一個索引節點號。
其實很多OS對文件的具體結構都不知道太多(比如這個文件是圖片、文檔還是C代碼)。OS對文件的責任主要是存取數據到磁盤上,確保當你需要原先存儲的數據時,能夠將其正確讀出來。
目錄(directory)也是文件的一種,也稱爲目錄文件(directory file)。目錄包含了一系列有序對,有序對的兩個元素分別是用戶可讀的名稱(即文件名)和索引節點號。

2、可以用目錄樹(directory tree)來表示目錄間的關係:目錄中的目錄在樹上能夠清晰地表示出來。

UNIX系統的目錄樹總是從根目錄“/”開始,“/”符號也是目錄樹上相鄰目錄的分隔符。我們可以看到,從任意一個目錄對應的節點向下走一步,就定位到了該目錄的一個子目錄或該目錄包含的文件。設字符串的內容一開始爲空,從目錄樹的根節點一路走下來,每向下走到一個非根節點就將該字符串先添加一個“/”,再添加走到的節點對應的目錄名或文件名,直到找到所需的目錄或文件。最後,這個字符串含有的內容就叫做找到的文件或目錄的絕對路徑名(absolute pathname),或絕對路徑(absolute path)。

3、擴展名(extension)標示了一個文件的類型。但是標註擴展名並不是強制的。在UNIX中,訪問磁盤、USB存儲設備和光驅乃至許多其它設備都可以通過目錄樹。

4、運行下面的代碼:
#include <fcntl.h>
int main() {
int fd = open(“foo”, O_CREAT);
return 0;
}
open()系統調用配合宏定義O_CREAT,在當前目錄下創建一個文件。
open()返回的是文件描述符(file descriptor)。它是一個int型數據,爲每個進程私有。文件描述符在UNIX系統中用於訪問文件。一個文件打開後,可以用open()返回的文件描述符來對文件進行操作(前提是你有相應的權限)。文件描述符可以看作一個文件的指針。
UNIX系統內核中存在專門的結構來追蹤每個進程已打開的文件。系統範圍的已打開文件表(open file table)的每一項對應一個進程的這種結構。

5、在終端上依次輸入:
echo hello>foo
sudo cat foo
sudo vim foo
可以看到,不但終端出現了輸入的內容,文件中也出現了我們輸入的內容。(提示輸入管理員密碼時,請輸入超級用戶的密碼)

6、strace是一個很實用的工具。它可以追蹤一個程序的系統調用,查看函數調用時傳遞的參數(實參)和返回值。在終端輸入strace -h獲得更多幫助。

7、打開文件後,使用系統調用lseek()可以重新定位文件中的偏移。在一個文件打開後,OS會追蹤這個文件讀寫到什麼位置。一次讀取或寫入n個字節後,這個偏移就增加n,下一次讀寫總是從這個偏移指向的位置進行。使用lseek可以將偏移定位到自己想要的位置。不過,lseek並不進行磁盤尋道,而只是修改OS追蹤的偏移變量的值。只有在偏移指定的位置發起一次讀寫操作時,纔會引發磁盤尋道(如果需要的話)。
lseek的參數如下:
__off_t lseek (int __fd, __off_t __offset, int __whence)
第一個參數是文件描述符,第二個參數是偏移,第三個參數有三種:
當設爲SEEK_SET時,偏移設爲第二個參數本身;當設爲SEEK_CUR時,偏移設爲當前的偏移加上第二個參數;當設爲SEEK_END時,偏移設爲文件的長度加上第二個參數。

8、系統調用和分別用於讀寫文件:
ssize_t read (int __fd, void *__buf, size_t __nbytes)
ssize_t write (int __fd, const void *__buf, size_t __n)
各個參數的作用已經非常明顯了,這裏不再贅述。

9、已打開文件表中,每一項都有一個成員:引用計數(reference count)。每多一個子進程使用同一文件,該計數就加1。

10、系統調用dup()(以及其變體dup2()、dup3())用於創建一個指向同一個文件的新的文件描述符。dup()在編寫UNIX shell或者在執行輸出重定向等操作時很有用。

11、調用write()後,數據不會被立刻寫入到文件;但有時候要求立刻將還在緩衝區的待寫入數據寫入相應文件,這時候就要用到系統調用fsync()。待寫入數據全部寫入完畢後,fsync()才返回。
但是fsync不會寫入對指向文件的目錄項的修改,也就是說如果新創建了一個文件,要是確保下次能正確讀出的話,就需要把所在目錄也fsync一下。這一點常被忽略,從而引發了許多bug。

12、mv命令可以重命名或移動文件。這個操作是原子的。如果計算機在重命名期間斷電了,那麼被操作的文件要麼被修改爲新名稱,要麼仍然保持舊名稱。這一點對要求原子更新操作的應用程序很有用。該命令調用系統調用rename()來完成重命名。但實際上rename()的執行過程是:將新文件寫入指定位置,同時刪除原文件。

13、除了訪問文件以外,我們希望文件系統還能爲每個文件記錄一些有用的信息。這些信息稱爲元數據(metadata)。查看元數據可以使用系統調用stat()或fstat()。在命令行中也可以使用命令stat來查看元數據。
頭文件<stat.h>中定義了stat和stat64結構體。篇幅所限,這裏不予列出其定義。
不同的文件包括的元數據不一定是相同的。例如文檔文件的元數據包含作者、標題、日期、關鍵字等;音樂文件的元數據包含標題、藝術家、專輯、流派、年代等。
我們在命令行中查看剛纔創建的新文件的信息。輸入:
stat foo
結果:

索引節點中,保存了一些文件信息;所有的索引節點都保存在硬盤上,當然有一些會被讀入內存,以加快訪問速率。

14、目錄是不能直接寫入的,因爲目錄的格式被認爲是文件系統元數據的一部分。文件系統認爲自己要對目錄數據的完整性負責。如果要修改一個目錄,只能間接通過創建文件、創建目錄等方式。
創建目錄使用系統調用mkdir()。終端亦可直接使用mkdir命令。
創建目錄後,雖然這個目錄包含了少量內容,但一般還是認爲它是空的。用“.”代表當前目錄,“…”代表上級目錄。使用ls -a可以將當前目錄下的文件連同目錄本身及其上級目錄一起列出。使用“ls -al”還可以將它們的常見信息也一併列寫。

15、使用Linux時,一定要當心一些存在危險性的命令。例如:
rm *
會刪除當前目錄下的全部文件。
rm命令不會刪除不爲空的目錄。但是如果要求遞歸刪除(-r)、強制刪除(-f),命令的危險性就比較大了。例如:
sudo rm -rf /*
將會刪除整個計算機的全部文件和目錄。
關於錯誤使用rm -rf的後果,大家可以在網絡上查找相關的資料。

16、下面的代碼展示了目錄的常見系統調用的用法:
#include <dirent.h>
#include <unistd.h>
#include
#include
int main() {
DIR* dp = opendir(".");
assert(dp);
dirent* d;
while (d = readdir(dp)) {
printf("%lu %s\n", d->d_ino, d->d_name);
}
closedir(dp);
return 0;
}
結構體dirent和dirent64的定義如下:
struct dirent {
#ifndef __USE_FILE_OFFSET64
__ino_t d_ino;
__off_t d_off;
#else
__ino64_t d_ino;
__off64_t d_off;
#endif
unsigned short int d_reclen;
unsigned char d_type;
char d_name[256]; /* We must not include limits.h! */
};

#ifdef __USE_LARGEFILE64
struct dirent64 {
__ino64_t d_ino;
__off64_t d_off;
unsigned short int d_reclen;
unsigned char d_type;
char d_name[256]; /* We must not include limits.h! */
};
#endif
d_name、d_ino、d_off、d_reclen、d_type分別代表:
文件名、索引節點號、離下一個dirent的偏移、文件名長度、文件類型。
其實目錄並不存儲太多信息,而是提供一個到索引節點的映射。一個程序如需獲得更多信息,就需要調用stat()。在使用ls命令的時候,-l參數就調用了這個系統調用。

17、rmdir系統調用(終端下爲rmdir命令)用於刪除目錄。當然,如果目錄非空,會拒絕刪除。rm既可以刪除文件也可以刪除目錄。

18、系統調用link()需要兩個參數:舊路徑和新路徑。當你將新文件名鏈接到舊文件名時,就創建了另一個可以訪問這個文件的方式。這樣創建的鏈接稱爲硬鏈接(hard link)。
link()在你創建鏈接的目標目錄創建一個新文件名,然後將其指向與原文件相同的索引節點號。被鏈接的文件不會被複制,不過現在可以通過兩個文件名訪問同一個文件了。對於文件系統來說,這兩個文件名沒有任何區別。
在命令行下執行:
ln foo foo2
ls -i foo foo2
可以看到foo和foo2的索引節點號都是一樣的:

創建硬鏈接後,相應的索引節點號中的引用計數(鏈接數)會增加1。rm也可以移除硬鏈接,移除以後引用計數減去1。但是,除非引用計數降低到0,原文件不會刪除。unlink也可以移除鏈接,但不能通過unlink刪除目錄。當刪除文件時,rm和unlink的效果是完全一樣的。

19、符號鏈接(symbolic link)也稱軟鏈接(soft link)。硬鏈接的使用有限制:不能爲一個目錄創建硬鏈接(否則目錄樹會出現迴路);也不可以硬鏈接到位於其它磁盤分區的文件(因爲索引節點號只在同一個文件系統的實例中有效,在其它文件系統的實例中無效)。而軟鏈接不存在這些限制。
使用ln命令時,加上-s,就可以創建軟鏈接。
但是軟鏈接和硬鏈接不同。通過stat命令就可以看出來。在終端下輸入:
touch test
ls
ln -s test test2
stat test
ls
stat test2
(touch命令用於修改文件或者目錄的時間屬性,包括存取時間和更改時間。若文件不存在,系統會建立一個新文件。)
結果:

硬鏈接是普通文件(regular file),但軟鏈接與硬鏈接的文件類型是不同的。我們可以看到,空文件test的大小是0字節,但軟鏈接test2的大小卻是4字節。因爲軟鏈接保存了鏈接到的文件的路徑。如果被鏈接的文件的路徑(包括名稱)很長,那麼軟鏈接的大小還會更大。
現在,我們將被鏈接到的文件test移除,然後再訪問它的軟鏈接test2。在終端下輸入:

可以看到,原文件移除後,軟鏈接就失效了。這稱爲懸掛引用(dangling reference)。

20、與內存不同,每個進程都具有自己單獨的內存空間,但文件常常爲大量程序所共享。因此,我們有必要使用新的機制來劃定文件的訪問權限。
通過ls -l可以查看文件的權限位(permission bits):

前面的“-rw-rw-r–”就是權限位的內容。第一位爲“-”,代表其爲普通文件(d爲目錄,l爲軟鏈接),與權限無關。剩下的9位分成3組:owner、group和other。每組的三位要麼爲“-”,要麼爲r(讀)、w(寫)或x(執行)。在本例中,後面的兩個andy,第一個是所有者,第二個是組名,也就是group指代的名稱。
通過命令chmod可以改變權限。輸入:
chmod 600 demo.cpp
結果:

600是八進制數,轉換成二進制是110 000 000,即rw-------。
如果一個可執行文件的執行位沒有置位,那麼試圖執行該文件時將因爲權限不足而被拒絕。對於目錄,執行位的意義有一點不同:它允許用戶(或組,或所有人)能夠進行改變目錄(cd)、在目錄下創建文件等操作。關於權限位的作用,請大家多進行上機實驗。

21、mount命令可以將其它文件系統掛載(mount)到指定的目錄下。例如Android手機中,TF卡被掛載的位置爲/storage/emulated/0。被掛載的位置稱爲掛載點(mount point)。通過掛載點可以訪問被掛載的文件系統中的文件。
掛載的意義是使得不同的文件系統可以通過一個統一的目錄樹進行訪問。

22、TOCTTOU(Time Of Check To Time Of Use)可以被用於攻擊。舉例:
設有一個郵件服務運行在root下。這個服務接收到了一封新郵件。經檢查發現,這封郵件確實是屬於收件人的普通文件,並不是重定向到郵件服務不應該更新的其它文件的鏈接,沒有問題。所以,郵件服務決定將這封郵件正常投遞至收件箱(即向對應的目錄新增這個文件)。
但就在這時,攻擊者設法獲得了下一個時間片,CPU轉而執行攻擊者發出的rename()系統調用(或建立一個鏈接)。這個調用將收件箱指向了存放用戶名及密碼的文件夾(假設收件箱和保存密碼的位置都已被攻擊者確定)。如果郵件內容是一個新的具有最高權限的用戶名及其密碼,那麼攻擊者就獲得了最高權限。他將可以通過這個權限用戶執行特權級操作。
防範TOCTTOU攻擊目前並沒有普適的辦法。常見的措施有:向open()傳遞一個參數O_NOFOLLOW,一旦嘗試打開一個符號鏈接則立即拒絕。一些更激進的策略還有:使用事務型文件系統(transactional file system)、儘量減少擁有最高權限的程序數量。但事務型文件系統應用並不廣泛,所以比較穩妥的辦法大概也只有後者。
獲得CPU時間片的方法主要有:文件系統迷惑(file system mazes)和算法複雜度攻擊(algorithmic complexity attacks)等。文件系統迷惑是強制應用程序讀取一個不在操作系統緩衝中的目錄入口,於是操作系統可能將被攻擊的應用程序置於睡眠狀態(隨後從磁盤中讀取)。而算法複雜度攻擊是強制應用程序浪費掉操作系統爲它分配的CPU資源(使得攻擊目標的優先級往下掉),例如攻擊者可以創建一大堆hash值相同的文件,從而導致內核hash表的鏈非常長,然後讓用戶去索引相關的文件,耗費其資源(在拒絕服務的漏洞中也有這種攻擊)。

在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章