這一篇介紹一下系統(tǒng)調(diào)用优妙,熟悉一下流程竞穷。很多做客戶端的同學(xué)根本不知道這些內(nèi)容。建議花時(shí)間看看相關(guān)的知識(shí)鳞溉。最好的方式還是去看源碼瘾带,反匯編,才能深刻的理解熟菲。
系統(tǒng)調(diào)用
程序運(yùn)行的時(shí)候看政,本身是沒有權(quán)限訪問多少系統(tǒng)資源的。系統(tǒng)資源有限抄罕,如果操作系統(tǒng)不進(jìn)行控制允蚣,那么各個(gè)程序難免會(huì)產(chǎn)生沖突。線程操作系統(tǒng)都將可能產(chǎn)生沖突的系統(tǒng)資源保護(hù)起來呆贿,阻止程序直接訪問嚷兔。比如文件、網(wǎng)絡(luò)做入、IO冒晰、各種設(shè)備等。
比如無論在Windows還是Linux中竟块,程序員都不能直接去訪問硬盤的某扇區(qū)上的數(shù)據(jù)壶运,必須通過文件系統(tǒng),也不能擅自修改任意文件浪秘。所有這些操作必須經(jīng)過操作系統(tǒng)規(guī)定的方式進(jìn)行蒋情。比如用fopen打開沒有權(quán)限的文件就會(huì)失敗埠况。
比如:想要程序延遲執(zhí)行一段時(shí)間,不借助操作系統(tǒng)就是使用循環(huán)棵癣,這樣會(huì)白白消耗CPU辕翰,造成資源浪費(fèi)。如果使用操作系統(tǒng)提供的定時(shí)器就可以方便有效狈谊。
系統(tǒng)調(diào)用涵蓋的功能很廣金蜀,有程序運(yùn)行鎖必須的支持,如創(chuàng)建和退出進(jìn)程的畴、線程渊抄,進(jìn)程內(nèi)存管理,對(duì)系統(tǒng)資源的訪問等丧裁。
Linux系統(tǒng)調(diào)用
在x86下护桦,系統(tǒng)調(diào)用是通過0x80中斷完成,各個(gè)通用寄存器用于傳遞參數(shù)煎娇。EAX寄存器用于表示系統(tǒng)調(diào)用的接口號(hào)二庵。
比如:EAX=1表示退出進(jìn)程,EAX=2表示創(chuàng)建進(jìn)程缓呛,EAX=3表示讀文件或IO催享,EAX=4表示寫文件或IO。每個(gè)系統(tǒng)調(diào)用對(duì)應(yīng)到內(nèi)核源碼中的一個(gè)函數(shù)哟绊,他們都是以sys_
開頭的因妙,比如exit調(diào)用對(duì)應(yīng)內(nèi)核中的sys_exit函數(shù)
Linux內(nèi)核提供了幾百個(gè)系統(tǒng)調(diào)用,下面列舉部分
所以完全可以不使用glibc封裝的fopen票髓、fread攀涵、fclose操作文件,而直接使用系統(tǒng)函數(shù)open,read,close實(shí)現(xiàn)洽沟。
技巧:Linux中可以使用man查看系統(tǒng)調(diào)用詳情以故,使用參數(shù)2表示系統(tǒng)調(diào)用手冊(cè)(比如 man 2 read)
如果直接使系統(tǒng)調(diào)用會(huì)有非常多的問題:
- 使用不方便,操作系統(tǒng)提供的系統(tǒng)調(diào)用接口往往過于原始裆操。程序員需要了解很多跟操作系統(tǒng)相關(guān)的細(xì)節(jié)
- 各個(gè)操作系統(tǒng)之間系統(tǒng)調(diào)用不兼容
于是增加一個(gè)層來解決怒详,系統(tǒng)調(diào)用與程序之間增加一個(gè)抽象層。這個(gè)層就是前面所說的glibc踪区,或者API.
系統(tǒng)調(diào)用原理
這里單單以Linux為例昆烁,至于Windows調(diào)用原理暫時(shí)省略。
用戶態(tài)朽缴、內(nèi)核態(tài)及中斷
現(xiàn)代操作系統(tǒng)中有兩種特權(quán)級(jí)別善玫,分為用戶模式和內(nèi)核模式水援。
多個(gè)模式存在密强,那么操作系統(tǒng)就可以讓不同代碼運(yùn)行在不同模式下茅郎,進(jìn)而限制代碼的權(quán)限,提高穩(wěn)定性或渤、安全性系冗。一般普通程序在用戶態(tài),操作會(huì)受到限制薪鹦。系統(tǒng)調(diào)用運(yùn)行在內(nèi)核態(tài)掌敬,應(yīng)用程序基本都是運(yùn)行在用戶態(tài)被限制。
用戶態(tài)的程序通過中斷來運(yùn)行內(nèi)核態(tài)的代碼池磁,進(jìn)而從用戶態(tài)切換到內(nèi)核態(tài)奔害。
中斷
中斷是操作系統(tǒng)的一個(gè)概念。中斷是一個(gè)硬件或者軟件發(fā)出的請(qǐng)求地熄,要求CPU暫停當(dāng)前的工作轉(zhuǎn)手去執(zhí)行更加重要的事情华临。
比如在編輯文本的時(shí)候,敲擊鍵盤那CPU如何得知道呢端考?一種是輪詢雅潭,CPU每隔一小段時(shí)間就去詢問鍵盤是否有鍵按下,但是除非一直打字却特,否則大部分輪訓(xùn)都是得到?jīng)]有鍵按下的結(jié)果扶供。這樣就白白浪費(fèi)掉了很多資源。
另一種方式就是當(dāng)鍵盤按下的時(shí)候裂明,鍵盤芯片發(fā)一個(gè)信號(hào)給CPU椿浓,然后CPU接收到信號(hào)之后就只到鍵盤按下了。然后再去詢鍵盤具體是哪個(gè)鍵被按下闽晦,這樣的信號(hào)就是一種中斷轰绵。——硬件中斷
中斷一般有兩個(gè)屬性尼荆,一個(gè)是中斷號(hào)(從0開始)左腔,一個(gè)稱為中斷處理程序(ISR),不同中斷具有不同的中斷號(hào),一個(gè)中斷號(hào)對(duì)應(yīng)一個(gè)中斷處理程序捅儒。在內(nèi)核中保存了一個(gè)數(shù)組液样,叫做中斷向量表,這個(gè)數(shù)組第n項(xiàng)包含了指向第n個(gè)中斷號(hào)的中斷處理程序的指針巧还。當(dāng)中斷來的時(shí)候鞭莽,CPU就會(huì)暫停當(dāng)前執(zhí)行的代碼,根據(jù)中斷的中斷號(hào)麸祷,在中斷向量表中找到對(duì)應(yīng)的中斷處理程序澎怒,并且調(diào)用他,處理完之后阶牍,CPU繼續(xù)執(zhí)行之前的代碼喷面。
中斷有硬件中斷星瘾,這種來至于硬件的異常或者其他時(shí)間的發(fā)生比如斷電惧辈,鍵盤按下琳状。另一種是軟件中斷,軟件中斷一般是一條指令(i386下是int)盒齿,帶有一個(gè)參數(shù)記錄中斷號(hào)念逞,這個(gè)指令用戶可以手動(dòng)觸發(fā)某個(gè)中斷并執(zhí)行中斷處理程序。比如int 0x80這條指令就會(huì)調(diào)用第0x80號(hào)的中斷處理程序边翁。
中斷號(hào)是有限的翎承,不可能每一個(gè)系統(tǒng)調(diào)用都對(duì)應(yīng)一個(gè)中斷號(hào),更加合理的是用一個(gè)或少數(shù)幾個(gè)中斷號(hào)來對(duì)應(yīng)所有的系統(tǒng)滴啊用符匾。比如Linux中int 0x80來觸發(fā)所有的系統(tǒng)調(diào)用审洞。
系統(tǒng)調(diào)用會(huì)有一個(gè)系統(tǒng)調(diào)用號(hào),表明是哪一個(gè)系統(tǒng)調(diào)用待讳,這個(gè)系統(tǒng)調(diào)用通常就是系統(tǒng)調(diào)用在系統(tǒng)調(diào)用表中的位置芒澜。比如Linux中的fork函數(shù)調(diào)用號(hào)是2,這個(gè)系統(tǒng)調(diào)用號(hào)在執(zhí)行int指令前就會(huì)被放到某個(gè)固定的寄存器中创淡,對(duì)應(yīng)的中斷代碼會(huì)取得這個(gè)系統(tǒng)調(diào)用號(hào)痴晦,并且調(diào)用正確的函數(shù)。
比如Linux中int 0x80為例琳彩,系統(tǒng)調(diào)用號(hào)使用eax來傳入誊酌,用戶將系統(tǒng)調(diào)用號(hào)保存在eax,然后使用int 0x80調(diào)用中斷露乏,中斷服務(wù)傳給信號(hào)就可以從eax取得系統(tǒng)調(diào)用號(hào)碧浊,進(jìn)行調(diào)用相應(yīng)的函數(shù)。
基于int的Linux經(jīng)典系統(tǒng)調(diào)用
下面以fork為例
觸發(fā)中斷
首先當(dāng)程序在代碼里面調(diào)用一個(gè)系統(tǒng)調(diào)用時(shí)瘟仿,是以一個(gè)函數(shù)的形式調(diào)用的箱锐,比如fork:
int main() {
fork();
}
fork函數(shù)是對(duì)系統(tǒng)調(diào)用fork的封裝,可以用下面的宏定義:
_syscall0(pid_t, fork)
_syscall0
是一個(gè)宏劳较,用于定義一個(gè)沒有參數(shù)的系統(tǒng)調(diào)用封裝驹止。他的第一個(gè)參數(shù)為這個(gè)系統(tǒng)調(diào)用的返回值類型,pid_t代表進(jìn)程的id观蜗。第二個(gè)參數(shù)是系統(tǒng)調(diào)用的名字臊恋。_syscall0
展開之后 會(huì)形成一個(gè)與系統(tǒng)調(diào)用名稱同名的函數(shù)。下面是i386的syscall0
匯編解釋:
-
__asm__
是一個(gè)gcc關(guān)鍵字墓捻,表示接下來要嵌入?yún)R編代碼抖仅,volatile關(guān)鍵字告訴GCC對(duì)這段代碼不進(jìn)行任何優(yōu)化 -
__asm__
第一個(gè)參數(shù)是一個(gè)字符串,代表匯編代碼的文本,這里匯編代煤制油int $0x80
撤卢,表示要調(diào)用0x80號(hào)中斷 -
=a(__res)
表示用eax(a表示eax)輸出返回?cái)?shù)據(jù)并存儲(chǔ)到_res中 -
0(_NR ##name)
表示用_NR ##name
為輸入环凿,0
指示由編譯器選擇和輸出相同的寄存器(eax)來傳遞參數(shù)。
__syscal_return
是另外一個(gè)宏
最終fork函數(shù)匯編之后
當(dāng)系統(tǒng)調(diào)用如果有參數(shù)的話會(huì)用syscall1
相比syscall0多了個(gè)
b
凸丸,它表示把a(bǔ)rg1強(qiáng)制轉(zhuǎn)換為long拷邢,然后保存在ebx最為輸入袱院。
所以如果系統(tǒng)內(nèi)調(diào)用如果有1個(gè)參數(shù)屎慢,則參數(shù)通過ebx來傳遞。x86下的linux系統(tǒng)調(diào)用參數(shù)最多有6個(gè)忽洛。分別使用6個(gè)寄存器來傳遞腻惠。分別是ebx,ecx欲虚,ed想集灌,esi,edi和ebp复哆。
當(dāng)進(jìn)行系統(tǒng)調(diào)用的時(shí)候欣喧,CPU執(zhí)行到int $0x80時(shí),會(huì)保存現(xiàn)場以便恢復(fù)梯找。接著切換到內(nèi)核態(tài)唆阿,然后CPU查找中斷向量表低0x80元素。
切換堆棧
在實(shí)際執(zhí)行中斷向量表中的第0x80好中斷之前锈锤,CPU還要進(jìn)行堆棧的奇幻驯鳖,在Linx中,用戶態(tài)和內(nèi)核態(tài)使用的是不同的棧久免,兩者各自負(fù)責(zé)各自的函數(shù)調(diào)用浅辙,互不干擾。
在執(zhí)行0x80中斷的時(shí)候阎姥,程序從用戶態(tài)切換到內(nèi)核態(tài)记舆。這時(shí)相應(yīng)的棧也必須切換到內(nèi)核棧,從中斷處理函數(shù)中返回時(shí)呼巴,程序當(dāng)前棧需要從內(nèi)核棧切換到用戶棧氨淌。
當(dāng)前棧是指ESP值所在的棧空間伊磺,如果ESP的值位于用戶棧范圍能盛正,那么程序的當(dāng)前棧就是用戶棧。內(nèi)核棧同理屑埋。并且SS的值需要指向當(dāng)前棧所在的頁豪筝。所以用戶棧切換到內(nèi)核棧就是:
- 保存當(dāng)前ESP、SS的值
- 將ESP、SS的值設(shè)置為內(nèi)核棧的值
(反過來同理)
- 恢復(fù)原理ESP续崖、SS的值
- 用戶態(tài)的ESP和SS的值保存在內(nèi)核棧上敲街,
當(dāng)發(fā)生中斷的時(shí)候,CPU除了進(jìn)入內(nèi)核態(tài)之外還會(huì)做如下事情:
- 找到當(dāng)前進(jìn)程的內(nèi)核棧(每一個(gè)進(jìn)程都有一個(gè)內(nèi)核棧)
- 在內(nèi)核棧中一猜壓入用戶態(tài)的寄存器SS严望、ESP多艇、EFLAGS、CS像吻、EIP
當(dāng)內(nèi)核從系統(tǒng)調(diào)用中返回峻黍,則調(diào)動(dòng)iret指令回到用戶態(tài),iret指令會(huì)從內(nèi)核棧里面彈出SS拨匆、ESP姆涩、EFLAGS、CS惭每、EIP的值骨饿。使得棧恢復(fù)到用戶態(tài)的狀態(tài)台腥。
中斷處理程序
在int指令切換了棧之后宏赘,程序就切換到中斷向量表轉(zhuǎn)給你記錄0x80號(hào)中斷處理陳旭。下面是linux i386中斷服務(wù)流程
內(nèi)核的系統(tǒng)調(diào)用函數(shù)往往以sys_加上系統(tǒng)調(diào)用函數(shù)名了黎侈,比如sys_fork察署,sys_open等。
關(guān)于sys開頭的系統(tǒng)內(nèi)調(diào)用函數(shù)如何從用戶那里獲得參數(shù)的蜓竹。是通過寄存實(shí)現(xiàn)的箕母。我們知道用戶調(diào)用系統(tǒng)調(diào)用時(shí),根據(jù)系統(tǒng)電泳參數(shù)數(shù)量不同俱济,一次將參數(shù)放入EBX,EXC嘶是,EDX,ESI,EBP這6個(gè)寄存器蛛碌。如果一個(gè)參數(shù)的系統(tǒng)調(diào)用就是EBX聂喇,兩個(gè)參數(shù)的系統(tǒng)調(diào)用就是EBX和ECX
小結(jié)
通過閱讀,歸根結(jié)底還是要懂匯編并且去看源碼才能把整個(gè)過程分析正確蔚携。平時(shí)所使用的把所有的細(xì)節(jié)都已經(jīng)屏蔽了贡这。地下還深藏著許多玄機(jī)鬓催。——而這一點(diǎn)確實(shí)大都數(shù)開發(fā)人員的短板