在現代操做系統裏,因爲系統資源可能同時被多個應用程序訪問,若是不加保護,那各個應用程序之間可能會產生衝突,對於惡意應用程序更可能致使系統奔潰。這裏所說的系統資源包括文件、網絡、各類硬件設備等。好比要操做文件必須藉助操做系統提供的api(好比linux下的fopen)。html
系統調用在咱們工做中無時無刻不打着交道,那系統調用的原理是什麼呢?在其過程當中作了哪些事情呢?linux
本文將闡述系統調用原理,讓你們對於系統調用有一個清晰的認識。c++
更多文章見我的博客:github.com/farmerjohng…git
現代cpu一般有多種特權級別,通常來講特權級總共有4個,編號從Ring 0(最高特權)到Ring 3(最低特權),在Linux上之用到Ring 0和RIng 3,用戶態對應Ring 3,內核態對應Ring 0。程序員
普通應用程序運行在用戶態下,其諸多操做都受到限制,好比改變特權級別、訪問硬件等。特權高的代碼能將本身降至低等級的級別,但反之則是不行的。而系統調用是運行在內核態的,那麼運行在用戶態的應用程序如何運行內核態的代碼呢?操做系統通常是經過中斷來從用戶態切換到內核態的。學過操做系統課程的同窗對中斷這個詞確定都不陌生。github
中斷通常有兩個屬性,一個是中斷號,一個是中斷處理程序。不一樣的中斷有不一樣的中斷號,每一箇中斷號都對應了一箇中斷處理程序。在內核中有一個叫中斷向量表的數組來映射這個關係。當中斷到來時,cpu會暫停正在執行的代碼,根據中斷號去中斷向量表找出對應的中斷處理程序並調用。中斷處理程序執行完成後,會繼續執行以前的代碼。api
中斷分爲硬件中斷和軟件中斷,咱們這裏說的是軟件中斷,軟件中斷一般是一條指令,使用這條指令用戶能夠手動觸發某個中斷。例如在i386下,對應的指令是int,在int指令後指定對應的中斷號,如int 0x80表明你調用第0x80號的中斷處理程序。數組
中斷號是有限的,全部不會用一箇中斷來對應一個系統調用(系統調用有不少)。Linux下用int 0x80觸發全部的系統調用,那如何區分不一樣的調用呢?對於每一個系統調用都有一個系統調用號,在觸發中斷以前,會將系統調用號放入到一個固定的寄存器,0x80對應的中斷處理程序會讀取該寄存器的值,而後決定執行哪一個系統調用的代碼。bash
在Linux2.5(具體版本不是很肯定)以前的版本,是使用int 0x80這樣的方式實現系統調用的,但其實int指令這樣的形式性能不太好,緣由以下(出自這篇文章):網絡
在 x86 保護模式中,處理 INT 中斷指令時,CPU 首先從中斷描述表 IDT 取出對應的門描述符,判斷門描述符的種類,而後檢查門描述符的級別 DPL 和 INT 指令調用者的級別 CPL,當 CPL<=DPL 也就是說 INT 調用者級別高於描述符指定級別時,才能成功調用,最後再根據描述符的內容,進行壓棧、跳轉、權限級別提高。內核代碼執行完畢以後,調用 IRET 指令返回,IRET 指令恢復用戶棧,並跳轉會低級別的代碼。
其實,在發生系統調用,由 Ring3 進入 Ring0 的這個過程浪費了很多的 CPU 週期,例如,系統調用必然須要由 Ring3 進入 Ring0(由內核調用 INT 指令的方式除外,這多半屬於 Hacker 的內核模塊所爲),權限提高以前和以後的級別是固定的,CPL 確定是 3,而 INT 80 的 DPL 確定也是 3,這樣 CPU 檢查門描述符的 DPL 和調用者的 CPL 就是徹底不必。
複製代碼
正是因爲如此,在linux2.5開始支持一種新的系統調用,其基於Intel 奔騰2代處理器就開始支持的一組專門針對系統調用的指令sysenter
/sysexit
。sysenter
指令用於由 Ring3 進入 Ring0,sysexit
指令用於由 Ring0 返回 Ring3。因爲沒有特權級別檢查的處理,也沒有壓棧的操做,因此執行速度比 INT n/IRET 快了很多。
本文分析的是int指令,新型的系統調用機制能夠參見下面幾篇文章:
咱們以系統調用fork
爲例,fork函數的定義在glibc(2.17版本)的unistd.h
/* Clone the calling process, creating an exact copy. Return -1 for errors, 0 to the new process, and the process ID of the new process to the old process. */
extern __pid_t fork (void) __THROWNL;
複製代碼
fork
函數的實現代碼比較難找,在nptl\sysdeps\unix\sysv\linux\fork.c
中有這麼一段代碼
weak_alias (__libc_fork, __fork)
libc_hidden_def (__fork)
weak_alias (__libc_fork, fork)
複製代碼
其做用簡單的說就是將__libc_fork
看成__fork
的別名,因此fork函數的實現是在__libc_fork
中,核心代碼以下
#ifdef ARCH_FORK
pid = ARCH_FORK ();
#else
# error "ARCH_FORK must be defined so that the CLONE_SETTID flag is used"
pid = INLINE_SYSCALL (fork, 0);
#endif
複製代碼
咱們分析定義了ARCH_FORK
的狀況,ARCH_FORK
定義在nptl\sysdeps\unix\sysv\linux\i386\fork.c
中,代碼以下:
#define ARCH_FORK() \ INLINE_SYSCALL (clone, 5, \ CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, 0, \ NULL, NULL, &THREAD_SELF->tid)
複製代碼
INLINE_SYSCALL代碼在sysdeps\unix\sysv\linux\i386\sysdep.h
#undef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr, args...) \ ({ \ unsigned int resultvar = INTERNAL_SYSCALL (name, , nr, args); \ if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (resultvar, ), 0)) \ { \ __set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \ resultvar = 0xffffffff; \ } \ (int) resultvar; })
複製代碼
INLINE_SYSCALL
主要是調用同文件下的INTERNAL_SYSCALL
# define INTERNAL_SYSCALL(name, err, nr, args...) \ ({ \ register unsigned int resultvar; \ EXTRAVAR_##nr \ asm volatile ( \ LOADARGS_##nr \ "movl %1, %%eax\n\t" \ "int $0x80\n\t" \ RESTOREARGS_##nr \ : "=a" (resultvar) \ : "i" (__NR_##name) ASMFMT_##nr(args) : "memory", "cc"); \ (int) resultvar; })
複製代碼
#define __NR_clone 120
複製代碼
這裏是一段內聯彙編代碼, 其中__NR_##name
的值爲 __NR_clone
即120。這裏主要是兩個步驟:
int $0x80
陷入中斷int $0x80
指令會讓cpu陷入中斷,執行對應的0x80中斷處理函數。不過在這以前,cpu還須要進行棧切換。
由於在linux中,用戶態和內核態使用的是不一樣的棧(能夠看看這篇文章),二者負責各自的函數調用,互不干擾。在執行int $0x80
時,程序須要由用戶態切換到內核態,因此程序當前棧也要從用戶棧切換到內核棧。與之對應,當中斷程序執行結束返回時,當前棧要從內核棧切換回用戶棧。
這裏說的當前棧指的就是ESP寄存器的值所指向的棧。ESP的值位於用戶棧的範圍,那程序的當前棧就是用戶棧,反之亦然。此外寄存器SS的值指向當前棧所在的頁。所以,將用戶棧切換到內核棧的過程是:
反之,從內核棧切換回用戶棧的過程:恢復ESP、SS等寄存器的值,也就是用保存在內核棧的原ESP、SS等值設置回對應寄存器。
在切換到內核棧以後,就開始執行中斷向量表的0x80
號中斷處理程序。中斷處理程序除了系統調用(0x80
)還有如除0異常(0x00
)、缺頁異常(0x14
)等等,在arch\i386\kernel\traps.c
文件的trap_init
方法中描述了中斷處理程序向中斷向量表註冊的過程:
void __init trap_init(void) {
#ifdef CONFIG_EISA
void __iomem *p = ioremap(0x0FFFD9, 4);
if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {
EISA_bus = 1;
}
iounmap(p);
#endif
#ifdef CONFIG_X86_LOCAL_APIC
init_apic_mappings();
#endif
set_trap_gate(0,÷_error);
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_gate(3, &int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
set_trap_gate(18,&machine_check);
#endif
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&system_call);
/* * Should be a barrier for any external CPU state. */
cpu_init();
trap_init_hook();
}
複製代碼
SYSCALL_VECTOR
定義以下:
#define SYSCALL_VECTOR 0x80
複製代碼
因此0x80
對應的處理程序就是system_call
這個方法,該方法位於arch\i386\kernel\entry.S
ENTRY(system_call)
//code 1: 保存各類寄存器
SAVE_ALL
...
jnz syscall_trace_entry
//若是傳入的系統調用號大於最大的系統調用號,則跳轉到無效調用處理
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
//code 2: 根據系統調用號(存儲在eax中)來調用對應的系統調用程序
call *sys_call_table(,%eax,4)
//保存系統調用返回值到eax寄存器中
movl %eax,EAX(%esp) # store the return value
...
restore_all:
//code 3:恢復各類寄存器的值 以及執行iret指令
RESTORE_ALL
...
複製代碼
主要分爲幾步:
1.保存各類寄存器
2.根據系統調用號執行對應的系統調用程序,將返回結果存入到eax中
3.恢復各類寄存器
其中保存各類寄存器的SAVE_ALL
定義在entry.S中:
#define SAVE_ALL \ cld; \ pushl %es; \ pushl %ds; \ pushl %eax; \ pushl %ebp; \ pushl %edi; \ pushl %esi; \ pushl %edx; \ pushl %ecx; \ pushl %ebx; \ movl $(__USER_DS), %edx; \ movl %edx, %ds; \ movl %edx, %es;
複製代碼
sys_call_table
定義在entry.S中:
.data
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
...
.long sys_sigreturn
.long sys_clone /* 120 */
...
複製代碼
sys_call_table
就是系統調用表,每個long元素(4字節)都是一個系統調用地址,因此 *sys_call_table(,%eax,4)
的含義就是sys_call_table
上偏移量爲0+%eax*4
元素所指向的系統調用,即第%eax
個系統調用。上文中fork
系統調用最終設置到eax的值是120,那最終執行的就是sys_clone
這個函數,注意其實現和第2個系統調用sys_fork
基本同樣,只是參數不一樣,關於fork和clone的區別能夠看這裏,代碼以下:
//kernel\fork.c
asmlinkage int sys_fork(struct pt_regs regs) {
return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}
asmlinkage int sys_clone(struct pt_regs regs) {
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;
clone_flags = regs.ebx;
newsp = regs.ecx;
parent_tidptr = (int __user *)regs.edx;
child_tidptr = (int __user *)regs.edi;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
}
複製代碼
一次系統調用的基本過程已經分析完,剩下的具體處理邏輯和本文無關就不分析了,有興趣的同窗能夠本身看看。
總體調用流程圖以下:
想寫這篇文章的緣由主要是年前在看《《程序員的自我修養》》這本書,以前對於系統調用這塊有一些瞭解但很零碎和模糊,看完本書系統調用這一章後消除了我許多疑問。整體來講這是一本不錯的書,但我相關的基礎比較薄弱,因此收穫很少。