| 2007 年 4 月 17 日 Linux? 系統調用 —— 我們每天都在使用它們。不過(guò)您清楚系統調用是如何在用戶(hù)空間和內核之間執行的嗎?本文將探究 Linux 系統調用接口(SCI),學(xué)習如何添加新的系統調用(以及實(shí)現這種功能的其他方法),并介紹與 SCI 有關(guān)的一些工具。 系統調用就是用戶(hù)空間應用程序和內核提供的服務(wù)之間的一個(gè)接口。由于服務(wù)是在內核中提供的,因此無(wú)法執行直接調用;相反,您必須使用一個(gè)進(jìn)程來(lái)跨越用戶(hù)空間與內核之間的界限。在特定架構中實(shí)現此功能的方法會(huì )有所不同。因此,本文將著(zhù)眼于最通用的架構 —— i386。 在 本文中,我將探究 Linux SCI,演示如何向 2.6.20 內核添加一個(gè)系統調用,然后從用戶(hù)空間來(lái)使用這個(gè)函數。我們還將研究在進(jìn)行系統調用開(kāi)發(fā)時(shí)非常有用的一些函數,以及系統調用的其他選擇。最后,我們將介紹 與系統調用有關(guān)的一些輔助機制,比如在某個(gè)進(jìn)程中跟蹤系統調用的使用情況。 SCI Linux 中系統調用的實(shí)現會(huì )根據不同的架構而有所變化,而且即使在某種給定的體架構上也會(huì )不同。例如,早期的 x86 處理器使用了中斷機制從用戶(hù)空間遷移到內核空間中,不過(guò)新的 IA-32 處理器則提供了一些指令對這種轉換進(jìn)行優(yōu)化(使用 sysenter 和 sysexit 指令)。由于存在大量的方法,最終結果也非常復雜,因此本文將著(zhù)重于接口細節的表層討論上。更詳盡的內容請參看本文最后的 參考資料。 要對 Linux 的 SCI 進(jìn)行改進(jìn),您不需要完全理解 SCI 的內部原理,因此我將使用一個(gè)簡(jiǎn)單的系統調用進(jìn)程(請參看圖 1)。每個(gè)系統調用都是通過(guò)一個(gè)單一的入口點(diǎn)多路傳入內核。eax 寄存器用來(lái)標識應當調用的某個(gè)系統調用,這在 C 庫中做了指定(來(lái)自用戶(hù)空間應用程序的每個(gè)調用)。當加載了系統的 C 庫調用索引和參數時(shí),就會(huì )調用一個(gè)軟件中斷(0x80 中斷),它將執行 system_call 函數(通過(guò)中斷處理程序),這個(gè)函數會(huì )按照 eax 內容中的標識處理所有的系統調用。在經(jīng)過(guò)幾個(gè)簡(jiǎn)單測試之后,使用 system_call_table 和 eax 中包含的索引來(lái)執行真正的系統調用了。從系統調用中返回后,最終執行 syscall_exit,并調用 resume_userspace 返回用戶(hù)空間。然后繼續在 C 庫中執行,它將返回到用戶(hù)應用程序中。 圖 1. 使用中斷方法的系統調用的簡(jiǎn)化流程 SCI 的核心是系統調用多路分解表。這個(gè)表如圖 2 所示,使用 eax 中提供的索引來(lái)確定要調用該表中的哪個(gè)系統調用(sys_call_table)。圖中還給出了表內容的一些樣例,以及這些內容的位置。(有關(guān)多路分解的更多內容,請參看側欄 “系統調用多路分解”) 圖 2. 系統調用表和各種鏈接
添加一個(gè) Linux 系統調用 | 系統調用多路分解 有些系統調用會(huì )由內核進(jìn)一步進(jìn)行多路分解。例如,BSD(Berkeley Software Distribution)socket 調用(socket、bind、 connect 等)都與一個(gè)單獨的系統調用索引(__NR_socketcall)關(guān)聯(lián)在一起,不過(guò)在內核中會(huì )進(jìn)行多路分解,通過(guò)另外一個(gè)參數進(jìn)入適當的調用。請參看 ./linux/net/socket.c 中的 sys_socketcall 函數。 | | 添加一個(gè)新系統調用主要是一些程序性的操作,但應該注意幾件事情。本節將介紹幾個(gè)系統調用的構造,從而展示它們的實(shí)現和用戶(hù)空間應用程序對它們的使用。 向內核中添加新系統調用,需要執行 3 個(gè)基本步驟: - 添加新函數。
- 更新頭文件。
- 針對這個(gè)新函數更新系統調用表。
注意: 這個(gè)過(guò)程忽略了用戶(hù)空間的需求,我將稍后介紹。 最常見(jiàn)的情況是,您會(huì )為自己的函數創(chuàng )建一個(gè)新文件。不過(guò),為了簡(jiǎn)單起見(jiàn),我將自己的新函數添加到現有的源文件中。清單 1 所示的前兩個(gè)函數,是系統調用的簡(jiǎn)單示例。清單 2 提供了一個(gè)使用指針參數的稍微復雜的函數。 清單 1. 系統調用示例的簡(jiǎn)單內核函數 asmlinkage long sys_getjiffies( void ) { return (long)get_jiffies_64(); } asmlinkage long sys_diffjiffies( long ujiffies ) { return (long)get_jiffies_64() - ujiffies; } | 在清單 1 中,我們?yōu)檫M(jìn)行 jiffies 監視提供了兩個(gè)函數。(有關(guān) jiffies 的更多信息,請參看側欄 “Kernel jiffies”)。第一個(gè)函數會(huì )返回當前 jiffy,而第二個(gè)函數則返回當前值與所傳遞進(jìn)來(lái)的值之間的差值。注意 asmlinkage 修飾符的使用。這個(gè)宏(在 linux/include/asm-i386/linkage.h 中定義)告訴編譯器將傳遞棧中的所有函數參數。 清單 2. 系統調用示例的最后內核函數 asmlinkage long sys_pdiffjiffies( long ujiffies, long __user *presult ) { long cur_jiffies = (long)get_jiffies_64(); long result; int err = 0; if (presult) { result = cur_jiffies - ujiffies; err = put_user( result, presult ); } return err ? -EFAULT : 0; } | | 內核 jiffies Linux 內核具有一個(gè)名為 jiffies 的全局變量,它代表從機器啟動(dòng)時(shí)算起的時(shí)間滴答數。這個(gè)變量最初被初始化為 0,每次時(shí)鐘中斷時(shí)都會(huì )加 1。您可以使用 get_jiffies_64 函數來(lái)讀取 jiffies 的值,然后使用 jiffies_to_msecs 將其換算成毫秒或使用 jiffies_to_usecs 將其換算成微秒。jiffies 的全局定義和相關(guān)函數是在 ./linux/include/linux/jiffies.h 中提供的。 | | 清單 2 給出了第三個(gè)函數。這個(gè)函數使用了兩個(gè)參數:一個(gè) long 類(lèi)型,以及一個(gè)指向被定義為 __user 的 long 的指針。__user 宏簡(jiǎn)單告訴編譯器(通過(guò) noderef)不應該解除這個(gè)指針的引用(因為在當前地址空間中它是沒(méi)有意義的)。這個(gè)函數會(huì )計算這兩個(gè) jiffies 值之間的差值,然后通過(guò)一個(gè)用戶(hù)空間指針將結果提供給用戶(hù)。put_user 函數將結果值放入 presult 所指定的用戶(hù)空間位置。如果在這個(gè)操作過(guò)程中出現錯誤,將立即返回,您也可以通知用戶(hù)空間調用者。 對于步驟 2 來(lái)說(shuō),我對頭文件進(jìn)行了更新:在系統調用表中為這幾個(gè)新函數安排空間。對于本例來(lái)說(shuō),我使用新系統調用號更新了 linux/include/asm/unistd.h 頭文件。更新如清單 3 中的黑體所示。 清單 3. 更新 unistd.h 文件為新系統調用安排空間 #define __NR_getcpu 318 #define __NR_epoll_pwait 319 #define __NR_getjiffies 320 #define __NR_diffjiffies 321 #define __NR_pdiffjiffies 322 #define NR_syscalls 323 | 現 在已經(jīng)有了自己的內核系統調用,以及表示這些系統調用的編號。接下來(lái)需要做的是要在這些編號(表索引)和函數本身之間建立一種對等關(guān)系。這就是第 3 個(gè)步驟,更新系統調用表。如清單 4 所示,我將為這個(gè)新函數更新 linux/arch/i386/kernel/syscall_table.S 文件,它會(huì )填充清單 3 顯示的特定索引。 清單 4. 使用新函數更新系統調用表 .long sys_getcpu .long sys_epoll_pwait .long sys_getjiffies /* 320 */ .long sys_diffjiffies .long sys_pdiffjiffies | 注意: 這個(gè)表的大小是由符號常量 NR_syscalls 定義的。 現在,我們已經(jīng)完成了對內核的更新。接下來(lái)必須對內核重新進(jìn)行編譯,并在測試用戶(hù)空間應用程序之前使引導使用的新映像變?yōu)榭捎谩?/p> 對用戶(hù)內存進(jìn)行讀寫(xiě) Linux 內核提供了幾個(gè)函數,可以用來(lái)將系統調用參數移動(dòng)到用戶(hù)空間中,或從中移出。方法包括一些基本類(lèi)型的簡(jiǎn)單函數(例如 get_user 或 put_user)。要移動(dòng)一塊兒數據(如結構或數組),您可以使用另外一組函數: copy_from_user 和 copy_to_user??梢允褂脤?zhuān)門(mén)的調用移動(dòng)以 null 結尾的字符串: strncpy_from_user 和 strlen_from_user。您也可以通過(guò)調用 access_ok 來(lái)測試用戶(hù)空間指針是否有效。這些函數都是在 linux/include/asm/uaccess.h 中定義的。 您可以使用 access_ok 宏來(lái)驗證給定操作的用戶(hù)空間指針。這個(gè)函數有 3 個(gè)參數,分別是訪(fǎng)問(wèn)類(lèi)型(VERIFY_READ 或 VERIFY_WRITE),指向用戶(hù)空間內存塊的指針,以及塊的大?。▎挝粸樽止潱?。如果成功,這個(gè)函數就返回 0: int access_ok( type, address, size ); | 要在內核和用戶(hù)空間移動(dòng)一些簡(jiǎn)單類(lèi)型(例如 int 或 long 類(lèi)型),可以使用 get_user 和 put_user 輕松地實(shí)現。這兩個(gè)宏都包含一個(gè)值以及一個(gè)指向變量的指針。get_user 函數將用戶(hù)空間地址(ptr)指定的值移動(dòng)到所指定的內核變量(var)中。 put_user 函數則將內核變量(var)指定的值移動(dòng)到用戶(hù)空間地址(ptr)。 如果成功,這兩個(gè)函數都返回 0: int get_user( var, ptr ); int put_user( var, ptr ); | 要移動(dòng)更大的對象,例如結構或數組,您可以使用 copy_from_user 和 copy_to_user 函數。這些函數將在用戶(hù)空間和內核之間移動(dòng)完整的數據塊。 copy_from_user 函數會(huì )將一塊數據從用戶(hù)空間移動(dòng)到內核空間,copy_to_user 則會(huì )將一塊數據從內核空間移動(dòng)到用戶(hù)空間: unsigned long copy_from_user( void *to, const void __user *from, unsigned long n ); unsigned long copy_to_user( void *to, const void __user *from, unsigned long n ); | 最后,您可以使用 strncpy_from_user 函數將一個(gè)以 NULL 結尾的字符串從用戶(hù)空間移動(dòng)到內核空間中。在調用這個(gè)函數之前,您可以通過(guò)調用 strlen_user 宏來(lái)獲得用戶(hù)空間字符串的大?。?/p> long strncpy_from_user( char *dst, const char __user *src, long count ); strlen_user( str ); | 這些函數為內核和用戶(hù)空間之間的內存移動(dòng)提供了基本功能。實(shí)際上還可以使用另外一些函數(例如減少執行檢查數量的函數)。您可以在 uaccess.h 中找到這些函數。
使用系統調用 現在內核已經(jīng)使用新系統調用完成更新了,接下來(lái)看一下從用戶(hù)空間應用程序中使用這些系統調用需要執行的操作。使用新的內核系統調用有兩種方法。第一種方法非常方便(但是在產(chǎn)品代碼中您可能并不希望使用),第二種方法是傳統方法,需要多做一些工作。 使用第一種方法,您可以通過(guò) syscall 函數調用由其索引所標識的新函數。使用 syscall 函數,您可以通過(guò)指定它的調用索引和一組參數來(lái)調用系統調用。例如,清單 5 顯示的簡(jiǎn)單應用程序就使用其索引調用了 sys_getjiffies。 清單 5. 使用 syscall 調用系統調用 #include <linux/unistd.h> #include <sys/syscall.h> #define __NR_getjiffies 320 int main() { long jiffies; jiffies = syscall( __NR_getjiffies ); printf( "Current jiffies is %lx\n", jiffies ); return 0; } | 正如您所見(jiàn),syscall 函數使用了系統調用表中使用的索引作為第一個(gè)參數。如果還有其他參數需要傳遞,可以加在調用索引之后。大部分系統調用都包括了一個(gè) SYS_ 符號常量來(lái)指定自己到 __NR_ 索引的映射。例如,使用 syscall 調用 __NR_getpid 索引: syscall 函數特定于架構,使用一種機制將控制權交給內核。其參數是基于 __NR 索引與 /usr/include/bits/syscall.h 提供的 SYS_ 符號之間的映射(在編譯 libc 時(shí)定義)。永遠都不要直接引用這個(gè)文件;而是要使用 /usr/include/sys/syscall.h 文件。 傳統的方法要求我們創(chuàng )建函數調用,這些函數調用必須匹配內核中的系統調用索引(這樣就可以調用正確的內核服務(wù)),而且參數也必須匹配。Linux 提供了一組宏來(lái)提供這種功能。_syscallN 宏是在 /usr/include/linux/unistd.h 中定義的,格式如下: _syscall0( ret-type, func-name ) _syscall1( ret-type, func-name, arg1-type, arg1-name ) _syscall2( ret-type, func-name, arg1-type, arg1-name, arg2-type, arg2-name ) | | 用戶(hù)空間和 __NR 常量 注意清單 6 中提供了 __NR 符號常量。您可以在 /usr/include/asm/unistd.h 中找到它們(對于標準系統調用來(lái)說(shuō))。 | | _syscall 宏最多可定義 6 個(gè)參數(不過(guò)此處只顯示了 3 個(gè))。 現在,讓我們來(lái)看一下如何使用 _syscall 宏來(lái)使新系統調用對于用戶(hù)空間可見(jiàn)。清單 6 顯示的應用程序使用了 _syscall 宏定義的所有系統調用。 清單 6. 將 _syscall 宏 用于用戶(hù)空間應用程序開(kāi)發(fā) #include <stdio.h> #include <linux/unistd.h> #include <sys/syscall.h> #define __NR_getjiffies 320 #define __NR_diffjiffies 321 #define __NR_pdiffjiffies 322 _syscall0( long, getjiffies ); _syscall1( long, diffjiffies, long, ujiffies ); _syscall2( long, pdiffjiffies, long, ujiffies, long*, presult ); int main() { long jifs, result; int err; jifs = getjiffies(); printf( "difference is %lx\n", diffjiffies(jifs) ); err = pdiffjiffies( jifs, &result ); if (!err) { printf( "difference is %lx\n", result ); } else { printf( "error\n" ); } return 0; } | 注意 __NR 索引在這個(gè)應用程序中是必需的,因為 _syscall 宏使用了 func-name 來(lái)構造 __NR 索引(getjiffies -> __NR_getjiffies)。其結果是您可以使用它們的名字來(lái)調用內核函數,就像其他任何系統調用一樣。
用戶(hù)/內核交互的其他選擇 系 統調用是請求內核中服務(wù)的一種有效方法。使用這種方法的最大問(wèn)題就是它是一個(gè)標準接口,很難將新的系統調用增加到內核中,因此可以通過(guò)其他方法來(lái)實(shí)現類(lèi)似 服務(wù)。如果您無(wú)意將自己的系統調用加入公共的 Linux 內核中,那么系統調用就是將內核服務(wù)提供給用戶(hù)空間的一種方便而且有效的方法。 讓您的服務(wù)對用戶(hù)空間可見(jiàn)的另外一種方法是通過(guò) /proc 文件系統。/proc 文件系統是一個(gè)虛擬文件系統,您可以通過(guò)它來(lái)向用戶(hù)提供一個(gè)目錄和文件,然后通過(guò)文件系統接口(讀、寫(xiě)等)在內核中為新服務(wù)提供一個(gè)接口。
使用 strace 跟蹤系統調用 Linux 內核提供了一種非常有用的方法來(lái)跟蹤某個(gè)進(jìn)程所調用的系統調用(以及該進(jìn)程所接收到的信號)。這個(gè)工具就是 strace,它可以在命令行中執行,使用希望跟蹤的應用程序作為參數。例如,如果您希望了解在執行 date 命令時(shí)都執行了哪些系統調用,可以鍵入下面的命令: 結果會(huì )產(chǎn)生大量信息,顯示在執行 date 命令過(guò)程中所執行的各個(gè)系統調用。您會(huì )看到加載共享庫、映射內存,最后跟蹤到的是在標準輸出中生成日期信息: ... write(1, "Fri Feb 9 23:06:41 MST 2007\n", 29Fri Feb 9 23:06:41 MST 2007) = 29 munmap(0xb747a000, 4096) = 0 exit_group(0) = ? $ | 當當前系統調用請求具有一個(gè)名為 syscall_trace 的特定字段集(它導致 do_syscall_trace 函數的調用)時(shí),將在內核中完成跟蹤。您還可以看到跟蹤調用是 ./linux/arch/i386/kernel/entry.S 中系統調用請求的一部分(請參看 syscall_trace_entry)。
結束語(yǔ) 系統調用是穿越用戶(hù)空間和內核空間,請求內核空間服務(wù)的一種有效方法。不過(guò)對這種方法的控制也很?chē)栏?,更?jiǎn)單的方式是增加一個(gè)新的 /proc 文件系統項來(lái)提供用戶(hù)/內核間的交互。不過(guò)當速度因素非常重要時(shí),系統調用則是使應用程序獲得最佳性能的理想方法。請參看 參考資料 的內容進(jìn)一步了解 SCI。
參考資料 學(xué)習
關(guān)于作者 | | | | M. Tim Jones 是一名嵌入式軟件工程師,他是 GNU/Linux Application Programming、AI Application Programming 以及 BSD Sockets Programming from a Multilanguage Perspective 等書(shū)的作者。他的工程背景非常廣泛,從同步宇宙飛船的內核開(kāi)發(fā)到嵌入式架構設計,再到網(wǎng)絡(luò )協(xié)議的開(kāi)發(fā)。Tim 是位于科羅拉多州 Longmont 的 Emulex Corp. 的一名顧問(wèn)工程師。 | |