在什麼是操做系統這篇文章中,介紹過操做系統像是一個代理同樣,爲咱們去管理計算機的衆多硬件,咱們須要計算機的一些計算服務、數據管理的服務,都由操做系統提供接口來完成。這樣作的好處是讓通常的計算機使用者不用關心硬件的細節。html
既然使用者是經過操做系統接口來使用計算機的,那究竟是什麼是操做系統提供的接口呢?linux
接口(interface)這個詞來源於電氣工程學科,指的是插座與插頭的鏈接口,起到將電與電器鏈接起爲的功能。後來延伸到軟件工程裏指軟件包向外提供的功能模塊的函數接口。因此接口是用來鏈接兩個東西、信號轉換和屏蔽細節。數組
那對於操做系統來講:操做系統經過接口的方式,創建了用戶與計算機硬件的溝通方式。用戶經過調用操做系統的接口來使用計算機的各類計算服務。爲了用戶友好性,操做系統通常會提供兩個重要的接口來知足用戶的一些通常性的使用需求:安全
bash/sh
的端終程序提供的功能,該程序底層的實質仍是調用一些操做系統提供的函數。對於非通常性使用需求,操做系統提供了一系列的函數調用給軟件開發者,由軟件開發者來實現一些用戶須要的功能。這些函數調用因爲是操做系統內核提供的,爲了有別於通常的函數調用,被稱爲系統調用。好比咱們使用C語言進行軟件開發時,常常用的printf
函數,它的內部實際就是經過write
這個系統調用,讓操做系統內核爲咱們把字符打印在屏幕上的。bash
爲了規範操做系統提供的系統調用,IEEE制定了一個標準接口族,被稱爲POSIX
(Portable Operating System Interface of Unix)。一些咱們熟悉的接口好比:fork
、pthread_create
、open
等。數據結構
計算機硬件資源都是操做系統內核進行管理的,那咱們能夠直接用內核中的一些功能模塊來操做硬件資源嗎?能夠直接訪問內核中維護的一些數據結構嗎? 固然不行!有人會說,爲何不行呢?我買的電腦,內核代碼在內存中,那內存不都是我本身買的嗎?,我本身不能訪問嗎?
如今咱們運行的操做系統都是一個多任務、多用戶的操做系統。若是每一個用戶進程均可以隨便訪問操做系統內核的模塊,改變狀態,那整個操做系統的穩定性、安全性都大大下降了。函數
爲了將內核程序與用戶程序隔離開,在硬件層面上提供了一次機制,將程序執行的狀態分爲了避免同的級別,從0到3,數字越小,訪問級別越高。0表明內核態,在該特權級別下,全部內存上的數據都是可見的,可訪問的。3表明用戶態,在這個特權級下,程序只能訪問一部分的內存區域,只能執行一些限定的指令。ui
操做系統在創建GTD表的時候,將GTD的每一個表項中的2位(4種特權級別)設置爲特權位(DPL),而後操做系統將整個內存分爲不一樣的段,不一樣的段,在GDT對應的表項中的DPL位是不一樣的。好比內核內存段的全部特權位都爲00
。而用戶程序訪存時,在保護模式下都是經過段寄存器+IP寄存器來訪問的,而段寄存器裏則用兩位表示當前進程的級別(CPL),是位於內核態仍是用戶態。spa
既然如此,那咱們還有什麼辦法能夠調用操做系統的內核代碼呢?操做系統爲了實現系統調用,提供了一個主動進入內核的唯一方式:中斷指令int
。int
指令會將GDT表中的DPL改成3,讓咱們能夠訪問內核中的函數。因此全部的系統調用都必須經過調用int
指令來實現,大體的過程以下:操作系統
下面咱們以printf
函數的調用爲例,說明該函數是如何一步一步最終落在內核函數上去的。
printf函數是C語言的一個庫函數,它並非真正的系統調用,在Unix下,它是經過調用write
函數來完成功能的。
write函數內部就是調用了int
中斷。通常的系統調用都是調用0x80號中斷。而操做系統中通常不會的顯式的寫出write的實現代碼,而是經過_syscall3
宏展開的實現。_syscall3
是專門用來處理有3個參數的系統調用的函數的實現。同理還有_syscall0
、_syscall1
和_syscall2
等,目前最大支持的參數個數爲3個,這三個參數是經過ebx
, ecx
,edx
傳遞的。若是有系統調用的參數超過了3個,那麼能夠經過一個參數結構體來進行傳遞。
// linux/lib/write.c #define __LIBRARY__ #include <unistd.h> // _syscall3(int,write,int,fd,const char *,buf,off_t,count)
// linux/include/unistd.h #define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a,btype b,ctype c) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \ if (__res>=0) \ return (type) __res; \ errno=-__res; \ return -1; \ }
因此宏展開後,write函數的實現實現爲:
int write(int fd, const char *buf, off_t count) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_write),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); if (__res>=0) return (type) __res; errno=-__res; return -1; }
咱們看到實際函數內部並無作太多的事情,主要就是調用int 0x80
,將把相關的參數傳遞給一些通用寄存器,調用的結果經過eax
返回。其中一個很重要的調用參數是__NR_write
這個也是一個宏,就是wirte的系統調用號,在linux/include/unistd.h中被定義爲4,一樣還有不少其餘系統調用號。由於全部的系統調用都是經過int 0x80
,那怎麼知道具體須要什麼功能呢,只能經過系統調用號來識別。
下面咱們來看看int 0x80
是如何執行的。這是一個系統中斷,操做系統對於中斷處理流程通常爲:
前3項一般由處理中斷的硬件電路完成,後3項一般由軟件(中斷服務程序)完成。
那0x80號中斷的處理程序是什麼呢,咱們能夠看一下操做系統是如何設置這個中斷向量表的。在操做系統初始化時shecd_init
函數裏,調用了
set_system_gate(0x80, &system_call);
咱們深刻看一下set_system_gate
函數作了什麼
#define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ("movw %%dx,%%ax\n\t" \ "movw %0,%%dx\n\t" \ "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000)) #define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)
經過上面的代碼,咱們能夠看出,set_system_gate
把第0x80中斷表的表項中中斷處理程序入口地址設置爲&system_call。而且把那一項IDT表中的DPL設置了爲3, 方便用戶程序能夠去訪問這個地址。
因此init 0x80
最終會被system_call
這個函數地址處的代碼來實際處理。讓咱們看下system_call
作了什麼事情。
# linux/kernel/system_call.s nr_system_calls=72 # 最大的系統調用個數 .globl _system_call system_call: cmpl $nr_system_calls-1,%eax # eax中放的系統調用號,在write的調用過程當中爲__NR_write = 4 ja bad_sys_call push %ds # 下面是一些寄存器保護,後面還要彈出 push %es push %fs pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds # 把ds的段標號設置爲0001 0000(最後2位是特權級),因此段號爲4,內核態數據段 mov %dx,%es movl $0x17,%edx # 把fs的段標號設置爲0001 0111(最後2位是特權級),因此段號爲5,用戶態數據段 mov %dx,%fs call sys_call_table(,%eax,4) # 實際的系統調用 pushl %eax movl current,%eax cmpl $0,state(%eax) # state 檢測是否爲就緒狀態 jne reschedule # 進入調度程序 cmpl $0,counter(%eax) # counter 查看信號狀態 je reschedule ret_from_sys_call: movl current,%eax # task[0] cannot have signals cmpl task,%eax je 3f cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f movl signal(%eax),%ebx movl blocked(%eax),%ecx notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal(%eax) incl %ecx pushl %ecx call do_signal popl %eax 3: popl %eax popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret
咱們能夠發現,上面代碼中大部分代碼是寄存器狀態保存與恢復,堆棧段的切換。核心代碼爲call sys_call_table(,%eax,4)
,它是一個函數調用,函數的地址爲sys_call_table(,%eax,4) = sys_call_table + 4*%eax
說明sys_call_table
爲一個數組入口,數組中的元素長度都爲4個字節,咱們要訪問數組中的第%eax
個元素。而%eax
即爲系統調用號。sys_call_table
就是全部系統調用的函數指針數組。
// 定義在 linux/include/linux/sys.h fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link, sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod, sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount, sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm, sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access, sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir, sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid, sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys, sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit, sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid, sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask, sys_setreuid,sys_setregid };
到這裏,咱們找到了最終真正的執行核心函數地址sys_write
,這個是操做實現的內核代碼,全部的屏幕打印就是由該函數最終實現。它裏面涉及IO的一些硬件驅動函數,咱們在這裏就再也不繼續深刻了。
到此,咱們已經經過printf這樣一個上層的函數接口,清楚操做系統是如何一步步爲了咱們提供了一個內核調用的方法。如此的精細控制,讓人感嘆。
下面簡單說明一下,如何在操做系統源碼中添加兩個咱們本身的系統調用whoami
和iam
下面是具體的操做步驟。
要注意的是:在系統調用的過程當中,段寄存器ds和es指向內核數據空間,而fs被設置指向用戶數據空間。所以在實際數據塊信息傳遞過程當中Linux內核就能夠利用fs寄存器來執行內核數據空間與用戶數據空間之間的數據複製工做,而且在複製過程當中內核程序不須要對數據邊界範圍做任何檢查操做。邊界檢查操做由CPU自動完成。內核程序的實際數據傳送工做可使用get_fs_byte()
和puts_fs_bypte()
等函數進行。
[1] 《Linux內核徹底剖析基於0.12內核》 趙炯著。 [2] 網易雲課堂,哈爾濱工業大學《操做系統之應用》 李治軍。