1 Linux 文件系統的守護神
傳統的 Linux 文件系統呈現給用戶程序的界面,確實是十分的干淨利落。用戶程序可以打開一個文件,向文件中線性的寫入數據,從文件的某一位置開始,線性的讀出數據,關閉一個文件,刪除一個文件,創建一個文件,等等。請看,只有這麼若干個簡潔的操作原語,可是卻能提供這麼多豐富的應用。但是,我們注意到,用於訪問 Linux 的文件系統的這些操作原語,並沒有提供非常復雜的加鎖解鎖的功能。這是一件很奇妙的事情,如果來自不同的用戶程序的請求發生了沖突怎麼辦呢?
我們不妨走的再靠近一點,仔細的看看刪除一個文件是怎樣進行的。如果已經有一個用戶程序在訪問一個文件,而另外一個用戶程序正好要刪除這一個文件,這時會發生些什麼呢?我們知道,Linux 的文件系統是基於所謂的 inode 的,每個文件都相伴有一個 inode。在 inode 中記錄了關於這個文件的一些系統信息,比如文件的所有者,文件相關的一些權限記錄,關於文件的若干個時間戳,等等。在內存中的 inode 還維持著一個關於自己的使用計數。每當一個 inode 所代表的文件被打開一次,這個 inode 就把關於自己的使用計數加一。每當這個 inode 所代表的文件一被關閉,這個 inode 就把關於自己的使用計數減一。當用戶程序刪除一個文件的時候,相關的系統調用很快就返回到這個用戶程序,告訴它,相應的文件已經被刪除了。但是相應的 inode 還是保留在系統中,inode 首先要檢查自己的使用計數,如果使用計數為零,那麼 Linux Kernel 才可以真正的去刪除這個文件。如果使用計數大於零,也就是說,還有其它的用戶程序在訪問這一個文件,那麼 Linux Kernel 需要等待這些其他的用戶程序一個個都完成對這一個文件的訪問才行。也就是說,要等到這個 inode 的使用計數掉到零,才能真正的去刪除這一個文件。
我們可以設想一下,如果有一個 MP3 播放程序在播放一首 MP3 音樂,我們覺得它不好聽,就到硬盤上找到這個文件,把它 rm 掉了。這時候,MP3 播放程序並不受到影響,還是可以繼續播放這首 MP3 音樂,雖然這時候在文件系統上用 ls 已經找不到這個 MP3 音樂文件了。實際上,一直要到 MP3 播放程序停止播放這首 MP3 音樂,然後 Linux 文件系統才真正的從硬盤上刪除這個 MP3 文件。這個經驗和我們在 Windows 平台上遇到的截然不同。
在 Windows 平台上,當我們試圖在文件夾窗口中用鼠標點擊右鍵菜單刪除 Winamp 正在播放的一首 MP3 音樂的時候,Windows 系統會用一個彈出對話框告訴我們,這個文件正在被使用,沒辦法刪除。Windows 系統的關於刪除文件的這樣一個解釋,如果使用不當的話,會帶來一個滑稽可笑的問題。我們可以設想一下,用戶的一個 P2P 的文件共享程序提供了一個 MP3 文件以供別人下載,恰巧這個 MP3 音樂文件十分的熱門,不斷的有人來下載,這個用戶最終決定要節省一下帶寬,想要把這個 MP3 音樂文件刪除掉,但是 Windows 系統卻不允許用戶這樣做,因為這個 P2P 的文件共享程序總是在使用這個 MP3 文件。用戶要想刪除這個文件,不得不先把 P2P 的文件共享程序給停下來!呵呵。
但是 Linux 的文件系統的操作原語也有它自己的問題。我們知道,在一個 Linux Shell 的命令行上,先 rm,然後再 ls,非常的干淨,被 rm 的文件沒有了,被刪除了。但是我們可以設想有一個圖形界面的文件管理程序,當用戶從 Shell 的命令行上 rm 掉一個文件的時候,這個圖形界面的文件管理程序並沒有收到任何人發給它的任何消息,它還以為什麼都沒有發生,被刪除掉的文件還在那兒。這實在是很 U.G.L.Y. 啊。
那麼要想解決這個問題,一個明顯的但是非常不好的辦法,就是讓一個後台進程 Daemon 每隔一個很短的時間間隔,就檢查一下文件系統上這個目錄的情況,看看有沒有發生什麼變化。這個辦法的缺點真的是顯而易見的,不但系統的性能受到影響,而且它的反應也還不是實時的。
如果我們需要用戶程序能夠實時地了解文件系統上某一個目錄的變化情況,從實時這個角度出發,顯然,我們需要有一個中斷機制。我們都知道,硬件中斷能夠實時地把系統某一個部件的情況反映給中央處理器,同樣的,要想把位於系統內核中的文件系統的情況實時地反映給用戶程序,我們也需要一個由操作系統內核到達用戶進程的軟件中斷機制。熟悉 Linux 系統編程的讀者朋友們立即就會想到,這個中斷機制在 Linux 系統中早已就有了,這就是信號傳遞 signal。
找到了信號傳遞這樣一個中斷用戶進程的機制,一切似乎都已齊備,看來可以動手實現這樣一個 Linux 文件系統的守護神,來實時地監視文件系統的變化情況,並且及時地把消息通知給用戶程序了。不過且慢,讓我們搜索一下 Linux Kernel,看看是否有別人也在做同樣的工作。哈哈,果不其然,原來這樣一個實時地監視文件系統情況的機制早已在 Linux 內核中實現了。下面一段就是取自 Linux Kernel 文檔的一段小小例程,說明了 Linux Kernel 中的 dnotify 功能的用法。dnotify 就是指 directory notification,監視文件系統上一個目錄中的情況。
#define _GNU_SOURCE /* needed to get the defines */ #include /* in glibc 2.2 this has the needed values defined */ #include #include #include static volatile int event_fd; // 信號處理例程 static void handler(int sig, siginfo_t *si, void *data) { event_fd = si->si_fd; } int main(void) { struct sigaction act; int fd; // 登記信號處理例程 act.sa_sigaction = handler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; sigaction(SIGRTMIN, &act, NULL); // 需要了解當前目錄"."的情況 fd = open(".", O_RDONLY); fcntl(fd, F_SETSIG, SIGRTMIN); fcntl(fd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_MULTISHOT); /* we will now be notified if any of the files in "." is modified or new files are created */ while (1) { // 收到信號後,就會執行信號處理例程。 // 而 pause() 也就結束了。 pause(); printf("Got event on fd=%d\n", event_fd); } }
上面這一小段例程,對於熟悉 Linux 系統編程的讀者朋友們來說,是很容易理解的。程序首先注冊一個信號處理例程,然後通知 Kernel,我要觀察 fd 上的 DN_MODIFY 和 DN_CREATE 和 DN_MULTISHOT 事件。(關於這些事件的詳細定義,請讀者朋友們參閱文後所列的參考資料。) Linux Kernel 收到這個請求後,把相應的 fd 的 inode 給做上記號,然後 Linux Kernel 和用戶應用程序就自顧自去處理各自的別的事情去了。等到 inode 上發生了相應的事件,Linux Kernel 就把信號發給用戶進程,於是開始執行信號處理例程,用戶程序對文件系統上的變化也就可以及時的做出反應了。而在這整個過程中,系統以及用戶程序的正常運行基本上未受到性能上的影響。這裡還需要說明的是,dnotify 並沒有通過增加新的系統調用來完成它的功能,而是通過 fcntl 來完成任務的。增加一個系統調用,相對來說是一個很大的手術,而且如果設計不當,處理得不好的話,傷疤會一直留在那裡,這是 Linux Kernel 的開發者們所非常不願意見到的事情。
2 Linux 文件系統的異步 I/O 擴展
對於桌面計算機系統來說,能夠快速的響應用戶的請求,這也是十分關鍵的。換句話說,當用戶移動鼠標的時候,不管系統正在進行什麼天大的、重要的、神聖的、不可打斷的工作,它都得立即停下,並且要讓鼠標立即流暢的在計算機屏幕上完美地運動起來。對於習慣在傳統的 Linux 命令行上工作的讀者朋友們來說,讓鼠標能夠在任何時間都可以在計算機屏幕上向無頭蒼蠅一樣地亂竄,竟然被當成是最重要的系統任務,這實在有一點讓人難以接受。不過,當你從 Linux 命令行上轉移到 GNOME 或者 KDE 這樣的圖形界面的用戶環境的時候,鼠標被鎖死,百分之百的也是會讓你失去理智的。所以,還是讓我們接受這一個現實,看一看如何才能增加系統的響應速度吧。
從文件系統的角度講,特別是考慮到網絡文件系統,它的響應速度有可能會相當的慢。當用戶在文件管理程序中,選擇了對文件進行某一個操作以後,文件系統可能會需要相當長的時間,才能完成這一操作。如果文件管理程序必須要等待文件系統完成這一操作,然後才能繼續的話,這顯然會給文件管理程序的用戶帶來非常不愉快的經歷。解決這一個問題的辦法,就是要實現異步的文件系統 I/O。
在 Linux 的 Gnome 桌面環境中,由 GnomeVFS 包裹了真正的 Linux 文件系統 I/O,實現了一個異步的文件系統 I/O 接口 API。我們可以看到下面這個用 GnomeVFS 打開文件的例子。
enum _GnomeVFSOpenMode { GNOME_VFS_OPEN_NONE = 0, GNOME_VFS_OPEN_READ = 1