計算機系統的各類硬件資源是有限的,在現代多任務操做系統上同時運行的多個進程都須要訪問這些資源,爲了更好的管理這些資源進程是不容許直接操做的,全部對這些資源的訪問都必須有操做系統控制。也就是說操做系統是使用這些資源的惟一入口,而這個入口就是操做系統提供的系統調用(System Call)。在linux中系統調用是用戶空間訪問內核的惟一手段,除異常和陷入外,他們是內核惟一的合法入口。linux
通常狀況下應用程序經過應用編程接口API,而不是直接經過系統調用來編程。在Unix世界,最流行的API是基於POSIX標準的。程序員
操做系統通常是經過中斷從用戶態切換到內核態。中斷就是一個硬件或軟件請求,要求CPU暫停當前的工做,去處理更重要的事情。好比,在x86機器上能夠經過int指令進行軟件中斷,而在磁盤完成讀寫操做後會向CPU發起硬件中斷。golang
中斷有兩個重要的屬性,中斷號和中斷處理程序。中斷號用來標識不一樣的中斷,不一樣的中斷具備不一樣的中斷處理程序。在操做系統內核中維護着一箇中斷向量表(Interrupt Vector Table),這個數組存儲了全部中斷處理程序的地址,而中斷號就是相應中斷在中斷向量表中的偏移量。編程
通常地,系統調用都是經過軟件中斷實現的,x86系統上的軟件中斷由int $0x80指令產生,而128號異常處理程序就是系統調用處理程序system_call(),它與硬件體系有關,在entry.S中用匯編寫。接下來就來看一下Linux下系統調用具體的實現過程。api
linux內核中設置了一組用於實現系統功能的子程序,稱爲系統調用。系統調用和普通庫函數調用很是類似,只是系統調用由操做系統核心提供,運行於內核態,而普通的函數調用由函數庫或用戶本身提供,運行於用戶態。數組
通常的,進程是不能訪問內核的。它不能訪問內核所佔內存空間也不能調用內核函數。CPU硬件決定了這些(這就是爲何它被稱做「保護模式」)。安全
爲了和用戶空間上運行的進程進行交互,內核提供了一組接口。透過該接口,應用程序能夠訪問硬件設備和其餘操做系統資源。這組接口在應用程序和內核之間扮演了使者的角色,應用程序發送各類請求,而內核負責知足這些請求(或者讓應用程序暫時擱置)。實際上提供這組接口主要是爲了保證系統穩定可靠,避免應用程序肆意妄行,惹出大麻煩。服務器
系統調用在用戶空間進程和硬件設備之間添加了一箇中間層。該層主要做用有三個:網絡
一、它爲用戶空間提供了一種統一的硬件的抽象接口。好比當須要讀些文件的時候,應用程序就能夠不去管磁盤類型和介質,甚至不用去管文件所在的文件系統究竟是哪一種類型。架構
二、系統調用保證了系統的穩定和安全。做爲硬件設備和應用程序之間的中間人,內核能夠基於權限和其餘一些規則對須要進行的訪問進行裁決。舉例來講,這樣能夠避免應用程序不正確地使用硬件設備,竊取其餘進程的資源,或作出其餘什麼危害系統的事情。
三、每一個進程都運行在虛擬系統中,而在用戶空間和系統的其他部分提供這樣一層公共接口,也是出於這種考慮。若是應用程序能夠隨意訪問硬件而內核又對此一無所知的話,幾乎就無法實現多任務和虛擬內存,固然也不可能實現良好的穩定性和安全性。在Linux中,系統調用是用戶空間訪問內核的唯一手段;除異常和中斷外,它們是內核唯一的合法入口。
通常狀況下,應用程序經過應用編程接口(API)而不是直接經過系統調用來編程。這點很重要,由於應用程序使用的這種編程接口實際上並不須要和內核提供的系統調用一一對應。
一個API定義了一組應用程序使用的編程接口。它們能夠實現成一個系統調用,也能夠經過調用多個系統調用來實現,而徹底不使用任何系統調用也不存在問題。實際上,API能夠在各類不一樣的操做系統上實現,給應用程序提供徹底相同的接口,而它們自己在這些系統上的實現卻可能迥異。
在Unix世界中,最流行的應用編程接口是基於POSIX標準的,其目標是提供一套大致上基於Unix的可移植操做系統標準。POSIX是說明API和系統調用之間關係的一個極好例子。在大多數Unix系統上,根據POSIX而定義的API函數和系統調用之間有着直接關係。
Linux的系統調用像大多數Unix系統同樣,做爲C庫的一部分提供以下圖所示。C庫實現了 Unix系統的主要API,包括標準C庫函數和系統調用。全部的C程序均可以使用C庫,而因爲C語言自己的特色,其餘語言也能夠很方便地把它們封裝起來使用。
從程序員的角度看,系統調用可有可無,他們只須要跟API打交道就能夠了。相反,內核只跟系統調用打交道;庫函數及應用程序是怎麼使用系統調用不是內核所關心的。
關於Unix的界面設計有一句通用的格言「提供機制而不是策略」。換句話說,Unix的系統調用抽象出了用於完成某種肯定目的的函數。至幹這些函數怎麼用徹底不須要內核去關心。區別對待機制(mechanism)和策略(policy)是Unix設計中的一大亮點。大部分的編程問題均可以被切割成兩個部分:「須要提供什麼功能」(機制)和「怎樣實現這些功能」(策略)。
api是函數的定義,規定了這個函數的功能,跟內核無直接關係。而系統調用是經過中斷向內核發請求,實現內核提供的某些服務。
一個api可能會須要一個或多個系統調用來完成特定功能。通俗點說就是若是這個api須要跟內核打交道就須要系統調用,不然不須要。
程序員調用的是API(API函數),而後經過與系統調用共同完成函數的功能。
所以,API是一個提供給應用程序的接口,一組函數,是與程序員進行直接交互的。
系統調用則不與程序員進行交互的,它根據API函數,經過一個軟中斷機制向內核提交請求,以獲取內核服務的接口。
並非全部的API函數都一一對應一個系統調用,有時,一個API函數會須要幾個系統調用來共同完成函數的功能,甚至還有一些API函數不須要調用相應的系統調用(所以它所完成的不是內核提供的服務)
須要C/C++ Linux高級服務器架構師學習資料後臺加羣812855908(包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)
前文已經提到了Linux下的系統調用是經過0x80實現的,可是咱們知道操做系統會有多個系統調用(Linux下有319個系統調用),而對於同一個中斷號是如何處理多個不一樣的系統調用的?最簡單的方式是對於不一樣的系統調用採用不一樣的中斷號,可是中斷號明顯是一種稀缺資源,Linux顯然不會這麼作;還有一個問題就是系統調用是須要提供參數,而且具備返回值的,這些參數又是怎麼傳遞的?也就是說,對於系統調用咱們要搞清楚兩點:
首先看第一個問題。實際上,Linux中每一個系統調用都有相應的系統調用號做爲惟一的標識,內核維護一張系統調用表,sys_call_table,表中的元素是系統調用函數的起始地址,而系統調用號就是系統調用在調用表的偏移量。在x86上,系統調用號是經過eax寄存器傳遞給內核的。好比fork()的實現:
用戶空間的程序沒法直接執行內核代碼。它們不能直接調用內核空間中的函數,由於內核駐留在受保護的地址空間上。若是進程能夠直接在內核的地址空間上讀寫的話,系統安全就會失去控制。因此,應用程序應該以某種方式通知系統,告訴內核本身須要執行一個系統調用,但願系統切換到內核態,這樣內核就能夠表明應用程序來執行該系統調用了。
通知內核的機制是靠軟件中斷實現的。首先,用戶程序爲系統調用設置參數。其中一個參數是系統調用編號。參數設置完成後,程序執行「系統調用」指令。x86系統上的軟中斷由int產生。這個指令會致使一個異常:產生一個事件,這個事件會導致處理器切換到內核態並跳轉到一個新的地址,並開始執行那裏的異常處理程序。此時的異常處理程序實際上就是系統調用處理程序。它與硬件體系結構緊密相關。
新地址的指令會保存程序的狀態,計算出應該調用哪一個系統調用,調用內核中實現那個系統調用的函數,恢復用戶程序狀態,而後將控制權返還給用戶程序。系統調用是設備驅動程序中定義的函數最終被調用的一種方式。
從系統分析的角度,linux的系統調用涉及4個方面的問題。
響應函數名以「sys_」開頭,後跟該系統調用的名字。
例如系統調用fork()的響應函數是sys_fork()(見Kernel/fork.c),
exit()的響應函數是sys_exit()(見kernel/fork.)。
文件include/asm/unisted.h爲每一個系統調用規定了惟一的編號。
在咱們系統中/usr/include/asm/unistd_32.h,能夠經過find / -name unistd_32.h -print查找)
而內核中的頭文件路徑不一樣的內核版本以及不一樣的發行版,文件的存儲結構可能有所區別
假設用name表示系統調用的名稱,那麼系統調用號與系統調用響應函數的關係是:以系統調用號_NR_name做爲下標,可找出系統調用表sys_call_table(見arch/i386/kernel/entry.S)中對應表項的內容,它正好是該系統調用的響應函數sys_name的入口地址。
系統調用表sys_call_table記錄了各sys_name函數在表中的位置,共190項。有了這張表,就很容易根據特定系統調用
在表中的偏移量,找到對應的系統調用響應函數的入口地址。系統調用表共256項,餘下的項是可供用戶本身添加的系統調用空間。
在Linux中,每一個系統調用被賦予一個系統調用號。這樣,經過這個獨一無二的號就能夠關聯繫統調用。當用戶空間的進程執行一個系統調用的時候,這個系統調用號就被用來指明究竟是要執行哪一個系統調用。進程不會說起系統調用的名稱。
系統調用號至關關鍵,一旦分配就不能再有任何變動,不然編譯好的應用程序就會崩潰。Linux有一個「未實現」系統調用sys_ni_syscall(),它除了返回一ENOSYS外不作任何其餘工做,這個錯誤號就是專門針對無效的系統調用而設的。
由於全部的系統調用陷入內核的方式都同樣,因此僅僅是陷入內核空間是不夠的。所以必須把系統調用號一併傳給內核。在x86上,系統調用號是經過eax寄存器傳遞給內核的。在陷人內核以前,用戶空間就把相應系統調用所對應的號放入eax中了。這樣系統調用處理程序一旦運行,就能夠從eax中獲得數據。其餘體系結構上的實現也都相似。
內核記錄了系統調用表中的全部已註冊過的系統調用的列表,存儲在sys_call_table中。它與體系結構有關,通常在entry.s中定義。這個表中爲每個有效的系統調用指定了唯一的系統調用號。sys_call_table是一張由指向實現各類系統調用的內核函數的函數指針組成的表:
system_call()函數經過將給定的系統調用號與NR_syscalls作比較來檢查其有效性。若是它大於或者等於NR syscalls,該函數就返回一ENOSYS。不然,就執行相應的系統調用。
call *sys_ call-table(,%eax, 4)1
因爲系統調用表中的表項是以32位(4字節)類型存放的,因此內核須要將給定的系統調用號乘以4,而後用所得的結果在該表中查詢其位置
宏定義_syscallN()見include/asm/unisted.h)用於系統調用的格式轉換和參數的傳遞。N取0~5之間的整數。
參數個數爲N的系統調用由_syscallN()負責格式轉換和參數傳遞。系統調用號放入EAX寄存器,啓動INT 0x80後,規定返回值送EAX寄存器。
對系統調用的初始化也就是對INT 0x80的初始化。
系統啓動時,彙編子程序setup_idt(見arch/i386/kernel/head.S)準備了1張256項的idt表,由start_kernel()(見init/main.c),trap_init()(見arch/i386/kernel/traps.c)調用的C語言宏定義set_system_gate(0x80,&system_call)(見include/asm/system.h)設置0x80號軟中斷的服務程序爲 system_call(見arch/i386/kernel/entry.S), system.call就是全部系統調用的總入口。
當進程須要進行系統調用時,必須以C語言函數的形式寫一句系統調用命令。該命令若是已在某個頭文件中由相應的_syscallN()展開,則用戶程序必須包含該文件。當進程執行到用戶程序的系統調用命令時,實際上執行了由宏命令_syscallN()展開的函數。系統調用的參數 由各通用寄存器傳遞,而後執行INT 0x80,之內核態進入入口地址system_call。
以ret_from_sys_call入口的彙編程序段在linux進程管理中起到了十分重要的做用。
全部系統調用結束前以及大部分中斷服務返回前,都會跳轉至此處入口地址。 該段程序不只僅爲系統調用服務,它還處理中斷嵌套、CPU調度、信號等事務。
除了系統調用號之外,大部分系統調用都還須要一些外部的參數輸人。因此,在發生異常的時候,應該把這些參數從用戶空間傳給內核。最簡單的辦法就是像傳遞系統調用號同樣把這些參數也存放在寄存器裏。在x86系統上,ebx, ecx, edx, esi和edi按照順序存放前五個參數。須要六個或六個以上參數的狀況很少見,此時,應該用一個單獨的寄存器存放指向全部這些參數在用戶空間地址的指針。
給用戶空間的返回值也經過寄存器傳遞。在x86系統上,它存放在eax寄存器中。接下來許多關於系統調用處理程序的描述都是針對x86版本的。但不用擔憂,全部體系結構的實現都很相似。
系統調用必須仔細檢查它們全部的參數是否合法有效。舉例來講,與文件I/O相關的系統調用必須檢查文件描述符是否有效。與進程相關的函數必須檢查提供的PID是否有效。必須檢查每一個參數,保證它們不但合法有效,並且正確。
最重要的一種檢查就是檢查用戶提供的指針是否有效。試想,若是一個進程能夠給內核傳遞指針而又無須被檢查,那麼它就能夠給出一個它根本就沒有訪問權限的指針,哄騙內核去爲它拷貝本不容許它訪問的數據,如本來屬於其餘進程的數據。在接收一個用戶空間的指針以前,內核必須保證:
內核提供了兩個方法來完成必須的檢查和內核空間與用戶空間之間數據的來回拷貝。注意,內核不管什麼時候都不能輕率地接受來自用戶空間的指針!這兩個方法中必須有一個被調用。爲了向用戶空間寫入數據,內核提供了copy_to_user(),它須要三個參數。第一個參數是進程空間中的目的內存地址。第二個是內核空間內的源地址。最後一個參數是須要拷貝的數據長度(字節數)。
爲了從用戶空間讀取數據,內核提供了copy_from_ user(),它和copy-to-User()類似。該函數把第二個參數指定的位置上的數據拷貝到第一個參數指定的位置上,拷貝的數據長度由第三個參數決定。
若是執行失敗,這兩個函數返回的都是沒能完成拷貝的數據的字節數。若是成功,返回0。當出現上述錯誤時,系統調用返回標準-EFAULT。
注意copy_to_user()和copy_from_user()都有可能引發阻塞。當包含用戶數據的頁被換出到硬盤上而不是在物理內存上的時候,這種狀況就會發生。此時,進程就會休眠,直到缺頁處理程序將該頁從硬盤從新換回物理內存。
系統調用(在Linux中常稱做syscalls)一般經過函數進行調用。它們一般都須要定義一個或幾個參數(輸入)並且可能產生一些反作用,例如寫某個文件或向給定的指針拷貝數據等等。爲防止和正常的返回值混淆,系統調用並不直接返回錯誤碼,而是將錯誤碼放入一個名爲errno的全局變量中。一般用一個負的返回值來代表錯誤。返回一個0值一般代表成功。若是一個系統調用失敗,你能夠讀出errno的值來肯定問題所在。經過調用perror()庫函數,能夠把該變量翻譯成用戶能夠理解的錯誤字符串。
errno不一樣數值所表明的錯誤消息定義在errno.h中,你也能夠經過命令」man 3 errno」來察看它們。須要注意的是,errno的值只在函數發生錯誤時設置,若是函數不發生錯誤,errno的值就無定義,並不會被置爲0。另外,在處理errno前最好先把它的值存入另外一個變量,由於在錯誤處理過程當中,即便像printf()這樣的函數出錯時也會改變errno的值。
固然,系統調用最終具備一種明確的操做。舉例來講,如getpid()系統調用,根據定義它會返回當前進程的PID。內核中它的實現很是簡單:
asmlinkage long sys_ getpid(void) { return current-> tgid; }
上述的系統調用盡管很是簡單,但咱們仍是能夠從中發現兩個特別之處。首先,注意函數聲明中的asmlinkage限定詞,這是一個小戲法,用於通知編譯器僅從棧中提取該函數的參數。全部的系統調用都須要這個限定詞。其次,注意系統調用get_pid()在內核中被定義成sys_ getpid。這是Linux中全部系統調用都應該遵照的命名規則。
內核在執行系統調用的時候處於進程上下文。current指針指向當前任務,即引起系統調用的那個進程。
在進程上下文中,內核能夠休眠而且能夠被搶佔。這兩點都很重要。首先,可以休眠說明系統調用可使用內核提供的絕大部分功能。休眠的能力會給內核編程帶來極大便利。在進程上下文中可以被搶佔,其實代表,像用戶空間內的進程同樣,當前的進程一樣能夠被其餘進程搶佔。由於新的進程可使用相同的系統調用,因此必須當心,保證該系統調用是可重人的。固然,這也是在對稱多處理中必須一樣關心的問題。
當系統調用返回的時候,控制權仍然在system_call()中,它最終會負責切換到用戶空間並讓用戶進程繼續執行下去。
操做系統使用系統調用表將系統調用編號翻譯爲特定的系統調用。系統調用表包含有實現每一個系統調用的函數的地址。例如,read() 系統調用函數名爲sys_read。read()系統調用編號是 3,因此sys_read() 位於系統調用表的第四個條目中(由於系統調用起始編號爲0)。從地址 sys_call_table + (3 * word_size) 讀取數據,獲得sys_read()的地址。
找到正確的系統調用地址後,它將控制權轉交給那個系統調用。咱們來看定義sys_read()的位置,即fs/read_write.c文件。這個函數會找到關聯到 fd 編號(傳遞給 read() 函數的)的文件結構體。那個結構體包含指向用來讀取特定類型文件數據的函數的指針。進行一些檢查後,它調用與文件相關的 read() 函數,來真正從文件中讀取數據並返回。與文件相關的函數是在其餘地方定義的 —— 好比套接字代碼、文件系統代碼,或者設備驅動程序代碼。這是特定內核子系統最終與內核其餘部分協做的一個方面。
讀取函數結束後,從sys_read()返回,它將控制權切換給 ret_from_sys。它會去檢查那些在切換回用戶空間以前須要完成的任務。若是沒有須要作的事情,那麼就恢復用戶進程的狀態,並將控制權交還給用戶程序。
一般,系統調用靠C庫支持。用戶程序經過包含標準頭文件並和C庫連接,就可使用系統調用(或者調用庫函數,再由庫函數實際調用)。但若是你僅僅寫出系統調用,glibc庫恐怕並不提供支持。值得慶幸的是,Linux自己提供了一組宏,用於直接對系統調用進行訪問。它會設置好寄存器並調用陷人指令。這些宏是_syscalln(),其中n的範圍從0到6。表明須要傳遞給系統調用的參數個數,這是因爲該宏必須瞭解到底有多少參數按照什麼次序壓入寄存器。舉個例子,open()系統調用的定義是:
long open(const char *filename, int flags, int mode)
而不靠庫支持,直接調用此係統調用的宏的形式爲:
#define NR_ open 5 syscall3(long, open, const char*,filename, int, flags, int, mode)
這樣,應用程序就能夠直接使用open()
對於每一個宏來講,都有2+ n個參數。
第一個參數對應着系統調用的返回值類型。
第二個參數是系統調用的名稱。再之後是按照系統調用參數的順序排列的每一個參數的類型和名稱。
NR open在<asm/unistd.h>中定義,是系統調用號。該宏會被擴展成爲內嵌彙編的C函數。由彙編語言執行前一節所討論的步驟,將系統調用號和參數壓入寄存器並觸發軟中斷來陷入內核。調用open()系統調用直接把上面的宏放置在應用程序中就能夠了。
讓咱們寫一個宏來使用前面編寫的foo()系統調用,而後再寫出測試代碼炫耀一下咱們所作的努力。
#define NR foo 283 _sysca110(long, foo) int main() { long stack size; stack_ size=foo(); printf("The kernel stack size is 81d/n",stack_ size); return; }
經過以上分析linux系統調用的過程,
將本身的系統調用加到內核中就是一件容易的事情。下面介紹一個實際的系統調用,
並把它加到內核中去。要增長的系統調用是:inttestsyscall(),其功能是在控制終端屏幕上顯示hello world,
執行成功後返回0。
編寫一個系統調用意味着要給內核增長1個函數,將新函數放入文件kernel/sys.c中。新函數代碼以下:
asmlingkage sys_testsyscall() { print("hello worldn"); return 0; }
編寫了新的系統調用過程後,下一項任務是使內核的其他部分知道這一程序的存在,而後重建包含新的系統調用的內核。爲了把新的函數鏈接到已有的內核中去, 須要編輯2個文件:
1).inculde/asm/unistd.h在這個文件中加入
#define_NR_testsyscall 191
2).are/i386/kernel/entry.s這個文件用來對指針數組初始化,在這個文件中增長一行:
.long SYMBOL_NAME(_sys_tsetsycall)
將.rept NR_syscalls-190改成NR_SYSCALLS-191,而後從新編譯和運行新內核。
在保證的C語言庫中沒有新的系統調用的程序段,必須本身創建其代碼以下
#inculde _syscall0(int,testsyscall) main() { tsetsyscall(); }
在這裏使用了_syscall0宏指令,宏指令自己在程序中將擴展成名爲syscall()的函數,它在main()函數內部加以調用。
在testsyscall()函數中, 預處理程序產生全部必要的機器指令代碼,包括用系統調用參數值加載相應的cpu寄存器, 而後執行int 0x80中斷指令。
在linux-3.8.4/kernel/sys.c 文件末尾添加新的系統調用函數如:
asmlinkage int sys_mycall(int number) { printk("這是我添加的第一個系統調用"); return number; }
在arch/x86/syscall_32.tbl下找到unused 223號調用而後替換如:
223 i386 mycall sys_mycall
若是是64位系統,在arch/x86/syscalls/syscall_64.tbl下找到313號系統調用,而後在其下面加上314號本身的中斷如:
`314 common mycall sys_mycall
模塊是內核的一部分,可是並無被編譯到內核裏面去。它們被分別編譯並鏈接成一組目標文件, 這些文件能被插入到正在運行的內核,或者從正在運行的內核中移走。內核模塊至少必須有2個函數:init_module和cleanup_module。
第一個函數是在把模塊插入內核時調用的;
第二個函數則在刪除該模塊時調用。因爲內核模塊是內核的一部分,因此能訪問全部內核資源。根據對linux系統調用機制的分析,
若是要增長系統調用,能夠編寫本身的函數來實現,而後在sys_call_table表中增長一項,使該項中的指針指向本身編寫的函數,
就能夠實現系統調用。下面用該方法實如今控制終端上打印「hello world」 的系統調用testsyscall()。
#inculde(linux/kernel.h) #inculde(linux/module.h) #inculde(linux/modversions.h) #inculde(linux/sched.h) #inculde(asm/uaccess.h) #define_NR_testsyscall 191 extern viod *sys_call+table[]; asmlinkage int testsyscall() { printf("hello worldn"); return 0; } int init_module() { sys_call_table[_NR_tsetsyscall]=testsyscall; printf("system call testsyscall() loaded successn"); return 0; } void cleanup_module() { }
#define_NR_testsyscall 191 _syscall0(int,testsyscall) main() { testsyscall(); }
如下是Linux系統調用的一個列表,包含了大部分經常使用系統調用和由系統調用派生出的的函數。
進程控制
文件讀寫操做
文件系統操做
系統控制
內存管理
網絡管理
socket控制
用戶管理
進程間通訊
信號
消息
管道
信號量
共享內存