1. 程式人生 > >Linux系統呼叫過程分析

Linux系統呼叫過程分析

Linux系統呼叫過程分析

參考:

《Linux核心設計與實現》

0 摘要

linux的系統呼叫過程:
層次例如以下:
使用者程式------>C庫(即API):INT 0x80 ----->system_call------->系統呼叫服務例程-------->核心程式
先說明一下,我們常說的使用者API事實上就是系統提供的C庫。
系統呼叫是通過軟中斷指令 INT 0x80 實現的,而這條INT 0x80指令就被封裝在C庫的函式中。


(軟中斷和我們常說的硬中斷不同之處在於,軟中斷是由指令觸發的,而不是由硬體外設引起的。)
INT 0x80 這條指令的執行會讓系統跳轉到一個預設的核心空間地址,它指向系統呼叫處理程式。即system_call函式。
(注意:。。!系統呼叫處理程式system_call 並非系統呼叫服務例程,系統呼叫服務例程是對一個詳細的系統呼叫的核心實現函式。而系統呼叫處理程式是在執行系統呼叫服務例程之前的一個引導過程。是針對INT 0x80這條指令,面向全部的系統呼叫的。

簡單來講,執行不論什麼系統呼叫。都是先通過呼叫C庫中的函式,這個函式裡面就會有軟中斷 INT 0x80 語句。然後轉到執行系統呼叫處理程式 system_call ,
system_call 再依據詳細的系統呼叫號轉到執行詳細的系統呼叫服務例程。)
system_call函式是怎麼找到詳細的系統呼叫服務例程的呢?通過系統呼叫號查詢系統呼叫表sys_call_table!軟中斷指令INT 0x80執行時,系統呼叫號會被放入 eax 暫存器中,system_call函式能夠讀取eax暫存器獲取,然後將其乘以4,生成偏移地址,然後以sys_call_table為基址。基址加上偏移地址,就能夠得到詳細的系統呼叫服務例程的地址了!


然後就到了系統呼叫服務例程了。

須要說明的是。系統呼叫服務例程僅僅會從堆疊裡獲取引數,所以在system_call執行前。會先將引數存放在暫存器中。system_call執行時會首先將這些暫存器壓入堆疊。

system_call退出後。使用者能夠從暫存器中獲得(被改動過的)引數。
 
另外:系統呼叫通過軟中斷INT 0x80陷入核心。跳轉到系統呼叫處理程式system_call函式,然後執行對應的服務例程。可是因為是代表使用者程序,所以這個執行過程並不屬於中斷上下文,而是程序上下文。因此。系統呼叫執行過程中,能夠訪問使用者程序的很多資訊,能夠被其它程序搶佔,能夠休眠。
當系統呼叫完畢後,把控制權交回到發起呼叫的使用者程序前。核心會有一次排程。

假設發現有優先順序更高的程序或當前程序的時間片用完,那麼會選擇優先順序更高的程序或又一次選擇程序執行。

1       系統呼叫意義
linux核心中設定了一組用於實現系統功能的子程式,稱為系統呼叫。

系統呼叫和普通庫函式呼叫很相似,僅僅是系統呼叫由作業系統核心提供,執行於核心態。而普通的函式呼叫由函式庫或使用者自己提供。執行於使用者態。
 
一般的,程序是不能訪問核心的。它不能訪問核心所佔記憶體空間也不能呼叫核心函式。CPU硬體決定了這些(這就是為什麼它被稱作"保護模式")。為了和使用者空間上執行的程序進行互動,核心提供了一組介面。透過該介面,應用程式能夠訪問硬體裝置和其它作業系統資源。這組介面在應用程式和核心之間扮演了使者的角色,應用程式傳送各種請求。而核心負責滿足這些請求(或者讓應用程式臨時擱置)。

實際上提供這組介面主要是為了保證系統穩定可靠。避免應用程式肆意妄行,惹出大麻煩。


 
系統呼叫在使用者空間程序和硬體裝置之間加入了一箇中間層。該層主要作用有三個:
(1) 它為使用者空間提供了一種統一的硬體的抽象介面。

比方當須要讀些檔案的時候,應用程式就能夠不去管磁碟型別和介質,甚至不用去管檔案所在的檔案系統究竟是哪種型別。
(2)系統呼叫保證了系統的穩定和安全。作為硬體裝置和應用程式之間的中間人,核心能夠基於許可權和其它一些規則對須要進行的訪問進行裁決。

舉例來說,這樣能夠避免應用程式不對地使用硬體裝置,竊取其它程序的資源,或做出其它什麼危害系統的事情。
(3) 每一個程序都執行在虛擬系統中,而在使用者空間和系統的其餘部分提供這樣一層公共介面。也是出於這樣的考慮。假設應用程式能夠任意訪問硬體而核心又對此一無所知的話,差點兒就沒法實現多工和虛擬記憶體,當然也不可能實現良好的穩定性和安全性。

在Linux中。系統呼叫是使用者空間訪問核心的惟一手段。除異常和中斷外,它們是核心惟一的合法入口。
 
2       API/POSIX/C庫的關係
普通情況下,應用程式通過應用程式設計介面(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設計中的一大亮點。大部分的程式設計問題都能夠被分割成兩個部分:“須要提供什麼功能”(機制)和“如何實現這些功能”(策略)。

 
3       系統呼叫的實現
3.1    系統呼叫處理程式
您也許疑惑: “當我輸入 cat /proc/cpuinfo 時。cpuinfo() 函式是怎樣被呼叫的?”核心完畢引導後,控制流就從相對直觀的“接下來呼叫哪個函式?”改變為取決於系統呼叫、異常和中斷。
 
使用者空間的程式無法直接執行核心程式碼。它們不能直接呼叫核心空間中的函式,由於核心駐留在受保護的地址空間上。

假設程序能夠直接在核心的地址空間上讀寫的話,系統安全就會失去控制。

所以,應用程式應該以某種方式通知系統,告訴核心自己須要執行一個系統呼叫,希望系統切換到核心態,這樣核心就能夠代表應用程式來執行該系統呼叫了。
 
通知核心的機制是靠軟體中斷實現的。首先,使用者程式為系統呼叫設定引數。當中一個引數是系統呼叫編號。引數設定完畢後,程式執行“系統呼叫”指令。x86系統上的軟中斷由int產生。

這個指令會導致一個異常:產生一個事件,這個事件會致使處理器切換到核心態並跳轉到一個新的地址,並開始執行那裡的異常處理程式。此時的異常處理程式實際上就是系統呼叫處理程式。

它與硬體體系結構緊密相關。
 
新地址的指令會儲存程式的狀態。計算出應該呼叫哪個系統呼叫,呼叫核心中實現那個系統呼叫的函式,恢復使用者程式狀態,然後將控制權返還給使用者程式。

系統呼叫是裝置驅動程式中定義的函式終於被呼叫的一種方式。 
3.2    系統呼叫號
在Linux中。每一個系統呼叫被賦予一個系統呼叫號。

這樣,通過這個獨一無二的號就能夠關聯絡統呼叫。當用戶空間的程序執行一個系統呼叫的時候。這個系統呼叫號就被用來指明究竟是要執行哪個系統呼叫。程序不會提及系統呼叫的名稱。
 
系統呼叫號相當關鍵。一旦分配就不能再有不論什麼變更,否則編譯好的應用程式就會崩潰。Linux有一個“未實現”系統呼叫sys_ni_syscall(),它除了返回一ENOSYS外不做不論什麼其它工作,這個錯誤號就是專門針對無效的系統呼叫而設的。
 
由於全部的系統呼叫陷入核心的方式都一樣,所以不過陷入核心空間是不夠的。因此必須把系統呼叫號一併傳給核心。在x86上,系統呼叫號是通過eax暫存器傳遞給核心的。在陷人核心之前,使用者空間就把相應系統呼叫所相應的號放入eax中了。

這樣系統呼叫處理程式一旦執行,就能夠從eax中得到資料。其它體系結構上的實現也都類似。
 
核心記錄了系統呼叫表中的全部已註冊過的系統呼叫的列表。儲存在sys_call_table中。它與體系結構有關,一般在entry.s中定義。

這個表中為每個有效的系統呼叫指定了惟一的系統呼叫號。sys_call_table是一張由指向實現各種系統呼叫的核心函式的函式指標組成的表:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open) /* 5 */
.long SYMBOL_NAME(sys_close)
.long SYMBOL_NAME(sys_waitpid)
。。

。。。
.long SYMBOL_NAME(sys_capget)
.long SYMBOL_NAME(sys_capset)      /* 185 */
.long SYMBOL_NAME(sys_sigaltstack)
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork)      /* 190 */
 
system_call()函式通過將給定的系統呼叫號與NR_syscalls做比較來檢查其有效性。

假設它大於或者等於NR syscalls,該函式就返回一ENOSYS。

否則。就執行對應的系統呼叫。


      call *sys_ call-table(,%eax, 4)
因為系統呼叫表中的表項是以32位(4位元組)型別存放的,所以核心須要將給定的系統呼叫號乘以4,然後用所得的結果在該表中查詢其位置
 
3.3    引數傳遞
除了系統呼叫號以外。大部分系統呼叫都還須要一些外部的引數輸人。所以。在發生異常的時候。應該把這些引數從使用者空間傳給核心。最簡單的辦法就是像傳遞系統呼叫號一樣把這些引數也存放在暫存器裡。

在x86系統上,ebx, ecx, edx, esi和edi依照順序存放前五個引數。須要六個或六個以上引數的情況不多見,此時,應該用一個單獨的暫存器存放指向全部這些引數在使用者空間地址的指標。


 
給使用者空間的返回值也通過暫存器傳遞。在x86系統上。它存放在eax暫存器中。接下來很多關於系統呼叫處理程式的描寫敘述都是針對x86版本號的。

但不用操心,全部體系結構的實現都非常類似。
 
3.4    引數驗證
系統呼叫必須細緻檢查它們全部的引數是否合法有效。

舉例來說。與檔案I/O相關的系統呼叫必須檢查檔案描寫敘述符是否有效。

與程序相關的函式必須檢查提供的PID是否有效。必須檢查每一個引數。保證它們不但合法有效,並且正確。


 
最重要的一種檢查就是檢查使用者提供的指標是否有效。試想,假設一個程序能夠給核心傳遞指標而又無須被檢查,那麼它就能夠給出一個它根本就沒有訪問許可權的指標,哄騙核心去為它拷貝本不同意它訪問的資料,如原本屬於其它程序的資料。

在接收一個使用者空間的指標之前,核心必須保證:
²      指標指向的記憶體區域屬於使用者空間。程序決不能哄騙核心去讀核心空間的資料。
²      指標指向的記憶體區域在程序的地址空間裡。程序決不能哄騙核心去讀其它程序的資料。
²      假設是讀,該記憶體應被標記為可讀。假設是寫。該記憶體應被標記為可寫。程序決不能繞過記憶體訪問限制。


 
核心提供了兩個方法來完畢必須的檢查和核心空間與使用者空間之間資料的來回拷貝。注意。核心不管何時都不能輕率地接受來自使用者空間的指標!這兩個方法中必須有一個被呼叫。

為了向用戶空間寫入資料,核心提供了copy_to_user(),它須要三個引數。第一個引數是程序空間中的目的記憶體地址。

第二個是核心空間內的源地址。

最後一個引數是須要拷貝的資料長度(位元組數)。
 
為了從使用者空間讀取資料,核心提供了copy_from_ user(),它和copy-to-User()相似。該函式把第二個引數指定的位置上的資料複製到第一個引數指定的位置上,拷貝的資料長度由第三個引數決定。


 
假設執行失敗,這兩個函式返回的都是沒能完畢拷貝的資料的位元組數。

假設成功,返回0。當出現上述錯誤時,系統呼叫返回標準-EFAULT。


 
注意copy_to_user()和copy_from_user()都有可能引起堵塞。當包括使用者資料的頁被換出到硬碟上而不是在實體記憶體上的時候,這樣的情況就會發生。此時。程序就會休眠,直到缺頁處理程式將該頁從硬碟又一次換回實體記憶體。
 
3.5    系統呼叫的返回值
系統呼叫(在Linux中常稱作syscalls)通常通過函式進行呼叫。

它們通常都須要定義一個或幾個引數(輸入)並且可能產生一些副作用。比如寫某個檔案或向給定的指標拷貝資料等等。

為防止和正常的返回值混淆,系統呼叫並不直接返回錯誤碼,而是將錯誤碼放入一個名為errno的全域性變數中。

通經常使用一個負的返回值來表明錯誤。返回一個0值通常表明成功。假設一個系統呼叫失敗,你能夠讀出errno的值來確定問題所在。通過呼叫perror()庫函式,能夠把該變數翻譯成使用者能夠理解的錯誤字串。
 
errno不同數值所代表的錯誤訊息定義在errno.h中,你也能夠通過命令"man 3 errno"來察看它們。

須要注意的是,errno的值僅僅在函式錯誤發生時設定,假設函式不錯誤發生,errno的值就無定義,並不會被置為0。另外,在處理errno前最好先把它的值存入還有一個變數。由於在錯誤處理過程中。即使像printf()這種函數出錯時也會改變errno的值。
 
當然,系統呼叫終於具有一種明白的操作。

舉例來說,如getpid()系統呼叫。依據定義它會返回當前程序的PID。核心中它的實現很easy:
asmlinkage long sys_ getpid(void)
{
    return current-> tgid;
}
 
上述的系統呼叫雖然很easy,但我們還是能夠從中發現兩個特別之處。首先,注意函式宣告中的asmlinkage限定詞,這是一個小戲法。用於通知編譯器僅從棧中提取該函式的引數。全部的系統呼叫都須要這個限定詞。其次,注意系統呼叫get_pid()在核心中被定義成sys_ getpid。

這是Linux中全部系統呼叫都應該遵守的命名規則
 
4       加入新系統呼叫
給Linux加入一個新的系統呼叫是件相對easy的工作。如何設計和實現一個系統呼叫是難題所在,而把它加到核心裡卻無須太多周折。讓我們關注一下實現一個新的Linux系統呼叫所需的步驟。


 
實現一個新的系統呼叫的第一步是決定它的用途。

它要做些什麼?每一個系統呼叫都應該有一個明白的用途。

在Linux中不提倡採用多用途的系統呼叫(一個系統呼叫通過傳遞不同的引數值來選擇完畢不同的工作)。ioctl()就應該被視為一個反例。


 
新系統呼叫的引數、返回值和錯誤碼又該是什麼呢?系統呼叫的介面應該力求簡潔。引數儘可能少。

設計介面的時候要儘量為將來多做考慮。

你是不是對函式做了不必要的限制?系統呼叫設計得越通用越好。

不要如果這個系統呼叫如今怎麼用將來也一定就是這麼用。

系統呼叫的目的可能不變,但它的使用方法卻可能改變。

這個系統呼叫可移植嗎?

別對機器的位元組長度和位元組序做如果。

當你寫一個系統呼叫的時候,要時刻注意可移植性和健壯性。不但要考慮當前,還要為將來做打算。
 
當編寫完一個系統呼叫後。把它註冊成一個正式的系統呼叫是件瑣碎的工作:
在系統呼叫表的最後增加一個表項。

每種支援該系統呼叫的硬體體系都必須做這種工作。從0開始算起,系統呼叫在該表中的位置就是它的系統呼叫號。
對於所支援的各種體系結構,系統呼叫號都必須定義於<asm/unistd.h>中。
系統呼叫必須被編譯進核心映象(不能被編譯成模組)。這僅僅要把它放進kernel/下的一個相關檔案裡就能夠。


 
讓我們通過一個虛構的系統呼叫f00()來細緻觀察一下這些步驟。

首先,我們要把sys_foo增加到系統呼叫表中去。對於大多數體系結構來說,該表位幹entry.s檔案裡。形式例如以下:
ENTRY(sys_ call_ table)
      ·long sys_ restart_ syscall/*0*/
      .long sys_ exit
      ·long sys_ fork
      ·long sys_ read
      .long sys_write
我們把新的系統呼叫加到這個表的末尾:
     .long sys_foo
儘管沒有明白地指定編號,但我們增加的這個系統呼叫被依照次序分配給了283這個系統呼叫號。對於每種須要支援的體系結構,我們都必須將自己的系統呼叫加人到其系統呼叫表中去。每種體系結構不須要相應同樣的系統呼叫號。
 
接下來,我們把系統呼叫號增加到<asm/unistd.h>中,它的格式例如以下:
/*本檔案包括系統呼叫號*/
#define_ NR_ restart_ syscall
#define NR exit
#define NR fork
#define NR read
#define NR write
#define NR- mq getsetattr 282
然後,我們在該列表中增加以下這行:
#define_ NR_ foo 283
 
最後,我們來實現f00()系統呼叫。不管何種配置,該系統呼叫都必須編譯到核心的核心映象中去,所以我們把它放進kernel/sys.c檔案裡。你也能夠將其放到與其功能聯絡最緊密的程式碼中去
 
asmlinkage long sys-foo(void)
{
return THREAD SIZE
)
就是這樣!嚴格說來,如今就能夠在使用者空間呼叫f00()系統呼叫了。
 
建立一個新的系統呼叫很easy。但卻絕不提倡這麼做。通常模組能夠更好的取代新建一個系統呼叫。
 
5       訪問系統呼叫
5.1    系統呼叫上下文
核心在執行系統呼叫的時候處於程序上下文。

current指標指向當前任務,即引發系統呼叫的那個程序。


 
在程序上下文中,核心能夠休眠而且能夠被搶佔。這兩點都非常重要。首先。能夠休眠說明系統呼叫能夠使用核心提供的絕大部分功能。

休眠的能力會給核心程式設計帶來極大便利。在程序上下文中能夠被搶佔,事實上表明,像使用者空間內的程序一樣。當前的程序相同能夠被其它程序搶佔。由於新的程序能夠使用相同的系統呼叫。所以必須小心。保證該系統呼叫是可重人的。

當然。這也是在對稱多處理中必須相同關心的問題。
 
當系統呼叫返回的時候,控制權仍然在system_call()中。它終於會負責切換到使用者空間並讓使用者程序繼續執行下去。


 
5.2    系統呼叫訪問演示樣例
作業系統使用系統呼叫表將系統呼叫編號翻譯為特定的系統呼叫。系統呼叫表包括有實現每一個系統呼叫的函式的地址。比如,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。它會去檢查那些在切換回使用者空間之前須要完畢的任務。假設沒有須要做的事情,那麼就恢復使用者程序的狀態。並將控制權交還給使用者程式。
5.3    從使用者空間直接訪問系統呼叫
通常。系統呼叫靠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;
}

6 實際使用的注意

(1)系統呼叫是須要提前編譯固化到核心中的。並且須要官方分配一個系統呼叫號

(2)須要將系統呼叫註冊到支援的每一種體系結構中

(3)系統呼叫一般不能在指令碼中直接訪問

(4)儘量避免新建系統呼叫,可用建立裝置結點的方法取代。