linux內存(三)內核與用戶空間交互

來自網址http://www.kerneltravel.net/jiaoliu/005.htm   linux

用戶程序和內核的信息交換是雙向的,也就是說既能夠主動從用戶空間向內核空間發送信息,也能夠從內核空間向用戶空間提交數據。固然,用戶程序也能夠主動地從內核提取數據。下面咱們就針對內核和用戶交互數據的方法作一總結、概括。編程

   信息交互按信息傳輸發起方能夠分爲用戶向內核傳送/提取數據和內核向用戶空間提交請求兩大類,先來講說:由用戶級程序主動發起的信息交互安全

用戶級程序主動發起的信息交互

A編寫本身的系統調用

    系統調用是用戶級程序訪問內核最基本的方法。目前linux大體提供了二百多個標準的系統調用(參見內核代碼樹中的include/ asm-i386/unistd.h和arch/i386/kernel/entry.S文件),而且容許咱們添加本身的系統調用來實現和內核的信息交換。好比咱們但願創建一個系統調用日誌系統,將全部的系統調用動做記錄下來,以便進行入侵檢測。此時,咱們能夠編寫一個內核服務程序。該程序負責收集全部的系統調用請求,並將這些調用信息記錄到在內核中自建的緩衝裏。咱們沒法在內核裏實現複雜的入侵檢測程序,所以必須將該緩衝裏的記錄提取到用戶空間。最直截了當的方法是本身編寫一個新系統調用實現這種提取緩衝數據的功能。當內核服務程序和新系統調用都實現後,咱們就能夠在用戶空間裏編寫用戶程序進行入侵檢測任務了,入侵檢測程序能夠定時、輪訓或在須要的時候調用新系統調用從內核提取數據,而後進行入侵檢測了。服務器

B編寫驅動程序

    Linux/UNIX的一個特色就是把全部的東西都看做是文件(every thing is a file)。系統定義了簡潔完善的驅動程序界面,客戶程序能夠用統一的方法透過這個界面和內核驅動程序交互。而大部分系統的使用者和開發者已經很是熟悉這種界面以及相應的開發流程了。網絡

驅動程序運行於內核空間,用戶空間的應用程序經過文件系統中/dev/目錄下的一個文件來和它交互。這就是咱們熟悉的那個文件操做流程:open() —— read() —— write() —— ioctl() —— close()。(須要注意的是也不是全部的內核驅動程序都是這個界面,網絡驅動程序和各類協議棧的使用就不大一致,好比說套接口編程雖然也有open()close()等概念,但它的內核實現以及外部使用方式都和普通驅動程序有很大差別。)異步

設備驅動程序在內核中要作的中斷響應、設備管理、數據處理等等各類工做這篇文章不去關心,咱們把注意力集中在它與用戶級程序交互這一部分。操做系統爲此定義了一種統一的交互界面,就是前面所說的open(), read(), write(), ioctl()和close()等等。每一個驅動程序按照本身的須要作獨立實現,把本身提供的功能和服務隱藏在這個統一界面下。客戶級程序選擇須要的驅動程序或服務(其實就是選擇/dev/目錄下的文件),按照上述界面和文件操做流程,就能夠跟內核中的驅動交互了。其實用面向對象的概念會更容易解釋,系統定義了一個抽象的界面(abstract interface),每一個具體的驅動程序都是這個界面的實現(implementation)。函數

因此驅動程序也是用戶空間和內核信息交互的重要方式之一。其實ioctl, read, write本質上講也是經過系統調用去完成的,只是這些調用已被內核進行了標準封裝,統必定義。所以用戶沒必要向填加新系統調用那樣必須修改內核代碼,從新編譯新內核,使用虛擬設備只須要經過模塊方法將新的虛擬設備安裝到內核中(insmod上)就能方便使用。關於此方面設計細節請查閱參考資料5,編程細節請查閱參考資料6。工具

在linux中,設備大體可分爲:字符設備,塊設備,和網絡接口(字符設備包括那些必須以順序方式,像字節流同樣被訪問的設備;如字符終端,串口等。塊設備是指那些能夠用隨機方式,以整塊數據爲單位來訪問的設備,如硬盤等;網絡接口,就指一般網卡和協議棧等複雜的網絡輸入輸出服務)。若是將咱們的系統調用日誌系統用字符型驅動程序的方式實現,也是一件輕鬆愜意地工做。咱們能夠將內核中收集和記錄信息的那一部分編寫成一個字符設備驅動程序。雖然沒有實際對應的物理設備,但這並沒什麼問題:Linux的設備驅動程序原本就是一個軟件抽象,它能夠結合硬件提供服務,也徹底能夠做爲純軟件提供服務(固然,內存的使用咱們是沒法避免的)。在驅動程序中,咱們能夠用open來啓動服務,用read()返回處理好的記錄,用ioctl()設置記錄格式等,用close()中止服務,write()沒有用到,那麼咱們能夠不去實現它。而後在/dev/目錄下創建一個設備文件對應咱們新加入內核的系統調用日誌系統驅動程序。性能

C: 使用proc 文件系統

    proc是Linux提供的一種特殊的文件系統,推出它的目的就是提供一種便捷的用戶和內核間的交互方式。它以文件系統做爲使用界面,使應用程序能夠以文件操做的方式安全、方便的獲取系統當前運行的狀態和其它一些內核數據信息。spa

proc文件系統多用於監視、管理和調試系統,咱們使用的不少管理工具如ps,top等,都是利用proc來讀取內核信息的。除了讀取內核信息,proc文件系統還提供了寫入功能。因此咱們也就能夠利用它來向內核輸入信息。好比,經過修改proc文件系統下的系統參數配置文件(/proc/sys),咱們能夠直接在運行時動態更改內核參數;再如,經過下面這條指令:

echo 1 > /proc/sys/net/ip_v4/ip_forward

開啓內核中控制IP轉發的開關,咱們就可讓運行中的Linux系統啓用路由功能。相似的,還有許多內核選項能夠直接經過proc文件系統進行查詢和調整。

除了系統已經提供的文件條目,proc還爲咱們留有接口,容許咱們在內核中建立新的條目從而與用戶程序共享信息數據。好比,咱們能夠爲系統調用日誌程序(無論是做爲驅動程序也好,仍是做爲單純的內核模塊也好)在proc文件系統中建立新的文件條目,在此條目中顯示系統調用的使用次數,每一個單獨系統調用的使用頻率等等。咱們也能夠增長另外的條目,用於設置日誌記錄規則,好比說不記錄open系統調用的使用狀況等。關於proc文件系統得使用細節,請查閱參考資料7。

D: 使用虛擬文件系統

有些內核開發者認爲利用ioctl()系統調用每每會似的系統調用意義不明確,並且難控制。而將信息放入到proc文件系統中會使信息組織混亂,所以也不同意過多使用。他們建議實現一種孤立的虛擬文件系統來代替ioctl()和/proc,由於文件系統接口清楚,並且便於用戶空間訪問,同時利用虛擬文件系統使得利用腳本執行系統管理任務更家方便、有效。

咱們舉例來講如何經過虛擬文件系統修改內核信息。咱們能夠實現一個名爲sagafs的虛擬文件系統,其中文件log對應內核存儲的系統調用日誌。咱們能夠經過文件訪問特廣泛方法得到日誌信息:如

# cat /sagafs/log

使用虛擬文件系統——VFS實現信息交互使得系統管理更加方便、清晰。但有些編程者也許會說VFS 的API 接口複雜不容易掌握,不要擔憂2.5內核開始就提供了一種叫作libfs的例程序幫助不熟悉文件系統的用戶封裝了實現VFS的通用操做。有關利用VFS實現交互的方法看參考資料。

E: 使用內存映像

    Linux經過內存映像機制來提供用戶程序對內存直接訪問的能力。內存映像的意思是把內核中特定部分的內存空間映射到用戶級程序的內存空間去。也就是說,用戶空間和內核空間共享一塊相同的內存。這樣作的直觀效果顯而易見:內核在這塊地址內存儲變動的任何數據,用戶能夠當即發現和使用,根本無須數據拷貝。而在使用系統調用交互信息時,在整個操做過程當中必須有一步數據拷貝的工做——或者是把內核數據拷貝到用戶緩衝區,或只是把用戶數據拷貝到內核緩衝區——這對於許多數據傳輸量大、時間要求高的應用,這無疑是致命的一擊:許多應用根本就沒法忍受數據拷貝所耗費的時間和資源。

咱們曾經爲一塊高速採樣設備開發過驅動程序,該設備要求在20兆採樣率下以1KHz的重複頻率進行16位實時採樣,每毫秒須要採樣、DMA和處理的數據量驚人,若是要使用數據拷貝的方法,根本沒法達成要求。此時,內存映像成爲惟一的選擇:咱們在內存中保留了一塊空間,將其配置成環形隊列供採樣設備DMA輸出數據。再把這塊內存空間映射到在用戶空間運行的數據處理程序上,因而,採樣設備剛剛獲得並傳送到主機上的數據,立刻就能夠被用戶空間的程序處理。

實際上,內存影射方式一般也正是應用在那些內核和用戶空間須要快速大量交互數據的狀況下,特別是那些對實時性要求較強的應用。X window系統的服務器的虛擬內存區域,就能夠被看作是內存映像用法的一個典型例子:X服務器須要對視頻內存進行大量的數據交換,相對於lseek/write來講,將圖形顯示內存直接影射到用戶空間能夠顯著提升效能。

並非任何類型的應用都適合mmap,好比像串口和鼠標這些基於流數據的字符設備,mmap就沒有太大的用武之地。而且,這種共享內存的方式存在很差同步的問題。因爲沒有專門的同步機制可讓用戶程序和內核程序共享,因此在讀取和寫入數據時要有很是謹慎的設計以保證不會產生幹繞。

mmap徹底是基於共享內存的觀念了,也正由於此,它能提供額外的便利,但也特別難以控制。

內核主動發起的信息交互

從內核空間調用用戶程序

    即便在內核中,咱們有時也須要執行一些在用戶級才提供的操做:如打開某個文件以讀取特定數據,執行某個用戶程序從而完成某個功能。由於許多數據和功能在用戶空間是現有的或者已經被實現了,那麼沒有必要耗費大量的資源去重複。此外,內核在設計時,爲了擁有更好的彈性或者性能以支持未知但有可能發生的變化,自己就要求使用用戶空間的資源來配合完成任務。好比內核中動態加載模塊的部分須要調用kmod。但在編譯kmod的時候不可能把全部的內核模塊都訂下來(要是這樣的話動態加載模塊就沒有存在乎義了),因此它不可能知道在它之後纔出現的那些模塊的位置和加載方法。所以,模塊的動態加載就採用了以下策略:加載任務實際上由位於用戶空間的modprobe程序幫助完成——最簡單的情形是modprobe用內核傳過來的模塊名字做爲參數調用insmod。用這種方法來加載所須要的模塊。

內核中啓動用戶程序仍是要經過execve這個系統調用原形,只是此時的調用發生在內核空間,而通常的系統調用則在用戶空間進行。若是系統調用帶參數,那將會碰到一個問題:由於在系統調用的具體實現代碼中要檢查參數合法性,該檢查要求全部的參數必須位於用戶空間——地址處於0x0000000——0xC0000000之間,因此若是咱們從內核傳遞參數(地址大於0xC0000000),那麼檢查就會拒絕咱們的調用請求。爲了解決這個問題,咱們能夠利用set_fs宏來修改檢查策略,使得容許參數地址爲內核地址。這樣內核就能夠直接使用該系統調用了。

例如:在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宏。

B 利用brk系統調用來導出內核數據

內核和用戶空間傳遞數據主要是用get_user(ptr)和put_user(datum,ptr)例程。因此在大部分須要傳遞數據的系統調用中均可以找到它們的身影。但是,若是咱們不是經過用戶程序發起的系統調用——也就是說,沒有明確的提供用戶空間內的緩衝區位置——的狀況下,如何向用戶空間傳遞內核數據呢?

顯然,咱們不能再直接使用put_user()了,由於咱們沒有辦法給它指定目的緩衝區。因此,咱們要借用brk系統調用和當前進程空間:brk用於給進程設置堆空間的大小。每一個進程擁有一個獨立的堆空間,malloc等動態內存分配函數其實就是進程的堆空間中獲取內存的。咱們將利用brk在當前進程(current process)的堆空間上擴展一塊新的臨時緩衝區,再用put_user將內核數據導出到這個肯定的用戶空間去。

還記得剛纔咱們在內核中調用用戶程序的過程嗎?在那裏,咱們有一個跳過參數檢查的操做,如今有了這種方法,能夠另闢蹊徑了:咱們在當前進程的堆上擴展一塊空間,把系統調用要用到的參數經過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);

這種方法沒有通常性(具體的說,這種方法有負面效應嗎),只能做爲一種技巧,但咱們不難發現:若是你熟悉內核結構,就能夠作到不少意想不到的事情!

C: 使用信號:

    信號在內核裏的用途主要集中在通知用戶程序出現重大錯誤,強行殺死當前進程,這時內核經過發送SIGKILL信號通知進程終止,內核發送信號使用send_sign(pid,sig)例程,能夠看到信號發送必需要事先知道進程序號(pid),因此要想從內核中經過發信號的方式異步通知用戶進程執行某項任務,那麼必須事先知道用戶進程的進程號纔可。而內核運行時搜索到特定進程的進程號是個費事的工做,可能要遍歷整個進程控制塊鏈表。因此用信號通知特定用戶進程的方法很糟糕,通常在內核不會使用。內核中使用信號的情形只出如今通知當前進程(能夠從current變量中方便得到pid)作某些通用操做,如終止操做等。所以對內核開發者該方法用處不大。相似狀況還有消息操做。這裏不羅嗦了。

 

總結  由用戶級程序主動發起的信息交互,不管是採用標準的調用方式仍是透過驅動程序界面,通常都要用到系統調用。而由內核主動發起信息交互的狀況很少。也沒有標準的界面,操做大不方便。因此通常狀況下,儘量用本文描述的前幾種方法進行信息交互。畢竟,在設計的根源上,相對於客戶級程序,內核就被定義爲一個被動的服務提供者。所以,咱們本身的開發也應該儘可能遵循這種設計原則。

相關文章
相關標籤/搜索