Linux系统调用过程分析
參考:
《Linux內(nèi)核設(shè)計與實現(xiàn)》
0 摘要
linux的系統(tǒng)調(diào)用過程:
層次如下:
用戶程序------>C庫(即API):INT 0x80 ----->system_call------->系統(tǒng)調(diào)用服務(wù)例程-------->內(nèi)核程序
先說明一下,我們常說的用戶API其實就是系統(tǒng)提供的C庫。
系統(tǒng)調(diào)用是通過軟中斷指令 INT 0x80 實現(xiàn)的,而這條INT 0x80指令就被封裝在C庫的函數(shù)中。
(軟中斷和我們常說的硬中斷不同之處在于,軟中斷是由指令觸發(fā)的,而不是由硬件外設(shè)引起的。)
INT 0x80 這條指令的執(zhí)行會讓系統(tǒng)跳轉(zhuǎn)到一個預(yù)設(shè)的內(nèi)核空間地址,它指向系統(tǒng)調(diào)用處理程序,即system_call函數(shù)。
(注意:!!!系統(tǒng)調(diào)用處理程序system_call 并不是系統(tǒng)調(diào)用服務(wù)例程,系統(tǒng)調(diào)用服務(wù)例程是對一個具體的系統(tǒng)調(diào)用的內(nèi)核實現(xiàn)函數(shù),而系統(tǒng)調(diào)用處理程序是在執(zhí)行系統(tǒng)調(diào)用服務(wù)例程之前的一個引導(dǎo)過程,是針對INT 0x80這條指令,面向所有的系統(tǒng)調(diào)用的。簡單來講,執(zhí)行任何系統(tǒng)調(diào)用,都是先通過調(diào)用C庫中的函數(shù),這個函數(shù)里面就會有軟中斷 INT 0x80 語句,然后轉(zhuǎn)到執(zhí)行系統(tǒng)調(diào)用處理程序 system_call ,
system_call 再根據(jù)具體的系統(tǒng)調(diào)用號轉(zhuǎn)到執(zhí)行具體的系統(tǒng)調(diào)用服務(wù)例程。)
system_call函數(shù)是怎么找到具體的系統(tǒng)調(diào)用服務(wù)例程的呢?通過系統(tǒng)調(diào)用號查找系統(tǒng)調(diào)用表sys_call_table!軟中斷指令I(lǐng)NT 0x80執(zhí)行時,系統(tǒng)調(diào)用號會被放入 eax 寄存器中,system_call函數(shù)可以讀取eax寄存器獲取,然后將其乘以4,生成偏移地址,然后以sys_call_table為基址,基址加上偏移地址,就可以得到具體的系統(tǒng)調(diào)用服務(wù)例程的地址了!
然后就到了系統(tǒng)調(diào)用服務(wù)例程了。需要說明的是,系統(tǒng)調(diào)用服務(wù)例程只會從堆棧里獲取參數(shù),所以在system_call執(zhí)行前,會先將參數(shù)存放在寄存器中,system_call執(zhí)行時會首先將這些寄存器壓入堆棧。system_call退出后,用戶可以從寄存器中獲得(被修改過的)參數(shù)。
?
另外:系統(tǒng)調(diào)用通過軟中斷INT 0x80陷入內(nèi)核,跳轉(zhuǎn)到系統(tǒng)調(diào)用處理程序system_call函數(shù),然后執(zhí)行相應(yīng)的服務(wù)例程。但是由于是代表用戶進(jìn)程,所以這個執(zhí)行過程并不屬于中斷上下文,而是進(jìn)程上下文。因此,系統(tǒng)調(diào)用執(zhí)行過程中,可以訪問用戶進(jìn)程的許多信息,可以被其他進(jìn)程搶占,可以休眠。
當(dāng)系統(tǒng)調(diào)用完成后,把控制權(quán)交回到發(fā)起調(diào)用的用戶進(jìn)程前,內(nèi)核會有一次調(diào)度。如果發(fā)現(xiàn)有優(yōu)先級更高的進(jìn)程或當(dāng)前進(jìn)程的時間片用完,那么會選擇優(yōu)先級更高的進(jìn)程或重新選擇進(jìn)程執(zhí)行。
1 ? ? ? 系統(tǒng)調(diào)用意義
linux內(nèi)核中設(shè)置了一組用于實現(xiàn)系統(tǒng)功能的子程序,稱為系統(tǒng)調(diào)用。系統(tǒng)調(diào)用和普通庫函數(shù)調(diào)用非常相似,只是系統(tǒng)調(diào)用由操作系統(tǒng)核心提供,運行于核心態(tài),而普通的函數(shù)調(diào)用由函數(shù)庫或用戶自己提供,運行于用戶態(tài)。
?
一般的,進(jìn)程是不能訪問內(nèi)核的。它不能訪問內(nèi)核所占內(nèi)存空間也不能調(diào)用內(nèi)核函數(shù)。CPU硬件決定了這些(這就是為什么它被稱作"保護(hù)模式")。為了和用戶空間上運行的進(jìn)程進(jìn)行交互,內(nèi)核提供了一組接口。透過該接口,應(yīng)用程序可以訪問硬件設(shè)備和其他操作系統(tǒng)資源。這組接口在應(yīng)用程序和內(nèi)核之間扮演了使者的角色,應(yīng)用程序發(fā)送各種請求,而內(nèi)核負(fù)責(zé)滿足這些請求(或者讓應(yīng)用程序暫時擱置)。實際上提供這組接口主要是為了保證系統(tǒng)穩(wěn)定可靠,避免應(yīng)用程序肆意妄行,惹出大麻煩。
?
系統(tǒng)調(diào)用在用戶空間進(jìn)程和硬件設(shè)備之間添加了一個中間層。該層主要作用有三個:
(1) 它為用戶空間提供了一種統(tǒng)一的硬件的抽象接口。比如當(dāng)需要讀些文件的時候,應(yīng)用程序就可以不去管磁盤類型和介質(zhì),甚至不用去管文件所在的文件系統(tǒng)到底是哪種類型。
(2)系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定和安全。作為硬件設(shè)備和應(yīng)用程序之間的中間人,內(nèi)核可以基于權(quán)限和其他一些規(guī)則對需要進(jìn)行的訪問進(jìn)行裁決。舉例來說,這樣可以避免應(yīng)用程序不正確地使用硬件設(shè)備,竊取其他進(jìn)程的資源,或做出其他什么危害系統(tǒng)的事情。
(3) 每個進(jìn)程都運行在虛擬系統(tǒng)中,而在用戶空間和系統(tǒng)的其余部分提供這樣一層公共接口,也是出于這種考慮。如果應(yīng)用程序可以隨意訪問硬件而內(nèi)核又對此一無所知的話,幾乎就沒法實現(xiàn)多任務(wù)和虛擬內(nèi)存,當(dāng)然也不可能實現(xiàn)良好的穩(wěn)定性和安全性。在Linux中,系統(tǒng)調(diào)用是用戶空間訪問內(nèi)核的惟一手段;除異常和中斷外,它們是內(nèi)核惟一的合法入口。
?
2 ? ? ? API/POSIX/C庫的關(guān)系
一般情況下,應(yīng)用程序通過應(yīng)用編程接口(API)而不是直接通過系統(tǒng)調(diào)用來編程。這點很重要,因為應(yīng)用程序使用的這種編程接口實際上并不需要和內(nèi)核提供的系統(tǒng)調(diào)用一一對應(yīng)。一個API定義了一組應(yīng)用程序使用的編程接口。它們可以實現(xiàn)成一個系統(tǒng)調(diào)用,也可以通過調(diào)用多個系統(tǒng)調(diào)用來實現(xiàn),而完全不使用任何系統(tǒng)調(diào)用也不存在問題。實際上,API可以在各種不同的操作系統(tǒng)上實現(xiàn),給應(yīng)用程序提供完全相同的接口,而它們本身在這些系統(tǒng)上的實現(xiàn)卻可能迥異。
?
在Unix世界中,最流行的應(yīng)用編程接口是基于POSIX標(biāo)準(zhǔn)的,其目標(biāo)是提供一套大體上基于Unix的可移植操作系統(tǒng)標(biāo)準(zhǔn)。POSIX是說明API和系統(tǒng)調(diào)用之間關(guān)系的一個極好例子。在大多數(shù)Unix系統(tǒng)上,根據(jù)POSIX而定義的API函數(shù)和系統(tǒng)調(diào)用之間有著直接關(guān)系。
?
Linux的系統(tǒng)調(diào)用像大多數(shù)Unix系統(tǒng)一樣,作為C庫的一部分提供如下圖所示。C庫實現(xiàn)了 Unix系統(tǒng)的主要API,包括標(biāo)準(zhǔn)C庫函數(shù)和系統(tǒng)調(diào)用。所有的C程序都可以使用C庫,而由于C語言本身的特點,其他語言也可以很方便地把它們封裝起來使用。?
從程序員的角度看,系統(tǒng)調(diào)用無關(guān)緊要,他們只需要跟API打交道就可以了。相反,內(nèi)核只跟系統(tǒng)調(diào)用打交道;庫函數(shù)及應(yīng)用程序是怎么使用系統(tǒng)調(diào)用不是內(nèi)核所關(guān)心的。
?
關(guān)于Unix的界面設(shè)計有一句通用的格言“提供機制而不是策略”。換句話說,Unix的系統(tǒng)調(diào)用抽象出了用于完成某種確定目的的函數(shù)。至干這些函數(shù)怎么用完全不需要內(nèi)核去關(guān)心。區(qū)別對待機制(mechanism)和策略(policy)是Unix設(shè)計中的一大亮點。大部分的編程問題都可以被切割成兩個部分:“需要提供什么功能”(機制)和“怎樣實現(xiàn)這些功能”(策略)。?
3 ? ? ? 系統(tǒng)調(diào)用的實現(xiàn)
3.1 ? ?系統(tǒng)調(diào)用處理程序
您或許疑惑: “當(dāng)我輸入 cat /proc/cpuinfo 時,cpuinfo() 函數(shù)是如何被調(diào)用的?”內(nèi)核完成引導(dǎo)后,控制流就從相對直觀的“接下來調(diào)用哪個函數(shù)?”改變?yōu)槿Q于系統(tǒng)調(diào)用、異常和中斷。
?
用戶空間的程序無法直接執(zhí)行內(nèi)核代碼。它們不能直接調(diào)用內(nèi)核空間中的函數(shù),因為內(nèi)核駐留在受保護(hù)的地址空間上。如果進(jìn)程可以直接在內(nèi)核的地址空間上讀寫的話,系統(tǒng)安全就會失去控制。所以,應(yīng)用程序應(yīng)該以某種方式通知系統(tǒng),告訴內(nèi)核自己需要執(zhí)行一個系統(tǒng)調(diào)用,希望系統(tǒng)切換到內(nèi)核態(tài),這樣內(nèi)核就可以代表應(yīng)用程序來執(zhí)行該系統(tǒng)調(diào)用了。
?
通知內(nèi)核的機制是靠軟件中斷實現(xiàn)的。首先,用戶程序為系統(tǒng)調(diào)用設(shè)置參數(shù)。其中一個參數(shù)是系統(tǒng)調(diào)用編號。參數(shù)設(shè)置完成后,程序執(zhí)行“系統(tǒng)調(diào)用”指令。x86系統(tǒng)上的軟中斷由int產(chǎn)生。這個指令會導(dǎo)致一個異常:產(chǎn)生一個事件,這個事件會致使處理器切換到內(nèi)核態(tài)并跳轉(zhuǎn)到一個新的地址,并開始執(zhí)行那里的異常處理程序。此時的異常處理程序?qū)嶋H上就是系統(tǒng)調(diào)用處理程序。它與硬件體系結(jié)構(gòu)緊密相關(guān)。
?
新地址的指令會保存程序的狀態(tài),計算出應(yīng)該調(diào)用哪個系統(tǒng)調(diào)用,調(diào)用內(nèi)核中實現(xiàn)那個系統(tǒng)調(diào)用的函數(shù),恢復(fù)用戶程序狀態(tài),然后將控制權(quán)返還給用戶程序。系統(tǒng)調(diào)用是設(shè)備驅(qū)動程序中定義的函數(shù)最終被調(diào)用的一種方式。?
3.2 ? ?系統(tǒng)調(diào)用號
在Linux中,每個系統(tǒng)調(diào)用被賦予一個系統(tǒng)調(diào)用號。這樣,通過這個獨一無二的號就可以關(guān)聯(lián)系統(tǒng)調(diào)用。當(dāng)用戶空間的進(jìn)程執(zhí)行一個系統(tǒng)調(diào)用的時候,這個系統(tǒng)調(diào)用號就被用來指明到底是要執(zhí)行哪個系統(tǒng)調(diào)用。進(jìn)程不會提及系統(tǒng)調(diào)用的名稱。
?
系統(tǒng)調(diào)用號相當(dāng)關(guān)鍵,一旦分配就不能再有任何變更,否則編譯好的應(yīng)用程序就會崩潰。Linux有一個“未實現(xiàn)”系統(tǒng)調(diào)用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,這個錯誤號就是專門針對無效的系統(tǒng)調(diào)用而設(shè)的。
?
因為所有的系統(tǒng)調(diào)用陷入內(nèi)核的方式都一樣,所以僅僅是陷入內(nèi)核空間是不夠的。因此必須把系統(tǒng)調(diào)用號一并傳給內(nèi)核。在x86上,系統(tǒng)調(diào)用號是通過eax寄存器傳遞給內(nèi)核的。在陷人內(nèi)核之前,用戶空間就把相應(yīng)系統(tǒng)調(diào)用所對應(yīng)的號放入eax中了。這樣系統(tǒng)調(diào)用處理程序一旦運行,就可以從eax中得到數(shù)據(jù)。其他體系結(jié)構(gòu)上的實現(xiàn)也都類似。
?
內(nèi)核記錄了系統(tǒng)調(diào)用表中的所有已注冊過的系統(tǒng)調(diào)用的列表,存儲在sys_call_table中。它與體系結(jié)構(gòu)有關(guān),一般在entry.s中定義。這個表中為每一個有效的系統(tǒng)調(diào)用指定了惟一的系統(tǒng)調(diào)用號。sys_call_table是一張由指向?qū)崿F(xiàn)各種系統(tǒng)調(diào)用的內(nèi)核函數(shù)的函數(shù)指針組成的表:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open) /* 5 */
.long SYMBOL_NAME(sys_close)
.long SYMBOL_NAME(sys_waitpid)
。。。。。
.long SYMBOL_NAME(sys_capget)
.long SYMBOL_NAME(sys_capset) /* 185 */
.long SYMBOL_NAME(sys_sigaltstack)
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
?
system_call()函數(shù)通過將給定的系統(tǒng)調(diào)用號與NR_syscalls做比較來檢查其有效性。如果它大于或者等于NR syscalls,該函數(shù)就返回一ENOSYS。否則,就執(zhí)行相應(yīng)的系統(tǒng)調(diào)用。
? ? ? call *sys_ call-table(,%eax, 4)
由于系統(tǒng)調(diào)用表中的表項是以32位(4字節(jié))類型存放的,所以內(nèi)核需要將給定的系統(tǒng)調(diào)用號乘以4,然后用所得的結(jié)果在該表中查詢其位置
?
3.3 ? ?參數(shù)傳遞
除了系統(tǒng)調(diào)用號以外,大部分系統(tǒng)調(diào)用都還需要一些外部的參數(shù)輸人。所以,在發(fā)生異常的時候,應(yīng)該把這些參數(shù)從用戶空間傳給內(nèi)核。最簡單的辦法就是像傳遞系統(tǒng)調(diào)用號一樣把這些參數(shù)也存放在寄存器里。在x86系統(tǒng)上,ebx, ecx, edx, esi和edi按照順序存放前五個參數(shù)。需要六個或六個以上參數(shù)的情況不多見,此時,應(yīng)該用一個單獨的寄存器存放指向所有這些參數(shù)在用戶空間地址的指針。
?
給用戶空間的返回值也通過寄存器傳遞。在x86系統(tǒng)上,它存放在eax寄存器中。接下來許多關(guān)于系統(tǒng)調(diào)用處理程序的描述都是針對x86版本的。但不用擔(dān)心,所有體系結(jié)構(gòu)的實現(xiàn)都很類似。
?
3.4 ? ?參數(shù)驗證
系統(tǒng)調(diào)用必須仔細(xì)檢查它們所有的參數(shù)是否合法有效。舉例來說,與文件I/O相關(guān)的系統(tǒng)調(diào)用必須檢查文件描述符是否有效。與進(jìn)程相關(guān)的函數(shù)必須檢查提供的PID是否有效。必須檢查每個參數(shù),保證它們不但合法有效,而且正確。
?
最重要的一種檢查就是檢查用戶提供的指針是否有效。試想,如果一個進(jìn)程可以給內(nèi)核傳遞指針而又無須被檢查,那么它就可以給出一個它根本就沒有訪問權(quán)限的指針,哄騙內(nèi)核去為它拷貝本不允許它訪問的數(shù)據(jù),如原本屬于其他進(jìn)程的數(shù)據(jù)。在接收一個用戶空間的指針之前,內(nèi)核必須保證:
2 ? ? ?指針指向的內(nèi)存區(qū)域?qū)儆谟脩艨臻g。進(jìn)程決不能哄騙內(nèi)核去讀內(nèi)核空間的數(shù)據(jù)。
2 ? ? ?指針指向的內(nèi)存區(qū)域在進(jìn)程的地址空間里。進(jìn)程決不能哄騙內(nèi)核去讀其他進(jìn)程的數(shù)據(jù)。
2 ? ? ?如果是讀,該內(nèi)存應(yīng)被標(biāo)記為可讀。如果是寫,該內(nèi)存應(yīng)被標(biāo)記為可寫。進(jìn)程決不能繞過內(nèi)存訪問限制。
?
內(nèi)核提供了兩個方法來完成必須的檢查和內(nèi)核空間與用戶空間之間數(shù)據(jù)的來回拷貝。注意,內(nèi)核無論何時都不能輕率地接受來自用戶空間的指針!這兩個方法中必須有一個被調(diào)用。為了向用戶空間寫入數(shù)據(jù),內(nèi)核提供了copy_to_user(),它需要三個參數(shù)。第一個參數(shù)是進(jìn)程空間中的目的內(nèi)存地址。第二個是內(nèi)核空間內(nèi)的源地址。最后一個參數(shù)是需要拷貝的數(shù)據(jù)長度(字節(jié)數(shù))。
?
為了從用戶空間讀取數(shù)據(jù),內(nèi)核提供了copy_from_ user(),它和copy-to-User()相似。該函數(shù)把第二個參數(shù)指定的位置上的數(shù)據(jù)拷貝到第一個參數(shù)指定的位置上,拷貝的數(shù)據(jù)長度由第三個參數(shù)決定。
?
如果執(zhí)行失敗,這兩個函數(shù)返回的都是沒能完成拷貝的數(shù)據(jù)的字節(jié)數(shù)。如果成功,返回0。當(dāng)出現(xiàn)上述錯誤時,系統(tǒng)調(diào)用返回標(biāo)準(zhǔn)-EFAULT。
?
注意copy_to_user()和copy_from_user()都有可能引起阻塞。當(dāng)包含用戶數(shù)據(jù)的頁被換出到硬盤上而不是在物理內(nèi)存上的時候,這種情況就會發(fā)生。此時,進(jìn)程就會休眠,直到缺頁處理程序?qū)⒃擁搹挠脖P重新?lián)Q回物理內(nèi)存。
?
3.5 ? ?系統(tǒng)調(diào)用的返回值
系統(tǒng)調(diào)用(在Linux中常稱作syscalls)通常通過函數(shù)進(jìn)行調(diào)用。它們通常都需要定義一個或幾個參數(shù)(輸入)而且可能產(chǎn)生一些副作用,例如寫某個文件或向給定的指針拷貝數(shù)據(jù)等等。為防止和正常的返回值混淆,系統(tǒng)調(diào)用并不直接返回錯誤碼,而是將錯誤碼放入一個名為errno的全局變量中。通常用一個負(fù)的返回值來表明錯誤。返回一個0值通常表明成功。如果一個系統(tǒng)調(diào)用失敗,你可以讀出errno的值來確定問題所在。通過調(diào)用perror()庫函數(shù),可以把該變量翻譯成用戶可以理解的錯誤字符串。
?
errno不同數(shù)值所代表的錯誤消息定義在errno.h中,你也可以通過命令"man 3 errno"來察看它們。需要注意的是,errno的值只在函數(shù)發(fā)生錯誤時設(shè)置,如果函數(shù)不發(fā)生錯誤,errno的值就無定義,并不會被置為0。另外,在處理errno前最好先把它的值存入另一個變量,因為在錯誤處理過程中,即使像printf()這樣的函數(shù)出錯時也會改變errno的值。
?
當(dāng)然,系統(tǒng)調(diào)用最終具有一種明確的操作。舉例來說,如getpid()系統(tǒng)調(diào)用,根據(jù)定義它會返回當(dāng)前進(jìn)程的PID。內(nèi)核中它的實現(xiàn)非常簡單:
asmlinkage long sys_ getpid(void)
{
? ? return current-> tgid;
}
?
上述的系統(tǒng)調(diào)用盡管非常簡單,但我們還是可以從中發(fā)現(xiàn)兩個特別之處。首先,注意函數(shù)聲明中的asmlinkage限定詞,這是一個小戲法,用于通知編譯器僅從棧中提取該函數(shù)的參數(shù)。所有的系統(tǒng)調(diào)用都需要這個限定詞。其次,注意系統(tǒng)調(diào)用get_pid()在內(nèi)核中被定義成sys_ getpid。這是Linux中所有系統(tǒng)調(diào)用都應(yīng)該遵守的命名規(guī)則
?
4 ? ? ? 添加新系統(tǒng)調(diào)用
給Linux添加一個新的系統(tǒng)調(diào)用是件相對容易的工作。怎樣設(shè)計和實現(xiàn)一個系統(tǒng)調(diào)用是難題所在,而把它加到內(nèi)核里卻無須太多周折。讓我們關(guān)注一下實現(xiàn)一個新的Linux系統(tǒng)調(diào)用所需的步驟。
?
實現(xiàn)一個新的系統(tǒng)調(diào)用的第一步是決定它的用途。它要做些什么?每個系統(tǒng)調(diào)用都應(yīng)該有一個明確的用途。在Linux中不提倡采用多用途的系統(tǒng)調(diào)用(一個系統(tǒng)調(diào)用通過傳遞不同的參數(shù)值來選擇完成不同的工作)。ioctl()就應(yīng)該被視為一個反例。
?
新系統(tǒng)調(diào)用的參數(shù)、返回值和錯誤碼又該是什么呢?系統(tǒng)調(diào)用的接口應(yīng)該力求簡潔,參數(shù)盡可能少。設(shè)計接口的時候要盡量為將來多做考慮。你是不是對函數(shù)做了不必要的限制?系統(tǒng)調(diào)用設(shè)計得越通用越好。不要假設(shè)這個系統(tǒng)調(diào)用現(xiàn)在怎么用將來也一定就是這么用。系統(tǒng)調(diào)用的目的可能不變,但它的用法卻可能改變。這個系統(tǒng)調(diào)用可移植嗎?別對機器的字節(jié)長度和字節(jié)序做假設(shè)。當(dāng)你寫一個系統(tǒng)調(diào)用的時候,要時刻注意可移植性和健壯性,不但要考慮當(dāng)前,還要為將來做打算。
?
當(dāng)編寫完一個系統(tǒng)調(diào)用后,把它注冊成一個正式的系統(tǒng)調(diào)用是件瑣碎的工作:
在系統(tǒng)調(diào)用表的最后加入一個表項。每種支持該系統(tǒng)調(diào)用的硬件體系都必須做這樣的工作。從0開始算起,系統(tǒng)調(diào)用在該表中的位置就是它的系統(tǒng)調(diào)用號。
對于所支持的各種體系結(jié)構(gòu),系統(tǒng)調(diào)用號都必須定義于<asm/unistd.h>中。
系統(tǒng)調(diào)用必須被編譯進(jìn)內(nèi)核映象(不能被編譯成模塊)。這只要把它放進(jìn)kernel/下的一個相關(guān)文件中就可以。
?
讓我們通過一個虛構(gòu)的系統(tǒng)調(diào)用f00()來仔細(xì)觀察一下這些步驟。首先,我們要把sys_foo加入到系統(tǒng)調(diào)用表中去。對于大多數(shù)體系結(jié)構(gòu)來說,該表位干entry.s文件中,形式如下:
ENTRY(sys_ call_ table)
? ? ? ·long sys_ restart_ syscall/*0*/
? ? ? .long sys_ exit
? ? ? ·long sys_ fork
? ? ? ·long sys_ read
? ? ? .long sys_write
我們把新的系統(tǒng)調(diào)用加到這個表的末尾:
? ? ?.long sys_foo
雖然沒有明確地指定編號,但我們加入的這個系統(tǒng)調(diào)用被按照次序分配給了283這個系統(tǒng)調(diào)用號。對于每種需要支持的體系結(jié)構(gòu),我們都必須將自己的系統(tǒng)調(diào)用加人到其系統(tǒng)調(diào)用表中去。每種體系結(jié)構(gòu)不需要對應(yīng)相同的系統(tǒng)調(diào)用號。
?
接下來,我們把系統(tǒng)調(diào)用號加入到<asm/unistd.h>中,它的格式如下:
/*本文件包含系統(tǒng)調(diào)用號*/
#define_ NR_ restart_ syscall
#define NR exit
#define NR fork
#define NR read
#define NR write
#define NR- mq getsetattr 282
然后,我們在該列表中加入下面這行:
#define_ NR_ foo 283
?
最后,我們來實現(xiàn)f00()系統(tǒng)調(diào)用。無論何種配置,該系統(tǒng)調(diào)用都必須編譯到核心的內(nèi)核映象中去,所以我們把它放進(jìn)kernel/sys.c文件中。你也可以將其放到與其功能聯(lián)系最緊密的代碼中去
?
asmlinkage long sys-foo(void)
{
return THREAD SIZE
)
就是這樣!嚴(yán)格說來,現(xiàn)在就可以在用戶空間調(diào)用f00()系統(tǒng)調(diào)用了。
?
建立一個新的系統(tǒng)調(diào)用非常容易,但卻絕不提倡這么做。通常模塊可以更好的代替新建一個系統(tǒng)調(diào)用。
?
5 ? ? ? 訪問系統(tǒng)調(diào)用
5.1 ? ?系統(tǒng)調(diào)用上下文
內(nèi)核在執(zhí)行系統(tǒng)調(diào)用的時候處于進(jìn)程上下文。current指針指向當(dāng)前任務(wù),即引發(fā)系統(tǒng)調(diào)用的那個進(jìn)程。
?
在進(jìn)程上下文中,內(nèi)核可以休眠并且可以被搶占。這兩點都很重要。首先,能夠休眠說明系統(tǒng)調(diào)用可以使用內(nèi)核提供的絕大部分功能。休眠的能力會給內(nèi)核編程帶來極大便利。在進(jìn)程上下文中能夠被搶占,其實表明,像用戶空間內(nèi)的進(jìn)程一樣,當(dāng)前的進(jìn)程同樣可以被其他進(jìn)程搶占。因為新的進(jìn)程可以使用相同的系統(tǒng)調(diào)用,所以必須小心,保證該系統(tǒng)調(diào)用是可重人的。當(dāng)然,這也是在對稱多處理中必須同樣關(guān)心的問題。
?
當(dāng)系統(tǒng)調(diào)用返回的時候,控制權(quán)仍然在system_call()中,它最終會負(fù)責(zé)切換到用戶空間并讓用戶進(jìn)程繼續(xù)執(zhí)行下去。
?
5.2 ? ?系統(tǒng)調(diào)用訪問示例
操作系統(tǒng)使用系統(tǒng)調(diào)用表將系統(tǒng)調(diào)用編號翻譯為特定的系統(tǒng)調(diào)用。系統(tǒng)調(diào)用表包含有實現(xiàn)每個系統(tǒng)調(diào)用的函數(shù)的地址。例如,read() 系統(tǒng)調(diào)用函數(shù)名為 sys_read。read() 系統(tǒng)調(diào)用編號是 3,所以 sys_read() 位于系統(tǒng)調(diào)用表的第四個條目中(因為系統(tǒng)調(diào)用起始編號為0)。從地址 sys_call_table + (3 * word_size) 讀取數(shù)據(jù),得到 sys_read() 的地址。
?
找到正確的系統(tǒng)調(diào)用地址后,它將控制權(quán)轉(zhuǎn)交給那個系統(tǒng)調(diào)用。我們來看定義 sys_read() 的位置,即 fs/read_write.c 文件。這個函數(shù)會找到關(guān)聯(lián)到 fd 編號(傳遞給 read() 函數(shù)的)的文件結(jié)構(gòu)體。那個結(jié)構(gòu)體包含指向用來讀取特定類型文件數(shù)據(jù)的函數(shù)的指針。進(jìn)行一些檢查后,它調(diào)用與文件相關(guān)的 read() 函數(shù),來真正從文件中讀取數(shù)據(jù)并返回。與文件相關(guān)的函數(shù)是在其他地方定義的 —— 比如套接字代碼、文件系統(tǒng)代碼,或者設(shè)備驅(qū)動程序代碼。這是特定內(nèi)核子系統(tǒng)最終與內(nèi)核其他部分協(xié)作的一個方面。
?
讀取函數(shù)結(jié)束后,從 sys_read() 返回,它將控制權(quán)切換給 ret_from_sys。它會去檢查那些在切換回用戶空間之前需要完成的任務(wù)。如果沒有需要做的事情,那么就恢復(fù)用戶進(jìn)程的狀態(tài),并將控制權(quán)交還給用戶程序。
5.3 ? ?從用戶空間直接訪問系統(tǒng)調(diào)用
通常,系統(tǒng)調(diào)用靠C庫支持。用戶程序通過包含標(biāo)準(zhǔn)頭文件并和C庫鏈接,就可以使用系統(tǒng)調(diào)用(或者調(diào)用庫函數(shù),再由庫函數(shù)實際調(diào)用)。但如果你僅僅寫出系統(tǒng)調(diào)用,glibc庫恐怕并不提供支持。值得慶幸的是,Linux本身提供了一組宏,用于直接對系統(tǒng)調(diào)用進(jìn)行訪問。它會設(shè)置好寄存器并調(diào)用陷人指令。這些宏是_syscalln(),其中n的范圍從0到6。代表需要傳遞給系統(tǒng)調(diào)用的參數(shù)個數(shù),這是由于該宏必須了解到底有多少參數(shù)按照什么次序壓入寄存器。舉個例子,open()系統(tǒng)調(diào)用的定義是:
long open(const char *filename, int flags, int mode)
而不靠庫支持,直接調(diào)用此系統(tǒng)調(diào)用的宏的形式為:
#define NR_ open 5
syscall3(long, open, const char*,filename, int, flags, int, mode)
這樣,應(yīng)用程序就可以直接使用open()
?
對于每個宏來說,都有2+ n個參數(shù)。第一個參數(shù)對應(yīng)著系統(tǒng)調(diào)用的返回值類型。第二個參數(shù)是系統(tǒng)調(diào)用的名稱。再以后是按照系統(tǒng)調(diào)用參數(shù)的順序排列的每個參數(shù)的類型和名稱。_NR_ open在<asm/unistd.h>中定義,是系統(tǒng)調(diào)用號。該宏會被擴展成為內(nèi)嵌匯編的C函數(shù)。由匯編語言執(zhí)行前一節(jié)所討論的步驟,將系統(tǒng)調(diào)用號和參數(shù)壓入寄存器并觸發(fā)軟中斷來陷入內(nèi)核。調(diào)用open()系統(tǒng)調(diào)用直接把上面的宏放置在應(yīng)用程序中就可以了。
?
讓我們寫一個宏來使用前面編寫的foo()系統(tǒng)調(diào)用,然后再寫出測試代碼炫耀一下我們所做的努力。
#define NR foo 283
_sysca110(long, foo)
int main()
{
long stack size;
stack_ size=foo();
printf("The kernel stack
size is 81d\n",stack_ size);
return;
}
6 實際使用的注意
(1)系統(tǒng)調(diào)用是需要提前編譯固化到內(nèi)核中的,而且需要官方分配一個系統(tǒng)調(diào)用號
(2)需要將系統(tǒng)調(diào)用注冊到支持的每一種體系結(jié)構(gòu)中
(3)系統(tǒng)調(diào)用一般不能在腳本中直接訪問
(4)盡量避免新建系統(tǒng)調(diào)用,可用創(chuàng)建設(shè)備結(jié)點的方法代替。
總結(jié)
以上是生活随笔為你收集整理的Linux系统调用过程分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 函数名/函数地址/函数指针
- 下一篇: Linux信号处理机制