本文是Linux系統調用專欄系列文章的第一篇,對Linux系統調用的定義、基本原理、使用方法和注意事項大概作了一個介紹,以便讀者對Linux系統調用建立一個大致的印象。
1、什麼是系統調用?
Linux內核中設置了一組用於實現各種系統功能的子程序,稱為系統調用。用戶可以通過系統調用命令在自己的應用程序中調用它們。從某種角度來看,系統調用和普通的函數調用非常相似。區別僅僅在於,系統調用由操作系統核心提供,運行於核心態;而普通的函數調用由函數庫或用戶自己提供,運行於用戶態。二者在使用方式上也有相似之處,在下面將會提到。
隨Linux核心還提供了一些C語言函數庫,這些庫對系統調用進行了一些包裝和擴展,因為這些庫函數與系統調用的關系非常緊密,所以習慣上把這些函數也稱為系統調用。
2、Linux中共有多少個系統調用?
這個問題可不太好回答,就算讓Linus Torvaldz本人也不見得一下子就能說清楚。
在2.4.4版內核中,狹義上的系統調用共有221個,你可以在<內核源碼目錄>/include/asm-i386/unistd.h中找到它們的原本,也可以通過命令"man 2 syscalls"察看它們的目錄(man pages的版本一般比較老,可能有很多最新的調用都沒有包含在內)。廣義上的系統調用,也就是以庫函數的形式實現的那些,它們的個數從來沒有人統計過,這是一件吃力不討好的活,新內核不斷地在推出,每一個新內核中函數數目的變化根本就沒有人在乎,至少連內核的修改者本人都不在乎,因為他們從來沒有發布過一個此類的聲明。
隨本文一起有一份經過整理的列表,它不可能非常全面,但常見的系統調用基本都已經包含在內,那裡面只有不多的一部分是你平時用得到的,本專欄將會有選擇的對它們進行介紹。
3、為什麼要用系統調用?
實際上,很多已經被我們習以為常的C語言標准函數,在Linux平台上的實現都是靠系統調用完成的,所以如果想對系統底層的原理作深入的了解,掌握各種系統調用是初步的要求。進一步,若想成為一名Linux下編程高手,也就是我們常說的Hacker,其標志之一也是能對各種系統調用有透徹的了解。
即使除去上面的原因,在平常的編程中你也會發現,在很多情況下,系統調用是實現你的想法的簡潔有效的途徑,所以有可能的話應該盡量多掌握一些系統調用,這會對你的程序設計過程帶來意想不到的幫助。
4、系統調用是怎麼工作的?
一般的,進程是不能訪問內核的。它不能訪問內核所占內存空間也不能調用內核函數。CPU硬件決定了這些(這就是為什麼它被稱作"保護模式")。系統調用是這些規則的一個例外。其原理是進程先用適當的值填充寄存器,然後調用一個特殊的指令,這個指令會跳到一個事先定義的內核中的一個位置(當然,這個位置是用戶進程可讀但是不可寫的)。在Intel CPU中,這個由中斷0x80實現。硬件知道一旦你跳到這個位置,你就不是在限制模式下運行的用戶,而是作為操作系統的內核--所以你就可以為所欲為。
進程可以跳轉到的內核位置叫做sysem_call。這個過程檢查系統調用號,這個號碼告訴內核進程請求哪種服務。然後,它查看系統調用表(sys_call_table)找到所調用的內核函數入口地址。接著,就調用函數,等返回後,做一些系統檢查,最後返回到進程(或到其他進程,如果這個進程時間用盡)。如果你希望讀這段代碼,它在<內核源碼目錄>/kernel/entry.S,Entry(system_call)的下一行。
5、如何使用系統調用?
先來看一個例子:
#include /*定義宏_syscall1*/
#include /*定義類型time_t*/
_syscall1(time_t,time,time_t *,tloc) /*宏,展開後得到time()函數的原型*/
main()
{
time_t the_time;
the_time=time((time_t *)0); /*調用time系統調用*/
printf("The time is %ld\n",the_time);
}
系統調用time返回從格林尼治時間1970年1月1日0:00開始到現在的秒數。
這是最標准的系統調用的形式,宏_syscall1()展開來得到一個函數原型,稍後我會作詳細解釋。但事實上,如果把程序改成下面的樣子,程序也可以運行得同樣的結果。
#include
main()
{
time_t the_time;
the_time=time((time_t *)0); /*調用time系統調用*/
printf("The time is %ld\n",the_time);
}
這是因為在time.h中實際上已經用庫函數的形式實現了time這個系統調用,替我們省掉了調用_syscall1宏展開得到函數原型這一步。
大多數系統調用都在各種C語言函數庫中有所實現,所以在一般情況下,我們都可以像調用普通的庫函數那樣調用系統調用,只在極個別的情況下,我們才有機會用到_syscall*()這幾個宏。
6、_syscall*()是什麼?
在unistd.h裡定義了7個宏,分別是
_syscall0(type,name)
_syscall1(type,name,type1,arg1)
_syscall2(type,name,type1,arg1,type2,arg2)
_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)
_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)
_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)
_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)
它們看起來似乎不太像宏,但其實質和
#define MAXSIZE 100
裡面的MAXSIZE沒有任何區別。
它們的作用是形成相應的系統調用函數原型,供我們在程序中調用。我們很容易就能發現規律,_syscall後面的數字和typeN,argN的數目一樣多。事實上,_syscall後面跟的數字指明了展開後形成函數的參數的個數,讓我們看一個實例,就是剛剛用過的time系統調用:
_syscall1(time_t,time,time_t *,tloc)
展開後的情形是這樣:
time_t time(time_t * tloc)
{
long __res;
__asm__ volatile("int $0x80" : "=a" (__res) : "0" (13),"b" ((long)(tloc)));
do {
if ((unsigned long)(__res) >= (unsigned long)(-125)) {
errno = -(__res);
__res = -1;
}
return (time_t) (__res);
} while (0) ;
}
可以看出,_syscall1(time_t,time,time_t *,tloc)展開成一個名為time的函數,原參數time_t就是函數的返回類型,原參數time_t *和tloc分別構成新函數的參數。事實上,程序中用到的time函數的原型就是它。
7、errno是什麼?
為防止和正常的返回值混淆,系統調用並不直接返回錯誤碼,而是將錯誤碼放入一個名為errno的全局變量中。如果一個系統調用失敗,你可以讀出errno的值來確定問題所在。
errno不同數值所代表的錯誤消息定義在errno.h中,你也可以通過命令"man 3 errno"來察看它們。
需要注意的是,errno的值只在函數發生錯誤時設置,如果函數不發生錯誤,errno的值就無定義,並不會被置為0。另外,在處理errno前最好先把它的值存入另一個變量,因為在錯誤處理過程中,即使像printf()這樣的函數出錯時也會改變errno的值。
8、系統調用兼容性好嗎?
很遺憾,答案是--不好。但這決不意味著你的程序會三天兩頭的導致系統崩潰,因為系統調用是Linux的內核提供的,所以它們工作起來非常穩定,對於此點無需絲毫懷疑,在絕大多數的情況下,系統調用要比你自己編寫的代碼可靠而高效的多。
但是,在Linux的各版本內核之間,系統調用的兼容性表現得並不像想象那麼好,這是由Linux本身的性質決定的。Linux是一群程序設計高手利用業余時間開發出來的,他們中間的大部分人沒有把Linux當成一個嚴肅的商業軟件,(現在的情況有些不同了,隨著Linux商業公司和以Linux為生的人的增長,不少人的腦筋發生了變化。)結果就是,如果新的方案在效率和兼容性上發生了矛盾,他們往往捨棄兼容性而追求效率,就這樣,如果他們認為某個系統調用實現的比較糟糕,他們就會毫不猶豫的作出修改,有些時候甚至連接口也一起改掉了,更可怕的是,很多時候,他們對自己的修改連個招呼也不打,在任何文檔裡都找不到關於修改的提示。這樣,每當新內核推出的時候,很可能都會悄悄的更新一些系統調用,用戶編制的應用程序也會跟著出錯。
說到這裡,你是不是感覺前途一片昏暗呢?呵呵,不用太緊張,如前面所說,隨著越來越多的人把Linux當成自己的飯碗,不兼容的情況也越來越罕見。從2.2版本以後的Linux內核已經非常穩定了,不過盡管如此,你還是有必要在每個新內核推出之後,對自己的應用程序進行兼容性測試,以防止意外的發生。
9、該如何學習使用Linux系統調用呢?
你可以用"man 2 系統調用名稱"的命令來查看各條系統調用的介紹,但這首先要求你要有不錯的英語基礎,其次還得有一定的程序設計和系統編程的功底,man pages不會涉及太多的應用細節,因為它只是一個手冊而非教程。如果man pages所提供的東西不能使你感到非常滿意,那就跟我來吧,本專欄將向你展示Linux系統調用編程的無窮魅力。 <