linux系统调用理解之摘录(2)
原文博客?http://blog.csdn.net/gatieme/article/details/50779184
Linux系統(tǒng)調(diào)用的實現(xiàn)機制分析
本文介紹了系統(tǒng)調(diào)用的一些細(xì)節(jié)。
首先,分析了系統(tǒng)調(diào)用的意義,他們與庫函數(shù)和應(yīng)用程序接口的關(guān)系。
然后,我們分析內(nèi)核如何實現(xiàn)系統(tǒng)調(diào)用,以及執(zhí)行系統(tǒng)調(diào)用的連鎖反應(yīng):
陷入內(nèi)核——>傳遞系統(tǒng)調(diào)用號和外部輸入?yún)?shù)——>執(zhí)行對應(yīng)的系統(tǒng)調(diào)用函數(shù)——>把返回值帶回用戶空間。
最后,分析如何增加系統(tǒng)調(diào)用,并提供從用戶空間訪問系統(tǒng)調(diào)用的例子;
1.系統(tǒng)調(diào)用過的意義
Linux內(nèi)核中設(shè)置了一組用于實現(xiàn)系統(tǒng)服務(wù)的子程序,這些程序成為系統(tǒng)調(diào)用(程序)。注意:請自行根據(jù)上下文理解“系統(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)核空間,也不能調(diào)用內(nèi)核函數(shù)。這是由CPU硬件決定的(這就是為什么它被稱為“保護模式”)。為了和用戶空間上的進程進行交互,內(nèi)核提供了一組接口,即系統(tǒng)調(diào)用。通過接口,應(yīng)用程序可以訪問硬件設(shè)備和其他操作系統(tǒng)資源。
系統(tǒng)調(diào)用相當(dāng)于在用戶空間和硬件設(shè)備之間添加了一個中間層。它的主要作用有三個:
(1)為用戶空間提供一個統(tǒng)一的硬件的抽象接口。比如,當(dāng)需要讀取文件的時候,應(yīng)用程序就可以不去管磁盤類型和介質(zhì),甚至不用管文件所在的文件系統(tǒng)是哪種類型,直接通過接口就能達(dá)到讀文件的目的。
(2)系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定和安全。作為硬件設(shè)備和應(yīng)用程序之間的中間人,內(nèi)核可以基于權(quán)限和其他一些規(guī)則,對需要進行的用戶程序請求進行裁決。比如,這樣可以避免應(yīng)用程序不正確地使用硬件設(shè)備,或是竊取其他進程的而資源,或是做出危害系統(tǒng)的事情。
(3)每個進程都運行在虛擬系統(tǒng)中,而在用戶空間和系統(tǒng)的其他部分之間增加一層公共接口,也是出于這種考慮。如果應(yīng)用程序可以隨意訪問硬件而內(nèi)核又對此一無所知的話,那就沒法實現(xiàn)多任務(wù)和虛擬內(nèi)存。
(迷糊??)
在Linux中,系統(tǒng)調(diào)用時用戶空間訪問內(nèi)核的唯一手段;除異常和中斷外,系統(tǒng)調(diào)用時內(nèi)核唯一的合法入口。
2.API/POSIX/C庫的關(guān)系
一般情況下,應(yīng)用程序通過應(yīng)用程序接口(API)而不是使用syscall來實現(xiàn)系統(tǒng)調(diào)用。
這點很重要,因為應(yīng)用程序使用API接口實際上并不需要和內(nèi)核提供的系統(tǒng)調(diào)用一一對應(yīng)。一個API可以通過一個系統(tǒng)調(diào)用實現(xiàn),也可以通過使用多個系統(tǒng)調(diào)用來實現(xiàn),甚至不適用任何系統(tǒng)調(diào)用也是可以的。實際上,API可以在各種不同的操作系統(tǒng)上實現(xiàn),給應(yīng)用程序提供完全一樣的接口,但是在不同系統(tǒng)上,他們的內(nèi)部實現(xiàn)可能是不同的(比如通過 ifdef 來區(qū)分)。
在UNIX中,最流行的API是基于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標(biāo)準(zhǔn)定義的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)應(yīng)用層的庫函數(shù)和系統(tǒng)調(diào)用封裝函數(shù)。所有的C程序員都可以使用C庫。
從程序員的角度看,系統(tǒng)調(diào)用無關(guān)緊要,他們只需要和API打交道。相反,內(nèi)核只跟系統(tǒng)調(diào)用打交道;
關(guān)于Unix的界面設(shè)計有一句通用的格言“提供機制而不是策略”。換句話說,Unix的系統(tǒng)調(diào)用抽象出了用于完成某種確定目標(biāo)的函數(shù)。至于這些函數(shù)怎么用完全不需要內(nèi)核去關(guān)心。區(qū)別對待機制(mechanism)和策略(policy)是Unix設(shè)計的一大亮點。大部分編程問題都可以被分割成兩部分:“需要提供什么功能(機制)”和“怎么實現(xiàn)這些功能(策略)”
(不明覺厲。。。)
3.系統(tǒng)調(diào)用的實現(xiàn)
您或許疑惑:“當(dāng)輸入cat proc/CPUinfo時,cupinfo()函數(shù)怎么如何被調(diào)用的?”
實際上,內(nèi)核在完成引導(dǎo)后,控制流就從相對之間的“接下來調(diào)用哪個函數(shù)?”改變成為“等待模式”:等待系統(tǒng)調(diào)用、異常和中斷。
用戶空間的程序無法直接執(zhí)行內(nèi)核代碼,而是以某種方式通知系統(tǒng),告訴內(nèi)核自己需要執(zhí)行一個系統(tǒng)調(diào)用,希望系統(tǒng)切換到內(nèi)核態(tài),并執(zhí)行那里的異常處理程序。
通知內(nèi)核的機制是靠軟中斷實現(xiàn)的。過程如下:
首先,用戶程序設(shè)置系統(tǒng)調(diào)用號和外部輸入?yún)?shù);
然后,應(yīng)用程序執(zhí)行“系統(tǒng)調(diào)用”指令(特殊的機器指令,在x86上是:“INT $0x80”,)。
在x86上,這個指令:產(chǎn)生一個編號為0x80的編程異常,這個編程異常對應(yīng)的是中斷描述符表IDT中的第128項——也就是對應(yīng)的系統(tǒng)門描述符。門描述符中含有一個預(yù)設(shè)的內(nèi)核空間地址,它指向了系統(tǒng)調(diào)用處理程序:system_call()(別和系統(tǒng)調(diào)用服務(wù)程序混淆,這個程序在entry.S文件中用匯編語言編寫)。
system_call()的主要作用:
a、保存程序的現(xiàn)有狀態(tài),即進程在用戶態(tài)下的CPU主要寄存器的值(所以叫軟中斷)(???有問題)
b、根據(jù)系統(tǒng)調(diào)用號計算出應(yīng)該使用哪一種系統(tǒng)調(diào)用,內(nèi)核進程查看系統(tǒng)調(diào)用表sys_call_table找到對應(yīng)的系統(tǒng)調(diào)用服務(wù)例程的入口地址;
c、轉(zhuǎn)到對應(yīng)的系統(tǒng)調(diào)用服務(wù)例程,并進一步調(diào)用執(zhí)行內(nèi)核中的相關(guān)功能函數(shù);
d、上述系統(tǒng)服務(wù)例程執(zhí)行完成后,返回系統(tǒng)調(diào)用返回值。
e、恢復(fù)用戶程序狀態(tài),將控制權(quán)交給應(yīng)用程序。
(注意:bcd沒有問題,ae的表述有問題。。)
3.2系統(tǒng)調(diào)用號
在linux中,每一個系統(tǒng)調(diào)用都會被賦予一個系統(tǒng)調(diào)用號。
同時,Linux有一個“未實現(xiàn)”系統(tǒng)調(diào)用sysy_ni_syscall(),它除了返回ENOSYS外,不做任何工作,這個錯誤號就是專門為無效的系統(tǒng)調(diào)用設(shè)定的。
內(nèi)核中所有已經(jīng)注冊過的系統(tǒng)調(diào)用都會保存在sys_call_table表中。一般在entry.s中定義。
sys_call_table是一張由指向?qū)崿F(xiàn)各種系統(tǒng)調(diào)用的系統(tǒng)服務(wù)例程的函數(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 */
(還是不明白,這里面SYMBOL_NAME作用是?sys_vfork的宏定義是??)
system_call()函數(shù)通過將給定的系統(tǒng)調(diào)用好與NR-syscall作比較來檢查器有效性。如果它大于或者等于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)用必須檢查文件描述符是否有效。與進程相關(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)存訪問限制。
3.5 內(nèi)核空間與用戶空間之間數(shù)據(jù)的傳遞
內(nèi)核提供了2種方法來實現(xiàn)用戶空間和內(nèi)核空間之間數(shù)據(jù)的來回拷貝。
(1)向用戶空間寫入數(shù)據(jù):copy_to_user()函數(shù)
(2)從用戶空間讀數(shù)據(jù):copy_from_user()函數(shù)
注意copy_to_user()和copy_from_user()都有可能引起進程阻塞。當(dāng)包含用戶數(shù)據(jù)的頁被換出到硬盤上而不是在物理內(nèi)存上的時候,這種情況就會發(fā)生。此時,進程就會休眠,直到缺頁處理程序?qū)⒃擁搹挠脖P重新?lián)Q回物理內(nèi)存。
3.6? ? 系統(tǒng)調(diào)用的返回值
系統(tǒng)調(diào)用(在Linux中常稱作syscalls)通常通過函數(shù)進行調(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)前進程的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)核的過程比較簡單。
在添加一個系統(tǒng)調(diào)用是我們需要考慮幾個問題:
(1)明確系統(tǒng)調(diào)用的用途。
注意:Linux不提倡采用多用途的系統(tǒng)調(diào)用(一個系統(tǒng)調(diào)用通過傳遞不同的參數(shù)值來選擇不同類別的功能),不要讓一個系統(tǒng)調(diào)用太復(fù)雜!
但是,這里有一個反例,ioctl()系統(tǒng)調(diào)用(可以查看詳細(xì)教程https://blog.csdn.net/zifehng/article/details/59576539)
(2)確定系統(tǒng)調(diào)用的參數(shù),返回值和錯誤碼。
系統(tǒng)調(diào)用的接口應(yīng)該盡量簡潔,設(shè)計越通用約好。這個系統(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)用是一件瑣碎的工作,有如下:
(1)在系統(tǒng)調(diào)用表的最后添加一項。每種支持該系統(tǒng)調(diào)用的硬件體系都必須做這樣的工作。從0開始算起,系統(tǒng)調(diào)用在該表中的位置就是它的系統(tǒng)調(diào)用號。(這一點非常重要,在表中并不會出現(xiàn)具體的數(shù)值號)
(2)對于各種體系結(jié)構(gòu),系統(tǒng)調(diào)用號必須定義在<asm/unistd.h>中。
(3)系統(tǒng)調(diào)用必須編譯進內(nèi)核映像中(不能編譯成模塊)。可以通過把它放進kernel/下的一個相關(guān)文件中就可以。或是自己定義一個文件,并被包含編譯(這樣比較麻煩)。
以下:
我們通過虛構(gòu)一個系統(tǒng)調(diào)用f00()來觀察一下這些步驟。
(1)首先,將sys_f00加入系統(tǒng)調(diào)用表中,對于大多數(shù)體系結(jié)構(gòu)來說,sys_call_table表位于entry.s文件中,形式如下:
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 */
......
我們將新的系統(tǒng)調(diào)用添加在表的尾行:
.long SYMBOL_NAME(sys_f00)
雖然,這里沒有明確指明系統(tǒng)調(diào)用號,但我們加入的這個系統(tǒng)調(diào)用被按照次序分配給了283這個系統(tǒng)調(diào)用號!
對于每種需要支持的體系結(jié)構(gòu),我們必須將自己的系統(tǒng)調(diào)用添加到其sys_call_table中。(說明表不止一個,每種體系都有一個)
(2)將自己的系統(tǒng)調(diào)用號加入<asm/unistd.h>中。
它的格式如下:
/*本文件包含系統(tǒng)調(diào)用號*/
#define __NR_read ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?1
__SYSCALL(__NR_write, sys_write)
#define __NR_open ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?3
__SYSCALL(__NR_close, sys_close)
..................
然后,我們再該列表的加入自己的系統(tǒng)調(diào)用號
#define? __NR_f00? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 283
(3)f00系統(tǒng)調(diào)用的函數(shù)實現(xiàn)。
因為f00系統(tǒng)調(diào)用要被編譯進內(nèi)核映像,因此我們將它寫進 kernel/sys.c 文件中。
asmlinkage long sys_f00(void)
{
return 1;
}
這樣嚴(yán)格來說,現(xiàn)在就可以在用戶空間調(diào)用f00()系統(tǒng)調(diào)用了。
?
?
總結(jié)
以上是生活随笔為你收集整理的linux系统调用理解之摘录(2)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux系统调用理解之摘录(1)
- 下一篇: Linux 发行版与Linux内核