一、系統調用簡介
操作系統爲用戶態的進程與硬件設備進行交互提供了一組接口,在應用程序和硬件之間設置一個額外的層有諸多優點:
1. 使得編程更容易,不需要用戶學習硬件設備的低級編程
2. 提高了系統的安全性,內核在滿足某個請求之前可以檢查請求的正確性
3. 添加的接口使得程序具有可移植性
Unix系統通過向內核發送系統調用(system call)實現用戶空間進程和硬件設備之間的大部分接口。 這裏使用wiki的一幅圖片簡單說明:
從圖中我們可以看到,無論是在用戶空間中調用GNU C庫函數後由GNU 的C庫與系統調用通信還是應用程序直接調用函數觸發系統調用,Linux的內核部分總是通過系統調用接口與外部交流。在當初學習操作系統課程時,我們也知道系統調用是應用程序訪問內核空間的唯一手段。從邏輯上來說,系統調用可被看成是一個內核與用戶空間程序交互的接口——它好比一箇中間人,把用戶進程的請求傳達給內核,待內核把請求處理完畢後再將處理結果送回給用戶空間。比如,用戶程序中使用 open() 函數打開一個文件,gnu C庫 glibc 將會調用系統調用sys_open(這也是系統調用名稱的一般規律,xyz() 函數對應的系統調用名字一般爲 sys_xyz ,而在較新內核比如3.10上,絕大多數系統調用的名字都改以“SyS_”開頭,也即 open() 函數對應的爲 SyS_open 系統調用 )。
換句話說,用戶訪問內核的路徑是事先規定好的,只能從規定位置進入內核,而不准許肆意跳入內核。有了這樣的陷入內核的統一訪問路徑限制才能保證內核安全無虞。我們可以形象地描述這種機制:作爲一個遊客,你可以買票要求進入野生動物園,但你必須老老實實地坐在觀光車上,按照規定的路線觀光遊覽。當然,不準下車,因爲那樣太危險,不是讓你丟掉小命,就是讓你嚇壞了野生動物。
二、系統調用實現方法
實現系統調用需要涉及包括架構相關的特性的一個控制傳輸方法,典型的實現就是使用軟中斷(software interrupt)或者陷入(trap)。
Linux 系統調用的可以通過兩種不同的方式調用:
* 第一種實現機制是一個多路匯聚以及分解的過程,該匯聚點就是 0x80 中斷這個入口點(X86 系統結構)。也就是說,所有系統調用都從用戶空間中匯聚到 0x80 中斷點,同時保存具體的系統調用號。當 0x80 中斷處理程序運行時,將根據系統調用號對不同的系統調用分別處理(調用不同的內核函數處理)。
* 第二種實現爲SYSCALL / SYSRET ,SYSENTER / SYSEXIT 彙編語言指令(獨立於AMD和intel建立,本質上一樣的)。
三、Linux中的定義及相關解釋
如果我們使用 ctags 或者 cscope 在Linux 內核源代碼中查找系統調用的話(如 “ vim -t sys_open ”或者在 cscope界面下查找 sys_open 的函數定義),插件會告訴我們源代碼中並沒有對於 sys_open 這個系統調用的定義,所以我們只能簡介查找。
我們都知道 sys_open 系統調用的實際操作基本上都是在 do_sys_open 函數中完成的,當然,如果你不知道,你可以使用 ftrace 追蹤一會系統調用,然後在追蹤結果中進行篩選並找出 sys_open 系統調用的大致調用路徑,下面是我的一次追蹤:
0) 1-1.tes-21282 | | SyS_open() {
0) 1-1.tes-21282 | | do_sys_open() {
0) 1-1.tes-21282 | | getname() {
0) 1-1.tes-21282 | | ...
0) 1-1.tes-21282 | 1.134 us | }
0) 1-1.tes-21282 | 1.695 us | }
0) 1-1.tes-21282 | 2.214 us | }
0) 1-1.tes-21282 | | get_unused_fd_flags() {
0) 1-1.tes-21282 | | __alloc_fd() {
0) 1-1.tes-21282 | | ...
0) 1-1.tes-21282 | 1.644 us | }
0) 1-1.tes-21282 | 6.201 us | }
0) 1-1.tes-21282 | 6.691 us | }
0) 1-1.tes-21282 | | do_filp_open() {
0) 1-1.tes-21282 | | path_openat() {
0) 1-1.tes-21282 | | ...
0) 1-1.tes-21282 | | do_last() {
0) 1-1.tes-21282 | + 57.283 us | ...
0) 1-1.tes-21282 | | path_put() {
0) 1-1.tes-21282 | 5.327 us | ...
0) 1-1.tes-21282 | 6.043 us | }
0) 1-1.tes-21282 | | putname() {
0) 1-1.tes-21282 | 1.631 us | ...
0) 1-1.tes-21282 | 2.124 us | }
0) 1-1.tes-21282 | ! 274.155 us | } /* do_sys_open */
0) 1-1.tes-21282 | ! 274.725 us | } /* SyS_open */
在 ftrace的結果中我們也可以看到,SyS_open 系統調用進入後第一個調用的函數就是 do_sys_open,而且SyS_open 系統調用的所有工作都是do_sys_open 完成的,所以我們可以查看do_sys_open 函數在系統中是如何定義的,而調用它的肯定就是SyS_open 系統調用了,使用 ctags 查找 do_sys_open 函數發現它是在 “ fs/open.c ”中定義的,而且調用它也是在這個文件中:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
看到這個定義,我們也就知道了爲什麼 SyS_open 系統調用的定義直接搜索不到了,因爲它是由宏定義SYSCALL_DEFINE3 完成,Linux 中定義系統調用宏SYSCALL_DEFINEx 的文件爲: linux-kenel-dir/include/linux/syscalls.h(注:本文選取較新內核3.12.9)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
SYSCALL_ALIAS(sys##name, SyS##name); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
其實我等菜鳥一看到這樣的宏定義肯定瞬間被秒殺了,而這樣的宏定義在內核中使用頗爲頻繁,在這裏我們簡單熟悉一下這個龐大的宏定義中的一些語法或者關鍵字:
3.1 asmlinkage[5]
從Linux文檔[5]中我們可以得知,asmlinkage是一種gcc 的標籤,它告訴編譯器不要去寄存器上尋找參數,而是去CPU的堆棧上尋找。
其實asmlinkage 本身也是一個宏,使用 ctags 我們可以很輕鬆的尋找到在 "arch/x86/include/asm/linkage.h" 中的宏定義:
#ifdef CONFIG_X86_32
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
呵呵,貌似又惹上了新麻煩阿:-),那麼CPP_ASMLINKAGE和__attribute__ (( regparm(0))) 又是什麼?
在解釋這兩個新概念之前,我們還是把asmlinkage 簡單介紹清楚: 函數定義前加宏asmlinkage,表示這些函數通過堆棧而不是通過寄存器傳遞參數。gcc編譯器在彙編過程中調用c語言函數時傳遞參數有兩種方法:一種是通過堆棧,另一種是通過寄存器。缺省時採用寄存器,假如你要在你的彙編過程中調用c語言函數,並且想通過堆棧傳遞參數,你定義的c函數時要在函數前加上宏asmlinkage。
3.1.1 CPP_ASMLINKAGE [6]
使用ctags 可以很輕鬆的找到CPP_ASMLINKAGE的定義:(在 include/linux/linkage.h中)
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif
所以CPP_ASMLINKAGE就是被定義爲 extern
"C" ,而 extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。
(1) 被extern "C"限定的函數或變量是extern類型的extern是C/C++語言中表明函數和全局變量作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。與extern對應的關鍵字是static,被它修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其不可能被extern “C”修飾。
(2) 被extern "C"修飾的變量和函數是按照C語言方式編譯和連接的。
3.1.2 __attribute__ (( regparm(0)))
__attribute__是關鍵字,是gcc的c語言擴展。__attribute__機制是GNU
C的一大特色,它可以設置函數屬性、變量屬性和類型屬性等。可以通過它們向編譯器提供更多數據,幫助編譯器執行優化等。
__attribute__((regparm(0))):告訴gcc編譯器該函數不需要通過任何寄存器來傳遞參數,參數只是通過堆棧來傳遞。
__attribute__((regparm(3))):告訴gcc編譯器這個函數可以通過寄存器傳遞多達3個的參數,這3個寄存器依次爲EAX、EDX 和 ECX。更多的參數才通過堆棧傳遞。這樣可以減少一些入棧出棧操作,因此調用比較快。
關於__attribute__ 更多可以參閱<1><2>。
3.1.3 爲什麼在系統調用中使用asmlinkage [7]
其實知道asmlinkage
是什麼意思並不好玩,要知道它爲什麼這麼用才比較好玩!上文已經說過,系統調用是用戶空間向內核空間發出請求來完成某些動作的,所以這些函數是非傳統的,你也別指望它能和正常的函數一樣,正常的函數通常都是將參數寫進寄存器而系統調用這是寫入程序堆棧。在用戶空間調用系統調用需要將確定值寫入寄存器(系統調用號永遠被寫入eax中,其他的參數被寫入ebx,exc等寄存器),這些在系統調用時都需要被翻譯。舉個例子sethostname,聲明爲int sethostname(char *name, size_t len),會是這樣:
mov ecx, len ; 名字的字節數
mov ebx, name ; 名字字符串的地址
mov eax, 170 ;
系統調用號(syscall number)
int 0x80 ; 通過0x80號中斷觸發系統調用
0x80號終端出發軟中斷並將CPU切至內核模式,然後執行系統調用sys_sethostname。具體步驟爲:首先它會現將所有寄存器中的數據存入CPU堆棧,然後檢查一些其他的東西(比如驗證參數),如果一切正常就會調用相應的系統調用。,比如這個例子中的sys_sethostname,定義爲:
SYSCALL_DEFINE2(sethostname, char __user *, name, int, len)
{
...
}
因爲用戶空間傳進的參數都已經被保存在棧中,所以編譯器必須被提示從棧中提取參數,所以我們需要asmlinkage。
注意:即使一個系統調用不需要參數(比如fork()),也還是需要向eax寄存器中填寫系統調用號的。
3.2 __VA_ARGS__
__VA_ARGS__ 是在C99中定義的一個可變參數的宏(目前只有GCC支持,vc6.0不支持的哦。此外,在很多使用到__VA_ARGS__的場合都會在其前面添加##,作用在於當可變參數的個數爲0時,這個##會把前面多餘的“,”去掉)。
3.3 static inline
當程序中調用一個函數時,程序跳到存儲器中保存函數的位置開始讀取代碼執行,執行完後再返回。爲了提高速度,C定義了inline函數,告訴編譯器把函數代碼在編譯時直接拷到程序中,這樣就不用執行時另外讀取函數代碼。
Static函數告訴編譯器其他文件看不到這個函數,因此該函數只能在當前文件中被調用。Static inline 函數只能在當前文件中被調用,同時執行速度快,幾個文件中都可以使用同樣的函數名。Static inline的內聯函數,一般情況下不會產生函數本身的代碼(函數本身不編譯),而是全部被嵌入在被調用的地方。如果不加static,則表示該函數有可能會被其他編譯單元所調用,所以一定會產生函數本身的代碼。所以加了static,一般可令可執行文件變小。內核裏一般見不到只用inline的情況,而都是使用static
inline。
3.4 其他的宏
代碼中還有一些其他的宏定義,比如__SC_DECL,這些都是在 include/linux/syscall.h 文件中定義的,但是目前看不太懂 =_=,這裏先預留着~
====================
引用:
[1] 《深入理解Linux內核》 第三版 - 第十章:系統調用
[2] Wiki -- system call http://en.wikipedia.org/wiki/Syscall
[3] 《系統調用原理》 http://hi.baidu.com/aniufngrxmhlrxe/item/c7ee284fd1de67e0bdf45198
[4] 《read 系統調用剖析》http://www.ibm.com/developerworks/cn/linux/l-cn-read/#ibm-pcon
[5] http://kernelnewbies.org/FAQ/asmlinkage
[6] http://blog.csdn.net/ce123_zhouwei/article/details/8446520
[7] http://www.quora.com/Linux-Kernel/What-does-asmlinkage-mean-in-the-definition-of-system-calls
====================