linux核心剖析---Linux系統呼叫詳解(實現機制分析)
本文介紹了系統呼叫的一些實現細節。首先分析了系統呼叫的意義,它們與庫函式和應用程式介面(API)有怎樣的關係。然後,我們考察了Linux核心如何實現系統呼叫,以及執行系統呼叫的連鎖反應:陷入核心,傳遞系統呼叫號和引數,執行正確的系統呼叫函式,並把返回值帶回使用者空間。最後討論瞭如何增加系統呼叫,並提供了從使用者空間訪問系統呼叫的簡單例子。
參考《Linux核心設計與實現---- 系統呼叫 http://www.cnblogs.com/wang_yb/archive/2012/09/17/2688263.html
系統呼叫概述
計算機系統的各種硬體資源是有限的,在現代多工作業系統上同時執行的多個程序都需要訪問這些資源,為了更好的管理這些資源程序是不允許直接操作的,所有對這些資源的訪問都必須有 作業系統控制。也就是說作業系統是使用這些資源的唯一入口,而這個入口就是作業系統提供的系統呼叫(System Call)。在linux中系統呼叫是使用者空間訪問核心的唯一手段,除異常和陷入外,他們是核心唯一的合法入口。
一般情況下應用程式通過應用程式設計介面API,而不是直接通過系統呼叫來程式設計。在Unix世界,最流行的API是基於POSIX標準的。
作業系統一般是通過中斷從使用者態切換到核心態。中斷就是一個硬體或軟體請求,要求CPU暫停當前的工作,去處理更重要的事情。比如,在x86機器上可以通過int指令進行軟體中斷,而在磁碟完成讀寫操作後會向CPU發起硬體中斷。
中斷有兩個重要的屬性,
一般地,系統呼叫都是通過軟體中斷實現的,x86系統上的軟體中斷由int $0x80指令產生,而128號異常處理程式就是系統呼叫處理程式system_call(),它與硬體體系有關,在entry.S中用匯編寫。接下來就來看一下Linux下系統呼叫具體的實現過程。
為什麼需要系統呼叫
linux核心中設定了一組用於實現系統功能的子程式,稱為系統呼叫。系統呼叫和普通庫函式呼叫非常相似,只是
一般的,程序是不能訪問核心的。它不能訪問核心所佔記憶體空間也不能呼叫核心函式。CPU硬體決定了這些(這就是為什麼它被稱作“保護模式”(詳細參見深入理解計算機系統-之-記憶體定址(二)–儲存保護機制(CPU真實模式與保護模式)))。
為了和使用者空間上執行的程序進行互動,核心提供了一組介面。透過該介面,應用程式可以訪問硬體裝置和其他作業系統資源。這組介面在應用程式和核心之間扮演了使者的角色,應用程式傳送各種請求,而核心負責滿足這些請求(或者讓應用程式暫時擱置)。實際上提供這組介面主要是為了保證系統穩定可靠,避免應用程式肆意妄行,惹出大麻煩。
系統呼叫在使用者空間程序和硬體裝置之間添加了一箇中間層。該層主要作用有三個:
-
它為使用者空間提供了一種統一的硬體的抽象介面。比如當需要讀些檔案的時候,應用程式就可以不去管磁碟型別和介質,甚至不用去管檔案所在的檔案系統到底是哪種型別。
-
系統呼叫保證了系統的穩定和安全。作為硬體裝置和應用程式之間的中間人,核心可以基於許可權和其他一些規則對需要進行的訪問進行裁決。舉例來說,這樣可以避免應用程式不正確地使用硬體裝置,竊取其他程序的資源,或做出其他什麼危害系統的事情。
-
每個程序都執行在虛擬系統中,而在使用者空間和系統的其餘部分提供這樣一層公共介面,也是出於這種考慮。如果應用程式可以隨意訪問硬體而核心又對此一無所知的話,幾乎就沒法實現多工和虛擬記憶體,當然也不可能實現良好的穩定性和安全性。在Linux中,系統呼叫是使用者空間訪問核心的惟一手段;除異常和中斷外,它們是核心惟一的合法入口。
API/POSIX/C庫的區別與聯絡
一般情況下,應用程式通過應用程式設計介面(API)而不是直接通過系統呼叫來程式設計。這點很重要,因為應用程式使用的這種程式設計介面實際上並不需要和核心提供的系統呼叫一一對應。
一個API定義了一組應用程式使用的程式設計介面。它們可以實現成一個系統呼叫,也可以通過呼叫多個系統呼叫來實現,而完全不使用任何系統呼叫也不存在問題。實際上,API可以在各種不同的作業系統上實現,給應用程式提供完全相同的介面,而它們本身在這些系統上的實現卻可能迥異。
在Unix世界中,最流行的應用程式設計介面是基於POSIX標準的(可移植作業系統介面(Portable Operating System Interface ,縮寫為 POSIX )),其目標是提供一套大體上基於Unix的可移植作業系統標準。POSIX是說明API和系統呼叫之間關係的一個極好例子。在大多數Unix系統上,根據POSIX而定義的API函式和系統呼叫之間有著直接關係。
Linux的系統呼叫像大多數Unix系統一樣。C庫實現了 Unix系統的主要API,包括標準C庫函式和系統呼叫。所有的C程式都可以使用C庫,而由於C語言本身的特點,其他語言也可以很方便地把它們封裝起來使用。
從程式設計師的角度看,系統呼叫無關緊要,他們只需要跟API打交道就可以了。相反,核心只跟系統呼叫打交道;庫函式及應用程式是怎麼使用系統呼叫不是核心所關心的。
關於Unix的介面設計有一句通用的格言“提供機制而不是策略”。換句話說,Unix的系統呼叫抽象出了用於完成某種確定目的的函式。至於這些函式怎麼用完全不需要核心去關心。區別對待機制(mechanism)和策略(policy)是Unix設計中的一大亮點。大部分的程式設計問題都可以被切割成兩個部分:“需要提供什麼功能”(機制)和“怎樣實現這些功能”(策略)。
區別
api是函式的定義,規定了這個函式的功能,跟核心無直接關係。而系統呼叫是通過中斷向核心發請求,實現核心提供的某些服務。
聯絡
一個api可能會需要一個或多個系統呼叫來完成特定功能。通俗點說就是如果這個api需要跟核心打交道就需要系統呼叫,否則不需要。
程式設計師呼叫的是API(API函式),然後通過與系統呼叫共同完成函式的功能。
因此,API是一個提供給應用程式的介面,一組函式,是與程式設計師進行直接互動的。
系統呼叫則不與程式設計師進行互動的,它根據API函式,通過一個軟中斷機制向核心提交請求,以獲取核心服務的介面。
並不是所有的API函式都一一對應一個系統呼叫,有時,一個API函式會需要幾個系統呼叫來共同完成函式的功能,甚至還有一些API函式不需要呼叫相應的系統呼叫(因此它所完成的不是核心提供的服務)
系統呼叫的實現原理
基本機制
前文已經提到了Linux下的系統呼叫是通過0x80實現的,但是我們知道作業系統會有多個系統呼叫(Linux下有319個系統呼叫),而對於同一個中斷號是如何處理多個不同的系統呼叫的?最簡單的方式是對於不同的系統呼叫採用不同的中斷號,但是中斷號明顯是一種稀缺資源,Linux顯然不會這麼做;還有一個問題就是系統呼叫是需要提供引數,並且具有返回值的,這些引數又是怎麼傳遞的?也就是說,對於系統呼叫我們要搞清楚兩點:
- 系統呼叫的函式名稱轉換。
- 系統呼叫的引數傳遞。
首先看第一個問題。實際上,Linux中每個系統呼叫都有相應的系統呼叫號作為唯一的標識,核心維護一張系統呼叫表,sys_call_table,表中的元素是系統呼叫函式的起始地址,而系統呼叫號就是系統呼叫在呼叫表的偏移量。在x86上,系統呼叫號是通過eax暫存器傳遞給核心的。比如fork()的實現:
使用者空間的程式無法直接執行核心程式碼。它們不能直接呼叫核心空間中的函式,因為核心駐留在受保護的地址空間上。如果程序可以直接在核心的地址空間上讀寫的話,系統安全就會失去控制。所以,應用程式應該以某種方式通知系統,告訴核心自己需要執行一個系統呼叫,希望系統切換到核心態,這樣核心就可以代表應用程式來執行該系統呼叫了。
通知核心的機制是靠軟體中斷實現的。首先,使用者程式為系統呼叫設定引數。其中一個引數是系統呼叫編號。引數設定完成後,程式執行“系統呼叫”指令。x86系統上的軟中斷由int產生。這個指令會導致一個異常:產生一個事件,這個事件會致使處理器切換到核心態並跳轉到一個新的地址,並開始執行那裡的異常處理程式。此時的異常處理程式實際上就是系統呼叫處理程式。它與硬體體系結構緊密相關。
新地址的指令會儲存程式的狀態,計算出應該呼叫哪個系統呼叫,呼叫核心中實現那個系統呼叫的函式,恢復使用者程式狀態,然後將控制權返還給使用者程式。系統呼叫是裝置驅動程式中定義的函式最終被呼叫的一種方式。
從系統分析的角度,linux的系統呼叫涉及4個方面的問題。
響應函式sys_xxx
響應函式名以“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
- 1
由於系統呼叫表中的表項是以32位(4位元組)型別存放的,所以核心需要將給定的系統呼叫號乘以4,然後用所得的結果在該表中查詢其位置
程序的系統呼叫命令轉換為INT 0x80中斷的過程
巨集定義_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
以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;
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
上述的系統調用盡管非常簡單,但我們還是可以從中發現兩個特別之處。首先,注意函式宣告中的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)
- 1
- 1
而不靠庫支援,直接呼叫此係統呼叫的巨集的形式為:
#define NR_ open 5
syscall3(long, open, const char*,filename, int, flags, int, mode)
- 1
- 2
- 1
- 2
這樣,應用程式就可以直接使用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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
新增系統呼叫
通過修改核心原始碼新增系統呼叫
linux-2.6.*
通過以上分析linux系統呼叫的過程,
將自己的系統呼叫加到核心中就是一件容易的事情。下面介紹一個實際的系統呼叫,
並把它加到核心中去。要增加的系統呼叫是:inttestsyscall(),其功能是在控制終端螢幕上顯示hello world,
執行成功後返回0。
編寫int testsyscall()系統呼叫–響應函式
編寫一個系統呼叫意味著要給核心增加1個函式,將新函式放入檔案kernel/sys.c中。新函式程式碼如下:
asmlingkage sys_testsyscall()
{
print("hello world\n");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
新增系統呼叫號
編寫了新的系統呼叫過程後,下一項任務是使核心的其餘部分知道這一程式的存在,然後重建包含新的系統呼叫的核心。為了把新的函式連線到已有的核心中去, 需要編輯2個檔案:
1).inculde/asm/unistd.h在這個檔案中加入
#define_NR_testsyscall 191
- 1
- 1
系統呼叫表中新增對應項
2).are/i386/kernel/entry.s這個檔案用來對指標陣列初始化,在這個檔案中增加一行:
.long SYMBOL_NAME(_sys_tsetsycall)
- 1
- 1
將.rept NR_syscalls-190
改為NR_SYSCALLS-191
,然後重新編譯和執行新核心。
使用新的系統呼叫
在保證的C語言庫中沒有新的系統呼叫的程式段,必須自己建立其程式碼如下
#inculde
_syscall0(int,testsyscall)
main()
{
tsetsyscall();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
在這裡使用了_syscall0
巨集指令,巨集指令本身在程式中將擴充套件成名為syscall()
的函式,它在main()
函式內部加以呼叫。
在testsyscall()
函式中, 預處理程式產生所有必要的機器指令程式碼,包括用系統呼叫引數值載入相應的cpu
暫存器, 然後執行int 0x80
中斷指令。
linux-3.*
在linux-3.8.4/kernel/sys.c 檔案末尾新增新的系統呼叫函式如:
asmlinkage int sys_mycall(int number)
{
printk("這是我新增的第一個系統呼叫");
return number; }
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
在arch/x86/syscall_32.tbl
下找到unused 223
號呼叫然後替換如:
223 i386 mycall sys_mycall
- 1
- 1
如果是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 world\n");
return 0;
}
int init_module()
{
sys_call_table[_NR_tsetsyscall]=testsyscall;
printf("system call testsyscall() loaded success\n");
return 0;
}
void cleanup_module()
{
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
使用新的系統呼叫
#define_NR_testsyscall 191
_syscall0(int,testsyscall)
main()
{
testsyscall();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
核心Linux系統呼叫的列表
以下是Linux系統呼叫的一個列表,包含了大部分常用系統呼叫和由系統呼叫派生出的的函式。
程序控制
系統呼叫 | 描述 |
---|---|
fork | 建立一個新程序 |
clone | 按指定條件建立子程序 |
execve | 執行可執行檔案 |
exit | 中止程序 |
_exit | 立即中止當前程序 |
getdtablesize | 程序所能開啟的最大檔案數 |
getpgid | 獲取指定程序組標識號 |
setpgid | 設定指定程序組標誌號 |
getpgrp | 獲取當前程序組標識號 |
setpgrp | 設定當前程序組標誌號 |
getpid | 獲取程序標識號 |
getppid | 獲取父程序標識號 |
getpriority | 獲取排程優先順序 |
setpriority | 設定排程優先順序 |
modify_ldt | 讀寫程序的本地描述表 |
nanosleep | 使程序睡眠指定的時間 |
nice | 改變分時程序的優先順序 |
pause | 掛起程序,等待訊號 |
personality | 設定程序執行域 |
prctl | 對程序進行特定操作 |
ptrace | 程序跟蹤 |
sched_get_priority_max | 取得靜態優先順序的上限 |
sched_get_priority_min | 取得靜態優先順序的下限 |
sched_getparam | 取得程序的排程引數 |
sched_getscheduler | 取得指定程序的排程策略 |
sched_rr_get_interval | 取得按RR演算法排程的實時程序的時間片長度 |
sched_setparam | 設定程序的排程引數 |
sched_setscheduler | 設定指定程序的排程策略和引數 |
sched_yield | 程序主動讓出處理器,並將自己等候排程佇列隊尾 |
vfork | 建立一個子程序,以供執行新程式,常與execve等同時使用 |
wait | 等待子程序終止 |
wait3 | 參見wait |
waitpid | 等待指定子程序終止 |
wait4 | 參見waitpid |
capget | 獲取程序許可權 |
capset | 設定程序許可權 |
getsid | 獲取會晤標識號 |
setsid | 設定會晤標識號 |
檔案系統控制
檔案讀寫操作
系統呼叫 | 描述 |
---|---|
fcntl | 檔案控制 |
open | 開啟檔案 |
creat | 建立新檔案 |
close | 關閉檔案描述字 |
read | 讀檔案 |
write | 寫檔案 |
readv | 從檔案讀入資料到緩衝陣列中 |
writev | 將緩衝數組裡的資料寫入檔案 |
pread | 對檔案隨機讀 |
pwrite | 對檔案隨機寫 |
lseek | 移動檔案指標 |
_llseek | 在64位地址空間裡移動檔案指標 |
dup | 複製已開啟的檔案描述字 |
dup2 | 按指定條件複製檔案描述字 |
flock | 檔案加/解鎖 |
poll | I/O多路轉換 |
truncat | e 截斷檔案 |
ftruncate | 參見truncate |
vumask | 設定檔案許可權掩碼 |
fsync | 把檔案在記憶體中的部分寫回磁碟 |
檔案系統操作
系統呼叫 | 描述 |
---|---|
access | 確定檔案的可存取性 |
chdir | 改變當前工作目錄 |
fchdir | 參見chdir |
chmod | 改變檔案方式 |
fchmod | 參見chmod |
chown | 改變檔案的屬主或使用者組 |
fchown | 參見chown |
lchown | 參見chown |
chroot | 改變根目錄 |
stat | 取檔案狀態資訊 |
lstat | 參見stat |
fstat | 參見stat |
statfs | 取檔案系統資訊 |
fstatfs | 參見statfs |
readdir | 讀取目錄項 |
getdents | 讀取目錄項 |
mkdir | 建立目錄 |
mknod | 建立索引節點 |
rmdir | 刪除目錄 |
rename | 檔案改名 |
link | 建立連結 |
symlink | 建立符號連結 |
unlink | 刪除連結 |
readlink | 讀符號連結的值 |
mount | 安裝檔案系統 |
umount | 卸下檔案系統 |
ustat | 取檔案系統資訊 |
utime | 改變檔案的訪問修改時間 |
utimes | 參見utime |
quotactl | 控制磁碟配額 |
系統控制
系統呼叫 | 描述 |
---|---|
ioctl | I/O總控制函式 |
_sysctl | 讀/寫系統引數 |
acct | 啟用或禁止程序記賬 |
getrlimit | 獲取系統資源上限 |
setrlimit | 設定系統資源上限 |
getrusage | 獲取系統資源使用情況 |
uselib | 選擇要使用的二進位制函式庫 |
ioperm | 設定埠I/O許可權 |
iopl | 改變程序I/O許可權級別 |
outb | 低階埠操作 |
reboot | 重新啟動 |
swapon | 開啟交換檔案和裝置 |
swapoff | 關閉交換檔案和裝置 |
bdflush | 控制bdflush守護程序 |
sysfs | 取核心支援的檔案系統型別 |
sysinfo | 取得系統資訊 |
adjtimex | 調整系統時鐘 |
alarm | 設定程序的鬧鐘 |
getitimer | 獲取計時器值 |
setitimer | 設定計時器值 |
gettimeofday | 取時間和時區 |
settimeofday | 設定時間和時區 |
stime | 設定系統日期和時間 |
time | 取得系統時間 |
times | 取程序執行時間 |
uname | 獲取當前UNIX系統的名稱、版本和主機等資訊 |
vhangup | 掛起當前終端 |
nfsservctl | 對NFS守護程序進行控制 |
vm86 | 進入模擬8086模式 |
create_module | 建立可裝載的模組項 |
delete_module | 刪除可裝載的模組項 |
init_module | 初始化模組 |
query_module | 查詢模組資訊 |
*get_kernel_syms | 取得核心符號,已被query_module代替 |
記憶體管理
系統呼叫 | 描述 |
---|---|
brk | 改變資料段空間的分配 |
sbrk | 參見brk |
mlock | 記憶體頁面加鎖 |
munlock | 記憶體頁面解鎖 |
mlockall | 呼叫程序所有記憶體頁面加鎖 |
munlockall | 呼叫程序所有記憶體頁面解鎖 |
mmap | 對映虛擬記憶體頁 |
munmap | 去除記憶體頁對映 |
mremap | 重新對映虛擬記憶體地址 |
msync | 將對映記憶體中的資料寫 |