本文介紹了系統(tǒng)調(diào)用的一些實現(xiàn)細節(jié)。首先分析了系統(tǒng)調(diào)用的意義,它們與庫函數(shù)和應(yīng)用程序接口(API)有怎樣的關(guān)系。然后,我們考察了Linux內(nèi)核如何實現(xiàn)系統(tǒng)調(diào)用,以及執(zhí)行系統(tǒng)調(diào)用的連鎖反應(yīng):陷入內(nèi)核,傳遞系統(tǒng)調(diào)用號和參數(shù),執(zhí)行正確的系統(tǒng)調(diào)用函數(shù),并把返回值帶回用戶空間。最后討論了如何增加系統(tǒng)調(diào)用,并提供了從用戶空間訪問系統(tǒng)調(diào)用的簡單例子。
系統(tǒng)調(diào)用概述
計算機系統(tǒng)的各種硬件資源是有限的,在現(xiàn)代多任務(wù)操作系統(tǒng)上同時運行的多個進程都需要訪問這些資源,為了更好的管理這些資源進程是不允許直接操作的,所有對這些資源的訪問都必須有操作系統(tǒng)控制。也就是說操作系統(tǒng)是使用這些資源的唯一入口,而這個入口就是操作系統(tǒng)提供的系統(tǒng)調(diào)用(System Call)。在linux中系統(tǒng)調(diào)用是用戶空間訪問內(nèi)核的唯一手段,除異常和陷入外,他們是內(nèi)核唯一的合法入口。
一般情況下應(yīng)用程序通過應(yīng)用編程接口API,而不是直接通過系統(tǒng)調(diào)用來編程。在Unix世界,最流行的API是基于POSIX標(biāo)準(zhǔn)的。
操作系統(tǒng)一般是通過中斷從用戶態(tài)切換到內(nèi)核態(tài)。中斷就是一個硬件或軟件請求,要求CPU暫停當(dāng)前的工作,去處理更重要的事情。比如,在x86機器上可以通過int指令進行軟件中斷,而在磁盤完成讀寫操作后會向CPU發(fā)起硬件中斷。
中斷有兩個重要的屬性,中斷號和中斷處理程序。中斷號用來標(biāo)識不同的中斷,不同的中斷具有不同的中斷處理程序。在操作系統(tǒng)內(nèi)核中維護著一個中斷向量表(Interrupt Vector Table),這個數(shù)組存儲了所有中斷處理程序的地址,而中斷號就是相應(yīng)中斷在中斷向量表中的偏移量。
一般地,系統(tǒng)調(diào)用都是通過軟件中斷實現(xiàn)的,x86系統(tǒng)上的軟件中斷由int $0x80指令產(chǎn)生,而128號異常處理程序就是系統(tǒng)調(diào)用處理程序system_call(),它與硬件體系有關(guān),在entry.S中用匯編寫。接下來就來看一下Linux下系統(tǒng)調(diào)用具體的實現(xiàn)過程。
為什么需要系統(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)核心提供,運行于內(nèi)核態(tài),而普通的函數(shù)調(diào)用由函數(shù)庫或用戶自己提供,運行于用戶態(tài)。
一般的,進程是不能訪問內(nèi)核的。它不能訪問內(nèi)核所占內(nèi)存空間也不能調(diào)用內(nèi)核函數(shù)。CPU硬件決定了這些(這就是為什么它被稱作“保護模式”
為了和用戶空間上運行的進程進行交互,內(nèi)核提供了一組接口。透過該接口,應(yīng)用程序可以訪問硬件設(shè)備和其他操作系統(tǒng)資源。這組接口在應(yīng)用程序和內(nèi)核之間扮演了使者的角色,應(yīng)用程序發(fā)送各種請求,而內(nèi)核負責(zé)滿足這些請求(或者讓應(yīng)用程序暫時擱置)。實際上提供這組接口主要是為了保證系統(tǒng)穩(wěn)定可靠,避免應(yīng)用程序肆意妄行,惹出大麻煩。
系統(tǒng)調(diào)用在用戶空間進程和硬件設(shè)備之間添加了一個中間層。該層主要作用有三個:
它為用戶空間提供了一種統(tǒng)一的硬件的抽象接口。比如當(dāng)需要讀些文件的時候,應(yīng)用程序就可以不去管磁盤類型和介質(zhì),甚至不用去管文件所在的文件系統(tǒng)到底是哪種類型。
系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定和安全。作為硬件設(shè)備和應(yīng)用程序之間的中間人,內(nèi)核可以基于權(quán)限和其他一些規(guī)則對需要進行的訪問進行裁決。舉例來說,這樣可以避免應(yīng)用程序不正確地使用硬件設(shè)備,竊取其他進程的資源,或做出其他什么危害系統(tǒng)的事情。
每個進程都運行在虛擬系統(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)核惟一的合法入口。
API/POSIX/C庫的區(qū)別與聯(liá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)這些功能”(策略)。
區(qū)別
api是函數(shù)的定義,規(guī)定了這個函數(shù)的功能,跟內(nèi)核無直接關(guān)系。而系統(tǒng)調(diào)用是通過中斷向內(nèi)核發(fā)請求,實現(xiàn)內(nèi)核提供的某些服務(wù)。
聯(lián)系
一個api可能會需要一個或多個系統(tǒng)調(diào)用來完成特定功能。通俗點說就是如果這個api需要跟內(nèi)核打交道就需要系統(tǒng)調(diào)用,否則不需要。
程序員調(diào)用的是API(API函數(shù)),然后通過與系統(tǒng)調(diào)用共同完成函數(shù)的功能。
因此,API是一個提供給應(yīng)用程序的接口,一組函數(shù),是與程序員進行直接交互的。
系統(tǒng)調(diào)用則不與程序員進行交互的,它根據(jù)API函數(shù),通過一個軟中斷機制向內(nèi)核提交請求,以獲取內(nèi)核服務(wù)的接口。
并不是所有的API函數(shù)都一一對應(yīng)一個系統(tǒng)調(diào)用,有時,一個API函數(shù)會需要幾個系統(tǒng)調(diào)用來共同完成函數(shù)的功能,甚至還有一些API函數(shù)不需要調(diào)用相應(yīng)的系統(tǒng)調(diào)用(因此它所完成的不是內(nèi)核提供的服務(wù))
系統(tǒng)調(diào)用的實現(xiàn)原理
基本機制
前文已經(jīng)提到了Linux下的系統(tǒng)調(diào)用是通過0x80實現(xiàn)的,但是我們知道操作系統(tǒng)會有多個系統(tǒng)調(diào)用(Linux下有319個系統(tǒng)調(diào)用),而對于同一個中斷號是如何處理多個不同的系統(tǒng)調(diào)用的?最簡單的方式是對于不同的系統(tǒng)調(diào)用采用不同的中斷號,但是中斷號明顯是一種稀缺資源,Linux顯然不會這么做;還有一個問題就是系統(tǒng)調(diào)用是需要提供參數(shù),并且具有返回值的,這些參數(shù)又是怎么傳遞的?也就是說,對于系統(tǒng)調(diào)用我們要搞清楚兩點:
系統(tǒng)調(diào)用的函數(shù)名稱轉(zhuǎn)換。
系統(tǒng)調(diào)用的參數(shù)傳遞。
首先看第一個問題。實際上,Linux中每個系統(tǒng)調(diào)用都有相應(yīng)的系統(tǒng)調(diào)用號作為唯一的標(biāo)識,內(nèi)核維護一張系統(tǒng)調(diào)用表,sys_call_table,表中的元素是系統(tǒng)調(diào)用函數(shù)的起始地址,而系統(tǒng)調(diào)用號就是系統(tǒng)調(diào)用在調(diào)用表的偏移量。在x86上,系統(tǒng)調(diào)用號是通過eax寄存器傳遞給內(nèi)核的。比如fork()的實現(xiàn):
用戶空間的程序無法直接執(zhí)行內(nèi)核代碼。它們不能直接調(diào)用內(nèi)核空間中的函數(shù),因為內(nèi)核駐留在受保護的地址空間上。如果進程可以直接在內(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)用的一種方式。
從系統(tǒng)分析的角度,linux的系統(tǒng)調(diào)用涉及4個方面的問題。
響應(yīng)函數(shù)sys_xxx
響應(yīng)函數(shù)名以“sys_”開頭,后跟該系統(tǒng)調(diào)用的名字。
例如 系統(tǒng)調(diào)用fork()的響應(yīng)函數(shù)是sys_fork()(見Kernel/fork.c), exit()的響應(yīng)函數(shù)是sys_exit()(見kernel/fork.)。
系統(tǒng)調(diào)用表與系統(tǒng)調(diào)用號-=>數(shù)組與下標(biāo)
文件include/asm/unisted.h為每個系統(tǒng)調(diào)用規(guī)定了唯一的編號。
?
在我們系統(tǒng)中/usr/include/asm/unistd_32.h,可以通過find / -name unistd_32.h -print查找) 而內(nèi)核中的頭文件路徑不同的內(nèi)核版本以及不同的發(fā)行版,文件的存儲結(jié)構(gòu)可能有所區(qū)別
?
?
?
?
假設(shè)用name表示系統(tǒng)調(diào)用的名稱,那么系統(tǒng)調(diào)用號與系統(tǒng)調(diào)用響應(yīng)函數(shù)的關(guān)系是:以系統(tǒng)調(diào)用號_NR_name作為下標(biāo),可找出系統(tǒng)調(diào)用表sys_call_table(見arch/i386/kernel/entry.S)中對應(yīng)表項的內(nèi)容,它正好是該系統(tǒng)調(diào)用的響應(yīng)函數(shù)sys_name的入口地址。 系統(tǒng)調(diào)用表sys_call_table記錄了各sys_name函數(shù)在表中的位置,共190項。有了這張表,就很容易根據(jù)特定系統(tǒng)調(diào)用
?
在表中的偏移量,找到對應(yīng)的系統(tǒng)調(diào)用響應(yīng)函數(shù)的入口地址。系統(tǒng)調(diào)用表共256項,余下的項是可供用戶自己添加的系統(tǒng)調(diào)用空間。
在Linux中,每個系統(tǒng)調(diào)用被賦予一個系統(tǒng)調(diào)用號。這樣,通過這個獨一無二的號就可以關(guān)聯(lián)系統(tǒng)調(diào)用。當(dāng)用戶空間的進程執(zhí)行一個系統(tǒng)調(diào)用的時候,這個系統(tǒng)調(diào)用號就被用來指明到底是要執(zhí)行哪個系統(tǒng)調(diào)用。進程不會提及系統(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ù)指針組成的表: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é)果在該表中查詢其位置
進程的系統(tǒng)調(diào)用命令轉(zhuǎn)換為INT 0x80中斷的過程
宏定義_syscallN()見include/asm/unisted.h)用于系統(tǒng)調(diào)用的格式轉(zhuǎn)換和參數(shù)的傳遞。N取0~5之間的整數(shù)。
參數(shù)個數(shù)為N的系統(tǒng)調(diào)用由_syscallN()負責(zé)格式轉(zhuǎn)換和參數(shù)傳遞。系統(tǒng)調(diào)用號放入EAX寄存器,啟動INT 0x80后,規(guī)定返回值送EAX寄存器。
系統(tǒng)調(diào)用功能模塊的初始化
對系統(tǒng)調(diào)用的初始化也就是對INT 0x80的初始化。
系統(tǒng)啟動時,匯編子程序setup_idt(見arch/i386/kernel/head.S)準(zhǔn)備了1張256項的idt表,由start_kernel()(見init/main.c),trap_init()(見arch/i386/kernel/traps.c)調(diào)用的C語言宏定義set_system_gate(0x80,&system_call)(見include/asm/system.h)設(shè)置0x80號軟中斷的服務(wù)程序為 system_call(見arch/i386/kernel/entry.S), system.call就是所有系統(tǒng)調(diào)用的總?cè)肟凇?/p>
內(nèi)核如何為各種系統(tǒng)調(diào)用服務(wù)
當(dāng)進程需要進行系統(tǒng)調(diào)用時,必須以C語言函數(shù)的形式寫一句系統(tǒng)調(diào)用命令。該命令如果已在某個頭文件中由相應(yīng)的_syscallN()展開,則用戶程序必須包含該文件。當(dāng)進程執(zhí)行到用戶程序的系統(tǒng)調(diào)用命令時,實際上執(zhí)行了由宏命令_syscallN()展開的函數(shù)。系統(tǒng)調(diào)用的參數(shù) 由各通用寄存器傳遞,然后執(zhí)行INT 0x80,以內(nèi)核態(tài)進入入口地址system_call。
ret_from_sys_call
以ret_from_sys_call入口的匯編程序段在linux進程管理中起到了十分重要的作用。
所有系統(tǒng)調(diào)用結(jié)束前以及大部分中斷服務(wù)返回前,都會跳轉(zhuǎn)至此處入口地址。 該段程序不僅僅為系統(tǒng)調(diào)用服務(wù),它還處理中斷嵌套、CPU調(diào)度、信號等事務(wù)。
內(nèi)核如何為系統(tǒng)調(diào)用的參數(shù)傳遞參數(shù)
參數(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)都很類似。
參數(shù)驗證
系統(tǒng)調(diào)用必須仔細檢查它們所有的參數(shù)是否合法有效。舉例來說,與文件I/O相關(guān)的系統(tǒng)調(diào)用必須檢查文件描述符是否有效。與進程相關(guān)的函數(shù)必須檢查提供的PID是否有效。必須檢查每個參數(shù),保證它們不但合法有效,而且正確。
最重要的一種檢查就是檢查用戶提供的指針是否有效。試想,如果一個進程可以給內(nèi)核傳遞指針而又無須被檢查,那么它就可以給出一個它根本就沒有訪問權(quán)限的指針,哄騙內(nèi)核去為它拷貝本不允許它訪問的數(shù)據(jù),如原本屬于其他進程的數(shù)據(jù)。在接收一個用戶空間的指針之前,內(nèi)核必須保證:
指針指向的內(nèi)存區(qū)域?qū)儆谟脩艨臻g。進程決不能哄騙內(nèi)核去讀內(nèi)核空間的數(shù)據(jù)。
指針指向的內(nèi)存區(qū)域在進程的地址空間里。進程決不能哄騙內(nèi)核去讀其他進程的數(shù)據(jù)。
如果是讀,該內(nèi)存應(yīng)被標(biāo)記為可讀。如果是寫,該內(nèi)存應(yīng)被標(biāo)記為可寫。進程決不能繞過內(nèi)存訪問限制。
內(nèi)核提供了兩個方法來完成必須的檢查和內(nèi)核空間與用戶空間之間數(shù)據(jù)的來回拷貝。注意,內(nèi)核無論何時都不能輕率地接受來自用戶空間的指針!這兩個方法中必須有一個被調(diào)用。為了向用戶空間寫入數(shù)據(jù),內(nèi)核提供了copy_to_user(),它需要三個參數(shù)。第一個參數(shù)是進程空間中的目的內(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ā)生。此時,進程就會休眠,直到缺頁處理程序?qū)⒃擁搹挠脖P重新?lián)Q回物理內(nèi)存。
系統(tǒng)調(diào)用的返回值
系統(tǒng)調(diào)用(在Linux中常稱作syscalls)通常通過函數(shù)進行調(diào)用。它們通常都需要定義一個或幾個參數(shù)(輸入)而且可能產(chǎn)生一些副作用,例如寫某個文件或向給定的指針拷貝數(shù)據(jù)等等。為防止和正常的返回值混淆,系統(tǒng)調(diào)用并不直接返回錯誤碼,而是將錯誤碼放入一個名為errno的全局變量中。通常用一個負的返回值來表明錯誤。返回一個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)前進程的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ī)則。
訪問系統(tǒng)調(diào)用
系統(tǒng)調(diào)用上下文
內(nèi)核在執(zhí)行系統(tǒng)調(diào)用的時候處于進程上下文。current指針指向當(dāng)前任務(wù),即引發(fā)系統(tǒng)調(diào)用的那個進程。
在進程上下文中,內(nèi)核可以休眠并且可以被搶占。這兩點都很重要。首先,能夠休眠說明系統(tǒng)調(diào)用可以使用內(nèi)核提供的絕大部分功能。休眠的能力會給內(nèi)核編程帶來極大便利。在進程上下文中能夠被搶占,其實表明,像用戶空間內(nèi)的進程一樣,當(dāng)前的進程同樣可以被其他進程搶占。因為新的進程可以使用相同的系統(tǒng)調(diào)用,所以必須小心,保證該系統(tǒng)調(diào)用是可重人的。當(dāng)然,這也是在對稱多處理中必須同樣關(guān)心的問題。
當(dāng)系統(tǒng)調(diào)用返回的時候,控制權(quán)仍然在system_call()中,它最終會負責(zé)切換到用戶空間并讓用戶進程繼續(xù)執(zhí)行下去。
系統(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ù)的指針。進行一些檢查后,它調(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ù)用戶進程的狀態(tài),并將控制權(quán)交還給用戶程序。
從用戶空間直接訪問系統(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)用進行訪問。它會設(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在中定義,是系統(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;
}
添加系統(tǒng)調(diào)用
通過修改內(nèi)核源代碼添加系統(tǒng)調(diào)用
linux-2.6.*
通過以上分析linux系統(tǒng)調(diào)用的過程,
將自己的系統(tǒng)調(diào)用加到內(nèi)核中就是一件容易的事情。下面介紹一個實際的系統(tǒng)調(diào)用,
并把它加到內(nèi)核中去。要增加的系統(tǒng)調(diào)用是:inttestsyscall(),其功能是在控制終端屏幕上顯示hello world,
執(zhí)行成功后返回0。
編寫int testsyscall()系統(tǒng)調(diào)用–響應(yīng)函數(shù)
編寫一個系統(tǒng)調(diào)用意味著要給內(nèi)核增加1個函數(shù),將新函數(shù)放入文件kernel/sys.c中。新函數(shù)代碼如下:
asmlingkage sys_testsyscall()
{
print("hello world\n");
return 0;
}
添加系統(tǒng)調(diào)用號
編寫了新的系統(tǒng)調(diào)用過程后,下一項任務(wù)是使內(nèi)核的其余部分知道這一程序的存在,然后重建包含新的系統(tǒng)調(diào)用的內(nèi)核。為了把新的函數(shù)連接到已有的內(nèi)核中去, 需要編輯2個文件:
1).inculde/asm/unistd.h在這個文件中加入
#define_NR_testsyscall 191
系統(tǒng)調(diào)用表中添加對應(yīng)項
2).are/i386/kernel/entry.s這個文件用來對指針數(shù)組初始化,在這個文件中增加一行:
.long SYMBOL_NAME(_sys_tsetsycall)
將.rept NR_syscalls-190改為NR_SYSCALLS-191,然后重新編譯和運行新內(nèi)核。
使用新的系統(tǒng)調(diào)用
在保證的C語言庫中沒有新的系統(tǒng)調(diào)用的程序段,必須自己建立其代碼如下
#inculde _syscall0(int,testsyscall) main() { tsetsyscall(); }
在這里使用了_syscall0宏指令,宏指令本身在程序中將擴展成名為syscall()的函數(shù),它在main()函數(shù)內(nèi)部加以調(diào)用。
在testsyscall()函數(shù)中, 預(yù)處理程序產(chǎn)生所有必要的機器指令代碼,包括用系統(tǒng)調(diào)用參數(shù)值加載相應(yīng)的cpu寄存器, 然后執(zhí)行int 0x80中斷指令。
linux-3.*
在linux-3.8.4/kernel/sys.c 文件末尾添加新的系統(tǒng)調(diào)用函數(shù)如:
asmlinkage int sys_mycall(int number) { printk("這是我添加的第一個系統(tǒng)調(diào)用"); return number; }
在arch/x86/syscall_32.tbl下找到unused 223號調(diào)用然后替換如:
223 i386 mycall sys_mycall
如果是64位系統(tǒng),在arch/x86/syscalls/syscall_64.tbl下找到313號系統(tǒng)調(diào)用,然后在其下面加上314號自己的中斷如: `314 common mycall sys_mycall
利用內(nèi)核模塊添加系統(tǒng)調(diào)用
模塊是內(nèi)核的一部分,但是并沒有被編譯到內(nèi)核里面去。它們被分別編譯并連接成一組目標(biāo)文件, 這些文件能被插入到正在運行的內(nèi)核,或者從正在運行的內(nèi)核中移走。內(nèi)核模塊至少必須有2個函數(shù):
init_module和cleanup_module。
第一個函數(shù)是在把模塊插入內(nèi)核時調(diào)用的;
第二個函數(shù)則在刪除該模塊時調(diào)用。由于內(nèi)核模塊是內(nèi)核的一部分,所以能訪問所有內(nèi)核資源。根據(jù)對linux系統(tǒng)調(diào)用機制的分析,
如果要增加系統(tǒng)調(diào)用,可以編寫自己的函數(shù)來實現(xiàn),然后在sys_call_table表中增加一項,使該項中的指針指向自己編寫的函數(shù),
就可以實現(xiàn)系統(tǒng)調(diào)用。下面用該方法實現(xiàn)在控制終端上打印“hello world” 的系統(tǒng)調(diào)用testsyscall()。
編寫系統(tǒng)調(diào)用內(nèi)核模塊
#inculde(linux/kernel.h)
#inculde(linux/module.h)
#inculde(linux/modversions.h)
#inculde(linux/sched.h)
#inculde(asm/uaccess.h)
#define_NR_testsyscall 191
extern viod *sys_call+table[];
asmlinkage int testsyscall()
{
printf("hello world\n");
return 0;
}
int init_module()
{
sys_call_table[_NR_tsetsyscall]=testsyscall;
printf("system call testsyscall() loaded success\n");
return 0;
}
void cleanup_module()
{
}
使用新的系統(tǒng)調(diào)用
#define_NR_testsyscall 191
_syscall0(int,testsyscall)
main()
{
testsyscall();
}
內(nèi)核Linux系統(tǒng)調(diào)用的列表
以下是Linux系統(tǒng)調(diào)用的一個列表,包含了大部分常用系統(tǒng)調(diào)用和由系統(tǒng)調(diào)用派生出的的函數(shù)。
進程控制
?
文件系統(tǒng)控制
文件讀寫操作
?
文件系統(tǒng)操作
?
系統(tǒng)控制
?
內(nèi)存管理
?
?
socket控制
?
用戶管理
?
進程間通信
?
信號
?
消息
?
管道
?
信號量
共享內(nèi)存
?
審核編輯:湯梓紅
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1363瀏覽量
40228 -
Linux
+關(guān)注
關(guān)注
87文章
11225瀏覽量
208920 -
系統(tǒng)調(diào)用
+關(guān)注
關(guān)注
0文章
28瀏覽量
8320
發(fā)布評論請先 登錄
相關(guān)推薦
評論