Linux系統調用過程分析

來源:互聯網
上載者:User

標籤:posix   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是否有效。必須檢查每個參數,保證它們不但合法有效,而且正確。
 
最重要的一種檢查就是檢查使用者提供的指標是否有效。試想,如果一個進程可以給核心傳遞指標而又無須被檢查,那麼它就可以給出一個它根本就沒有存取權限的指標,哄騙核心去為它拷貝本不允許它訪問的資料,如原本屬於其他進程的資料。在接收一個使用者空間的指標之前,核心必須保證:
2      指標指向的記憶體地區屬於使用者空間。進程決不能哄騙核心去讀核心空間的資料。
2      指標指向的記憶體地區在進程的地址空間裡。進程決不能哄騙核心去讀其他進程的資料。
2      如果是讀,該記憶體應被標記為可讀。如果是寫,該記憶體應被標記為可寫。進程決不能繞過記憶體訪問限制。
 
核心提供了兩個方法來完成必須的檢查和核心空間與使用者空間之間資料的來回拷貝。注意,核心無論何時都不能輕率地接受來自使用者空間的指標!這兩個方法中必須有一個被調用。為了向使用者空間寫入資料,核心提供了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。核心中它的實現非常簡單:
asmlinkage long sys_ getpid(void)
{
    return current-> tgid;
}
 
上述的系統調用儘管非常簡單,但我們還是可以從中發現兩個特別之處。首先,注意函式宣告中的asmlinkage限定詞,這是一個小戲法,用於通知編譯器僅從棧中提取該函數的參數。所有的系統調用都需要這個限定詞。其次,注意系統調用get_pid()在核心中被定義成sys_ getpid。這是Linux中所有系統調用都應該遵守的命名規則
 
4       添加新系統調用
給Linux添加一個新的系統調用是件相對容易的工作。怎樣設計和實現一個系統調用是難題所在,而把它加到核心裡卻無須太多周折。讓我們關注一下實現一個新的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()系統調用了。
 
建立一個新的系統調用非常容易,但卻絕不提倡這麼做。通常模組可以更好的代替建立一個系統調用。
 
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)盡量避免建立系統調用,可用建立裝置結點的方法代替。

Linux系統調用過程分析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.