1. 程式人生 > >第3章 Linux/UNIX 系統程式設計概念

第3章 Linux/UNIX 系統程式設計概念

第3章 系統程式設計概念

本章涉及到的多個主題是系統程式設計的預備知識。我們首先會介紹系統呼叫(system calls)以及在執行過程中的細節。然後我們會講到庫函式,以及它們與系統呼叫之間的區別,同時對C庫(GNU)進行相關描述。 當我們使用系統呼叫或者呼叫一個庫函式時,我們應該經常檢查一下返回的狀態,以確保是否成功返回了結果。我們會描述如何執行這些檢查,並且給出一組函式,用於診斷示例程式中的系統呼叫和庫函式執行是否正確。

3.1 System Calls

系統呼叫(system call)是核心的一個受控的切入點,程序通過該切入點可以向核心請求為它執行一些操作。通過系統呼叫應用程式設計介面(API),核心為程式提供了一系列可用的服務。這些服務包括,例如:建立一個新的程序、執行I/O操作、為程序間通訊建立管道等。 在深入瞭解系統呼叫如何工作之前,我們注意到:

  • 系統呼叫將處理器從使用者模式(user mode)切換到核心模式(kernel mode),因此CPU可以訪問受保護的核心記憶體。
  • 系統呼叫的集合是固定的。每個系統呼叫都被唯一的一個數字所標識(這些數字標識對程式而言是不可見的,程式通過名稱來識別系統呼叫)。
  • 每個系統呼叫可能存在一組引數,用於接收從使用者空間(user space)傳到核心空間(kernel spave)的資訊。

從程式設計的角度看,呼叫一個系統呼叫看上去更像是呼叫了一個C函式。然而在後臺,在系統呼叫執行的這個過程中,會產生很多步驟。為了說明這些,我們按順序來描述發生的這些步驟。這裡採用x86-32這個特定的硬體實現,步驟如下:

  1. 應用程式通過呼叫C庫中的 包裝函式(wrapper function) 來產生一個系統呼叫。
  2. 該包裝函式必須傳入處理trap的 系統呼叫例程(system call routine) 所需的所有引數。這些引數通過棧傳入到包裝函式中,但是核心希望這些引數在特定的暫存器(registers)中。所有包裝函式將這些引數複製到暫存器中。
  3. 因為所有的系統呼叫以相同的方式進入核心,所以核心需要通過某些方法來識別這些系統呼叫。為了能讓核心能夠識別,包裝函式需要將系統呼叫的識別符號複製到特定的CPU暫存器中。
  4. 包裝函式執行 trap 機器指令(int 0x80),該指令會使處理器從使用者模式切換到核心模式,並執行系統trap向量中位置0x80所指向的程式碼。

較新的x86-32架構實現了 sysenter 指令,該指令與傳統的 int 0x80 trap 指令相比,能夠更快地切換到核心模式。從2.6核心(2.6 kernel)和glibc2.3.2開始支援sysenter指令。

  1. 為響應位置0x80的trap,核心呼叫system_call()例程來解決這個trap: a) 將暫存器的值儲存到 核心棧(kernel stack) 中。 b) 檢查系統呼叫的識別符號(number)的有效性。 c) 根據 ++系統呼叫識別符號++ 從 系統呼叫列表(核心變數sys_call_table) (包含所有系統呼叫服務例程的列表) 中找到的相應的系統呼叫服務例程(system call service routine),並呼叫。如果該系統呼叫服務例程帶有任何引數,那麼首先會檢查引數的有效性。例如,它會檢查這個地址所指向的使用者記憶體的位置是否有效。然後服務例程會執行這個請求的任務,如修改該引數所指向地址的值,從使用者記憶體和核心記憶體之間傳輸資料(例如 I/O操作)。最後,服務例程將結果狀態值返回給system_call()例程。 d) 從核心棧中恢復暫存器的值,將系統呼叫(system call)返回的值放入棧中。 e) 返回給包裝函式,同時將處理器切換到使用者模式。
  2. 如果系統呼叫服務例程返回的值是一個表示產生了錯誤(an error)的值,那麼包裝函式會將該值賦給全域性變數 errno(正整數) 。然後從包裝函式返回一個整型值給呼叫者(caller),告知這次系統呼叫是成功的還是失敗的。

在Linux中,系統呼叫服務例程按照慣例,如果返回一個 非負數 則表示執行成功。如果出現了錯誤,則會返回一個 負數 ,這個數正好是errno常量的負值。當返回一個負數時,C庫包裝函式會對這個值再取負(使它變成正數),將結果賦給errno,並將-1作為包裝函式的結果進行返回,告知呼叫程式產生了一個錯誤。

圖3.1使用execve()系統呼叫描述了上面的一系列步驟。在Linux/x86-32中,execve()的系統呼叫識別符號是11 (__NR_execve)。因此,在sys_call_table向量中,條目11包含了sys_execve()的地址,sys_execve()就是這個系統呼叫所需的服務例程。

在Linux中,++系統呼叫服務例程++ 名稱的形式一般是 sys_xyz() , xyz() 是 ++系統呼叫++ 的名稱。

上面段落中描述的資訊已經超過了我們所需要知道的細節。但是,它告訴我們,即使是一個簡單的系統呼叫,其實有許多工作需要做,因此係統呼叫的開銷雖然小但還是可以感知的。

舉一個系統呼叫開銷的例子,getppid() 這個系統呼叫僅僅是返回呼叫程序的父程序的ID。但是在裝有Linux2.6.25的x86-32系統中,執行一次getppid()需要1000萬次呼叫,大概耗時2.2秒才能完成。每次呼叫大概是0.3毫秒。相比之下,在相同系統中,對C函式進行1000萬次呼叫僅僅只需0.11秒。大概只需要getppid()二十分之一的時間。當然,還有些系統呼叫的開銷遠遠大於getppid()

因此,從C程式的角度看,呼叫C庫包裝函式跟呼叫相應的系統呼叫服務例程是差不多的。在本書的剩下內容中,出現類似“呼叫系統呼叫xyz()” (invoking the system call xyz()) 就是指“通過呼叫包裝函式來呼叫系統呼叫xyz()” (calling the wrapper function that invokes the system call xyz())。

在這裡插入圖片描述

3.2 Library Functions

庫函式(library function) 僅僅是眾多函式中的一種,它們組成了標準的C庫(為簡潔起見,在本書的剩下章節中,我們用 ++函式(function)++ 指代 ++庫函式(library function)++ )。這些函式的作用是多樣的,例如開啟檔案、將時間轉換成可讀格式或者比較兩個字串。 很多庫函式不會使用系統呼叫(例如字串操作函式)。而有些庫函式依賴於系統呼叫。例如,fope()庫函式實際使用了open()系統呼叫來開啟檔案。通常,庫函式設計成比系統呼叫使用起來更加方便。例如,printf() 函式提供了輸出格式和資料快取,而 write() 系統呼叫僅僅輸出一些位元組。類似地,使用 和 函式比 brk() 系統呼叫可以更容易的分配和釋放一塊記憶體。

3.3 The Standard C Library; The GNU C Library (glibc)

在各種UNIX系統實現中,對標準C庫的實現也是各不相同的。在Linux中,最常用的C庫是 GNU C庫 (GNU C library)(glibc,http://www.gnu.org/software/libc/)。

Determining the version of glibc on the system

有時,我們需要知道系統中glibc的版本。glibc是一個可執行的程式,所以我們可以在shell中執行glibc共享庫檔案來得到它的版本號:

$ /lib/libc.so.6

在有些Linux系統中,GNU C庫不是在/lib/libc.so.6這個位置。確定該庫位置的一種方法是執行 ldd() 打印出某個可執行程式所依賴的共享庫列表(大部分程式都對libc.so.6有依賴):

$ ldd myprog | grep libc

ldd (list dynamic depedencies) 命令用於列印程式或者庫檔案所依賴的共享庫列表。

我的測試結果如下: 在這裡插入圖片描述

# include <stdio.h>
# include <gnu/libc-version.h>
int main()
{
	printf("glibc's version: %s\n", gnu_get_libc_version());
	return 0;
}

3.4 Handling Errors from system Calls and Library Funtions

3.5 Notes on the Example Programs in This Book

3.6 Portability Issues

3.7 summary