(2021) 24 [持久化] 文件系統API
南京大學操作系統課蔣炎巖老師網絡課程筆記。
視頻:https://www.bilibili.com/video/BV1HN41197Ko?p=24
講義:http://jyywiki.cn/OS/2021/slides/14.slides#/
背景
回顧
硬件視角:持久化的“層層抽象”
- 物理層1-bit存儲
- 設備層I/O設備(寄存器)
- 驅動層(可讀可寫可控制的對象)
用戶(應用程序)視角:對象 + API
- C:\Program Files\…
- /etc/apt/souces.list
- /bin/bash
中間的部分:文件系統
本次課的內容和目標
理解 “文件系統” 的設計
- “文件系統” 的需求分析
- 需要什么對象
- 提供什么 API
文件系統概述
為什么需要文件系統?
存點什么
計算機:輔助人類更好地完成物理世界中的任務開機以后,必須得有點 “代表物理世界” 的東西。
“開機” 后操作系統中應該有的對象
- 各類數據
- 《操作系統》點名冊/成績單/Online Judge 結果/…
- 處理這些數據的程序
- wc, grep, vim, LibreOffice, …
- libc, X11, gnome-settings-manager, …
- 這些 “對象” 應該被持久地保存在介質上
別亂套
沒問題!我們已經講過 1-bit 的存儲了,但讓應用程序直接通過驅動訪問存儲設備 (1950s)?不合適!這樣就亂套了。
今天的系統中有不止一個程序
- 每個程序還需要考慮各種訪問權限、并發控制……
- 程序出 bug 了,不小心弄壞了整塊磁盤
文件系統:設計目標
- 提供合理的 API 使多個應用程序能共享數據
- 提供一定的隔離,使惡意/出錯程序的傷害不能任意擴大
- 這就是文件系統
- 你會怎么辦?
- 這就是文件系統
文件系統:存儲設備的虛擬化
磁盤 (I/O 設備) = 一個可以讀/寫的字節序列 —> 虛擬磁盤 (文件) = 一個可以讀/寫/的動態字節序列
結合我們操作系統課前面所學,可以類比:
- 進程抽象:一個 CPU → 切分時間片,在時間上共享
- 虛擬存儲:一份內存 → 劃分給多個虛擬地址空間
- 文件系統:一個物理磁盤 → 多個虛擬磁盤
文件系統 API: 虛擬磁盤管理
- 需要解決的問題
- 虛擬磁盤的命名、查找、權限
- 虛擬磁盤的操作 (讀寫)
虛擬磁盤:命名管理
接下來我們就來看看,兩個最主流的文件系統是如何實現磁盤到虛擬磁盤的虛擬化的。
文件系統 = “虛擬磁盤名” 到 “虛擬磁盤對象” 的映射
我們的系統中可能會有上百萬個文件,這時候,局部性就發揮很大作用了。
目錄:文件/目錄的集合 (形成一棵樹),邏輯相關的數據存放在相近的目錄
.
└── 學習資料├── .學習資料(隱藏)├── 問題求解1├── 問題求解2├── 問題求解3└── 問題求解4
Windows文件系統
樹總得有個根結點
Windows: 每個驅動器是一棵樹
C:\
:“C 盤根目錄”C:\Program Files\
,C:\Windows
,C:\Users
, …
D:\
: “D 盤根目錄”D:\學習資料\
- 優盤分配給新的盤符
- 為什么沒有
A:\
,B:\
? A、B盤歷史上是為軟驅預留的
Linux、UNIX文件系統
UNIX/Linux只有一個根/
,其他沒有了,那第二個設備呢?優盤呢???該怎么辦
文件系統的掛載
掛載設備
UNIX 允許任何一個目錄都可以 “掛載” 一個設備代表的目錄樹
非常靈活的設計;充分利用了目錄的局部性。
比如 128G 優盤分成了兩個 64G 分區 (Linux/
和 exFAT)
可以直接使用mount
命令 “掛載” 到一個目錄上
-
目錄里原先的內容會暫時消失 (但不會丟失),改換為你掛載的設備的內容。
-
/
,/home
,/var
可以是獨立的磁盤
如何 mount 一個文件?
- disk-img.tar.gz的掛載:創建一個 “loopback” 設備(
lsblk
可以看到),實際上loopback相當于是將一個文件(虛擬磁盤設備)反虛擬化為一個設備。 - 然后就變成掛載設備了,可以 strace 一下,看看操作系統提供了哪些 API。
Filesystem Hierarchy Standard (FHS)
Linux的文件樹標準。
掛載機制的好處
掛載機制非常靈活,比如說,你有兩塊磁盤,那你可以將你的/
根目錄和/home
主目錄分別掛載在兩塊磁盤上,在/
根目錄下創建一個空的主目錄即可。這樣的好處是對根目錄和主目錄的訪問帶寬就分開了,你可以同時讀寫這兩個目錄下的內容。
目錄API(系統調用)
目錄管理:創建、刪除、遍歷
這個簡單
-
mkdir
- 創建一個目錄
- 可以設置訪問權限
-
rmdir
- 刪除一個空目錄
- 沒有 “遞歸刪除” 的系統調用
- (應用層能實現的,就不要在操作系統層實現)
rm -rf
會遍歷目錄,逐個刪除 (試試 strace)
-
getdents
-
返回
count
個目錄項 (ls, find, tree 都使用這個),以點開頭的目錄會被系統調用返回,只是 ls 沒有顯示。ls
不顯示以.
所有開頭的文件的文件,這時為了隱藏當前目錄.
和上一級目錄..
,但是在實現上所有以.
開頭的文件或目錄都不會被顯示。這使得我們可以通過在文件或目錄前面加.
來對其做簡單的隱藏。使用ls -a
可以顯示全部。
-
小啟示:我們遇到Linux系統中的問題時通常會上Stack Overflow來查找別人解決問題的方法,這當然是很好的,Stack Overflow有許多有用的技巧。但實際上,Linux系統是self-contain的,即就算沒有互聯網,我們也可以通過man page配合strace等工具來找到我們想要的一切說明,并且,man page是最權威的。當然,Stack Overflow上的一些別人的巧妙的方法也是互聯網搜索的優勢。
硬(hard)鏈接
UNIX文件指針
在UNIX中,文件和目錄完全不是同一個概念,雖然我們平時看著它們仿佛并列地躺在某個文件夾下。但實際上,目錄是樹狀結構組織的,而文件,卻是每個目錄指向某個文件的指針。并且,每個文件都有一個編號,可能會有多個目錄下的多個指針都指向同一個編號的文件。它們雖然存在于不同的目錄下,甚至名稱也不同,但是同一個編號的文件是完全相同的,修改也是同步的。如下圖所示:
我們可以做這樣的測試:
創建測試目錄并在其中的a.txt
寫入Hello World!
mkdir test && cd test && touch a.txt
vim a.txt # 寫入 Hello World!
創建a.txt
的硬鏈接b.txt
:
ln a.txt b.txt
我們查看兩個文件的內容,輸出顯示都是同樣的Hello World:
cat *.txt
# 輸出:
# Hello World!
# Hello World!
這時,我們修改b.txt
的內容為Hello World! Changed~
,再查看兩個文件的內容:
vim b.txt # 更改為 Hello World! Changed~
cat *.txt
# 輸出:
# Hello World! Changed~
# Hello World! Changed~
結果兩個文件都被修改了,這就是硬鏈接,我們可以通過-i
參數查看文件的編號:
ls -i
# 輸出:
# 8593746 a.txt 8593746 b.txt
可以看到,兩個文件其實是同一個編號的文件的不同鏈接。即硬鏈接的圖示如下:
硬鏈接
注意:
- 目錄中僅存儲指向文件數據的指針
- 允許一個文件被多個目錄引用
- 不能鏈接目錄 ?
- 不能跨文件系統 ?
小知識:其實所有的文件都是硬連接 (ls -i
查看)
- 刪除的系統調用稱為 “unlink” (引用計數)
應用場景
可以給文件起別名,同步,省空間。
需求:系統中可能有同一個運行庫的多個版本
libc-2.27.so
,libc-2.26.so
, …- 還需要一個 “當前版本的 libc”
- 程序需要鏈接 “
libc.so.6
” - 能否避免文件的一份拷貝?
- 程序需要鏈接 “
軟(symbolic)鏈接
軟鏈接:在文件里存儲一個 “跳轉提示”,相當于”快捷方式“。
- 軟鏈接也是一個文件
- 當引用這個文件時,去找另一個文件
- 另一個文件的絕對/相對路徑以文本形式存儲在文件里
- 可以跨文件系統、可以鏈接目錄、……好處多多
- 甚至,符號鏈接可以指向一個暫時不存在的文件或目錄,只要這個不存在文件或目錄將來某天存在了,這個符號鏈接就會生效
ln -s
創建軟鏈接,用的是symlink
系統調用。現在系統中/lib
下的共享庫,通常都是軟鏈接。
我們接著上面硬鏈接的例子來看一下二者的區別:
再在測試目錄下創建a.txt
的軟鏈接c.txt
:
ln -s a.txt c.txt
我們用-li參數查看測試目錄中的三個文件:
ls -li
# 輸出
# 8593746 -rw-rw-r-- 2 ps ps 22 10月 1 22:14 a.txt
# 8593746 -rw-rw-r-- 2 ps ps 22 10月 1 22:14 b.txt
# 8593742 lrwxrwxrwx 1 ps ps 5 10月 1 22:35 c.txt -> a.txt
在這里,b,c分別是a的硬、軟鏈接。可以看到,a和c的文件編號是不一樣的,因為它們是軟鏈接。但是,它們的修改仍然是同步的,因為我們在試圖修改c的時候,系統會順著上面輸出的軟鏈接箭頭去尋找,直到找到一個真實的文件或者目錄。我們還是來試一下:
vim c.txt # 修改為Hello World! Changed~ Soft~
cat *.txt
# 輸出:
# Hello World! Changed~ Soft~
# Hello World! Changed~ Soft~
# Hello World! Changed~ Soft~
與預期一致。此時測試目錄下的鏈接關系應該如下圖所示:
軟鏈接可能帶來的麻煩
軟鏈接可以隨意創建 (當前可能不合法;但未來可能合法),操作系統在處理軟鏈接時會執行路徑解析,,允許多次間接鏈接,會有意想不到的復雜性 ,a → b → c (遞歸解析)。可以創建軟連接的硬鏈接 (因為軟鏈接也是文件),通過ls -i
可以看到。
符號鏈接成環?ln -s . a
。所有處理符號鏈接的程序 (tree, find, …) 都要考慮遞歸的情況。它們默認遇到軟鏈接就跳過,如果加上-L參數強制使它們考慮軟鏈接的話,它們也會很小心的檢測成環和遞歸的情況并適時退出。
進程的 “當前目錄”
working/current directory
pwd
命令或$PWD
環境變量可以查看- 用
chdir
系統調用修改- 對應 shell 中的 cd
- 注意 cd 是 shell 的內部命令,不存在
/bin/cd
。不能被strace
問題:線程是共享 working directory, 還是各自獨立持有一個?
文件API(系統調用)
復習:文件和文件描述符
文件:虛擬的磁盤
- 磁盤是一個“字節序列”
- 支持讀/寫操作
文件描述符:進程訪問文件(操作系統對象)的 “指針”
- 通過open / pipe獲得
- 通過close釋放
- 通過dup / dup2復制
- fork時繼承
復習:mmap
將一整個文件映射到進程的地址空間。
使用 open 打開一個文件后
- 用
MAP_SHARED
將文件映射到地址空間中 - 用
MAP_PRIVATE
創建一個 copy-on-write 的副本
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); // 映射 fd 的 offset 開始的 length 字節
int munmap(void *addr, size_t length);
int msync(void *addr, size_t length, int flags);
小問題:
- 映射的長度超過文件大小會發生什么?
- (RTFM, “Errors” section):
SIGBUS
…- bus error 的常見來源 (M5)
- ftruncate 可以改變文件大小
- (RTFM, “Errors” section):
文件系統的游標(偏移量)
文件系統的游標(偏移量)介紹
文件的讀寫時,文件描述符自帶 “游標”,這樣就不用每次都指定文件讀/寫到哪里了,方便了程序員順序訪問文件
例子
read(fd, buf, 512);
- 第一個 512 字節read(fd, buf, 512);
- 第二個 512 字節lseek(fd, -1, SEEK_END);
- 最后一個字節
偏移量管理的問題 1
mmap, lseek, ftruncate 互相交互的情況
- 初始時文件大小為 0
- mmap (
length
= 2 MiB) - lseek to 3 MiB (
SEEK_SET
) - ftruncate to 1 MiB
- mmap (
在任何時刻,寫入數據的行為是什么?
- blog posts 不會告訴你全部
- RTFM & 做實驗!
偏移量管理的問題 2
我們知道,文件描述符在 fork 時會被子進程繼承。那父子進程應該共用偏移量,還是應該各自持有偏移量?
- 這決定了
offset
存儲在哪里
考慮應用場景
- 父子進程同時寫入文件
- 各自持有偏移量 → 父子進程需要協調偏移量的競爭
- (race condition)
- 共享偏移量 → 操作系統管理偏移量
- 雖然仍然共享,但操作系統保證
write
的原子性 ?
- 雖然仍然共享,但操作系統保證
- 各自持有偏移量 → 父子進程需要協調偏移量的競爭
偏移量管理:行為
操作系統的每一個 API 都可能和其他 API 有交互
- open 時,獲得一個獨立的 offset
- dup 時,兩個文件描述符共享 offset
- fork 時,父子進程共享 offset
- execve 時文件描述符不變
O_APPEND
方式打開的文件,偏移量永遠在最后 (無論是否 fork)- modification of the file offset and the write operation are performed as a single atomic step
這也是 fork 被批評的一個原因,(在當時) 好的設計可能成為系統演化過程中的包袱,今天的 fork 可謂是 “補丁滿滿”。
總結
本次課內容與目標
- 理解 “文件系統” 的設計
- 設備、文件和目錄
- mount, chdir, mkdir, rmdir, link, unlink, symlink, open, mmap, read, write, lseek, ftruncate, …
Takeaway messages
- 一個經典的設計:簡潔、通用、富有遠見
- 但無論多么有遠見,在時間面前都會千瘡百孔