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