1. 程式人生 > >Linux核心和使用者空間應用程式的介面—系統呼叫

Linux核心和使用者空間應用程式的介面—系統呼叫

系統呼叫

就是使用者空間應用程式和核心提供的服務之間的一個介面。由於服務是在核心中提供的,因此無法執行直接呼叫;相反,您必須使用一個程序來跨越使用者空間與核心之間的界限。在特定架構中實現此功能的方法會有所不同。因此,本文將著眼於最通用的架構 —— 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. 更新標頭檔案。
  3. 針對這個新函式更新系統呼叫表。

注意: 這個過程忽略了使用者空間的需求,我將稍後介紹。

最常見的情況是,您會為自己的函式建立一個新檔案。不過,為了簡單起見,我將自己的新函式新增到現有的原始檔中。清單 1 所示的前兩個函式,是系統呼叫的簡單示例。清單 2 提供了一個使用指標引數的稍微複雜的函式。

清單 1. 系統呼叫示例的簡單核心函式
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. 系統呼叫示例的最後核心函式
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 檔案為新系統呼叫安排空間
#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. 使用新函式更新系統呼叫表
.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:

int access_ok( type, address, size );
要在核心和使用者空間移動一些簡單型別(例如 int 或 long 型別),可以使用 get_user 和 put_user 輕鬆地實現。這兩個巨集都包含一個值以及一個指向變數的指標。get_user 函式將使用者空間地址(ptr)指定的值移動到所指定的核心變數(var)中。 put_user 函式則將核心變數(var)指定的值移動到使用者空間地址(ptr)。 如果成功,這兩個函式都返回 0:
int get_user( var, ptr );
int put_user( var, ptr );
要移動更大的物件,例如結構或陣列,您可以使用 copy_from_user 和 copy_to_user 函式。這些函式將在使用者空間和核心之間移動完整的資料塊。 copy_from_user 函式會將一塊資料從使用者空間移動到核心空間,copy_to_user 則會將一塊資料從核心空間移動到使用者空間:
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 巨集來獲得使用者空間字串的大小:
long strncpy_from_user( char *dst, const char __user *src, long count );
strlen_user( str );
這些函式為核心和使用者空間之間的記憶體移動提供了基本功能。實際上還可以使用另外一些函式(例如減少執行檢查數量的函式)。可以在 uaccess.h 中找到這些函式。

使用系統呼叫

現在核心已經使用新系統呼叫完成更新了,接下來看一下從使用者空間應用程式中使用這些系統呼叫需要執行的操作。使用新的核心系統呼叫有兩種方法。第一種方法非常方便(但是在產品程式碼中您可能並不希望使用),第二種方法是傳統方法,需要多做一些工作。

使用第一種方法,您可以通過 syscall 函式呼叫由其索引所標識的新函式。使用 syscall 函式,您可以通過指定它的呼叫索引和一組引數來呼叫系統呼叫。例如,清單 5 顯示的簡單應用程式就使用其索引呼叫了 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;
}
正如您所見,syscall 函式使用了系統呼叫表中使用的索引作為第一個引數。如果還有其他引數需要傳遞,可以加在呼叫索引之後。大部分系統呼叫都包括了一個 SYS_ 符號常量來指定自己到 __NR_ 索引的對映。例如,使用 syscall 呼叫 __NR_getpid 索引:
syscall( SYS_getpid )
<p style="margin-top: 0px; margin-bottom: 0px; padding-top: 6px; padding-bottom: 6px; border: 0px; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline: 0px; vertical-align: baseline; font-family: Arial, sans-serif; line-height: 1.5em; white-space: normal; background-color: rgb(255, 255, 255);"><span style="font-size:14px;"><code style="margin: 0px; padding: 0px; border: 0px; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline: 0px; vertical-align: baseline; font-family: 'Andale Mono', 'Lucida Console', Monaco, Liberation, fixed, monospace; line-height: 1.5em; color: rgb(0, 0, 0) !important;">syscall</code> 函式特定於架構,使用一種機制將控制權交給核心。其引數是基於 <code style="margin: 0px; padding: 0px; border: 0px; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline: 0px; vertical-align: baseline; font-family: 'Andale Mono', 'Lucida Console', Monaco, Liberation, fixed, monospace; line-height: 1.5em; color: rgb(0, 0, 0) !important;">__NR</code> 索引與 /usr/include/bits/syscall.h 提供的 <code style="margin: 0px; padding: 0px; border: 0px; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline: 0px; vertical-align: baseline; font-family: 'Andale Mono', 'Lucida Console', Monaco, Liberation, fixed, monospace; line-height: 1.5em; color: rgb(0, 0, 0) !important;">SYS_</code> 符號之間的對映(在編譯 libc 時定義)。永遠都不要直接引用這個檔案;而是要使用 /usr/include/sys/syscall.h 檔案。</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 6px; padding-bottom: 6px; border: 0px; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline: 0px; vertical-align: baseline; font-family: Arial, sans-serif; line-height: 1.5em; white-space: normal; background-color: rgb(255, 255, 255);"><span style="font-size:14px;">傳統的方法要求我們建立函式呼叫,這些函式呼叫必須匹配核心中的系統呼叫索引(這樣就可以呼叫正確的核心服務),而且引數也必須匹配。Linux 提供了一組巨集來提供這種功能。<code style="margin: 0px; padding: 0px; border: 0px; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline: 0px; vertical-align: baseline; font-family: 'Andale Mono', 'Lucida Console', Monaco, Liberation, fixed, monospace; line-height: 1.5em; color: rgb(0, 0, 0) !important;">_syscallN</code> 巨集是在 /usr/include/linux/unistd.h 中定義的,格式如下:</span><pre name="code" class="cpp"><span style="font-size:18px;">_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 )</span>

_syscall 巨集最多可定義 6 個引數(不過此處只顯示了 3 個)。

現在,讓我們來看一下如何使用 _syscall 巨集來使新系統呼叫對於使用者空間可見。清單 6 顯示的應用程式使用了 _syscall 巨集定義的所有系統呼叫。

清單 6. 將 _syscall 巨集 用於使用者空間應用程式開發
#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 命令時都執行了哪些系統呼叫,可以鍵入下面的命令:

strace date

結果會產生大量資訊,顯示在執行 date 命令過程中所執行的各個系統呼叫。您會看到載入共享庫、對映記憶體,最後跟蹤到的是在標準輸出中生成日期資訊:

...
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)			= ?
$

噹噹前系統呼叫請求具有一個名為 syscall_trace 的特定欄位集(它導致 do_syscall_trace 函式的呼叫)時,將在核心中完成跟蹤。您還可以看到跟蹤呼叫是 ./linux/arch/i386/kernel/entry.S 中系統呼叫請求的一部分(請參看 syscall_trace_entry)。

總結:

系統呼叫是穿越使用者空間和核心空間,請求核心空間服務的一種有效方法。不過對這種方法的控制也很嚴格,更簡單的方式是增加一個新的 /proc 檔案系統項來提供使用者/核心間的互動(參加我/proc 檔案系統相關部落格)。不過當速度因素非常重要時,系統呼叫則是使應用程式獲得最佳效能的理想方法。