系統調用就是調用操作系統提供的一系列內核功能函數,因為內核總是對用戶程序持不信任的態度,一些核心功能不能直接交由用戶程序來實現執行。用戶程序只能發出請求,然后內核調用相應的內核函數來幫著處理,將結果返回給應用程序。如此才能保證系統的穩定和安全。本文采用 的實例來講解系統調用具體是如何實現的。
系統調用是給用戶態下的程序使用的,但是用戶程序并不直接使用系統調用,而是系統調用在用戶態下的接口。這個用戶接口就是操作系統提供的系統調用 ,一般遵循 標準。
的系統調用是用 INT n 指令實現的,INT n 的作用就是觸發一個 號中斷,中斷的過程應該很熟悉了吧,不熟悉的可以看看前文:多處理器下的中斷機制。 里面系統調用使用的向量號是 , 里面使用的 (不同 版本可能不同)。只要這個向量號 不是一些異常使用的,一些保留的和一些外設默認使用的,用多少來表示系統調用其實無傷大雅, 要用 ,那就 好了。
上述說的用戶接口就會執行 INT 64 觸發一個 號中斷,這里 做了簡化,按照以前版本的 ,用戶接口是調用一個宏定義 ,這個宏再來執行 INT 指令觸發中斷。關于這部分可以看看前文:捋一捋系統調用
執行 INT 64 之后, 會根據向量號 去 中索引第 個門描述符(從 0 計數),這個門描述符中存放的有系統調用程序的偏移量和段選擇子,再根據段選擇子去 中索引段描述符,段描述符中記錄的有段基址,與門描述符中記錄的偏移量相加就是系統調用程序的地址。拿到系統調用程序的地址,就可以執行程序做相應的處理了。
可是系統調用是有很多的,雖然 中實現的系統調用沒多少,沒多少也還是有那么一些的,怎么區別它們呢?這就涉及了系統調用號概念,每一個系統調用都唯一分配了一個整數來標識,比如說 里面 系統調用的調用號就為 1。INT 64,表示觸發一個中斷向量號為 64 的中斷,而這個中斷表示系統調用,并沒有具體說是哪一個系統調用,所以還需要一個系統調用號來表示具體的系統調用。
系統調用通俗的講就是是用戶態下的程序托內核辦事,既然是托人辦事那得告訴人家你要辦什么事對吧。這個告訴人家具體要辦什么事就是要給內核傳遞系統調用號,問題是怎么傳呢?通常的做法就是將這個系統調用號放進 寄存器,當執行到系統調用入口程序的時候就會根據 eax 的值去調用具體的系統調用程序,比如說 中存放的是 1 那么就會去調用 這個系統調用的相關函數。這個系統調用的入口程序可以理解為在第 個門描述符中記錄的程序,因為肯定是要先根據向量號拿到總的中斷服務程序(在這兒就是總的系統調用程序),然后再根據 的值去調用的具體的內核功能函數。
上面只是說的一般的大致情況,如果看過前文多處理器下的中斷機制應該知道, 對所有中斷(包括系統調用)的處理是先執行共同的中斷入口程序,主要就是保護現場壓棧寄存器,然后根據向量號的不同執行不同的中斷處理程序。在這里就是執行系統調用入口程序,然后再根據 的值調用具體的內核功能函數。
這個具體的內核功能函數咱們就不討論了,內核中的表現形式就是一個個不同的函數,咱們這兒只討論兩件事:
一是參數,有些系統調用的是需要參數的,用戶接口不真正干活,真正干活的是內核功能函數,但是需要的參數在用戶態下,所以需要在用戶接口部分向內核傳遞參數。傳參有兩種方法:
直接傳給寄存器,寄存器是通用的,在用戶態將值傳給寄存器,進入內核態之后就可以直接使用,這可以使用內聯匯編來實現。
壓棧,壓棧有個問題,系統調用使用中斷/陷阱來實現,這期間會換棧,在用戶態下壓棧的參數對內核來說似乎沒什么用處。所以要想使用用戶態下棧中的參數,必須要獲得用戶棧的地址,這個值在哪呢?沒錯,在內核棧中的上下文保存著,從內核棧中取出用戶棧的棧頂 值,就可以取到系統調用的參數了, 就是這樣實現的。
二是返回值,函數的調用約定中規定了返回值應該放在 寄存器里面。而在系統調用的一開始我們將系統調用號傳進了 寄存器,然后中斷時保存上下文,將 壓入內核棧,系統調用處理程序將最后結果放到 寄存器中。下面注意了,如果不對上下文中的 作修改的話,中斷退出的時候恢復上下文彈出 ,彈出的值是啥?是系統調用號,也就是說將結果放到 寄存器中放了個寂寞,所以肯定會有一個步驟修改上下文中 為結果這么一個步驟,這樣回到用戶態的時候這個結果才會在 寄存器中。
上述差不多將系統調用的一些理論知識說完了,下面用 的實例來看看系統調用具體如何實現的。
首先便是用戶接口部分,用戶接口是操作系統提供的系統調用 函數,一般是 標準, 關于這用戶接口定義在 中,來隨便看兩個:
int fork(void);
int write(int, const void*, int);
這只是對函數原型的聲明。具體做了什么事呢?這個定義在 中:
#include “syscall.h”
#include “traps.h”
#define SYSCALL(name)
.globl name;
name:
movl $SYS_ ## name, %eax;
int $T_SYSCALL;
ret
SYSCALL(fork)
SYSCALL(write)
SYSCALL(getpid)
這是用匯編來寫的,而且使用了宏定義,我們來仔細閱讀一下這段代碼
.global name 聲明了一個全局可見的名字,可以是變量也可以是函數名,這里就與用戶接口的函數名。函數名就相當于一個地址,name: 后面的代碼就是這個函數具體要做的事,就像 c 語言編寫函數時的函數體,只不過這里是用匯編寫的而已。
所以這個函數做了什么事?應該一目了然啊,就三條指令:
movl $SYS_ ## name, %eax 將系統調用號傳到寄存器
int $T_SYSCALL 觸發 號中斷
ret 函數返回
這里還使用了一些宏定義,首先是系統調用號,定義在 當中,隨便看幾個意思一下:
#define SYS_fork 1#define SYS_getpid 11#define SYS_write 16
這個號就是自定義的,能夠將每個系統調用唯一區分開就好。
上面的宏定義中還涉及了 # 的用法,# 一般有兩種用法:
#define STR(x) #x#define CAT(x, y) x##y
一個 # 表示字符串化,如果 為 abc,則結果為 “abc”
兩個 ## 表示連接符,如果 為 ab, 為 cd,則結果為 abcd
所以上述 SYS_ ## name,如果 為 ,那么結果就是 SYS_fork,表示 的系統調用號。
代表的系統調用的向量號, 版本不同,這個數可能不同,我這兒是 ,所以 int $T_SYSCALL 相當于觸發了一個 號中斷。
接著就應該是中斷的處理過程,這一塊在前文多處理器下的中斷機制已經講述的很詳細了,而且還有過程圖,本文就不再贅述。本文重點講述執行了通用的中斷入口程序之后如何執行系統調用分支的,如何獲取用戶棧的參數,如何修改上下文中的 使其返回正確的結果。
問題很多,咱們一個一個來解決,首先從 IDT, GDT 中獲取到中斷入口程序的地址之后,執行中斷入口程序壓棧寄存器來保存上下文,這個上下文中包括了向量號。
保存了上下文之后跳到 這個總的中斷處理程序,這個程序中會根據向量號不同去執行不同的中斷處理程序,如果向量號表示的是系統調用的話,就會進行如下操作:
void trap(struct trapframe *tf)
{
if(tf-》trapno == T_SYSCALL){ //如果向量號表示的是系統調用
if(myproc()-》killed)
exit();
myproc()-》tf = tf; //當前進程的中斷棧幀
syscall(); //執行系統調用入口程序
if(myproc()-》killed)
exit();
return;
}
/*******略略略略********/
}
可以看到,如果中斷棧幀中的向量號表示的是系統調用號的話,就會去執行系統調用入口程序。
這個系統調用入口程序定義在 里面:
void syscall(void)
{
int num;
struct proc *curproc = myproc(); //獲取當前進程的PCB
num = curproc-》tf-》eax; //獲取系統調用號
if(num 》 0 && num 《 NELEM(syscalls) && syscalls[num]) {
curproc-》tf-》eax = syscalls[num](); //調用相應的系統調用處理函數,返回值賦給eax
} else {
cprintf(“%d %s: unknown sys call %d
”,
curproc-》pid, curproc-》name, num);
curproc-》tf-》eax = -1;
}
}
這個系統調用的入口函數的作用就是根據中斷棧幀中的系統調用號去調用相應的內核功能函數,然后將返回值再填寫到棧幀中的 處。
這個流程整個邏輯應該是很清晰的,主要注意一點,調用內核功能函數的方式:syscalls[num]() , 是系統調用號, 看形式應該是個數組,從這里其實應該就能猜出來了, 將所有具體的系統調用處理函數地址按照系統調用號的順序集合成了一個數組。事實也的確如此,同樣的來隨便看幾個:
extern int sys_fork(void);
extern int sys_getpid(void);
extern int sys_write(void);
static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
/**********************/
[SYS_getpid] sys_getpid,
/**********************/
[SYS_write] sys_write,
/***********************/
}
extern int sys_fork(void); 表示具體的 這個內核的功能函數,這個函數才是真正干事的,它在外面定義,所以用了 ,至于具體這個函數干了什么事,在本文不重要,本文主要事了解系統調用這個流程,后面講述進程的時候再具體講述這個函數,或者前面寫過一篇關于 的文章:使用分身術變身術創建新進程
接下來是定義了一個函數指針數組,就是將上述函數地址填到數組相應的位置上。[SYS_fork] sys_fork 這種填充數組元素的方式似乎不太常見,但在這里就非常實用,表示將 這個函數的地址填寫到索引為 的位置上去。
關于系統調用還剩下最后一個問題,根據上述內核中具體的系統調用函數原型可以看出,它們的返回類型都是 型且沒有參數,但是有些系統調用是需要參數的,所以那些需要參數的系統調用就要去獲取參數,去哪獲取呢?是的,去用戶棧獲取參數,因為 沒有使用寄存器來傳參,而是將參數直接壓入用戶棧里面的。
回到系統調用的開頭,何時將參數壓棧的,參數是為被調用函數準備的,所以調用函數之前一定會將參數壓棧。這個被調用函數就是用戶接口,舉個例子如果調用 ,則在這之前一定會將參數 按照這個順序壓棧,再 調用函數,只是在 語言中這個過程可能看起來不是那么真切,如果是用匯編來寫,或者查看編譯之后的程序,會有下面的大致過程:
push size
push buf
push fd
call write
在 call wirte 之后又會將下條指令地址壓棧當作放回地址, (用戶接口) 又做了三件事,傳系統調用號,int T_SYSCALL, ret 返回。int T_SYSCALL 之后換棧,用戶棧棧頂 保存在上下文中的 處。
捋清楚這個關系之后就知道怎么去拿參數了,直接去中斷棧幀中獲取用戶棧棧頂值 ,再根據參數返回地址的位置關系獲取一個個參數,來看 中有關獲取參數的幾個函數:
int argint(int n, int *ip) //獲取系統調用的第n個int型的參數,存到ip這個位置
{
return fetchint((myproc()-》tf-》esp) + 4 + 4*n, ip); //原棧中獲取n個int型參數,加4是跳過
}
int fetchint(uint addr, int *ip)
{
struct proc *curproc = myproc();
if(addr 》= curproc-》sz || addr+4 》 curproc-》sz)
return -1;
*ip = *(int*)(addr);
return 0;
}
這是獲取一個 t 型的參數,(myproc()-》tf-》esp) + 4 + 4*n 表示第 個參數( 型) 的位置,多加了一個 是因為要跳過返回地址 字節。然后調用 ) 去取參數,核心語句就一句:*ip = *(int*)(addr); 將這個地址轉化為 型再解引用放在地址 上,說著有些繞口,自己看一下應該還是很好明白的。至于這個函數中關于進程部分的一些條件檢查,現下可以不予理會。
int argptr(int n, char **pp, int size)
{
int i;
struct proc *curproc = myproc();
if(argint(n, &i) 《 0)
return -1;
if(size 《 0 || (uint)i 》= curproc-》sz || (uint)i+size 》 curproc-》sz)
return -1;
*pp = (char*)i;
return 0;
}
這個函數用來獲取一個指針,指針就是地址,地址就是一個 位無符號數,所以調用前面的 來獲取這個數存到 中,這個 本身其實是個地址值,所以將其轉化 類型,然后賦值給 。
注意這里使用的是二級指針,為什么要使用二級指針,我們來看看如果使用一級指針會發生什么,如果這個函數是這樣:
int argptr(int n, char *pp, int size) //pp類型變為char*
{
int i;
/*********************/
if(argint(n, &i) 《 0)
return -1;
/*********************/
pp = (char*)i; //這里變為直接給pp賦值
return 0;
}
如果這個函數變成這樣還對嗎,答案是不對的。舉個例子來說明,在 這個內核功能函數中會調用 :
char *p;
argptr(1, &p, n);
/**如果用一級指針**/
argptr(1, p, n);
調用 的本意是獲取第一個參數,也就是用戶接口 的 地址值,并將其賦給 。
假如 等于某個地址 ,如果使用一級指針:調用 argptr(1, p, n); int argptr(int n, char *pp, int size),這個 是實參, 是形參,,雖然 和 的值相等,但它們兩個是兩個不同的變量,所以如果修改 , 的值是不會變化的,因此使用一級指針就不對。
如果使用的是二級指針,調用 argptr(1, &p, n); int argptr(int n, char **pp, int size)。實參是 的地址, 本身就是個地址值,所以 ,修改 就是 。嗯這下就對了,所以這里要使用二級指針才對頭。
還有個獲取字符串的函數,跟獲取指針差不了太多,只是多了一個算字符串長度的步驟,這里就不贅述了。
這是以 write 系統調用為例的系統調用過程圖,圖是丑了點,不過這條線應該捋得還是挺清晰的,好啦,本文就到這里,有什么錯誤還請批評指正,也歡迎大家來同我討論交流學習進步。
責任編輯:haq
-
內核
+關注
關注
3文章
1372瀏覽量
40282 -
函數
+關注
關注
3文章
4327瀏覽量
62574 -
系統調用
+關注
關注
0文章
28瀏覽量
8324
原文標題:系統調用如何實現?
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論