使用 Linux 系统调用的内核命令
探究 SCI 并添加自己的調(diào)用
M. Jones2010 年 9 月 21 日發(fā)布 1
系統(tǒng)調(diào)用就是用戶空間應(yīng)用程序和內(nèi)核提供的服務(wù)之間的一個接口。由于服務(wù)是在內(nèi)核中提供的,因此無法執(zhí)行直接調(diào)用;相反,您必須使用一個進程來跨越用戶空間與內(nèi)核之間的界限。在特定架構(gòu)中實現(xiàn)此功能的方法會有所不同。因此,本文將著眼于最通用的架構(gòu) —— i386。
在本文中,我將探究 Linux SCI,演示如何向 2.6.20 內(nèi)核添加一個系統(tǒng)調(diào)用,然后從用戶空間來使用這個函數(shù)。我們還將研究在進行系統(tǒng)調(diào)用開發(fā)時非常有用的一些函數(shù),以及系統(tǒng)調(diào)用的其他選擇。最后,我們將介紹與系統(tǒng)調(diào)用有關(guān)的一些輔助機制,比如在某個進程中跟蹤系統(tǒng)調(diào)用的使用情況。
SCI
Linux 中系統(tǒng)調(diào)用的實現(xiàn)會根據(jù)不同的架構(gòu)而有所變化,而且即使在某種給定的體架構(gòu)上也會不同。例如,早期的 x86 處理器使用了中斷機制從用戶空間遷移到內(nèi)核空間中,不過新的 IA-32 處理器則提供了一些指令對這種轉(zhuǎn)換進行優(yōu)化(使用 sysenter 和 sysexit 指令)。由于存在大量的方法,最終結(jié)果也非常復(fù)雜,因此本文將著重于接口細節(jié)的表層討論上。更詳盡的內(nèi)容請參看本文最后的 參考資料。
要對 Linux 的 SCI 進行改進,您不需要完全理解 SCI 的內(nèi)部原理,因此我將使用一個簡單的系統(tǒng)調(diào)用進程(請參看圖 1)。每個系統(tǒng)調(diào)用都是通過一個單一的入口點多路傳入內(nèi)核。eax 寄存器用來標識應(yīng)當調(diào)用的某個系統(tǒng)調(diào)用,這在 C 庫中做了指定(來自用戶空間應(yīng)用程序的每個調(diào)用)。當加載了系統(tǒng)的 C 庫調(diào)用索引和參數(shù)時,就會調(diào)用一個軟件中斷(0x80 中斷),它將執(zhí)行 system_call 函數(shù)(通過中斷處理程序),這個函數(shù)會按照 eax 內(nèi)容中的標識處理所有的系統(tǒng)調(diào)用。在經(jīng)過幾個簡單測試之后,使用 system_call_table 和 eax 中包含的索引來執(zhí)行真正的系統(tǒng)調(diào)用了。從系統(tǒng)調(diào)用中返回后,最終執(zhí)行 syscall_exit,并調(diào)用 resume_userspace 返回用戶空間。然后繼續(xù)在 C 庫中執(zhí)行,它將返回到用戶應(yīng)用程序中。
圖 1. 使用中斷方法的系統(tǒng)調(diào)用的簡化流程
SCI 的核心是系統(tǒng)調(diào)用多路分解表。這個表如圖 2 所示,使用 eax 中提供的索引來確定要調(diào)用該表中的哪個系統(tǒng)調(diào)用(sys_call_table)。圖中還給出了表內(nèi)容的一些樣例,以及這些內(nèi)容的位置。(有關(guān)多路分解的更多內(nèi)容,請參看側(cè)欄 “系統(tǒng)調(diào)用多路分解”)
圖 2. 系統(tǒng)調(diào)用表和各種鏈接
添加一個 Linux 系統(tǒng)調(diào)用
添加一個新系統(tǒng)調(diào)用主要是一些程序性的操作,但應(yīng)該注意幾件事情。本節(jié)將介紹幾個系統(tǒng)調(diào)用的構(gòu)造,從而展示它們的實現(xiàn)和用戶空間應(yīng)用程序?qū)λ鼈兊氖褂谩?/p>
向內(nèi)核中添加新系統(tǒng)調(diào)用,需要執(zhí)行 3 個基本步驟:
注意: 這個過程忽略了用戶空間的需求,我將稍后介紹。
最常見的情況是,您會為自己的函數(shù)創(chuàng)建一個新文件。不過,為了簡單起見,我將自己的新函數(shù)添加到現(xiàn)有的源文件中。清單 1 所示的前兩個函數(shù),是系統(tǒng)調(diào)用的簡單示例。清單 2 提供了一個使用指針參數(shù)的稍微復(fù)雜的函數(shù)。
清單 1. 系統(tǒng)調(diào)用示例的簡單內(nèi)核函數(shù)
| 123456789 | 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行 jiffies 監(jiān)視提供了兩個函數(shù)。(有關(guān) jiffies 的更多信息,請參看側(cè)欄 “Kernel jiffies”)。第一個函數(shù)會返回當前 jiffy,而第二個函數(shù)則返回當前值與所傳遞進來的值之間的差值。注意 asmlinkage 修飾符的使用。這個宏(在 linux/include/asm-i386/linkage.h 中定義)告訴編譯器將傳遞棧中的所有函數(shù)參數(shù)。
清單 2. 系統(tǒng)調(diào)用示例的最后內(nèi)核函數(shù)
| 12345678910111213141516 | 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;} |
清單 2 給出了第三個函數(shù)。這個函數(shù)使用了兩個參數(shù):一個 long 類型,以及一個指向被定義為 __user 的 long 的指針。__user 宏簡單告訴編譯器(通過 noderef)不應(yīng)該解除這個指針的引用(因為在當前地址空間中它是沒有意義的)。這個函數(shù)會計算這兩個 jiffies 值之間的差值,然后通過一個用戶空間指針將結(jié)果提供給用戶。put_user 函數(shù)將結(jié)果值放入 presult 所指定的用戶空間位置。如果在這個操作過程中出現(xiàn)錯誤,將立即返回,您也可以通知用戶空間調(diào)用者。
對于步驟 2 來說,我對頭文件進行了更新:在系統(tǒng)調(diào)用表中為這幾個新函數(shù)安排空間。對于本例來說,我使用新系統(tǒng)調(diào)用號更新了 linux/include/asm/unistd.h 頭文件。更新如清單 3 中的黑體所示。
清單 3. 更新 unistd.h 文件為新系統(tǒng)調(diào)用安排空間
| 123 | #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 |
現(xiàn)在已經(jīng)有了自己的內(nèi)核系統(tǒng)調(diào)用,以及表示這些系統(tǒng)調(diào)用的編號。接下來需要做的是要在這些編號(表索引)和函數(shù)本身之間建立一種對等關(guān)系。這就是第 3 個步驟,更新系統(tǒng)調(diào)用表。如清單 4 所示,我將為這個新函數(shù)更新 linux/arch/i386/kernel/syscall_table.S 文件,它會填充清單 3 顯示的特定索引。
清單 4. 使用新函數(shù)更新系統(tǒng)調(diào)用表
| 1234 | .long sys_getcpu.long sys_epoll_pwait.long sys_getjiffies??????? /* 320 */.long sys_diffjiffies.long sys_pdiffjiffies |
注意: 這個表的大小是由符號常量 NR_syscalls 定義的。
現(xiàn)在,我們已經(jīng)完成了對內(nèi)核的更新。接下來必須對內(nèi)核重新進行編譯,并在測試用戶空間應(yīng)用程序之前使引導(dǎo)使用的新映像變?yōu)榭捎谩?/p>
對用戶內(nèi)存進行讀寫
Linux 內(nèi)核提供了幾個函數(shù),可以用來將系統(tǒng)調(diào)用參數(shù)移動到用戶空間中,或從中移出。方法包括一些基本類型的簡單函數(shù)(例如 get_user 或 put_user)。要移動一塊兒數(shù)據(jù)(如結(jié)構(gòu)或數(shù)組),您可以使用另外一組函數(shù): copy_from_user 和 copy_to_user。可以使用專門的調(diào)用移動以 null 結(jié)尾的字符串: strncpy_from_user 和 strlen_from_user。您也可以通過調(diào)用 access_ok 來測試用戶空間指針是否有效。這些函數(shù)都是在 linux/include/asm/uaccess.h 中定義的。
您可以使用 access_ok 宏來驗證給定操作的用戶空間指針。這個函數(shù)有 3 個參數(shù),分別是訪問類型(VERIFY_READ 或 VERIFY_WRITE),指向用戶空間內(nèi)存塊的指針,以及塊的大小(單位為字節(jié))。如果成功,這個函數(shù)就返回 0:
| 1 | int access_ok( type, address, size ); |
要在內(nèi)核和用戶空間移動一些簡單類型(例如 int 或 long 類型),可以使用 get_user 和 put_user 輕松地實現(xiàn)。這兩個宏都包含一個值以及一個指向變量的指針。get_user 函數(shù)將用戶空間地址(ptr)指定的值移動到所指定的內(nèi)核變量(var)中。 put_user 函數(shù)則將內(nèi)核變量(var)指定的值移動到用戶空間地址(ptr)。 如果成功,這兩個函數(shù)都返回 0:
| 12 | int get_user( var, ptr );int put_user( var, ptr ); |
要移動更大的對象,例如結(jié)構(gòu)或數(shù)組,您可以使用 copy_from_user 和 copy_to_user 函數(shù)。這些函數(shù)將在用戶空間和內(nèi)核之間移動完整的數(shù)據(jù)塊。 copy_from_user 函數(shù)會將一塊數(shù)據(jù)從用戶空間移動到內(nèi)核空間,copy_to_user 則會將一塊數(shù)據(jù)從內(nèi)核空間移動到用戶空間:
| 12 | 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 函數(shù)將一個以 NULL 結(jié)尾的字符串從用戶空間移動到內(nèi)核空間中。在調(diào)用這個函數(shù)之前,您可以通過調(diào)用 strlen_user 宏來獲得用戶空間字符串的大小:
| 12 | long strncpy_from_user( char *dst, const char __user *src, long count );strlen_user( str ); |
這些函數(shù)為內(nèi)核和用戶空間之間的內(nèi)存移動提供了基本功能。實際上還可以使用另外一些函數(shù)(例如減少執(zhí)行檢查數(shù)量的函數(shù))。您可以在 uaccess.h 中找到這些函數(shù)。
使用系統(tǒng)調(diào)用
現(xiàn)在內(nèi)核已經(jīng)使用新系統(tǒng)調(diào)用完成更新了,接下來看一下從用戶空間應(yīng)用程序中使用這些系統(tǒng)調(diào)用需要執(zhí)行的操作。使用新的內(nèi)核系統(tǒng)調(diào)用有兩種方法。第一種方法非常方便(但是在產(chǎn)品代碼中您可能并不希望使用),第二種方法是傳統(tǒng)方法,需要多做一些工作。
使用第一種方法,您可以通過 syscall 函數(shù)調(diào)用由其索引所標識的新函數(shù)。使用 syscall 函數(shù),您可以通過指定它的調(diào)用索引和一組參數(shù)來調(diào)用系統(tǒng)調(diào)用。例如,清單 5 顯示的簡單應(yīng)用程序就使用其索引調(diào)用了 sys_getjiffies。
清單 5. 使用 syscall 調(diào)用系統(tǒng)調(diào)用
| 123456789101112131415 | #include <linux/unistd.h>#include <sys/syscall.h>#define __NR_getjiffies???? 320int main(){??long jiffies;??jiffies = syscall( __NR_getjiffies );??printf( "Current jiffies is %lx\n", jiffies );??return 0;} |
正如您所見,syscall 函數(shù)使用了系統(tǒng)調(diào)用表中使用的索引作為第一個參數(shù)。如果還有其他參數(shù)需要傳遞,可以加在調(diào)用索引之后。大部分系統(tǒng)調(diào)用都包括了一個 SYS_ 符號常量來指定自己到 __NR_ 索引的映射。例如,使用 syscall 調(diào)用 __NR_getpid 索引:
| 1 | syscall( SYS_getpid ) |
syscall 函數(shù)特定于架構(gòu),使用一種機制將控制權(quán)交給內(nèi)核。其參數(shù)是基于 __NR 索引與 /usr/include/bits/syscall.h 提供的 SYS_ 符號之間的映射(在編譯 libc 時定義)。永遠都不要直接引用這個文件;而是要使用 /usr/include/sys/syscall.h 文件。
傳統(tǒng)的方法要求我們創(chuàng)建函數(shù)調(diào)用,這些函數(shù)調(diào)用必須匹配內(nèi)核中的系統(tǒng)調(diào)用索引(這樣就可以調(diào)用正確的內(nèi)核服務(wù)),而且參數(shù)也必須匹配。Linux 提供了一組宏來提供這種功能。_syscallN 宏是在 /usr/include/linux/unistd.h 中定義的,格式如下:
| 123 | _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 ) |
_syscall 宏最多可定義 6 個參數(shù)(不過此處只顯示了 3 個)。
現(xiàn)在,讓我們來看一下如何使用 _syscall 宏來使新系統(tǒng)調(diào)用對于用戶空間可見。清單 6 顯示的應(yīng)用程序使用了 _syscall 宏定義的所有系統(tǒng)調(diào)用。
清單 6. 將 _syscall 宏 用于用戶空間應(yīng)用程序開發(fā)
| 12345678910111213141516171819202122232425262728293031 | #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 索引在這個應(yīng)用程序中是必需的,因為 _syscall 宏使用了 func-name 來構(gòu)造 __NR 索引(getjiffies -> __NR_getjiffies)。其結(jié)果是您可以使用它們的名字來調(diào)用內(nèi)核函數(shù),就像其他任何系統(tǒng)調(diào)用一樣。
用戶/內(nèi)核交互的其他選擇
系統(tǒng)調(diào)用是請求內(nèi)核中服務(wù)的一種有效方法。使用這種方法的最大問題就是它是一個標準接口,很難將新的系統(tǒng)調(diào)用增加到內(nèi)核中,因此可以通過其他方法來實現(xiàn)類似服務(wù)。如果您無意將自己的系統(tǒng)調(diào)用加入公共的 Linux 內(nèi)核中,那么系統(tǒng)調(diào)用就是將內(nèi)核服務(wù)提供給用戶空間的一種方便而且有效的方法。
讓您的服務(wù)對用戶空間可見的另外一種方法是通過 /proc 文件系統(tǒng)。/proc 文件系統(tǒng)是一個虛擬文件系統(tǒng),您可以通過它來向用戶提供一個目錄和文件,然后通過文件系統(tǒng)接口(讀、寫等)在內(nèi)核中為新服務(wù)提供一個接口。
使用 strace 跟蹤系統(tǒng)調(diào)用
Linux 內(nèi)核提供了一種非常有用的方法來跟蹤某個進程所調(diào)用的系統(tǒng)調(diào)用(以及該進程所接收到的信號)。這個工具就是 strace,它可以在命令行中執(zhí)行,使用希望跟蹤的應(yīng)用程序作為參數(shù)。例如,如果您希望了解在執(zhí)行 date 命令時都執(zhí)行了哪些系統(tǒng)調(diào)用,可以鍵入下面的命令:
| 1 | strace date |
結(jié)果會產(chǎn)生大量信息,顯示在執(zhí)行 date 命令過程中所執(zhí)行的各個系統(tǒng)調(diào)用。您會看到加載共享庫、映射內(nèi)存,最后跟蹤到的是在標準輸出中生成日期信息:
| 12345 | ...write(1, "Fri Feb? 9 23:06:41 MST 2007\n", 29Fri Feb? 9 23:06:41 MST 2007) = 29munmap(0xb747a000, 4096)??? = 0exit_group(0)?????????? = ?$ |
當當前系統(tǒng)調(diào)用請求具有一個名為 syscall_trace 的特定字段集(它導(dǎo)致 do_syscall_trace 函數(shù)的調(diào)用)時,將在內(nèi)核中完成跟蹤。您還可以看到跟蹤調(diào)用是 ./linux/arch/i386/kernel/entry.S 中系統(tǒng)調(diào)用請求的一部分(請參看 syscall_trace_entry)。
結(jié)束語
系統(tǒng)調(diào)用是穿越用戶空間和內(nèi)核空間,請求內(nèi)核空間服務(wù)的一種有效方法。不過對這種方法的控制也很嚴格,更簡單的方式是增加一個新的 /proc 文件系統(tǒng)項來提供用戶/內(nèi)核間的交互。不過當速度因素非常重要時,系統(tǒng)調(diào)用則是使應(yīng)用程序獲得最佳性能的理想方法。請參看 參考資料 的內(nèi)容進一步了解 SCI。
總結(jié)
以上是生活随笔為你收集整理的使用 Linux 系统调用的内核命令的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux双网卡NAT共享上网
- 下一篇: linux中网络怎么检查,如何在Linu