Linux系統調用


什么是系統調用?

【轉自:https://woshijpf.github.io/%E5%86%85%E6%A0%B8/2016/05/10/Linux-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html

系統調用是應用程序與內核交互的一種方式。系統調用作為一種接口,通過系統調用,應用程序能夠進入操作系統內核,從而使用內核提供的各種資源,比如操作硬件,開關中斷,改變特權模式等等。首先,系統調用是一個軟中斷,既然是中斷那么一般就具有中斷號和中斷處理程序兩個屬性,Linux 使用 0x80 號中斷作為系統調用的入口,而中斷處理程序的地址放在中斷向量表里。

系統調用在內核空間和用戶空間之間建立了一個連接的中間層,它主要為系統提供了下面三個主要功能:

  • 第一,系統調用在底層硬件和用戶空間之間建立了一個抽象的接口。例如,當用戶空間的應用程序想讀寫一個文件時,程序員只需要通過系統調用接口完成讀寫操作即可,而不需要知道文件所在的底層磁盤是什么介質類型以及它所使用的是什么文件系統等一系列復雜的底層細節。
  • 第二,系統調用讓操作系統變得更加穩定和安全。Linux 中的內核好比就是一個仲裁者,它基於訪問權限、用戶組、臨界區等機制來控制用戶空間中應用程序用戶空間中應用程序對底層硬件資源的訪問,使得應用程序不會非法地使用硬件資源或者竊取其他應用程序所使用的資源以致對系統造成損害。
  • 第三,系統調用便於實現系統虛擬化。系統調用提供的中間層,它屏蔽了許多的底層的細節,使得應用程序和底層系統的耦合性降低,這樣系統的虛擬化變得更加簡單。

系統調用的處理過程

當我們在用戶態應用程序中調用一個系統調用函數時,它背后所隱藏的從用戶態到內核態的整個處理過程,如下圖所示:

在上圖中,應用程序中調用 libc 庫中的封裝好的 xyz() 系統調用函數,然后 xyz() 就接着執行 libc 庫中 xyz() 函數的具體實現的代碼,其中非常關鍵的一句代碼就是 int 0x80,它使得進程從用戶態切換到內核態,然后開始執行內核中系統調用處理程序的代碼 system_call,在后面的分析中我們會看到這部分代碼是用匯編語言編寫的,接着在 system_call 系統調用處理程序就根據傳入的系統調用號從系統調用服務程序數組中尋找對應系統調用服務程序,最后執行完成后按照調用順序的相反順序一步步返回結果。

在后面的系統調用內核代碼分析中,我們將對其中所使用到的名稱作一個規定說明:

  • xyz():系統調用庫函數
  • system_call:系統調用處理程序
  • sys_xyz(): 系統調用服務程序

系統調用例子

Linux 系統中實現了 300 多個系統調用,例如我們常用的 read、write、fork、time等等,有關 Linux 內核中定義了哪些系統調用,可以從 /arch/x86/syscalls/syscall_32.tbl中查閱到。

下面我們就介紹兩種用戶態下調用 sys_time 系統調用獲取當前系統時間的方法。

使用 libc 中封裝的好的 time 系統調用庫函數

int Time(int argc, char *argv[]) { time_t tt; struct tm *t; tt = time(NULL); t = localtime(&tt); printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year + 1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); return 0; } 

使用 Linux 內聯匯編來直接調用 13 號系統調用 sys_time

int TimeAsm(int argc, char *argv[]) { time_t tt; struct tm *t; asm volatile( "mov $0,%%ebx\n\t" "mov $0xd,%%eax\n\t" "int $0x80\n\t" "mov %%eax,%0\n\t" : "=m" (tt) ); t = localtime(&tt); printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year + 1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); return 0; } 

系統調用內核代碼分析

上面我們舉的例子是在用戶空間中調用系統調用函數,那么當從用戶空間切換到內核空間中,內核是如何處理系統調用的呢?

系統調用處理程序初始化

首先,在內核啟動初始化的 start_kernel 函數中就有一個對 trap 進行初始化工作的函數 trap_init() 函數,

/* 代碼文件路徑:/linux-3.18.6/init/main.c */ asmlinkage __visible void __init start_kernel(void) { ...... trap_init(); ...... } 

而在 trap_init() 函數中就包含了對系統調用對應中斷向量的初始化工作,它將系統調用對應的 0x80 號系統調用和它對應的處理程序關聯起來,如下所示:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/kernel/traps.c */ void __init trap_init(void) { ...... #ifdef CONFIG_X86_32 set_system_trap_gate(SYSCALL_VECTOR, &system_call); set_bit(SYSCALL_VECTOR, used_vectors); #endif ...... } 

上面第 6 行代碼中的 SYSCALL_VECTOR 的定義如下所示:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/include/asm/irq_vectors.h */ ...... #ifdef CONFIG_X86_32 # define SYSCALL_VECTOR 0x80 #endif ...... 

我們可以看到 SYSCALL_VECTOR 對應的中斷向量號就是我們上面使用內聯匯編例子中 int 0x80 指令中的中斷向量號。

而 0x80 中斷向量對應中斷向量的中斷服務程序就是上面 set_system_trap_gate() 函數中所設置的 system_call 程序,它是處理系統調用的入口程序。

系統調用處理程序分析

當執行 int 0x80 之后,系統就從用戶態切換到內核態,並跳轉到了 0x80 中斷向量號 對應的中斷向量服務程序 system_call,該服務程序的代碼位置是:/linux-3.18.6/arch/x86/kernel/entry_32.S 文件中的 490 行處的 ENTRY(system_call)。由於這部分的匯編代碼涉及到比較多的知識,所以下面的代碼是作了一些精簡,保留了最核心的那部分代碼:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/kernel/entry_32.S */ # system call handler stub # 系統調用匯編代碼的起點 ENTRY(system_call) # step 1: SAVE_ALL # 進入中斷處理程序前,保存內核態下的現場 ...... cmpl $(NR_syscalls), %eax # 比較傳入的系統調用號是否大於最大的系統調用號 jae syscall_badsys # 如果傳入的系統調用號太大,則跳轉到處理無效系統調用號的處理程序 # step 2: syscall_call: call *sys_call_table(,%eax,4) # 根據 eax 中傳入的系統調用號來調用對應的系統調用服務程序,在我們的例子中就是調用 sys_time # step 3: syscall_after_call: movl %eax,PT_EAX(%esp) # store the return value, 保存系統調用后返回的值到 eax # step 4: syscall_exit: # 會檢測要不要執行syscall_exit_work,正常情況會有一些需要處理的工作,比如當前進程有一些信號要處理,系統需要進行調度 ...... movl TI_flags(%ebp), %ecx testl $_TIF_ALLWORK_MASK, %ecx # current->work jne syscall_exit_work # step 5: restore_all: restore_nocheck: RESTORE_REGS 4 # skip orig_eax/error_code irq_return: INTERRUPT_RETURN # 這是一個宏定義,本質上是一條 iret 指令 # system_call 結束位置 ...... ENDPROC(system_call) 

由於代碼比較長,所以我們在上面的代碼中加入了 “step n” 的注釋,將代碼分成 5 個步驟進行解析。首先,通過一個流程圖對內核源碼中系統調用的處理過程進行一個概要性地描述。

step 1

內核源碼是從 Entry(system_call) 入口處開始處理系統調用,然后通過 SAVE_ALL 宏來保存一下當前的上下文狀態,SAVE_ALL 宏的具體代碼實現如下所示:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/kernel/entry_32.S */ # 在進入中斷處理程序前,保存相關寄存器的值 .macro SAVE_ALL cld PUSH_GS pushl_cfi %fs /*CFI_REL_OFFSET fs, 0;*/ pushl_cfi %es /*CFI_REL_OFFSET es, 0;*/ pushl_cfi %ds /*CFI_REL_OFFSET ds, 0;*/ pushl_cfi %eax CFI_REL_OFFSET eax, 0 pushl_cfi %ebp CFI_REL_OFFSET ebp, 0 pushl_cfi %edi CFI_REL_OFFSET edi, 0 pushl_cfi %esi CFI_REL_OFFSET esi, 0 pushl_cfi %edx CFI_REL_OFFSET edx, 0 pushl_cfi %ecx CFI_REL_OFFSET ecx, 0 pushl_cfi %ebx CFI_REL_OFFSET ebx, 0 movl $(__USER_DS), %edx movl %edx, %ds movl %edx, %es movl $(__KERNEL_PERCPU), %edx movl %edx, %fs SET_KERNEL_GS %edx .endm 

從上面 SAVE_ALL 的實現代碼中,我們可以知道 SAVE_ALL 的主要工作就是將 gs、fs、es、ds、eax、ebp 等寄存器中的值在調用系統調用服務程序之前將其壓入到內核棧中保存。

接下來的 cmpl 指令語句則是用來判斷從用戶態傳入的系統調用號是否大於系統中所實現的最大的系統調用編號,如果是,那么就說明我們傳入的系統調用編號不合法,程序就跳轉到 syscall_badsys 處執行相應的出錯處理程序。

step 2

在 step 2 中的工作就是就是去調用與傳入系統調用號對應的系統調用服務程序,而這一步中就只有一條非常簡單的 call 指令語句,如下所示:

call *sys_call_table(,%eax,4) # 根據 eax 中傳入的系統調用號來調用對應的系統調用服務程序,在我們的例子中就是調用 sys_time 

那么程序是怎么通過 %eax 中保存的系統調用號返回指定的系統調用服務程序的起始地址的呢?

我們首先可以從 sys_call_table 入手分析!

sys_call_table 代碼:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/kernel/syscall_32.c */ #define __SYSCALL_I386(nr, sym, compat) [nr] = sym, typedef asmlinkage void (*sys_call_ptr_t)(void); extern asmlinkage void sys_ni_syscall(void); __visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* * Smells like a compiler bug -- it doesn't work * when the & below is removed. */ [0 ... __NR_syscall_max] = &sys_ni_syscall, #include <asm/syscalls_32.h> }; 

從上面的代碼中,我們可知 sys_call_table 是一個數據元素為函數指針類型的的數組,數組長度就是系統中所包含的全部系統調用的數量: __NR_syscall_max + 1,然后在這個花括號里面的代碼完成的就是對數組元素初始化的工作。

也許你會對 [0…__NR_syscall_max] 這樣風格的代碼感到陌生和疑惑,其實它的功能就是將數組中從 0 號數組元素到 __NR_syscall_max 數組元素全部初始化成 &sys_ni_syscall(sys_ni_syscall 是系統中未實現的系統調用的默認系統調用服務程序),有關這個數組初始化的方式,更多詳細的內容請參考下面這篇博客 Linux Kernel代碼藝術——數組初始化

在默認的系統調用服務程序之后,就是每個已經在系統中有對應實現的系統調用的初始化,但是在這里只有一個 include 語句啊?我們還是首先來看看 include 的 asm/syscall_32.h 中內容是什么:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/include/asm/syscalls_32.h */ __SYSCALL_I386(0, sys_restart_syscall, sys_restart_syscall) __SYSCALL_I386(1, sys_exit, sys_exit) __SYSCALL_I386(2, sys_fork, stub32_fork) __SYSCALL_I386(3, sys_read, sys_read) __SYSCALL_I386(4, sys_write, sys_write) __SYSCALL_I386(5, sys_open, compat_sys_open) __SYSCALL_I386(6, sys_close, sys_close) __SYSCALL_I386(7, sys_waitpid, sys32_waitpid) __SYSCALL_I386(8, sys_creat, sys_creat) __SYSCALL_I386(9, sys_link, sys_link) __SYSCALL_I386(10, sys_unlink, sys_unlink) __SYSCALL_I386(11, sys_execve, stub32_execve) __SYSCALL_I386(12, sys_chdir, sys_chdir) __SYSCALL_I386(13, sys_time, compat_sys_time) ...... 

在這個文件中全部都是 __SYSCALL_I386 對應的宏代碼,而 __SYSCALL_I386 宏的定義就在前面的 syscall_32.c 文件中,

#define __SYSCALL_I386(nr, sym, compat) [nr] = sym, 

所以,展開一下 __SYSCALL_I386(1, sys_exit, sys_exit) 這樣一條宏語句,它本質上就是等價於:

[1] = sys_exit 

總結一下,也就是說 sys_call_table 數組是一個存儲系統調用服務程序的這么一個數組,例如系統中 0 號元素保存的是 0 號系統調用服務程序的地址,1 號元素保存的是 1 號系統調用的處理函數地址,如果某個系統調用沒有對應的服務程序,那么對應數組元素保存的就是用默認的系統調用服務程序 sys_ni_syscall 地址。

因此,call *sys_call_table(,%eax,4) 含義就是根據寄存器 %eax 中系統調用號,跳轉到對應的系統調用服務程序中去執行。在分析這條語句時,我卡殼了半天一直不知道 sys_call_table 后面括號中的參數是什么含義,后面 google 了以后才想起來這是 AT&T 匯編中的間接尋址方式,也就是說對應的系統調用處理函數的地址在是在 *(sys_call_table + %eax * 4) 的位置處,這里有個 4 表示的含義是數組中每個元素大小是 4 字節。

接下去,通過 call 指令程序就會跳轉到對應的系統調用服務程序中去執行具體的操作,最后將執行后的結果保存到 eax 寄存器中后返回。

step 3

step 3 中的代碼就比較簡單了,它就是將系統調用處理函數返回的狀態值保存在 eax 寄存器中的結果保存到內核棧中,保存的位置就是在 SAVE_ALL 宏中保存 eax 寄存器值的位置。

在這里補充一點的就是,系統調用服務程序執行完成之后保存在 eax 寄存器中返回值就是一個整型數據,用來表示系統調用成功與否!一般來說,返回的值是負數則表示此次系統調用失敗,而返回值為 0 則表示此次系統調用成功,具體系統調用失敗的原因,我們可以在應用程序中通過 perror() 函數來輸出相應的出錯信息。

step 4

step 4 中首先讀取 thread_info 結構體中的標志位,如果在發生系統調用的同時當前進程還接收到一些信號,那么程序就會接着跳轉到 syscall_exit_work 處去執行有關信號處理相關的相關的代碼,以及此時可能系統還會進行一個進程調度的工作,最后處理完這些系統調用返回前的操作之后,系統調用處理程序就接着往下執行到 step 5。

syscall_exit_work 的代碼如下所示:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/kernel/entry_32.S */ syscall_exit_work: # 退出系統調用后所做的一些工作 testl $_TIF_WORK_SYSCALL_EXIT, %ecx jz work_pending # 跳轉到 work_pending 去處理一些信號相關的處理程序 TRACE_IRQS_ON ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call # schedule() instead movl %esp, %eax call syscall_trace_leave jmp resume_userspace END(syscall_exit_work) 

step 5

完成系統調用服務程序的一系列處理之后,最后所需要做的工作就是返回到系統調用處理程序中,恢復調用系統調用服務程序前的內核的上下文狀態,也就是將保存在內核棧中的寄存器的值恢復到相應的寄存器中,而這一步主要是由 RESTORE_REGS 宏來完成,它的定義如下所示:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/kernel/entry_32.S */ # 執行完中斷處理程序后,返回時恢復原來保存在棧中的寄存器的值 .macro RESTORE_INT_REGS popl_cfi %ebx CFI_RESTORE ebx popl_cfi %ecx CFI_RESTORE ecx popl_cfi %edx CFI_RESTORE edx popl_cfi %esi CFI_RESTORE esi popl_cfi %edi CFI_RESTORE edi popl_cfi %ebp CFI_RESTORE ebp popl_cfi %eax CFI_RESTORE eax .endm .macro RESTORE_REGS pop=0 RESTORE_INT_REGS 1: popl_cfi %ds /*CFI_RESTORE ds;*/ 2: popl_cfi %es /*CFI_RESTORE es;*/ 3: popl_cfi %fs /*CFI_RESTORE fs;*/ POP_GS \pop .pushsection .fixup, "ax" 4: movl $0, (%esp) jmp 1b 5: movl $0, (%esp) jmp 2b 6: movl $0, (%esp) jmp 3b .popsection _ASM_EXTABLE(1b,4b) _ASM_EXTABLE(2b,5b) _ASM_EXTABLE(3b,6b) POP_GS_EX .endm 

我們可以從上面的代碼中看到 RESTORE_INT_REGS 宏從棧中恢復寄存器的值順序剛好是和前面所提到的 SAVE_ALL 宏壓棧的順序是相反的。

恢復了內核態中內核棧的狀態之后,接下來就是執行 INTERRUPT_RETURN 宏了,而這個宏實際上就是 iret 指令,它將系統從內核態又切換回用戶態,然后使得用戶態的應用程序往下接着執行。

系統調用服務程序分析

應用程序調用某個系統調用的目的就是完成一些無法在用戶空間完成的操作,從而得到想要的結果。我們前面大費周章講到的系統調用處理程序就像是一個領路者,它把應用程序想要內核代替執行的操作,從用戶空間傳遞到內核空間,然后再從內核空間中找到對應的服務程序去處理它。

因此,我們在這個小節中所要分析就是系統調用程序在內核中是如何實現的?我們還是以前面的 13 號系統調用 time 為例。

在前面一節系統調用處理程序分析中的 step 2 中,我們知道系統調用號和系統調用服務程序是一一對應關系,一個系統調用對應一個系統調用服務程序。而我們所要分析的 13 號系統調用對應的系統調用服務程序就是 sys_time,如下所示:

/* 代碼文件路徑:/linux-3.18.6/arch/x86/include/asm/syscalls_32.h */ __SYSCALL_I386(13, sys_time, compat_sys_time) 

那么,sys_time() 函數是怎么實現的呢?

sys_time() 函數的代碼實現如下所示:

/* 代碼文件路徑:/linux-3.18.6/kernel/time/time.c */ SYSCALL_DEFINE1(time, time_t __user *, tloc) { time_t i = get_seconds(); if (tloc) { if (put_user(i,tloc)) return -EFAULT; } force_successful_syscall_return(); return i; } 

為什么不是 sys_time() 作為函數名呢?其實規范統一的系統調用服務程序接口, Linux 系統中的系統調用服務函數都是使用 SYSCALL_DEFINEx (x可以0,1,2,3…等數字)宏來實現的。

SYSCALL_DEFINEx 的定義如下所示,

/* 代碼文件路徑:/linux-3.18.6/include/linux/syscalls.h */ #define SYSCALL_METADATA(sname, nb, ...) #endif #define SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ asmlinkage long sys_##sname(void) #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__) #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__) #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__) #define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__) #define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \ __attribute__((alias(__stringify(SyS##name)))); \ 

所以根據上面的定義,宏 SYSCALL_DEFINE1(time, time_t __user *, tloc) 展開后得到的結果就是:

asmlinkage long sys_time(time_t __user *tloc); 

參考文章

  1. Linux Kernel代碼藝術——數組初始化
  2. linux內核–系統調用(四)
  3. Sysenter Based System Call Mechanism in Linux 2.6


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM