linux系统调用理解之摘录(2)
原文博客?http://blog.csdn.net/gatieme/article/details/50779184
Linux系統調用的實現機制分析
本文介紹了系統調用的一些細節。
首先,分析了系統調用的意義,他們與庫函數和應用程序接口的關系。
然后,我們分析內核如何實現系統調用,以及執行系統調用的連鎖反應:
陷入內核——>傳遞系統調用號和外部輸入參數——>執行對應的系統調用函數——>把返回值帶回用戶空間。
最后,分析如何增加系統調用,并提供從用戶空間訪問系統調用的例子;
1.系統調用過的意義
Linux內核中設置了一組用于實現系統服務的子程序,這些程序成為系統調用(程序)。注意:請自行根據上下文理解“系統調用”指的是一種操作或是具體的子程序。
系統調用和普通函數調用非常類似,只是系統調用(這里指的是子程序)是由操作系統核心提供,運行在內核態,而普通的函數調用由用由函數庫或用戶自己提供,運行在用戶態。
一般,進程是不能訪問內核的:不能訪問內核空間,也不能調用內核函數。這是由CPU硬件決定的(這就是為什么它被稱為“保護模式”)。為了和用戶空間上的進程進行交互,內核提供了一組接口,即系統調用。通過接口,應用程序可以訪問硬件設備和其他操作系統資源。
系統調用相當于在用戶空間和硬件設備之間添加了一個中間層。它的主要作用有三個:
(1)為用戶空間提供一個統一的硬件的抽象接口。比如,當需要讀取文件的時候,應用程序就可以不去管磁盤類型和介質,甚至不用管文件所在的文件系統是哪種類型,直接通過接口就能達到讀文件的目的。
(2)系統調用保證了系統的穩定和安全。作為硬件設備和應用程序之間的中間人,內核可以基于權限和其他一些規則,對需要進行的用戶程序請求進行裁決。比如,這樣可以避免應用程序不正確地使用硬件設備,或是竊取其他進程的而資源,或是做出危害系統的事情。
(3)每個進程都運行在虛擬系統中,而在用戶空間和系統的其他部分之間增加一層公共接口,也是出于這種考慮。如果應用程序可以隨意訪問硬件而內核又對此一無所知的話,那就沒法實現多任務和虛擬內存。
(迷糊??)
在Linux中,系統調用時用戶空間訪問內核的唯一手段;除異常和中斷外,系統調用時內核唯一的合法入口。
2.API/POSIX/C庫的關系
一般情況下,應用程序通過應用程序接口(API)而不是使用syscall來實現系統調用。
這點很重要,因為應用程序使用API接口實際上并不需要和內核提供的系統調用一一對應。一個API可以通過一個系統調用實現,也可以通過使用多個系統調用來實現,甚至不適用任何系統調用也是可以的。實際上,API可以在各種不同的操作系統上實現,給應用程序提供完全一樣的接口,但是在不同系統上,他們的內部實現可能是不同的(比如通過 ifdef 來區分)。
在UNIX中,最流行的API是基于POSIX標準,其目標是提供一套基于unix的可移植操作系統標準。
POSIX是說明API和系統調用之間關系的一個極好的例子。在大多數Unix系統上,根據POSIX標準定義的API函數和系統調用之間有直接的關系。
Linux的系統調用與大多數Unix系統一樣,作為C庫的一部分提供,如下圖所示。C庫實現了Unix系統的主要API,包括標準應用層的庫函數和系統調用封裝函數。所有的C程序員都可以使用C庫。
從程序員的角度看,系統調用無關緊要,他們只需要和API打交道。相反,內核只跟系統調用打交道;
關于Unix的界面設計有一句通用的格言“提供機制而不是策略”。換句話說,Unix的系統調用抽象出了用于完成某種確定目標的函數。至于這些函數怎么用完全不需要內核去關心。區別對待機制(mechanism)和策略(policy)是Unix設計的一大亮點。大部分編程問題都可以被分割成兩部分:“需要提供什么功能(機制)”和“怎么實現這些功能(策略)”
(不明覺厲。。。)
3.系統調用的實現
您或許疑惑:“當輸入cat proc/CPUinfo時,cupinfo()函數怎么如何被調用的?”
實際上,內核在完成引導后,控制流就從相對之間的“接下來調用哪個函數?”改變成為“等待模式”:等待系統調用、異常和中斷。
用戶空間的程序無法直接執行內核代碼,而是以某種方式通知系統,告訴內核自己需要執行一個系統調用,希望系統切換到內核態,并執行那里的異常處理程序。
通知內核的機制是靠軟中斷實現的。過程如下:
首先,用戶程序設置系統調用號和外部輸入參數;
然后,應用程序執行“系統調用”指令(特殊的機器指令,在x86上是:“INT $0x80”,)。
在x86上,這個指令:產生一個編號為0x80的編程異常,這個編程異常對應的是中斷描述符表IDT中的第128項——也就是對應的系統門描述符。門描述符中含有一個預設的內核空間地址,它指向了系統調用處理程序:system_call()(別和系統調用服務程序混淆,這個程序在entry.S文件中用匯編語言編寫)。
system_call()的主要作用:
a、保存程序的現有狀態,即進程在用戶態下的CPU主要寄存器的值(所以叫軟中斷)(???有問題)
b、根據系統調用號計算出應該使用哪一種系統調用,內核進程查看系統調用表sys_call_table找到對應的系統調用服務例程的入口地址;
c、轉到對應的系統調用服務例程,并進一步調用執行內核中的相關功能函數;
d、上述系統服務例程執行完成后,返回系統調用返回值。
e、恢復用戶程序狀態,將控制權交給應用程序。
(注意:bcd沒有問題,ae的表述有問題。。)
3.2系統調用號
在linux中,每一個系統調用都會被賦予一個系統調用號。
同時,Linux有一個“未實現”系統調用sysy_ni_syscall(),它除了返回ENOSYS外,不做任何工作,這個錯誤號就是專門為無效的系統調用設定的。
內核中所有已經注冊過的系統調用都會保存在sys_call_table表中。一般在entry.s中定義。
sys_call_table是一張由指向實現各種系統調用的系統服務例程的函數指針組成的表。
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()函數通過將給定的系統調用好與NR-syscall作比較來檢查器有效性。如果它大于或者等于NR syscalls,該函數就返回一ENOSYS。否則,就執行相應的系統調用。
call *sys_call-table(, %eax, 4)
由于系統調用表中的表項是以32位(4字節)類型存放的,所以內核需要將給定的系統調用號乘以4,然后用所得的結果在該表中查詢其位
3.3????參數傳遞
除了系統調用號以外,大部分系統調用都還需要一些外部的參數輸人。所以,在發生異常的時候,應該把這些參數從用戶空間傳給內核。最簡單的辦法就是像傳遞系統調用號一樣把這些參數也存放在寄存器里。在x86系統上,ebx, ecx, edx, esi和edi按照順序存放前五個參數。需要六個或六個以上參數的情況不多見,此時,應該用一個單獨的寄存器存放指向所有這些參數在用戶空間地址的指針。
給用戶空間的返回值也通過寄存器傳遞。在x86系統上,它存放在eax寄存器中。接下來許多關于系統調用處理程序的描述都是針對x86版本的。但不用擔心,所有體系結構的實現都很類似。
?
3.4????參數驗證
系統調用必須仔細檢查它們所有的參數是否合法有效。舉例來說,與文件I/O相關的系統調用必須檢查文件描述符是否有效。與進程相關的函數必須檢查提供的PID是否有效。必須檢查每個參數,保證它們不但合法有效,而且正確。
最重要的一種檢查就是檢查用戶提供的指針是否有效。試想,如果一個進程可以給內核傳遞指針而又無須被檢查,那么它就可以給出一個它根本就沒有訪問權限的指針,哄騙內核去為它拷貝本不允許它訪問的數據,如原本屬于其他進程的數據。在接收一個用戶空間的指針之前,內核必須保證:
? ? ?指針指向的內存區域屬于用戶空間。進程決不能哄騙內核去讀內核空間的數據。
? ? ?指針指向的內存區域在進程的地址空間里。進程決不能哄騙內核去讀其他進程的數據。
? ? ?如果是讀,該內存應被標記為可讀。如果是寫,該內存應被標記為可寫。進程決不能繞過內存訪問限制。
3.5 內核空間與用戶空間之間數據的傳遞
內核提供了2種方法來實現用戶空間和內核空間之間數據的來回拷貝。
(1)向用戶空間寫入數據:copy_to_user()函數
(2)從用戶空間讀數據:copy_from_user()函數
注意copy_to_user()和copy_from_user()都有可能引起進程阻塞。當包含用戶數據的頁被換出到硬盤上而不是在物理內存上的時候,這種情況就會發生。此時,進程就會休眠,直到缺頁處理程序將該頁從硬盤重新換回物理內存。
3.6? ? 系統調用的返回值
系統調用(在Linux中常稱作syscalls)通常通過函數進行調用。它們通常都需要定義一個或幾個參數(輸入)而且可能產生一些副作用,例如寫某個文件或向給定的指針拷貝數據等等。為防止和正常的返回值混淆,系統調用并不直接返回錯誤碼,而是將錯誤碼放入一個名為errno的全局變量中。通常用一個負的返回值來表明錯誤。返回一個0值通常表明成功。如果一個系統調用失敗,你可以讀出errno的值來確定問題所在。通過調用perror()庫函數,可以把該變量翻譯成用戶可以理解的錯誤字符串。
errno不同數值所代表的錯誤消息定義在errno.h中,你也可以通過命令"man 3 errno"來察看它們。需要注意的是,errno的值只在函數發生錯誤時設置,如果函數不發生錯誤,errno的值就無定義,并不會被置為0。另外,在處理errno前最好先把它的值存入另一個變量,因為在錯誤處理過程中,即使像printf()這樣的函數出錯時也會改變errno的值。
當然,系統調用最終具有一種明確的操作。舉例來說,如getpid()系統調用,根據定義它會返回當前進程的PID。內核中它的實現非常簡單:
asmlinkage long sys_ getpid(void)
{
??? return current-> tgid;
}
上述的系統調用盡管非常簡單,但我們還是可以從中發現兩個特別之處。首先,注意函數聲明中的asmlinkage限定詞,這是一個小戲法,用于通知編譯器僅從棧中提取該函數的參數。所有的系統調用都需要這個限定詞。其次,注意系統調用get_pid()在內核中被定義成sys_ getpid。這是Linux中所有系統調用都應該遵守的命名規則
4.添加新的系統調用
給Linux添加一個新的系統調用是相對容易的工作。怎么設計和實現一個系統調用是難題所在,而把它添加進內核的過程比較簡單。
在添加一個系統調用是我們需要考慮幾個問題:
(1)明確系統調用的用途。
注意:Linux不提倡采用多用途的系統調用(一個系統調用通過傳遞不同的參數值來選擇不同類別的功能),不要讓一個系統調用太復雜!
但是,這里有一個反例,ioctl()系統調用(可以查看詳細教程https://blog.csdn.net/zifehng/article/details/59576539)
(2)確定系統調用的參數,返回值和錯誤碼。
系統調用的接口應該盡量簡潔,設計越通用約好。這個系統調用可移植嗎?別對機器的字節長度和字節序做假設。當你寫一個系統調用的時候,要時刻注意可移植性和健壯性,不但要考慮當前,還要為將來做打算。
當編譯完一個系統調用后,把它注冊成一個正式的系統調用是一件瑣碎的工作,有如下:
(1)在系統調用表的最后添加一項。每種支持該系統調用的硬件體系都必須做這樣的工作。從0開始算起,系統調用在該表中的位置就是它的系統調用號。(這一點非常重要,在表中并不會出現具體的數值號)
(2)對于各種體系結構,系統調用號必須定義在<asm/unistd.h>中。
(3)系統調用必須編譯進內核映像中(不能編譯成模塊)。可以通過把它放進kernel/下的一個相關文件中就可以。或是自己定義一個文件,并被包含編譯(這樣比較麻煩)。
以下:
我們通過虛構一個系統調用f00()來觀察一下這些步驟。
(1)首先,將sys_f00加入系統調用表中,對于大多數體系結構來說,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 */
......
我們將新的系統調用添加在表的尾行:
.long SYMBOL_NAME(sys_f00)
雖然,這里沒有明確指明系統調用號,但我們加入的這個系統調用被按照次序分配給了283這個系統調用號!
對于每種需要支持的體系結構,我們必須將自己的系統調用添加到其sys_call_table中。(說明表不止一個,每種體系都有一個)
(2)將自己的系統調用號加入<asm/unistd.h>中。
它的格式如下:
/*本文件包含系統調用號*/
#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)
..................
然后,我們再該列表的加入自己的系統調用號
#define? __NR_f00? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 283
(3)f00系統調用的函數實現。
因為f00系統調用要被編譯進內核映像,因此我們將它寫進 kernel/sys.c 文件中。
asmlinkage long sys_f00(void)
{
return 1;
}
這樣嚴格來說,現在就可以在用戶空間調用f00()系統調用了。
?
?
總結
以上是生活随笔為你收集整理的linux系统调用理解之摘录(2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux系统调用理解之摘录(1)
- 下一篇: Linux 发行版与Linux内核