本章講解了系統(tǒng)調(diào)用的抽象概念與實現(xiàn)方式琅束;總的來說募狂,系統(tǒng)調(diào)用是內(nèi)核提供的方便用戶進程與內(nèi)核進行交互的一組接口;主要系統(tǒng)穩(wěn)定可靠,避免應(yīng)用程序恣意妄為蜻懦;
作用
- 為用戶空間提供了一種硬件的抽象接口;
- 系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定與安全;
- 如果應(yīng)用程序可以隨意訪問硬件而內(nèi)核又對此一無所知的話,幾乎無法實現(xiàn)多任務(wù)和虛擬內(nèi)存煌恢;
系統(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)核駐留在受保護的地址空間上肴颊。如果進程可以直接在內(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)調(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ù)指針組成的表:
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é)果在該表中查詢其位置
參數(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)核必須保證:
2 指針指向的內(nèi)存區(qū)域?qū)儆谟脩艨臻g。進程決不能哄騙內(nèi)核去讀內(nèi)核空間的數(shù)據(jù)和媳。
2 指針指向的內(nèi)存區(qū)域在進程的地址空間里格遭。進程決不能哄騙內(nèi)核去讀其他進程的數(shù)據(jù)。
2 如果是讀留瞳,該內(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ī)則