摘要:在進行設備驅動程序,內核功能模塊等系統級開發時,通常需要在內核和用戶程序之間交換信息。Linux提供了多種方法可以用來完成這些任務。本文總結了各種常用的信息交換方法,並用簡單的例子演示這些方法各自的特點及用法。其中有大家非常熟悉的方法,也有特殊條件下方可使用的手段。通過對比明確這些方法,可以加深我們對Linux內核的認識,更重要的是,可以讓我們更熟練駕御linux內核級的應用開發技術。
相對的,其它部分被作為應用程序在用戶空間執行。它們只能看到允許它們使用的部分系統資源,並且不能使用某些特定的系統功能,不能直接訪問硬件,不能直接訪問內核空間,當然還有其他一些具體的使用限制。(Linux使用Intel體系的特權級0來運行用戶程序。)
從安全角度講將用戶空間和內核空間置於這種非對稱訪問機制下是很有效的,它能抵御惡意用戶的窺探,也能防止質量低劣的用戶程序的侵害,從而使系統運行得更穩定可靠。但是,如果像這樣完全不允許用戶程序訪問和使用內核空間的資源,那麼我們的系統就無法提供任何有意義的功能了。為了方便用戶程序使用在內核空間才能完全控制的資源,而又不違反上述的特權規定,從硬件體系結構本身到操作系統,都定義了標准的訪問界面。關於X86系統的細節,請查閱參考資料1
一般的硬件體系機構都提供一種“門”機制。“門”的含義是指在發生了特定事件的時候低特權的應用程序可以通過這些“門”進入高特權的內核空間。對於IntelX86體系來說,Linux操作系統正是利用了“系統門”這個硬件界面(通過調用int $0x80機器指令),構造了形形色色的系統調用作為軟件界面,為應用程序從用戶態陷入到內核態提供了通道。通過“系統調用”使用“系統門”並不需要特別的權限,但陷入到內核的具體位置卻不是隨意的,這個位置由“系統調用”來指定,有這樣的限制才能保證內核安全無虞。我們可以形象地描述這種機制:作為一個游客,你可以買票要求進入野生動物園,但你必須老老實實的坐在觀光車上,按照規定的路線觀光游覽。當然,不准下車,因為那樣太危險,不是讓你丟掉小命,就是讓你嚇壞了野生動物。
出於效率和代碼大小的考慮,內核程序不能使用標准庫函數(當然還有其它的顧慮,詳細原因請查閱參考資料2)因此內核開發不如用戶程序開發那麼方便。而且由於目前(linux2.6還沒正式發布)的內核是“非搶占”的,因此正在內核空間運行的進程是不會被其他進程取代的(除非該進程主動放棄CPU的控制,比如調用sleep(),schedule()等),所以無論是在進程上下文中(比如正在運行read系統調用),還是在中斷上下文(正在中斷服務程序中),內核程序都不能長時間占用CPU,否則其它程序將無法執行,只能等待。
由用戶級程序主動發起的信息交互。
驅動程序運行於內核空間,用戶空間的應用程序通過文件系統中/dev/目錄下的一個文件來和它交互。這就是我們熟悉的那個文件操作流程:open() —— read() —— write() —— ioctl() ——close()。(需要注意的是也不是所有的內核驅動程序都是這個界面,網絡驅動程序和各種協議棧的使用就不大一致,比如說套接口編程雖然也有open()close()等概念,但它的內核實現以及外部使用方式都和普通驅動程序有很大差異。)關於這部分的編程細節,請查閱參考資料3、4。
設備驅動程序在內核中要做的中斷響應、設備管理、數據處理等等各種工作這篇文章不去關心,我們把注意力集中在它與用戶級程序交互這一部分。操作系統為此定義了一種統一的交互界面,就是前面所說的open(), read(), write(), ioctl()和close()等等。每個驅動程序按照自己的需要做獨立實現,把自己提供的功能和服務隱藏在這個統一界面下。客戶級程序選擇需要的驅動程序或服務(其實就是選擇/dev/目錄下的文件),按照上述界面和文件操作流程,就可以跟內核中的驅動交互了。其實用面向對象的概念會更容易解釋,系統定義了一個抽象的界面(abstract
interface),每個具體的驅動程序都是這個界面的實現(implementation)。
所以驅動程序也是用戶空間和內核信息交互的重要方式之一。其實ioctl, read, write本質上講也是通過系統調用去完成的,只是這些調用已被內核進行了標准封裝,統一定義。因此用戶不必向填加新系統調用那樣必須修改內核代碼,重新編譯新內核,使用虛擬設備只需要通過模塊方法將新的虛擬設備安裝到內核中(insmod上)就能方便使用。關於此方面設計細節請查閱參考資料5,編程細節請查閱參考資料6。
在linux中,設備大致可分為:字符設備,塊設備,和網絡接口(字符設備包括那些必須以順序方式,像字節流一樣被訪問的設備;如字符終端,串口等。塊設備是指那些可以用隨機方式,以整塊數據為單位來訪問的設備,如硬盤等;網絡接口,就指通常網卡和協議棧等復雜的網絡輸入輸出服務)。如果將我們的系統調用日志系統用字符型驅動程序的方式實現,也是一件輕松惬意地工作。我們可以將內核中收集和記錄信息的那一部分編寫成一個字符設備驅動程序。雖然沒有實際對應的物理設備,但這並沒什麼問題:Linux的設備驅動程序本來就是一個軟件抽象,它可以結合硬件提供服務,也完全可以作為純軟件提供服務(當然,內存的使用我們是無法避免的)。在驅動程序中,我們可以用open來啟動服務,用read()返回處理好的記錄,用ioctl()設置記錄格式等,用close()停止服務,write()沒有用到,那麼我們可以不去實現它。然後在/dev/目錄下建立一個設備文件對應我們新加入內核的系統調用日志系統驅動程序。
echo 1 > /proc/sys/net/ip_v4/ip_forward
開啟內核中控制IP轉發的開關,我們就可以讓運行中的Linux系統啟用路由功能。類似的,還有許多內核選項可以直接通過proc文件系統進行查詢和調整。
除了系統已經提供的文件條目,proc還為我們留有接口,允許我們在內核中創建新的條目從而與用戶程序共享信息數據。比如,我們可以為系統調用日志程序(不管是作為驅動程序也好,還是作為單純的內核模塊也好)在proc文件系統中創建新的文件條目,在此條目中顯示系統調用的使用次數,每個單獨系統調用的使用頻率等等。我們也可以增加另外的條目,用於設置日志記錄規則,比如說不記錄open系統調用的使用情況等。關於proc文件系統得使用細節,請查閱參考資料7。
# cat /sagafs/log
使用虛擬文件系統——VFS實現信息交互使得系統管理更加方便、清晰。但有些編程者也許會說VFS 的API 接口復雜不容易掌握,不要擔心2.5內核開始就提供了一種叫做libfs的例程序幫助不熟悉文件系統的用戶封裝了實現VFS的通用操作。有關利用VFS實現交互的方法看參考資料。
實際上,內存影射方式通常也正是應用在那些內核和用戶空間需要快速大量交互數據的情況下,特別是那些對實時性要求較強的應用。X window系統的服務器的虛擬內存區域,就可以被看做是內存映像用法的一個典型例子:X服務器需要對視頻內存進行大量的數據交換,相對於lseek/write來說,將圖形顯示內存直接影射到用戶空間可以顯著提高效能。
並不是任何類型的應用都適合mmap,比如像串口和鼠標這些基於流數據的字符設備,mmap就沒有太大的用武之地。並且,這種共享內存的方式存在不好同步的問題。由於沒有專門的同步機制可以讓用戶程序和內核程序共享,所以在讀取和寫入數據時要有非常謹慎的設計以保證不會產生干繞。
mmap完全是基於共享內存的觀念了,也正因為此,它能提供額外的便利,但也特別難以控制。
例如:在kmod通過調用execve來執行modprobe的代碼前需要有set_fs(KERNEL_DS):
......
set_fs(KERNEL_DS);
/* Go, go, go... */
if (execve(program_path, argv, envp) < 0)
return -errno;
上述代碼中program_path 為"/sbin/modprobe",argv為{ modprobe_path, "-s", "-k", "--", (char*)module_name, NULL },envp為{ "HOME=/", "TERM=linux", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL }。
從內核中打開文件同樣使用帶參數的open系統調用,所需的仍是要先調用set_fs宏。
還記得剛才我們在內核中調用用戶程序的過程嗎?在那裡,我們有一個跳過參數檢查的操作,現在有了這種方法,可以另辟蹊徑了:我們在當前進程的堆上擴展一塊空間,把系統調用要用到的參數通過put_user()拷貝到新擴展得到的用戶空間裡,然後在調用execve的時候以這個新開辟空間地址作為參數,於是,參數檢查的障礙不復存在了。
char * program_path = "/bin/ls" ;
/* 找到當前堆頂的位置*/
mmm=current->mm->brk;
/* 用brk在堆頂上原擴展出一塊256字節的新緩沖區*/
ret = brk(*(void)(mmm+256));
/* 把execve需要用到的參數拷貝到新緩沖區上去*/
put_user((void*)2,program_path,strlen(program_path)+1);
/* 成功執行/bin/ls程序!*/
execve((char*)(mmm+2));
/* 恢復現場*/
tmp = brk((void*)mmm);
這種方法沒有一般性(具體的說,這種方法有負面效應嗎),只能作為一種技巧,但我們不難發現:如果你熟悉內核結構,就可以做到很多意想不到的事情!
總結 由用戶級程序主動發起的信息交互,無論是采用標准的調用方式還是透過驅動程序界面,一般都要用到系統調用。而由內核主動發起信息交互的情況不多。也沒有標准的界面,操作大不方便。所以一般情況下,盡可能用本文描述的前幾種方法進行信息交互。畢竟,在設計的根源上,相對於客戶級程序,內核就被定義為一個被動的服務提供者。因此,我們自己的開發也應該盡量遵循這種設計原