Linux系統呼叫講義(轉)
Linux系統呼叫講義(轉)[@more@]Linux下系統呼叫的實現
Linux中的系統呼叫
Linux中怎樣編譯和定製核心
Linux下系統呼叫的實現
Unix/Linux作業系統的體系結構及系統呼叫介紹
什麼是作業系統和系統呼叫
作業系統是從硬體抽象出來的虛擬機器,在該虛擬機器上使用者可以執行應用程式。它負責直接與硬體互動,
向使用者程式提供公共服務,並使它們同硬體特性隔離。因為程式不應該依賴於下層的硬體,只有這樣應用程
序才能很方便的在各種不同的Unix系統之間移動。系統呼叫是Unix/Linux作業系統向使用者程式提供支援的接
口,透過這些介面應用程式向作業系統請求服務,控制轉向作業系統,而作業系統在完成服務後,將控制和
結果返回給使用者程式。
Unix/Linux系統體系結構
一個Unix/Linux系統分為三個層次:使用者、核心以及硬體。
其中系統呼叫是使用者程式與核心間的邊界,透過系統呼叫程式可由使用者模式轉入核心模式,在核心模
式下完成一定的服務請求後在返回使用者模式。
系統呼叫介面看起來和C程式中的普通函式呼叫很相似,它們通常是透過庫把這些函式呼叫對映成進入
作業系統所需要的原語。
這些操作原語只是提供一個基本功能集,而透過庫對這些操作的引用和封裝,可以形成豐富而且強大的
系統呼叫庫。這裡體現了機制與策略相分離的程式設計思想——系統呼叫只是提供訪問核心的基本機制,而策略
是透過系統呼叫庫來體現。
例:execv, execl, execlv, opendir , readdir...
Unix/Linux執行模式,地址空間和上下文
執行模式(執行態):
一種計算機硬體要執行Unix/Linux系統,至少需要提供兩種執行模式:高優先順序的核心模式和低優先順序
的使用者模式。
實際上許多計算機都有兩種以上的執行模式。如:intel 80x86體系結構就有四層執行特權,內層特權
最高。Unix只需要兩層即可以了:核心執行在高優先順序,稱之為核心態;其它外圍軟體包括shell,編輯程
序,Xwindow等等都是在低優先順序執行,稱之為使用者態。之所以採取不同的執行模式主要原因時為了保護,
由於使用者程式在較低的特權級上執行,它們將不能意外或故意的破壞其它程式或核心。程式造成的破壞會被
區域性化而不影響系統中其它活動或者程式。當使用者程式需要完成特權模式下才能完成的某些功能時,必須嚴
格按照系統呼叫提供介面才能進入特權模式,然後執行呼叫所提供的有限功能。
每種執行態都應該有自己的堆疊。在Linux中,分為使用者棧和核心棧。使用者棧包括在使用者態執行時函式
呼叫的引數、區域性變數和其它資料結構。有些系統中專門為全域性中斷處理提供了中斷棧,但是x86中並沒有
中斷棧,中斷在當前程式的核心棧中處理。
地址空間:
採用特權模式進行保護的根本目的是對地址空間的保護,使用者程式不應該能夠訪問所有的地址空間:只
有透過系統呼叫這種受嚴格限制的介面,程式才能進入核心態並訪問到受保護的那一部分地址空間的資料,
這一部分通常是留給作業系統使用。另外,程式與程式之間的地址空間也不應該隨便互訪。這樣,就需要提
供一種機制來在一片實體記憶體上實現同一程式不同地址空間上的保護,以及不同程式之間地址空間的保護。
Unix/Linux中透過虛存管理機制很好的實現了這種保護,在虛存系統中,程式所使用的地址不直接對應
物理的儲存單元。每個程式都有自己的虛存空間,每個程式有自己的虛擬地址空間,對虛擬地址的引用透過
地址轉換機制轉換成為實體地址的引用。正因為所有程式共享實體記憶體資源,所以必須透過一定的方法來保
護這種共享資源,透過虛存系統很好的實現了這種保護:每個程式的地址空間透過地址轉換機制對映到不同
的物理儲存頁面上,這樣就保證了程式只能訪問自己的地址空間所對應的頁面而不能訪問或修改其它程式的
地址空間對應的頁面。
虛擬地址空間分為兩個部分:使用者空間和系統空間。在使用者模式下只能訪問使用者空間而在核心模式下可
以訪問系統空間和使用者空間。系統空間在每個程式的虛擬地址空間中都是固定的,而且由於系統中只有一個
核心例項在執行,因此所有程式都對映到單一核心地址空間。核心中維護全域性資料結構和每個程式的一些對
象資訊,後者包括的資訊使得核心可以訪問任何程式的地址空間。透過地址轉換機制程式可以直接訪問當前
程式的地址空間(透過MMU),而透過一些特殊的方法也可以訪問到其它程式的地址空間。
儘管所有程式都共享核心,但是系統空間是受保護的,程式在使用者態無法訪問。程式如果需要訪問核心
,則必須透過系統呼叫介面。程式呼叫一個系統呼叫時,透過執行一組特殊的指令(這個指令是與平臺相關
的,每種系統都提供了專門的trap命令,基於x86的Linux中是使用int 指令)使系統進入核心態,並將控制
權交給核心,由核心替代程式完成操作。當系統呼叫完成後,核心執行另一組特徵指令將系統返回到使用者態
,控制權返回給程式。
上下文:
一個程式的上下文可以分為三個部分:使用者級上下文、暫存器上下文以及系統級上下文。
使用者級上下文:正文、資料、使用者棧以及共享儲存區;
暫存器上下文:程式暫存器(IP),即CPU將執行的下條指令地址,處理機狀態暫存器(EFLAGS),棧
指標,通用暫存器;
系統級上下文:程式表項(proc結構)和U區,在Linux中這兩個部分被合成task_struct,區表及頁表
(mm_struct , vm_area_struct, pgd, pmd, pte等),核心棧等。
全部的上下文資訊組成了一個程式的執行環境。當發生程式排程時,必須對全部上下文資訊進行切換,
新排程的程式才能執行。程式就是上下文的集合的一個抽象概念。
系統呼叫的功能和分類
作業系統核心在執行期間的活動可以分為兩個部分:上半部分(top half)和下半部分(bottom half),
其中上半部分為應用程式提供系統呼叫或自陷的服務,是同步服務,由當前執行的程式引起,在當前程式上
下文中執行並允許直接訪問當前程式的資料結構;而下半部分則是由處理硬體中斷的子程式,是屬於非同步活
動,這些子程式的呼叫和執行與當前程式無關。上半部分允許被阻塞,因為這樣阻塞的是當前程式;下半部
分不允許被阻塞,因為阻塞下半部分會引起阻塞一個無辜的程式甚至整個核心。
系統呼叫可以看作是一個所有Unix/Linux程式共享的子程式庫,但是它是在特權方式下執行,可以存取
核心資料結構和它所支援的使用者級資料。系統呼叫的主要功能是使使用者可以使用作業系統提供的有關裝置管
理、檔案系統、程式控制程式通訊以及儲存管理方面的功能,而不必要了解作業系統的內部結構和有關硬體
的細節問題,從而減輕使用者負擔和保護系統以及提高資源利用率。
系統呼叫分為兩個部分:與檔案子系統互動的和程式子系統互動的兩個部分。其中和檔案子系統互動的
部分進一步由可以包括與裝置檔案的互動和與普通檔案的互動的系統呼叫(open, close, ioctl, create,
unlink, . . . );與程式相關的系統呼叫又包括程式控制系統呼叫(fork, exit, getpid, . . . ),進
程間通訊,儲存管理,程式排程等方面的系統呼叫。
2.Linux下系統呼叫的實現
(以i386為例說明)
A.在Linux中系統呼叫是怎樣陷入核心的?
系統呼叫在使用時和一般的函式呼叫很相似,但是二者是有本質性區別的,函式呼叫不能引起從使用者態
到核心態的轉換,而正如前面提到的,系統呼叫需要有一個狀態轉換。
在每種平臺上,都有特定的指令可以使程式的執行由使用者態轉換為核心態,這種指令稱作作業系統陷入
(operating system trap)。程式透過執行陷入指令後,便可以在核心態執行系統呼叫程式碼。
在Linux中是透過軟中斷來實現這種陷入的,在x86平臺上,這條指令是int 0x80。也就是說在Linux中
,系統呼叫的介面是一箇中斷處理函式的特例。具體怎樣透過中斷處理函式來實現系統呼叫的入口將在後面
詳細介紹。
這樣,就需要在系統啟動時,對INT 0x80進行一定的初始化,下面將描述其過程:
1.使用匯編子程式setup_idt(linux/arch/i386/kernel/head.S)初始化idt表(中斷描述符表),這時所
有的入口函式偏移地址都被設為ignore_int
( setup_idt:
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw x8E00,%dx /* interrupt gate - dpl=0, present */
lea SYMBOL_NAME(idt_table),%edi
mov 6,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl ,%edi
dec %ecx
jne rp_sidt
ret
selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1);
2.Start_kernel()(linux/init/main.c)呼叫trap_init()(linux/arch/i386/kernel/trap.c)函式設定中斷
描述符表。在該函式里,實際上是透過呼叫函式set_system_gate(SYSCALL_VECTOR,&system_call)來完成該
項的設定的。其中的SYSCALL_VECTOR就是0x80,而system_call則是一個彙編子函式,它即是中斷0x80的處
理函式,主要完成兩項工作:a. 暫存器上下文的儲存;b. 跳轉到系統呼叫處理函式。在後面會詳細介紹這
些內容。
(補充說明:門描述符
set_system_gate()是在linux/arch/i386/kernel/trap.S中定義的,在該檔案中還定義了幾個類似的函
數set_intr_gate(), set_trap_gate, set_call_gate()。這些函式都呼叫了同一個彙編子函式__set_gate
(),該函式的作用是設定門描述符。IDT中的每一項都是一個門描述符。
#define _set_gate(gate_addr,type,dpl,addr)
set_gate(idt_table+n,15,3,addr);
門描述符的作用是用於控制轉移,其中會包括選擇子,這裡總是為__KERNEL_CS(指向GDT中的一項段描
述符)、入口函式偏移地址、門訪問特權級(DPL)以及型別標識(TYPE)。Set_system_gate的DPL為3,表
示從特權級3(最低特權級)也可以訪問該門,type為15,表示為386中斷門。)
B.與系統呼叫相關的資料結構
1.系統呼叫處理函式的函式名的約定
函式名都以“sys_”開頭,後面跟該系統呼叫的名字。例如,系統呼叫fork()的處理函式名是
sys_fork()。
asmlinkage int sys_fork(struct pt_regs regs);
(補充關於asmlinkage的說明)
2.系統呼叫號(System Call Number)
核心中為每個系統呼叫定義了一個唯一的編號,這個編號的定義在linux/include/asm/unistd.h中,編
號的定義方式如下所示:
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
. . . . . .
使用者在呼叫一個系統呼叫時,系統呼叫號號作為引數傳遞給中斷0x80,而該標號實際上是後面將要提到
的系統呼叫表(sys_call_table)的下標,透過該值可以找到相映系統呼叫的處理函式地址。
3.系統呼叫表
系統呼叫表的定義方式如下:(linux/arch/i386/kernel/entry.S)
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
. . . . . .
系統呼叫表記錄了各個系統呼叫處理函式的入口地址,以系統呼叫號為偏移量很容易的能夠在該表中找到對
應處理函式地址。在linux/include/linux/sys.h中定義的NR_syscalls表示該表能容納的最大系統呼叫數,
NR_syscalls = 256。
C.系統呼叫函式介面是如何轉化為陷入命令
如前面提到的,系統呼叫是透過一條陷入指令進入核心態,然後根據傳給核心的系統呼叫號為索引在系
統呼叫表中找到相映的處理函式入口地址。這裡將詳細介紹這一過程。
我們還是以x86為例說明:
由於陷入指令是一條特殊指令,而且依賴與作業系統實現的平臺,如在x86中,這條指令是int 0x80,
這顯然不是使用者在程式設計時應該使用的語句,因為這將使得使用者程式難於移植。所以在作業系統的上層需要實
現一個對應的系統呼叫庫,每個系統呼叫都在該庫中包含了一個入口點(如我們看到的fork, open, close
等等),這些函式對程式設計師是可見的,而這些庫函式的工作是以對應系統呼叫號作為引數,執行陷入指令
int 0x80,以陷入核心執行真正的系統呼叫處理函式。當一個程式呼叫一個特定的系統呼叫庫的入口點,正
如同它呼叫任何函式一樣,對於庫函式也要建立一個棧幀。而當程式執行陷入指令時,它將處理機狀態轉換
到核心態,並且在核心棧執行核心程式碼。
這裡給出一個示例(linux/include/asm/unistd.h):
#define _syscallN(type, name, type1, arg1, type2, arg2, . . . )
type name(type1 arg1,type2 arg2)
{
long __res;
__asm__ volatile ("int x80"
: "=a" (__res)
: "" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)));
. . . . . .
__syscall_return(type,__res);
}
在執行一個系統呼叫庫中定義的系統呼叫入口函式時,實際執行的是類似如上的一段程式碼。這裡牽涉到
一些gcc的嵌入式組合語言,不做詳細的介紹,只簡單說明其意義:
其中__NR_##name是系統呼叫號,如name == ioctl,則為__NR_ioctl,它將被放在暫存器eax中作為參
數傳遞給中斷0x80的處理函式。而系統呼叫的其它引數arg1, arg2, …則依次被放入ebx, ecx, . . .等通
用暫存器中,並作為系統呼叫處理函式的引數,這些引數是怎樣傳入核心的將會在後面介紹。
下面將示例說明:
int func1()
{
int fd, retval;
fd = open(filename, ……);
……
ioctl(fd, cmd, arg);
. . .
}
func2()
{
int fd, retval;
fd = open(filename, ……);
……
__asm__ __volatile__(
"int x80 "
:"=a"(retval)
:"0"(__NR_ioctl),
"b"(fd),
"c"(cmd),
"d"(arg));
}
這兩個函式在Linux/x86上執行的結果應該是一樣的。
若干個庫函式可以對映到同一個系統呼叫入口點。系統呼叫入口點對每個系統呼叫定義其真正的語法和
語義,但庫函式通常提供一個更方便的介面。如系統呼叫exec有集中不同的呼叫方式:execl, execle,等,
它們實際上只是同一系統呼叫的不同介面而已。對於這些呼叫,它們的庫函式對它們各自的引數加以處理,
來實現各自的特點,但是最終都被對映到同一個核心入口點。
D.系統呼叫陷入核心後作何初始化處理
當程式執行系統呼叫時,先呼叫系統呼叫庫中定義某個函式,該函式通常被展開成前面提到的
_syscallN的形式透過INT 0x80來陷入核心,其引數也將被透過暫存器傳往核心。
在這一部分,我們將介紹INT 0x80的處理函式system_call。
思考一下就會發現,在呼叫前和呼叫後執行態完全不相同:前者是在使用者棧上執行使用者態程式,後者在
核心棧上執行核心態程式碼。那麼,為了保證在核心內部執行完系統呼叫後能夠返回撥用點繼續執行使用者程式碼
,必須在進入核心態時儲存時往核心中壓入一個上下文層;在從核心返回時會彈出一個上下文層,這樣使用者
程式就可以繼續執行。
那麼,這些上下文資訊是怎樣被儲存的,被儲存的又是那些上下文資訊呢?這裡仍以x86為例說明。
在執行INT指令時,實際完成了以下幾條操作:
1.由於INT指令發生了不同優先順序之間的控制轉移,所以首先從TSS(任務狀態段)中獲取高優先順序的核心堆
棧資訊(SS和ESP);2.把低優先順序堆疊資訊(SS和ESP)保留到高優先順序堆疊(即核心棧)中;
3.把EFLAGS,外層CS,EIP推入高優先順序堆疊(核心棧)中。
4.透過IDT載入CS,EIP(控制轉移至中斷處理函式)
然後就進入了中斷0x80的處理函式system_call了,在該函式中首先使用了一個宏SAVE_ALL,該宏的定義如
下所示:
#define SAVE_ALL
cld;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
movl $(__KERNEL_DS),%edx;
movl %edx,%ds;
movl %edx,%es;
該宏的功能一方面是將暫存器上下文壓入到核心棧中,對於系統呼叫,同時也是系統呼叫引數的傳入過
程,因為在不同特權級之間控制轉換時,INT指令不同於CALL指令,它不會將外層堆疊的引數自動複製到內
層堆疊中。所以在呼叫系統呼叫時,必須先象前面的例子裡提到的那樣,把引數指定到各個暫存器中,然後
在陷入核心之後使用SAVE_ALL把這些儲存在暫存器中的引數依次壓入核心棧,這樣核心才能使用使用者傳入的
引數。下面給出system_call的原始碼:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
. . . . . .
在這裡所做的所有工作是:
1.儲存EAX暫存器,因為在SAVE_ALL中儲存的EAX暫存器會被呼叫的返回值所覆蓋;
2.呼叫SAVE_ALL儲存暫存器上下文;
3.判斷當前呼叫是否是合法系統呼叫(EAX是系統呼叫號,它應該小於NR_syscalls);
4.如果設定了PF_TRACESYS標誌,則跳轉到syscall_trace,在那裡將會把當前程式掛起並向其
父程式傳送SIGTRAP,這主要是為了設 置除錯斷點而設計的;
5.如果沒有設定PF_TRACESYS標誌,則跳轉到該系統呼叫的處理函式入口。這裡是以EAX(即前
面提到的系統呼叫號)作為偏移,在系 統呼叫表sys_call_table中查詢處理函式入口地址,
並跳轉到該入口地址。
(補充說明:
1.GET_CURRENT宏
#define GET_CURRENT(reg)
movl %esp, reg;
andl $-8192, reg;
其作用是取得當前程式的task_struct結構的指標返回到reg中,因為在Linux中核心棧的位置是
task_struct之後的兩個頁面處(8192bytes),所以此處把棧指標與-8192則得到的是task_struct結構指標
,而task_struct中偏移為4的位置是成員flags,在這裡指令testb x20,flags(%ebx)檢測的就是
task_struct->flags。
2.堆疊中的引數
正如前面提到的,SAVE_ALL是系統呼叫引數的傳入過程,當執行完SAVE_ALL並且再由CALL指令呼叫其處
理函式時,堆疊的結構應該如上圖所示。這時的堆疊結構看起來和執行一個普通帶引數的函式呼叫是一樣的
,引數在堆疊中對應的順序是(arg1, ebx),(arg2, ecx),(arg3, edx). . . . . .,這正是
SAVE_ALL壓棧的反順序,這些引數正是使用者在使用系統呼叫時試圖傳送給核心的引數。下面是在核心的呼叫
處理函式中使用引數的兩種典型方法:
asmlinkage int sys_fork(struct pt_regs regs);
asmlinkage int sys_open(const char * filename, int flags, int mode);
在sys_fork中,把整個堆疊中的內容視為一個struct pt_regs型別的引數,該引數的結構和堆疊的結構
是一致的,所以可以使用堆疊中的全部資訊。而在sys_open中引數filename, flags, mode正好對應與堆疊
中的ebx, ecx, edx的位置,而這些暫存器正是使用者在透過C庫呼叫系統呼叫時給這些引數指定的暫存器。
__asm__ __volatile__(
"int x80 "
:"=a"(retval)
:"0"(__NR_open),
"b"(filename),
"c"(flags),
"d"(mode));
3.核心如何使用使用者空間的引數
在使用系統呼叫時,有些引數是指標,這些指標所指向的是使用者空間DS暫存器的段選擇子所描述段中的地址
,而在2.2之前的版本中,核心態的DS段暫存器的中的段選擇子和使用者態的段選擇子描述的段地址不同(前
者為0xC0000000, 後者為0x00000000),這樣在使用這些引數時就不能讀取到正確的位置。所以需要透過特
殊的核心函式(如:memcpy_fromfs, mencpy_tofs)來從使用者空間資料段讀取引數,在這些函式中,是使用
FS暫存器來作為讀取引數的段暫存器的,FS暫存器在系統呼叫進入核心態時被設成了USER_DS(DS被設成了
KERNEL_DS)。在2.2之後的版本使用者態和核心態使用的DS中段選擇子描述的段地址是一樣的(都是
0x00000000),所以不需要再經過上面那樣煩瑣的過程而直接使用引數了。
2.2及以後的版本linux/arch/i386/head.S
ENTRY(gdt_table)
.quad 0x0000000000000000/* NULL descriptor */
.quad 0x0000000000000000/* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
2.0 linux/arch/i386/head.S
ENTRY(gdt)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */
.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */
.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */
.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 *
在2.0版的核心中SAVE_ALL宏定義還有這樣幾條語句:
"movl $" STR(KERNEL_DS) ",%edx "
"mov %dx,%ds "
"mov %dx,%es "
"movl $" STR(USER_DS) ",%edx "
"mov %dx,%fs "
"movl ,%edx "
E.呼叫返回
呼叫返回的過程要做的工作比其響應過程要多一些,這些工作幾乎是每次從核心態返回使用者態都需要做的,
這裡將簡要的說明:
1.判斷有沒有軟中斷,如果有則跳轉到軟中斷處理;
2.判斷當前程式是否需要重新排程,如果需要則跳轉到排程處理;
3.如果當前程式有掛起的訊號還沒有處理,則跳轉到訊號處理;
4.使用用RESTORE_ALL來彈出所有被SAVE_ALL壓入核心棧的內容並且使用iret返回使用者態。
F.例項介紹
前面介紹了系統呼叫相關的資料結構以及在Linux中使用一個系統呼叫的過程中每一步是怎樣處理的,
下面將把前面的所有概念串起來,說明怎樣在Linux中增加一個系統呼叫。
這裡實現的系統呼叫hello僅僅是在控制檯上列印一條語句,沒有任何功能。
1.修改linux/include/i386/unistd.h,在裡面增加一條語句:
#define __NR_hello ???(這個數字可能因為核心版本不同而不同)
2.在某個合適的目錄中(如:linux/kernel)增加一個hello.c,修改該目錄下的Makefile(把相映的.o文
件列入Makefile中就可以了)。
3.編寫hello.c
. . . . . .
asmlinkage int sys_hello(char * str)
{
printk(“My syscall: hello, I know what you say to me: %s ! ”, str);
return 0;
}
4.修改linux/arch/i386/kernel/entry.S,在裡面增加一條語句:
ENTRY(sys_call_table)
. . . . . .
.long SYMBOL_NAME(sys_hello)
並且修改:
.rept NR_syscalls-??? /* ??? = ??? +1 */
.long SYMBOL_NAME(sys_ni_syscall)
5.在linux/include/i386/中增加hello.h,裡面至少應包括這樣幾條語句:
#include
#ifdef __KERNEL
#else
inline _syscall1(int, hello, char *, str);
#endif
這樣就可以使用系統呼叫hello了
Linux中的系統呼叫
1. 程式相關的系統呼叫
Fork & vfork & clone
程式是一個指令執行流及其執行環境,其執行環境是一個系統資源的集合,這些資源在Linux中被抽象
成各種資料物件:程式控制塊、虛存空間、檔案系統,檔案I/O、訊號處理函式。所以建立一個程式的過程
就是這些資料物件的建立過程。
在呼叫系統呼叫fork建立一個程式時,子程式只是完全複製父程式的資源,這樣得到的子程式獨立於父
程式,具有良好的併發性,但是二者之間的通訊需要透過專門的通訊機制,如:pipe,fifo,System V IPC
機制等,另外透過fork建立子程式系統開銷很大,需要將上面描述的每種資源都複製一個副本。這樣看來,
fork是一個開銷十分大的系統呼叫,這些開銷並不是所有的情況下都是必須的,比如某程式fork出一個子進
程後,其子程式僅僅是為了呼叫exec執行另一個執行檔案,那麼在fork過程中對於虛存空間的複製將是一個
多餘的過程(由於Linux中是採取了copy-on-write技術,所以這一步驟的所做的工作只是虛存管理部分的復
制以及頁表的建立,而並沒有包括物理也面的複製);另外,有時一個程式中具有幾個獨立的計算單元,可
以在相同的地址空間上基本無衝突進行運算,但是為了把這些計算單元分配到不同的處理器上,需要建立幾
個子程式,然後各個子程式分別計算最後透過一定的程式間通訊和同步機制把計算結果彙總,這樣做往往有
許多格外的開銷,而且這種開銷有時足以抵消平行計算帶來的好處。
這說明了把計算單元抽象到程式上是不充分的,這也就是許多系統中都引入了執行緒的概念的原因。在講
述執行緒前首先介紹以下vfork系統呼叫,vfork系統呼叫不同於fork,用vfork建立的子程式共享地址空間,
也就是說子程式完全執行在父程式的地址空間上,子程式對虛擬地址空間任何資料的修改同樣為父程式所見
。但是用vfork建立子程式後,父程式會被阻塞直到子程式呼叫exec或exit。這樣的好處是在子程式被建立
後僅僅是為了呼叫exec執行另一個程式時,因為它就不會對父程式的地址空間有任何引用,所以對地址空間
的複製是多餘的,透過vfork可以減少不必要的開銷。
在Linux中, fork和vfork都是呼叫同一個核心函式
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)
其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PID,CLONE_VFORK等
等標誌位,任何一位被置1了則表明建立的子程式和父程式共享該位對應的資源。所以在vfork的實現中,
cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,這表示子程式和父程式共享地址空間,同時do_fork會
檢查CLONE_VFORK,如果該位被置1了,子程式會把父程式的地址空間鎖住,直到子程式退出或執行exec時才
釋放該鎖。
在講述clone系統呼叫前先簡單介紹執行緒的一些概念。
執行緒是在程式的基礎上進一步的抽象,也就是說一個程式分為兩個部分:執行緒集合和資源集合。執行緒是
程式中的一個動態物件,它應該是一組獨立的指令流,程式中的所有執行緒將共享程式裡的資源。但是執行緒應
該有自己的私有物件:比如程式計數器、堆疊和暫存器上下文。
執行緒分為三種型別:
核心執行緒、輕量級程式和使用者執行緒。
核心執行緒:
它的建立和撤消是由核心的內部需求來決定的,用來負責執行一個指定的函式,一個核心執行緒不需要和
一個使用者程式聯絡起來。它共享核心的正文段核全域性資料,具有自己的核心堆疊。它能夠單獨的被排程並且
使用標準的核心同步機制,可以被單獨的分配到一個處理器上執行。核心執行緒的排程由於不需要經過態的轉
換並進行地址空間的重新對映,因此在核心執行緒間做上下文切換比在程式間做上下文切換快得多。
輕量級程式:
輕量級程式是核心支援的使用者執行緒,它在一個單獨的程式中提供多執行緒控制。這些輕量級程式被單獨的
排程,可以在多個處理器上執行,每一個輕量級程式都被繫結在一個核心執行緒上,而且在它的生命週期這種
繫結都是有效的。輕量級程式被獨立排程並且共享地址空間和程式中的其它資源,但是每個LWP都應該有自
己的程式計數器、暫存器集合、核心棧和使用者棧。
使用者執行緒:
使用者執行緒是透過執行緒庫實現的。它們可以在沒有核心參與下建立、釋放和管理。執行緒庫提供了同步和調
度的方法。這樣程式可以使用大量的執行緒而不消耗核心資源,而且省去大量的系統開銷。使用者執行緒的實現是
可能的,因為使用者執行緒的上下文可以在沒有核心干預的情況下儲存和恢復。每個使用者執行緒都可以有自己的用
戶堆疊,一塊用來儲存使用者級暫存器上下文以及如訊號遮蔽等狀態資訊的記憶體區。庫透過儲存當前執行緒的堆
棧和暫存器內容載入新排程執行緒的那些內容來實現使用者執行緒之間的排程和上下文切換。
核心仍然負責程式的切換,因為只有核心具有修改記憶體管理暫存器的權力。使用者執行緒不是真正的排程實
體,核心對它們一無所知,而只是排程使用者執行緒下的程式或者輕量級程式,這些程式再透過執行緒庫函式來調
度它們的執行緒。當一個程式被搶佔時,它的所有使用者執行緒都被搶佔,當一個使用者執行緒被阻塞時,它會阻塞下
面的輕量級程式,如果程式只有一個輕量級程式,則它的所有使用者執行緒都會被阻塞。
明確了這些概念後,來講述Linux的執行緒和clone系統呼叫。
在許多實現了MT的作業系統中(如:Solaris,Digital Unix等), 執行緒和程式透過兩種資料結構來抽
象表示: 程式表項和執行緒表項,一個程式表項可以指向若干個執行緒表項, 排程器在程式的時間片內再排程
執行緒。 但是在Linux中沒有做這種區分, 而是統一使用task_struct來管理所有程式/執行緒,只是執行緒與
執行緒之間的資源是共享的,這些資源可是是前面提到過的:虛存、檔案系統、檔案I/O以及訊號處理函式甚
至PID中的幾種。
也就是說Linux中,每個執行緒都有一個task_struct,所以執行緒和程式可以使用同一排程器排程。其實
Linux核心中,輕量級程式和程式沒有質上的差別,因為Linux中程式的概念已經被抽象成了計算狀態加資源
的集合,這些資源在程式間可以共享。如果一個task獨佔所有的資源,則是一個HWP,如果一個task和其它
task共享部分資源,則是LWP。
clone系統呼叫就是一個建立輕量級程式的系統呼叫:
int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);
其中fn是輕量級程式所執行的過程,stack是輕量級程式所使用的堆疊,flags可以是前面提到的
CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的組合。Clone 和fork,vfork在實現時都
是呼叫核心函式do_fork。
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);
和fork、vfork不同的是,fork時clone_flag = SIGCHLD;
vfork時clone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD;
而在clone中,clone_flag由使用者給出。
下面給出一個使用clone的例子。
Void * func(int arg)
{
. . . . . .
}
int main()
{
int clone_flag, arg;
. . . . . .
clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |
CLONE_FILES;
stack = (char *)malloc(STACK_FRAME);
stack += STACK_FRAME;
retval = clone((void *)func, stack, clone_flag, arg);
. . . . . .
}
看起來clone的用法和pthread_create有些相似,兩者的最根本的差別在於clone是建立一個LWP,對核
心是可見的,由核心排程,而pthread_create通常只是建立一個使用者執行緒,對核心是不可見的,由執行緒庫調
度。
Nanosleep & sleep
sleep和nanosleep都是使程式睡眠一段時間後被喚醒,但是二者的實現完全不同。
Linux中並沒有提供系統呼叫sleep,sleep是在庫函式中實現的,它是透過呼叫alarm來設定報警時間,
呼叫sigsuspend將程式掛起在訊號SIGALARM上,sleep只能精確到秒級上。
nanosleep則是Linux中的系統呼叫,它是使用定時器來實現的,該呼叫使呼叫程式睡眠,並往定時器隊
列上加入一個time_list型定時器,time_list結構裡包括喚醒時間以及喚醒後執行的函式,透過nanosleep
加入的定時器的執行函式僅僅完成喚醒當前程式的功能。系統透過一定的機制定時檢查這些佇列(比如透過
系統呼叫陷入核心後,從核心返回使用者態前,要檢查當前程式的時間片是否已經耗盡,如果是則呼叫
schedule()函式重新排程,該函式中就會檢查定時器佇列,另外慢中斷返回前也會做此檢查),如果定時時
間已超過,則執行定時器指定的函式喚醒呼叫程式。當然,由於系統時間片可能丟失,所以nanosleep精度
也不是很高。
alarm也是透過定時器實現的,但是其精度只精確到秒級,另外,它設定的定時器執行函式是在指定時
間向當前程式傳送SIGALRM訊號。
2.儲存相關的系統呼叫
mmap:檔案對映
在講述檔案對映的概念時,不可避免的要牽涉到虛存(SVR 4的VM)。實際上,檔案對映是虛存的中心
概念,檔案對映一方面給使用者提供了一組措施,似的使用者將檔案對映到自己地址空間的某個部分,使用簡單
的記憶體訪問指令讀寫檔案;另一方面,它也可以用於核心的基本組織模式,在這種模式種,核心將整個地址
空間視為諸如檔案之類的一組不同物件的對映。
Unix中的傳統檔案訪問方式是,首先用open系統呼叫開啟檔案,然後使用read,write以及lseek等呼叫
進行順序或者隨即的I/O。這種方式是非常低效的,每一次I/O操作都需要一次系統呼叫。另外,如果若干個
程式訪問同一個檔案,每個程式都要在自己的地址空間維護一個副本,浪費了記憶體空間。而如果能夠透過一
定的機制將頁面對映到程式的地址空間中,也就是說首先透過簡單的產生某些記憶體管理資料結構完成對映的
建立。當程式訪問頁面時產生一個缺頁中斷,核心將頁面讀入記憶體並且更新頁表指向該頁面。而且這種方式
非常方便於同一副本的共享。
下面給出以上兩種方式的對比圖:
VM是物件導向的方法設計的,這裡的物件是指記憶體物件:記憶體物件是一個軟體抽象的概念,它描述記憶體
區與後備儲存之間的對映。系統可以使用多種型別的後備儲存,比如交換空間,本地或者遠端檔案以及幀緩
存等等。VM系統對它們統一處理,採用同一操作集操作,比如讀取頁面或者回寫頁面等。每種不同的後備存
儲都可以用不同的方法實現這些操作。這樣,系統定義了一套統一的介面,每種後備儲存給出自己的實現方
法。
這樣,程式的地址空間就被視為一組對映到不同資料物件上的的對映組成。所有的有效地址就是那些映
射到資料物件上的地址。這些物件為對映它的頁面提供了永續性的後備儲存。對映使得使用者可以直接定址這
些物件。
值得提出的是,VM體系結構獨立於Unix系統,所有的Unix系統語義,如正文,資料及堆疊區都可以建構
在基本VM系統之上。同時,VM體系結構也是獨立於儲存管理的,儲存管理是由作業系統實施的,如:究竟採
取什麼樣的對換和請求調頁演算法,究竟是採取分段還是分頁機制進行儲存管理,究竟是如何將虛擬地址轉換
成為實體地址等等(Linux中是一種叫Three Level Page Table的機制),這些都與記憶體物件的概念無關。
下面介紹Linux中VM的實現。
如下圖所示,一個程式應該包括一個mm_struct(memory manage struct),該結構是程式虛擬地址空
間的抽象描述,裡面包括了程式虛擬空間的一些管理資訊:start_code, end_code, start_data,
end_data, start_brk, end_brk等等資訊。另外,也有一個指向程式虛存區表(vm_area_struct :virtual
memory area)的指標,該鏈是按照虛擬地址的增長順序排列的。
在Linux程式的地址空間被分作許多區(vma),每個區(vma)都對應虛擬地址空間上一段連續的區域
,vma是可以被共享和保護的獨立實體,這裡的vma就是前面提到的記憶體物件。這裡給出vm_area_struct的結
構,其中,前半部分是公共的,與型別無關的一些資料成員,如:指向mm_struct的指標,地址範圍等等,
後半部分則是與型別相關的成員,其中最重要的是一個指向vm_operation_struct向量表的指標vm_ops,
vm_pos向量表是一組虛擬函式,定義了與vma型別無關的介面。每一個特定的子類,即每種vma型別都必須在向
量表中實現這些操作。這裡包括了:open, close, unmap, protect, sync, nopage, wppage, swapout這些
操作。
struct vm_area_struct {
/*公共的,與vma型別無關的 */
struct mm_struct * vm_mm;
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
/* 與型別相關的 */
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff;
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data;
};
vm_ops: open, close, no_page, swapin, swapout . . . . . .
介紹完VM的基本概念後,我們可以講述mmap, munmap系統呼叫了。mmap呼叫實際上就是一個記憶體物件
vma的建立過程,mmap的呼叫格式是:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
其中start是對映地址,length是對映長度,如果flags的MAP_FIXED不被置位,則該引數通常被忽略,而查
找程式地址空間中第一個長度符合的空閒區域;Fd是對映檔案的檔案控制程式碼,offset是對映檔案中的偏移地址
;prot是對映保護許可權,可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE,flags則是指對映型別,
可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED,該引數必須被指定為MAP_PRIVATE和MAP_SHARED其中之一,
MAP_PRIVATE是建立一個寫時複製對映(copy-on-write),也就是說如果有多個程式同時對映到一個檔案上,
對映建立時只是共享同樣的儲存頁面,但是某程式企圖修改頁面內容,則複製一個副本給該程式私用,它的
任何修改對其它程式都不可見。而MAP_SHARED則無論修改與否都使用同一副本,任何程式對頁面的修改對其
它程式都是可見的。
Mmap系統呼叫的實現過程是:
1.先透過檔案系統定位要對映的檔案;
2.許可權檢查,對映的許可權不會超過檔案開啟的方式,也就是說如果檔案是以只讀方式開啟,那麼則不允
許建立一個可寫對映;
3.建立一個vma物件,並對之進行初始化;
4.呼叫對映檔案的mmap函式,其主要工作是給vm_ops向量表賦值;
5.把該vma鏈入該程式的vma連結串列中,如果可以和前後的vma合併則合併;
6.如果是要求VM_LOCKED(對映區不被換出)方式對映,則發出缺頁請求,把對映頁面讀入記憶體中;
munmap(void * start, size_t length):
該呼叫可以看作是mmap的一個逆過程。它將程式中從start開始length長度的一段區域的對映關閉,如
果該區域不是恰好對應一個vma,則有可能會分割幾個或幾個vma。
Msync(void * start, size_t length, int flags) :
把對映區域的修改回寫到後備儲存中。因為munmap時並不保證頁面回寫,如果不呼叫msync,那麼有可
能在munmap後丟失對對映區的修改。其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE,MS_SYNC要求回
寫完成後才返回,MS_ASYNC發出回寫請求後立即返回,MS_INVALIDATE使用回寫的內容更新該檔案的其它映
射。
該系統呼叫是透過呼叫對映檔案的sync函式來完成工作的。
brk(void * end_data_segement):
將程式的資料段擴充套件到end_data_segement指定的地址,該系統呼叫和mmap的實現方式十分相似,同樣
是產生一個vma,然後指定其屬性。不過在此之前需要做一些合法性檢查,比如該地址是否大於mm-
>end_code,end_data_segement和mm->brk之間是否還存在其它vma等等。透過brk產生的vma對映的檔案為空
,這和匿名對映產生的vma相似,關於匿名對映不做進一步介紹。我們使用的庫函式malloc就是透過brk實現
的,透過下面這個例子很容易證實這點:
main()
{
char * m, * n;
int size;
m = (char *)sbrk(0);
printf("sbrk addr = %08lx ", m);
do {
n = malloc(1024);
printf("malloc addr = %08lx ", n);
}w hile(n < m);
m = (char *)sbrk(0);
printf("new sbrk addr = %08lx ", m);
}
sbrk addr = 0804a000
malloc addr = 080497d8
malloc addr = 08049be0
malloc addr = 08049fe8
malloc addr = 0804a3f0
new sbrk addr = 0804b000
3.程式間通訊(IPC)
程式間通訊可以透過很多種機制,包括signal, pipe, fifo, System V IPC, 以及socket等等,前幾種
概念都比較好理解,這裡著重介紹關於System V IPC。
System V IPC包括三種機制:message(允許程式傳送格式化的資料流到任意的程式)、shared memory
(允許程式間共享它們虛擬地址空間的部分割槽域)和semaphore(允許程式間同步的執行)。
作業系統核心中為它們分別維護著一個表,這三個表是系統中所有這三種IPC物件的集合,表的索引是
一個數值ID,程式透過這個ID可以查詢到需要使用的IPC資源。程式每建立一個IPC物件,系統中都會在相應
的表中增加一項。之後其它程式(具有許可權的程式)只要透過該IPC物件的ID則可以引用它。
IPC物件必須使用IPC_RMID命令來顯示的釋放,否則這個物件就處於活動狀態,甚至所有的使用它的進
程都已經終止。這種機制某些時候十分有用,但是也正因為這種特徵,使得作業系統核心無法判斷IPC物件
是被使用者故意遺留下來供將來其它程式使用還是被無意拋棄的。
Linux中只提供了一個系統呼叫介面ipc()來完成所有System V IPC操作,我們常使用的是建立在該呼叫
之上的庫函式介面。對於這三種IPC,都有很相似的三種呼叫:xxxget, (msgsnd, msgrcv)|semopt |
(shmat, shmdt), xxxctl
Xxxget:獲取呼叫,在系統中申請或者查詢一個IPC資源,返回值是該IPC物件的ID,該呼叫類似於檔案
系統的open, create呼叫;
Xxxctl:控制呼叫,至少包括三種操作:XXX_RMID(釋放IPC物件), XXX_STAT(查詢狀態), XXX_SET
(設定狀態資訊);
(msgsnd, msgrcv) | Semopt | (shmat, shmdt)|:操作呼叫,這些呼叫的功能隨IPC物件的型別不同而
有較大差異。
4.檔案系統相關的呼叫
檔案是用來儲存資料的,而檔案系統則可以讓使用者組織,操縱以及存取不同的檔案。核心允許使用者透過
一個嚴格定義的過程性介面與檔案系統進行互動,這個介面對使用者遮蔽了檔案系統的細節,同時指定了所有
相關係統呼叫的行為和語義。Linux支援許多中檔案系統,如ext2,msdos, ntfs, proc, dev, ufs, nfs等
等,這些檔案系統都實現了相同的介面,因此給應用程式提供了一致性的檢視。但每種檔案系統在實現時可
能對某個方面加以了一定的限制。如:檔名的長度,是否支援所有的檔案系統介面呼叫。
為了支援多檔案系統,sun提出了一種vnode/vfs介面,SVR4中將之實現成了一種工業標準。而Linux作
為一種Unix的clone體,自然也實現了這種介面,只是它的介面定義和SVR4的稍有不同。Vnode/Vfs介面的設
計體現了物件導向的思想,Vfs(虛擬檔案系統)代表核心中的一個檔案系統,Vnode(虛擬節點)代表核心
中的一個檔案,它們都可以被視為抽象基類,並可以從中派生出不同的子類以實現不同的檔案系統。
由於篇幅原因,這裡只是大概的介紹一下怎樣透過Vnode/Vfs結構來實現檔案系統和訪問檔案。
在Linux中支援的每種檔案系統必須有一個file_system_type結構,此結構的核心是read_super函式,
該函式將讀取檔案系統的超級塊。Linux中支援的所有檔案系統都會被註冊在一條file_system_type結構鏈
中,註冊是在系統初始化時呼叫regsiter_filesystem()完成,如果檔案系統是以模組的方式實現,則是在
呼叫init_module時完成。
當mount某種塊裝置時,將呼叫系統呼叫mount,該呼叫中將會首先檢查該類檔案系統是否註冊在系統種
中,如果註冊了則先給該檔案系統分配一個super_block,並進行初始化,最後呼叫這種檔案系統的
read_super函式來完成super_block結構私有資料的賦值。其中最主要的工作是給super_block的s_ops賦值
,s_ops是一個函式向量表,由檔案系統各自實現了一組操作。
struct super_operations {
void (*read_inode) (struct inode *);
void (*write_inode) (struct inode *);
void (*put_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*statfs) (struct super_block *, struct statfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
};
由於這組操作中定義了檔案系統中對於inode的操作,所以是之後對於檔案系統中檔案所有操作的基礎
。
在給super_block的s_ops賦值後,再給該檔案系統分配一個vfsmount結構,將該結構註冊到系統維護的
另一條鏈vfsmntlist中,所有mount上的檔案系統都在該鏈中有一項。在umount時,則從鏈中刪除這一項並
且釋放超級塊。
對於一個已經mount的檔案系統中任何檔案的操作首先應該以產生一個inode例項,即根據檔案系統的類
型生成一個屬於該檔案系統的記憶體i節點。這首先呼叫檔案定位函式lookup_dentry查詢目錄快取看是否使用
過該檔案,如果還沒有則快取中找不到,於是需要的i接點則依次呼叫路徑上的所有目錄I接點的lookup函式
,在lookup函式中會呼叫iget函式,該函式中最終呼叫超級塊的s_ops->read_inode讀取目標檔案的磁碟I節
點(這一步再往下就是由裝置驅動完成了,透過呼叫驅動程式的read函式讀取磁碟I節點),read_inode函
數的主要功能是初始化inode的一些私有資料(比如資料儲存位置,檔案大小等等)以及給inode_operations
函式開關表賦值,最終該inode被繫結在一個目錄快取結構dentry中返回。
在獲得了檔案的inode之後,對於該檔案的其它一切操作都有了根基。因為可以從inode 獲得檔案操作
函式開關表file_operatoins,該開關表裡給出了標準的檔案I/O介面的實現,包括read, write, lseek,
mmap, ioctl等等。這些函式入口將是所有關於檔案的系統呼叫請求的最終處理入口,透過這些函式入口會
向儲存該檔案的硬裝置驅動發出請求並且由驅動程式返回資料。當然這中間還會牽涉到一些關於buffer的管
理問題,這裡就不贅述了。
透過講述這些,我們應該明白了為什麼可以使用統一的系統呼叫介面來訪問不同檔案系統型別的檔案了
:因為在檔案系統的實現一層,都把低層的差異遮蔽了,使用者可見的只是高層可見的一致的系統呼叫介面。
5.與module相關的系統呼叫
Linux中提供了一種動態載入或解除安裝核心元件的機制——模組。透過這種機制Linux使用者可以為自己可以
保持一個儘量小的核心映像檔案,另外,往核心中載入和解除安裝模組不需要重新編譯整個核心以及引導機器。
可以透過一定的命令或者呼叫在一個執行的系統中載入模組,在不需要時解除安裝模組。模組可以完成許多功能
,比如檔案系統、裝置驅動,系統支援的執行檔案格式,甚至系統呼叫和中斷處理都可以用模組來更新。
Linux中提供了往系統中新增和解除安裝模組的介面,create_module(),init_module (), delete_module
(),這些系統呼叫通常不是直接為程式設計師使用的,它們僅僅是為實現一些系統命令而提供的介面,如
insmod, rmmod,(在使用這些系統呼叫前必須先載入目標檔案到使用者程式的地址空間,這必須由目標檔案
格式所特定的庫函式(如:libobj.a中的一些函式)來完成)。
Linux的核心中維護了一個module_list列表,每個被載入到核心中的模組都在其中佔有一項,系統呼叫
create_module()就是在該列表裡註冊某個指定的模組,而init_module則是使用模組目標檔案內容的對映來
初始化核心中註冊的該模組,並且呼叫該模組的初始化函式,初始化函式通常完成一些特定的初始化操作,
比如檔案系統的初始化函式就是在作業系統中註冊該檔案系統。delete_module則是從系統中解除安裝一個模組
,其主要工作是從module_list中刪除該模組對應的module結構並且呼叫該模組的cleanup函式解除安裝其它私有
資訊。
Back
Linux中怎樣編譯和定製核心
1.編譯核心前注意的事項
檢查系統上其它資源是否符合新核心的要求。在linux/Document目錄下有一個叫Changes的檔案,裡面
列舉了當前核心版本所需要的其它軟體的版本號,
- Kernel modutils 2.1.121 ; insmod -V
- Gnu C 2.7.2.3 ; gcc --version
- Binutils 2.8.1.0.23 ; ld -v
- Linux libc5 C Library 5.4.46 ; ls -l /lib/libc*
- Linux libc6 C Library 2.0.7pre6 ; ls -l /lib/libc*
- Dynamic Linker (ld.so) 1.9.9 ; ldd --version or ldd -v
- Linux C++ Library 2.7.2.8 ; ls -l /usr/lib/libg++.so.*
. . . . . .
其中最後一項是列舉該軟體版本號的命令,如果不符合要求先給相應軟體升級,這一步通常可以忽略。
2.配置核心
使用make config或者make menuconfig, make xconfig配置新核心。其中包括選擇塊裝置驅動程式、網
絡選項、網路裝置支援、檔案系統等等,使用者可以根據自己的需求來進行功能配置。每個選項至少有“y”
和“n”兩種選擇,選擇“y”表示把相應的支援編譯進核心,選“n”表示不提供這種支援,還有的有第三
種選擇“m”,則表示把該支援編譯成可載入模組,即前面提到的module,怎樣編譯和安裝模組在後面會介
紹。
這裡,順便講述一下如何在核心中增加自己的功能支援。
假如我們現在需要在自己的核心中加入一個檔案系統tfile,在完成了檔案系統的程式碼後,在linux/fs
下建立一個tfile目錄,把原始檔複製到該目錄下,然後修改linux/fs下的Makefile,把對應該檔案系統的
目標檔案加入目標檔案列表中,最後修改linux/fs/Config.in檔案,加入
bool 'tfile fs support' CONFIG_TFILE_FS或者
tristate ‘tfile fs support' CONFIG_TFILE_FS
這樣在Make menuconfig時在filesystem選單下就可以看到
< > tfile fs support一項了
3.編譯核心
在配置好核心後就是編譯核心了,在編譯之前首先應該執行make dep命令建立好依賴關係,該命令將會
修改linux中每個子目錄下的.depend檔案,該檔案包含了該目錄下每個目標檔案所需要的標頭檔案(絕對路徑
的方式列舉)。
然後就是使用make bzImage命令來編譯核心了。該命令執行結束後將會在linux/arch/asm/boot/產生一
個名叫bzImage的映像檔案。
4.使用新核心引導
把前面編譯產生的映像檔案複製到/boot目錄下(也可以直接建立一個符號連線,這樣可以省去每次編
譯後的複製工作),這裡暫且命名為vmlinuz-new,那麼再修改/etc/lilo.conf,在其中增加這麼幾條:
image = /boot/vmlinuz-new
root = /dev/hda1
label = new
read-only
並且執行lilo命令,那麼系統在啟動時就可以選用新核心引導了。
5.編譯模組和使用模組
在linux/目錄下執行make modules編譯模組,然後使用命令make modules_install來安裝模組(所有的可加
載模組的目標檔案會被複製到/lib/modules/2.2.12/),這樣之後就可以透過執行insmod 〈模組名〉和
rmmod〈模組名〉命令來載入或解除安裝功能模組了。
Linux中的系統呼叫
Linux中怎樣編譯和定製核心
Linux下系統呼叫的實現
Unix/Linux作業系統的體系結構及系統呼叫介紹
什麼是作業系統和系統呼叫
作業系統是從硬體抽象出來的虛擬機器,在該虛擬機器上使用者可以執行應用程式。它負責直接與硬體互動,
向使用者程式提供公共服務,並使它們同硬體特性隔離。因為程式不應該依賴於下層的硬體,只有這樣應用程
序才能很方便的在各種不同的Unix系統之間移動。系統呼叫是Unix/Linux作業系統向使用者程式提供支援的接
口,透過這些介面應用程式向作業系統請求服務,控制轉向作業系統,而作業系統在完成服務後,將控制和
結果返回給使用者程式。
Unix/Linux系統體系結構
一個Unix/Linux系統分為三個層次:使用者、核心以及硬體。
其中系統呼叫是使用者程式與核心間的邊界,透過系統呼叫程式可由使用者模式轉入核心模式,在核心模
式下完成一定的服務請求後在返回使用者模式。
系統呼叫介面看起來和C程式中的普通函式呼叫很相似,它們通常是透過庫把這些函式呼叫對映成進入
作業系統所需要的原語。
這些操作原語只是提供一個基本功能集,而透過庫對這些操作的引用和封裝,可以形成豐富而且強大的
系統呼叫庫。這裡體現了機制與策略相分離的程式設計思想——系統呼叫只是提供訪問核心的基本機制,而策略
是透過系統呼叫庫來體現。
例:execv, execl, execlv, opendir , readdir...
Unix/Linux執行模式,地址空間和上下文
執行模式(執行態):
一種計算機硬體要執行Unix/Linux系統,至少需要提供兩種執行模式:高優先順序的核心模式和低優先順序
的使用者模式。
實際上許多計算機都有兩種以上的執行模式。如:intel 80x86體系結構就有四層執行特權,內層特權
最高。Unix只需要兩層即可以了:核心執行在高優先順序,稱之為核心態;其它外圍軟體包括shell,編輯程
序,Xwindow等等都是在低優先順序執行,稱之為使用者態。之所以採取不同的執行模式主要原因時為了保護,
由於使用者程式在較低的特權級上執行,它們將不能意外或故意的破壞其它程式或核心。程式造成的破壞會被
區域性化而不影響系統中其它活動或者程式。當使用者程式需要完成特權模式下才能完成的某些功能時,必須嚴
格按照系統呼叫提供介面才能進入特權模式,然後執行呼叫所提供的有限功能。
每種執行態都應該有自己的堆疊。在Linux中,分為使用者棧和核心棧。使用者棧包括在使用者態執行時函式
呼叫的引數、區域性變數和其它資料結構。有些系統中專門為全域性中斷處理提供了中斷棧,但是x86中並沒有
中斷棧,中斷在當前程式的核心棧中處理。
地址空間:
採用特權模式進行保護的根本目的是對地址空間的保護,使用者程式不應該能夠訪問所有的地址空間:只
有透過系統呼叫這種受嚴格限制的介面,程式才能進入核心態並訪問到受保護的那一部分地址空間的資料,
這一部分通常是留給作業系統使用。另外,程式與程式之間的地址空間也不應該隨便互訪。這樣,就需要提
供一種機制來在一片實體記憶體上實現同一程式不同地址空間上的保護,以及不同程式之間地址空間的保護。
Unix/Linux中透過虛存管理機制很好的實現了這種保護,在虛存系統中,程式所使用的地址不直接對應
物理的儲存單元。每個程式都有自己的虛存空間,每個程式有自己的虛擬地址空間,對虛擬地址的引用透過
地址轉換機制轉換成為實體地址的引用。正因為所有程式共享實體記憶體資源,所以必須透過一定的方法來保
護這種共享資源,透過虛存系統很好的實現了這種保護:每個程式的地址空間透過地址轉換機制對映到不同
的物理儲存頁面上,這樣就保證了程式只能訪問自己的地址空間所對應的頁面而不能訪問或修改其它程式的
地址空間對應的頁面。
虛擬地址空間分為兩個部分:使用者空間和系統空間。在使用者模式下只能訪問使用者空間而在核心模式下可
以訪問系統空間和使用者空間。系統空間在每個程式的虛擬地址空間中都是固定的,而且由於系統中只有一個
核心例項在執行,因此所有程式都對映到單一核心地址空間。核心中維護全域性資料結構和每個程式的一些對
象資訊,後者包括的資訊使得核心可以訪問任何程式的地址空間。透過地址轉換機制程式可以直接訪問當前
程式的地址空間(透過MMU),而透過一些特殊的方法也可以訪問到其它程式的地址空間。
儘管所有程式都共享核心,但是系統空間是受保護的,程式在使用者態無法訪問。程式如果需要訪問核心
,則必須透過系統呼叫介面。程式呼叫一個系統呼叫時,透過執行一組特殊的指令(這個指令是與平臺相關
的,每種系統都提供了專門的trap命令,基於x86的Linux中是使用int 指令)使系統進入核心態,並將控制
權交給核心,由核心替代程式完成操作。當系統呼叫完成後,核心執行另一組特徵指令將系統返回到使用者態
,控制權返回給程式。
上下文:
一個程式的上下文可以分為三個部分:使用者級上下文、暫存器上下文以及系統級上下文。
使用者級上下文:正文、資料、使用者棧以及共享儲存區;
暫存器上下文:程式暫存器(IP),即CPU將執行的下條指令地址,處理機狀態暫存器(EFLAGS),棧
指標,通用暫存器;
系統級上下文:程式表項(proc結構)和U區,在Linux中這兩個部分被合成task_struct,區表及頁表
(mm_struct , vm_area_struct, pgd, pmd, pte等),核心棧等。
全部的上下文資訊組成了一個程式的執行環境。當發生程式排程時,必須對全部上下文資訊進行切換,
新排程的程式才能執行。程式就是上下文的集合的一個抽象概念。
系統呼叫的功能和分類
作業系統核心在執行期間的活動可以分為兩個部分:上半部分(top half)和下半部分(bottom half),
其中上半部分為應用程式提供系統呼叫或自陷的服務,是同步服務,由當前執行的程式引起,在當前程式上
下文中執行並允許直接訪問當前程式的資料結構;而下半部分則是由處理硬體中斷的子程式,是屬於非同步活
動,這些子程式的呼叫和執行與當前程式無關。上半部分允許被阻塞,因為這樣阻塞的是當前程式;下半部
分不允許被阻塞,因為阻塞下半部分會引起阻塞一個無辜的程式甚至整個核心。
系統呼叫可以看作是一個所有Unix/Linux程式共享的子程式庫,但是它是在特權方式下執行,可以存取
核心資料結構和它所支援的使用者級資料。系統呼叫的主要功能是使使用者可以使用作業系統提供的有關裝置管
理、檔案系統、程式控制程式通訊以及儲存管理方面的功能,而不必要了解作業系統的內部結構和有關硬體
的細節問題,從而減輕使用者負擔和保護系統以及提高資源利用率。
系統呼叫分為兩個部分:與檔案子系統互動的和程式子系統互動的兩個部分。其中和檔案子系統互動的
部分進一步由可以包括與裝置檔案的互動和與普通檔案的互動的系統呼叫(open, close, ioctl, create,
unlink, . . . );與程式相關的系統呼叫又包括程式控制系統呼叫(fork, exit, getpid, . . . ),進
程間通訊,儲存管理,程式排程等方面的系統呼叫。
2.Linux下系統呼叫的實現
(以i386為例說明)
A.在Linux中系統呼叫是怎樣陷入核心的?
系統呼叫在使用時和一般的函式呼叫很相似,但是二者是有本質性區別的,函式呼叫不能引起從使用者態
到核心態的轉換,而正如前面提到的,系統呼叫需要有一個狀態轉換。
在每種平臺上,都有特定的指令可以使程式的執行由使用者態轉換為核心態,這種指令稱作作業系統陷入
(operating system trap)。程式透過執行陷入指令後,便可以在核心態執行系統呼叫程式碼。
在Linux中是透過軟中斷來實現這種陷入的,在x86平臺上,這條指令是int 0x80。也就是說在Linux中
,系統呼叫的介面是一箇中斷處理函式的特例。具體怎樣透過中斷處理函式來實現系統呼叫的入口將在後面
詳細介紹。
這樣,就需要在系統啟動時,對INT 0x80進行一定的初始化,下面將描述其過程:
1.使用匯編子程式setup_idt(linux/arch/i386/kernel/head.S)初始化idt表(中斷描述符表),這時所
有的入口函式偏移地址都被設為ignore_int
( setup_idt:
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw x8E00,%dx /* interrupt gate - dpl=0, present */
lea SYMBOL_NAME(idt_table),%edi
mov 6,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl ,%edi
dec %ecx
jne rp_sidt
ret
selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1);
2.Start_kernel()(linux/init/main.c)呼叫trap_init()(linux/arch/i386/kernel/trap.c)函式設定中斷
描述符表。在該函式里,實際上是透過呼叫函式set_system_gate(SYSCALL_VECTOR,&system_call)來完成該
項的設定的。其中的SYSCALL_VECTOR就是0x80,而system_call則是一個彙編子函式,它即是中斷0x80的處
理函式,主要完成兩項工作:a. 暫存器上下文的儲存;b. 跳轉到系統呼叫處理函式。在後面會詳細介紹這
些內容。
(補充說明:門描述符
set_system_gate()是在linux/arch/i386/kernel/trap.S中定義的,在該檔案中還定義了幾個類似的函
數set_intr_gate(), set_trap_gate, set_call_gate()。這些函式都呼叫了同一個彙編子函式__set_gate
(),該函式的作用是設定門描述符。IDT中的每一項都是一個門描述符。
#define _set_gate(gate_addr,type,dpl,addr)
set_gate(idt_table+n,15,3,addr);
門描述符的作用是用於控制轉移,其中會包括選擇子,這裡總是為__KERNEL_CS(指向GDT中的一項段描
述符)、入口函式偏移地址、門訪問特權級(DPL)以及型別標識(TYPE)。Set_system_gate的DPL為3,表
示從特權級3(最低特權級)也可以訪問該門,type為15,表示為386中斷門。)
B.與系統呼叫相關的資料結構
1.系統呼叫處理函式的函式名的約定
函式名都以“sys_”開頭,後面跟該系統呼叫的名字。例如,系統呼叫fork()的處理函式名是
sys_fork()。
asmlinkage int sys_fork(struct pt_regs regs);
(補充關於asmlinkage的說明)
2.系統呼叫號(System Call Number)
核心中為每個系統呼叫定義了一個唯一的編號,這個編號的定義在linux/include/asm/unistd.h中,編
號的定義方式如下所示:
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
. . . . . .
使用者在呼叫一個系統呼叫時,系統呼叫號號作為引數傳遞給中斷0x80,而該標號實際上是後面將要提到
的系統呼叫表(sys_call_table)的下標,透過該值可以找到相映系統呼叫的處理函式地址。
3.系統呼叫表
系統呼叫表的定義方式如下:(linux/arch/i386/kernel/entry.S)
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
. . . . . .
系統呼叫表記錄了各個系統呼叫處理函式的入口地址,以系統呼叫號為偏移量很容易的能夠在該表中找到對
應處理函式地址。在linux/include/linux/sys.h中定義的NR_syscalls表示該表能容納的最大系統呼叫數,
NR_syscalls = 256。
C.系統呼叫函式介面是如何轉化為陷入命令
如前面提到的,系統呼叫是透過一條陷入指令進入核心態,然後根據傳給核心的系統呼叫號為索引在系
統呼叫表中找到相映的處理函式入口地址。這裡將詳細介紹這一過程。
我們還是以x86為例說明:
由於陷入指令是一條特殊指令,而且依賴與作業系統實現的平臺,如在x86中,這條指令是int 0x80,
這顯然不是使用者在程式設計時應該使用的語句,因為這將使得使用者程式難於移植。所以在作業系統的上層需要實
現一個對應的系統呼叫庫,每個系統呼叫都在該庫中包含了一個入口點(如我們看到的fork, open, close
等等),這些函式對程式設計師是可見的,而這些庫函式的工作是以對應系統呼叫號作為引數,執行陷入指令
int 0x80,以陷入核心執行真正的系統呼叫處理函式。當一個程式呼叫一個特定的系統呼叫庫的入口點,正
如同它呼叫任何函式一樣,對於庫函式也要建立一個棧幀。而當程式執行陷入指令時,它將處理機狀態轉換
到核心態,並且在核心棧執行核心程式碼。
這裡給出一個示例(linux/include/asm/unistd.h):
#define _syscallN(type, name, type1, arg1, type2, arg2, . . . )
type name(type1 arg1,type2 arg2)
{
long __res;
__asm__ volatile ("int x80"
: "=a" (__res)
: "" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)));
. . . . . .
__syscall_return(type,__res);
}
在執行一個系統呼叫庫中定義的系統呼叫入口函式時,實際執行的是類似如上的一段程式碼。這裡牽涉到
一些gcc的嵌入式組合語言,不做詳細的介紹,只簡單說明其意義:
其中__NR_##name是系統呼叫號,如name == ioctl,則為__NR_ioctl,它將被放在暫存器eax中作為參
數傳遞給中斷0x80的處理函式。而系統呼叫的其它引數arg1, arg2, …則依次被放入ebx, ecx, . . .等通
用暫存器中,並作為系統呼叫處理函式的引數,這些引數是怎樣傳入核心的將會在後面介紹。
下面將示例說明:
int func1()
{
int fd, retval;
fd = open(filename, ……);
……
ioctl(fd, cmd, arg);
. . .
}
func2()
{
int fd, retval;
fd = open(filename, ……);
……
__asm__ __volatile__(
"int x80 "
:"=a"(retval)
:"0"(__NR_ioctl),
"b"(fd),
"c"(cmd),
"d"(arg));
}
這兩個函式在Linux/x86上執行的結果應該是一樣的。
若干個庫函式可以對映到同一個系統呼叫入口點。系統呼叫入口點對每個系統呼叫定義其真正的語法和
語義,但庫函式通常提供一個更方便的介面。如系統呼叫exec有集中不同的呼叫方式:execl, execle,等,
它們實際上只是同一系統呼叫的不同介面而已。對於這些呼叫,它們的庫函式對它們各自的引數加以處理,
來實現各自的特點,但是最終都被對映到同一個核心入口點。
D.系統呼叫陷入核心後作何初始化處理
當程式執行系統呼叫時,先呼叫系統呼叫庫中定義某個函式,該函式通常被展開成前面提到的
_syscallN的形式透過INT 0x80來陷入核心,其引數也將被透過暫存器傳往核心。
在這一部分,我們將介紹INT 0x80的處理函式system_call。
思考一下就會發現,在呼叫前和呼叫後執行態完全不相同:前者是在使用者棧上執行使用者態程式,後者在
核心棧上執行核心態程式碼。那麼,為了保證在核心內部執行完系統呼叫後能夠返回撥用點繼續執行使用者程式碼
,必須在進入核心態時儲存時往核心中壓入一個上下文層;在從核心返回時會彈出一個上下文層,這樣使用者
程式就可以繼續執行。
那麼,這些上下文資訊是怎樣被儲存的,被儲存的又是那些上下文資訊呢?這裡仍以x86為例說明。
在執行INT指令時,實際完成了以下幾條操作:
1.由於INT指令發生了不同優先順序之間的控制轉移,所以首先從TSS(任務狀態段)中獲取高優先順序的核心堆
棧資訊(SS和ESP);2.把低優先順序堆疊資訊(SS和ESP)保留到高優先順序堆疊(即核心棧)中;
3.把EFLAGS,外層CS,EIP推入高優先順序堆疊(核心棧)中。
4.透過IDT載入CS,EIP(控制轉移至中斷處理函式)
然後就進入了中斷0x80的處理函式system_call了,在該函式中首先使用了一個宏SAVE_ALL,該宏的定義如
下所示:
#define SAVE_ALL
cld;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
movl $(__KERNEL_DS),%edx;
movl %edx,%ds;
movl %edx,%es;
該宏的功能一方面是將暫存器上下文壓入到核心棧中,對於系統呼叫,同時也是系統呼叫引數的傳入過
程,因為在不同特權級之間控制轉換時,INT指令不同於CALL指令,它不會將外層堆疊的引數自動複製到內
層堆疊中。所以在呼叫系統呼叫時,必須先象前面的例子裡提到的那樣,把引數指定到各個暫存器中,然後
在陷入核心之後使用SAVE_ALL把這些儲存在暫存器中的引數依次壓入核心棧,這樣核心才能使用使用者傳入的
引數。下面給出system_call的原始碼:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
. . . . . .
在這裡所做的所有工作是:
1.儲存EAX暫存器,因為在SAVE_ALL中儲存的EAX暫存器會被呼叫的返回值所覆蓋;
2.呼叫SAVE_ALL儲存暫存器上下文;
3.判斷當前呼叫是否是合法系統呼叫(EAX是系統呼叫號,它應該小於NR_syscalls);
4.如果設定了PF_TRACESYS標誌,則跳轉到syscall_trace,在那裡將會把當前程式掛起並向其
父程式傳送SIGTRAP,這主要是為了設 置除錯斷點而設計的;
5.如果沒有設定PF_TRACESYS標誌,則跳轉到該系統呼叫的處理函式入口。這裡是以EAX(即前
面提到的系統呼叫號)作為偏移,在系 統呼叫表sys_call_table中查詢處理函式入口地址,
並跳轉到該入口地址。
(補充說明:
1.GET_CURRENT宏
#define GET_CURRENT(reg)
movl %esp, reg;
andl $-8192, reg;
其作用是取得當前程式的task_struct結構的指標返回到reg中,因為在Linux中核心棧的位置是
task_struct之後的兩個頁面處(8192bytes),所以此處把棧指標與-8192則得到的是task_struct結構指標
,而task_struct中偏移為4的位置是成員flags,在這裡指令testb x20,flags(%ebx)檢測的就是
task_struct->flags。
2.堆疊中的引數
正如前面提到的,SAVE_ALL是系統呼叫引數的傳入過程,當執行完SAVE_ALL並且再由CALL指令呼叫其處
理函式時,堆疊的結構應該如上圖所示。這時的堆疊結構看起來和執行一個普通帶引數的函式呼叫是一樣的
,引數在堆疊中對應的順序是(arg1, ebx),(arg2, ecx),(arg3, edx). . . . . .,這正是
SAVE_ALL壓棧的反順序,這些引數正是使用者在使用系統呼叫時試圖傳送給核心的引數。下面是在核心的呼叫
處理函式中使用引數的兩種典型方法:
asmlinkage int sys_fork(struct pt_regs regs);
asmlinkage int sys_open(const char * filename, int flags, int mode);
在sys_fork中,把整個堆疊中的內容視為一個struct pt_regs型別的引數,該引數的結構和堆疊的結構
是一致的,所以可以使用堆疊中的全部資訊。而在sys_open中引數filename, flags, mode正好對應與堆疊
中的ebx, ecx, edx的位置,而這些暫存器正是使用者在透過C庫呼叫系統呼叫時給這些引數指定的暫存器。
__asm__ __volatile__(
"int x80 "
:"=a"(retval)
:"0"(__NR_open),
"b"(filename),
"c"(flags),
"d"(mode));
3.核心如何使用使用者空間的引數
在使用系統呼叫時,有些引數是指標,這些指標所指向的是使用者空間DS暫存器的段選擇子所描述段中的地址
,而在2.2之前的版本中,核心態的DS段暫存器的中的段選擇子和使用者態的段選擇子描述的段地址不同(前
者為0xC0000000, 後者為0x00000000),這樣在使用這些引數時就不能讀取到正確的位置。所以需要透過特
殊的核心函式(如:memcpy_fromfs, mencpy_tofs)來從使用者空間資料段讀取引數,在這些函式中,是使用
FS暫存器來作為讀取引數的段暫存器的,FS暫存器在系統呼叫進入核心態時被設成了USER_DS(DS被設成了
KERNEL_DS)。在2.2之後的版本使用者態和核心態使用的DS中段選擇子描述的段地址是一樣的(都是
0x00000000),所以不需要再經過上面那樣煩瑣的過程而直接使用引數了。
2.2及以後的版本linux/arch/i386/head.S
ENTRY(gdt_table)
.quad 0x0000000000000000/* NULL descriptor */
.quad 0x0000000000000000/* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
2.0 linux/arch/i386/head.S
ENTRY(gdt)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */
.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */
.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */
.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 *
在2.0版的核心中SAVE_ALL宏定義還有這樣幾條語句:
"movl $" STR(KERNEL_DS) ",%edx "
"mov %dx,%ds "
"mov %dx,%es "
"movl $" STR(USER_DS) ",%edx "
"mov %dx,%fs "
"movl ,%edx "
E.呼叫返回
呼叫返回的過程要做的工作比其響應過程要多一些,這些工作幾乎是每次從核心態返回使用者態都需要做的,
這裡將簡要的說明:
1.判斷有沒有軟中斷,如果有則跳轉到軟中斷處理;
2.判斷當前程式是否需要重新排程,如果需要則跳轉到排程處理;
3.如果當前程式有掛起的訊號還沒有處理,則跳轉到訊號處理;
4.使用用RESTORE_ALL來彈出所有被SAVE_ALL壓入核心棧的內容並且使用iret返回使用者態。
F.例項介紹
前面介紹了系統呼叫相關的資料結構以及在Linux中使用一個系統呼叫的過程中每一步是怎樣處理的,
下面將把前面的所有概念串起來,說明怎樣在Linux中增加一個系統呼叫。
這裡實現的系統呼叫hello僅僅是在控制檯上列印一條語句,沒有任何功能。
1.修改linux/include/i386/unistd.h,在裡面增加一條語句:
#define __NR_hello ???(這個數字可能因為核心版本不同而不同)
2.在某個合適的目錄中(如:linux/kernel)增加一個hello.c,修改該目錄下的Makefile(把相映的.o文
件列入Makefile中就可以了)。
3.編寫hello.c
. . . . . .
asmlinkage int sys_hello(char * str)
{
printk(“My syscall: hello, I know what you say to me: %s ! ”, str);
return 0;
}
4.修改linux/arch/i386/kernel/entry.S,在裡面增加一條語句:
ENTRY(sys_call_table)
. . . . . .
.long SYMBOL_NAME(sys_hello)
並且修改:
.rept NR_syscalls-??? /* ??? = ??? +1 */
.long SYMBOL_NAME(sys_ni_syscall)
5.在linux/include/i386/中增加hello.h,裡面至少應包括這樣幾條語句:
#include
#ifdef __KERNEL
#else
inline _syscall1(int, hello, char *, str);
#endif
這樣就可以使用系統呼叫hello了
Linux中的系統呼叫
1. 程式相關的系統呼叫
Fork & vfork & clone
程式是一個指令執行流及其執行環境,其執行環境是一個系統資源的集合,這些資源在Linux中被抽象
成各種資料物件:程式控制塊、虛存空間、檔案系統,檔案I/O、訊號處理函式。所以建立一個程式的過程
就是這些資料物件的建立過程。
在呼叫系統呼叫fork建立一個程式時,子程式只是完全複製父程式的資源,這樣得到的子程式獨立於父
程式,具有良好的併發性,但是二者之間的通訊需要透過專門的通訊機制,如:pipe,fifo,System V IPC
機制等,另外透過fork建立子程式系統開銷很大,需要將上面描述的每種資源都複製一個副本。這樣看來,
fork是一個開銷十分大的系統呼叫,這些開銷並不是所有的情況下都是必須的,比如某程式fork出一個子進
程後,其子程式僅僅是為了呼叫exec執行另一個執行檔案,那麼在fork過程中對於虛存空間的複製將是一個
多餘的過程(由於Linux中是採取了copy-on-write技術,所以這一步驟的所做的工作只是虛存管理部分的復
制以及頁表的建立,而並沒有包括物理也面的複製);另外,有時一個程式中具有幾個獨立的計算單元,可
以在相同的地址空間上基本無衝突進行運算,但是為了把這些計算單元分配到不同的處理器上,需要建立幾
個子程式,然後各個子程式分別計算最後透過一定的程式間通訊和同步機制把計算結果彙總,這樣做往往有
許多格外的開銷,而且這種開銷有時足以抵消平行計算帶來的好處。
這說明了把計算單元抽象到程式上是不充分的,這也就是許多系統中都引入了執行緒的概念的原因。在講
述執行緒前首先介紹以下vfork系統呼叫,vfork系統呼叫不同於fork,用vfork建立的子程式共享地址空間,
也就是說子程式完全執行在父程式的地址空間上,子程式對虛擬地址空間任何資料的修改同樣為父程式所見
。但是用vfork建立子程式後,父程式會被阻塞直到子程式呼叫exec或exit。這樣的好處是在子程式被建立
後僅僅是為了呼叫exec執行另一個程式時,因為它就不會對父程式的地址空間有任何引用,所以對地址空間
的複製是多餘的,透過vfork可以減少不必要的開銷。
在Linux中, fork和vfork都是呼叫同一個核心函式
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)
其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PID,CLONE_VFORK等
等標誌位,任何一位被置1了則表明建立的子程式和父程式共享該位對應的資源。所以在vfork的實現中,
cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,這表示子程式和父程式共享地址空間,同時do_fork會
檢查CLONE_VFORK,如果該位被置1了,子程式會把父程式的地址空間鎖住,直到子程式退出或執行exec時才
釋放該鎖。
在講述clone系統呼叫前先簡單介紹執行緒的一些概念。
執行緒是在程式的基礎上進一步的抽象,也就是說一個程式分為兩個部分:執行緒集合和資源集合。執行緒是
程式中的一個動態物件,它應該是一組獨立的指令流,程式中的所有執行緒將共享程式裡的資源。但是執行緒應
該有自己的私有物件:比如程式計數器、堆疊和暫存器上下文。
執行緒分為三種型別:
核心執行緒、輕量級程式和使用者執行緒。
核心執行緒:
它的建立和撤消是由核心的內部需求來決定的,用來負責執行一個指定的函式,一個核心執行緒不需要和
一個使用者程式聯絡起來。它共享核心的正文段核全域性資料,具有自己的核心堆疊。它能夠單獨的被排程並且
使用標準的核心同步機制,可以被單獨的分配到一個處理器上執行。核心執行緒的排程由於不需要經過態的轉
換並進行地址空間的重新對映,因此在核心執行緒間做上下文切換比在程式間做上下文切換快得多。
輕量級程式:
輕量級程式是核心支援的使用者執行緒,它在一個單獨的程式中提供多執行緒控制。這些輕量級程式被單獨的
排程,可以在多個處理器上執行,每一個輕量級程式都被繫結在一個核心執行緒上,而且在它的生命週期這種
繫結都是有效的。輕量級程式被獨立排程並且共享地址空間和程式中的其它資源,但是每個LWP都應該有自
己的程式計數器、暫存器集合、核心棧和使用者棧。
使用者執行緒:
使用者執行緒是透過執行緒庫實現的。它們可以在沒有核心參與下建立、釋放和管理。執行緒庫提供了同步和調
度的方法。這樣程式可以使用大量的執行緒而不消耗核心資源,而且省去大量的系統開銷。使用者執行緒的實現是
可能的,因為使用者執行緒的上下文可以在沒有核心干預的情況下儲存和恢復。每個使用者執行緒都可以有自己的用
戶堆疊,一塊用來儲存使用者級暫存器上下文以及如訊號遮蔽等狀態資訊的記憶體區。庫透過儲存當前執行緒的堆
棧和暫存器內容載入新排程執行緒的那些內容來實現使用者執行緒之間的排程和上下文切換。
核心仍然負責程式的切換,因為只有核心具有修改記憶體管理暫存器的權力。使用者執行緒不是真正的排程實
體,核心對它們一無所知,而只是排程使用者執行緒下的程式或者輕量級程式,這些程式再透過執行緒庫函式來調
度它們的執行緒。當一個程式被搶佔時,它的所有使用者執行緒都被搶佔,當一個使用者執行緒被阻塞時,它會阻塞下
面的輕量級程式,如果程式只有一個輕量級程式,則它的所有使用者執行緒都會被阻塞。
明確了這些概念後,來講述Linux的執行緒和clone系統呼叫。
在許多實現了MT的作業系統中(如:Solaris,Digital Unix等), 執行緒和程式透過兩種資料結構來抽
象表示: 程式表項和執行緒表項,一個程式表項可以指向若干個執行緒表項, 排程器在程式的時間片內再排程
執行緒。 但是在Linux中沒有做這種區分, 而是統一使用task_struct來管理所有程式/執行緒,只是執行緒與
執行緒之間的資源是共享的,這些資源可是是前面提到過的:虛存、檔案系統、檔案I/O以及訊號處理函式甚
至PID中的幾種。
也就是說Linux中,每個執行緒都有一個task_struct,所以執行緒和程式可以使用同一排程器排程。其實
Linux核心中,輕量級程式和程式沒有質上的差別,因為Linux中程式的概念已經被抽象成了計算狀態加資源
的集合,這些資源在程式間可以共享。如果一個task獨佔所有的資源,則是一個HWP,如果一個task和其它
task共享部分資源,則是LWP。
clone系統呼叫就是一個建立輕量級程式的系統呼叫:
int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);
其中fn是輕量級程式所執行的過程,stack是輕量級程式所使用的堆疊,flags可以是前面提到的
CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的組合。Clone 和fork,vfork在實現時都
是呼叫核心函式do_fork。
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);
和fork、vfork不同的是,fork時clone_flag = SIGCHLD;
vfork時clone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD;
而在clone中,clone_flag由使用者給出。
下面給出一個使用clone的例子。
Void * func(int arg)
{
. . . . . .
}
int main()
{
int clone_flag, arg;
. . . . . .
clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |
CLONE_FILES;
stack = (char *)malloc(STACK_FRAME);
stack += STACK_FRAME;
retval = clone((void *)func, stack, clone_flag, arg);
. . . . . .
}
看起來clone的用法和pthread_create有些相似,兩者的最根本的差別在於clone是建立一個LWP,對核
心是可見的,由核心排程,而pthread_create通常只是建立一個使用者執行緒,對核心是不可見的,由執行緒庫調
度。
Nanosleep & sleep
sleep和nanosleep都是使程式睡眠一段時間後被喚醒,但是二者的實現完全不同。
Linux中並沒有提供系統呼叫sleep,sleep是在庫函式中實現的,它是透過呼叫alarm來設定報警時間,
呼叫sigsuspend將程式掛起在訊號SIGALARM上,sleep只能精確到秒級上。
nanosleep則是Linux中的系統呼叫,它是使用定時器來實現的,該呼叫使呼叫程式睡眠,並往定時器隊
列上加入一個time_list型定時器,time_list結構裡包括喚醒時間以及喚醒後執行的函式,透過nanosleep
加入的定時器的執行函式僅僅完成喚醒當前程式的功能。系統透過一定的機制定時檢查這些佇列(比如透過
系統呼叫陷入核心後,從核心返回使用者態前,要檢查當前程式的時間片是否已經耗盡,如果是則呼叫
schedule()函式重新排程,該函式中就會檢查定時器佇列,另外慢中斷返回前也會做此檢查),如果定時時
間已超過,則執行定時器指定的函式喚醒呼叫程式。當然,由於系統時間片可能丟失,所以nanosleep精度
也不是很高。
alarm也是透過定時器實現的,但是其精度只精確到秒級,另外,它設定的定時器執行函式是在指定時
間向當前程式傳送SIGALRM訊號。
2.儲存相關的系統呼叫
mmap:檔案對映
在講述檔案對映的概念時,不可避免的要牽涉到虛存(SVR 4的VM)。實際上,檔案對映是虛存的中心
概念,檔案對映一方面給使用者提供了一組措施,似的使用者將檔案對映到自己地址空間的某個部分,使用簡單
的記憶體訪問指令讀寫檔案;另一方面,它也可以用於核心的基本組織模式,在這種模式種,核心將整個地址
空間視為諸如檔案之類的一組不同物件的對映。
Unix中的傳統檔案訪問方式是,首先用open系統呼叫開啟檔案,然後使用read,write以及lseek等呼叫
進行順序或者隨即的I/O。這種方式是非常低效的,每一次I/O操作都需要一次系統呼叫。另外,如果若干個
程式訪問同一個檔案,每個程式都要在自己的地址空間維護一個副本,浪費了記憶體空間。而如果能夠透過一
定的機制將頁面對映到程式的地址空間中,也就是說首先透過簡單的產生某些記憶體管理資料結構完成對映的
建立。當程式訪問頁面時產生一個缺頁中斷,核心將頁面讀入記憶體並且更新頁表指向該頁面。而且這種方式
非常方便於同一副本的共享。
下面給出以上兩種方式的對比圖:
VM是物件導向的方法設計的,這裡的物件是指記憶體物件:記憶體物件是一個軟體抽象的概念,它描述記憶體
區與後備儲存之間的對映。系統可以使用多種型別的後備儲存,比如交換空間,本地或者遠端檔案以及幀緩
存等等。VM系統對它們統一處理,採用同一操作集操作,比如讀取頁面或者回寫頁面等。每種不同的後備存
儲都可以用不同的方法實現這些操作。這樣,系統定義了一套統一的介面,每種後備儲存給出自己的實現方
法。
這樣,程式的地址空間就被視為一組對映到不同資料物件上的的對映組成。所有的有效地址就是那些映
射到資料物件上的地址。這些物件為對映它的頁面提供了永續性的後備儲存。對映使得使用者可以直接定址這
些物件。
值得提出的是,VM體系結構獨立於Unix系統,所有的Unix系統語義,如正文,資料及堆疊區都可以建構
在基本VM系統之上。同時,VM體系結構也是獨立於儲存管理的,儲存管理是由作業系統實施的,如:究竟採
取什麼樣的對換和請求調頁演算法,究竟是採取分段還是分頁機制進行儲存管理,究竟是如何將虛擬地址轉換
成為實體地址等等(Linux中是一種叫Three Level Page Table的機制),這些都與記憶體物件的概念無關。
下面介紹Linux中VM的實現。
如下圖所示,一個程式應該包括一個mm_struct(memory manage struct),該結構是程式虛擬地址空
間的抽象描述,裡面包括了程式虛擬空間的一些管理資訊:start_code, end_code, start_data,
end_data, start_brk, end_brk等等資訊。另外,也有一個指向程式虛存區表(vm_area_struct :virtual
memory area)的指標,該鏈是按照虛擬地址的增長順序排列的。
在Linux程式的地址空間被分作許多區(vma),每個區(vma)都對應虛擬地址空間上一段連續的區域
,vma是可以被共享和保護的獨立實體,這裡的vma就是前面提到的記憶體物件。這裡給出vm_area_struct的結
構,其中,前半部分是公共的,與型別無關的一些資料成員,如:指向mm_struct的指標,地址範圍等等,
後半部分則是與型別相關的成員,其中最重要的是一個指向vm_operation_struct向量表的指標vm_ops,
vm_pos向量表是一組虛擬函式,定義了與vma型別無關的介面。每一個特定的子類,即每種vma型別都必須在向
量表中實現這些操作。這裡包括了:open, close, unmap, protect, sync, nopage, wppage, swapout這些
操作。
struct vm_area_struct {
/*公共的,與vma型別無關的 */
struct mm_struct * vm_mm;
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
/* 與型別相關的 */
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff;
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data;
};
vm_ops: open, close, no_page, swapin, swapout . . . . . .
介紹完VM的基本概念後,我們可以講述mmap, munmap系統呼叫了。mmap呼叫實際上就是一個記憶體物件
vma的建立過程,mmap的呼叫格式是:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
其中start是對映地址,length是對映長度,如果flags的MAP_FIXED不被置位,則該引數通常被忽略,而查
找程式地址空間中第一個長度符合的空閒區域;Fd是對映檔案的檔案控制程式碼,offset是對映檔案中的偏移地址
;prot是對映保護許可權,可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE,flags則是指對映型別,
可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED,該引數必須被指定為MAP_PRIVATE和MAP_SHARED其中之一,
MAP_PRIVATE是建立一個寫時複製對映(copy-on-write),也就是說如果有多個程式同時對映到一個檔案上,
對映建立時只是共享同樣的儲存頁面,但是某程式企圖修改頁面內容,則複製一個副本給該程式私用,它的
任何修改對其它程式都不可見。而MAP_SHARED則無論修改與否都使用同一副本,任何程式對頁面的修改對其
它程式都是可見的。
Mmap系統呼叫的實現過程是:
1.先透過檔案系統定位要對映的檔案;
2.許可權檢查,對映的許可權不會超過檔案開啟的方式,也就是說如果檔案是以只讀方式開啟,那麼則不允
許建立一個可寫對映;
3.建立一個vma物件,並對之進行初始化;
4.呼叫對映檔案的mmap函式,其主要工作是給vm_ops向量表賦值;
5.把該vma鏈入該程式的vma連結串列中,如果可以和前後的vma合併則合併;
6.如果是要求VM_LOCKED(對映區不被換出)方式對映,則發出缺頁請求,把對映頁面讀入記憶體中;
munmap(void * start, size_t length):
該呼叫可以看作是mmap的一個逆過程。它將程式中從start開始length長度的一段區域的對映關閉,如
果該區域不是恰好對應一個vma,則有可能會分割幾個或幾個vma。
Msync(void * start, size_t length, int flags) :
把對映區域的修改回寫到後備儲存中。因為munmap時並不保證頁面回寫,如果不呼叫msync,那麼有可
能在munmap後丟失對對映區的修改。其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE,MS_SYNC要求回
寫完成後才返回,MS_ASYNC發出回寫請求後立即返回,MS_INVALIDATE使用回寫的內容更新該檔案的其它映
射。
該系統呼叫是透過呼叫對映檔案的sync函式來完成工作的。
brk(void * end_data_segement):
將程式的資料段擴充套件到end_data_segement指定的地址,該系統呼叫和mmap的實現方式十分相似,同樣
是產生一個vma,然後指定其屬性。不過在此之前需要做一些合法性檢查,比如該地址是否大於mm-
>end_code,end_data_segement和mm->brk之間是否還存在其它vma等等。透過brk產生的vma對映的檔案為空
,這和匿名對映產生的vma相似,關於匿名對映不做進一步介紹。我們使用的庫函式malloc就是透過brk實現
的,透過下面這個例子很容易證實這點:
main()
{
char * m, * n;
int size;
m = (char *)sbrk(0);
printf("sbrk addr = %08lx ", m);
do {
n = malloc(1024);
printf("malloc addr = %08lx ", n);
}w hile(n < m);
m = (char *)sbrk(0);
printf("new sbrk addr = %08lx ", m);
}
sbrk addr = 0804a000
malloc addr = 080497d8
malloc addr = 08049be0
malloc addr = 08049fe8
malloc addr = 0804a3f0
new sbrk addr = 0804b000
3.程式間通訊(IPC)
程式間通訊可以透過很多種機制,包括signal, pipe, fifo, System V IPC, 以及socket等等,前幾種
概念都比較好理解,這裡著重介紹關於System V IPC。
System V IPC包括三種機制:message(允許程式傳送格式化的資料流到任意的程式)、shared memory
(允許程式間共享它們虛擬地址空間的部分割槽域)和semaphore(允許程式間同步的執行)。
作業系統核心中為它們分別維護著一個表,這三個表是系統中所有這三種IPC物件的集合,表的索引是
一個數值ID,程式透過這個ID可以查詢到需要使用的IPC資源。程式每建立一個IPC物件,系統中都會在相應
的表中增加一項。之後其它程式(具有許可權的程式)只要透過該IPC物件的ID則可以引用它。
IPC物件必須使用IPC_RMID命令來顯示的釋放,否則這個物件就處於活動狀態,甚至所有的使用它的進
程都已經終止。這種機制某些時候十分有用,但是也正因為這種特徵,使得作業系統核心無法判斷IPC物件
是被使用者故意遺留下來供將來其它程式使用還是被無意拋棄的。
Linux中只提供了一個系統呼叫介面ipc()來完成所有System V IPC操作,我們常使用的是建立在該呼叫
之上的庫函式介面。對於這三種IPC,都有很相似的三種呼叫:xxxget, (msgsnd, msgrcv)|semopt |
(shmat, shmdt), xxxctl
Xxxget:獲取呼叫,在系統中申請或者查詢一個IPC資源,返回值是該IPC物件的ID,該呼叫類似於檔案
系統的open, create呼叫;
Xxxctl:控制呼叫,至少包括三種操作:XXX_RMID(釋放IPC物件), XXX_STAT(查詢狀態), XXX_SET
(設定狀態資訊);
(msgsnd, msgrcv) | Semopt | (shmat, shmdt)|:操作呼叫,這些呼叫的功能隨IPC物件的型別不同而
有較大差異。
4.檔案系統相關的呼叫
檔案是用來儲存資料的,而檔案系統則可以讓使用者組織,操縱以及存取不同的檔案。核心允許使用者透過
一個嚴格定義的過程性介面與檔案系統進行互動,這個介面對使用者遮蔽了檔案系統的細節,同時指定了所有
相關係統呼叫的行為和語義。Linux支援許多中檔案系統,如ext2,msdos, ntfs, proc, dev, ufs, nfs等
等,這些檔案系統都實現了相同的介面,因此給應用程式提供了一致性的檢視。但每種檔案系統在實現時可
能對某個方面加以了一定的限制。如:檔名的長度,是否支援所有的檔案系統介面呼叫。
為了支援多檔案系統,sun提出了一種vnode/vfs介面,SVR4中將之實現成了一種工業標準。而Linux作
為一種Unix的clone體,自然也實現了這種介面,只是它的介面定義和SVR4的稍有不同。Vnode/Vfs介面的設
計體現了物件導向的思想,Vfs(虛擬檔案系統)代表核心中的一個檔案系統,Vnode(虛擬節點)代表核心
中的一個檔案,它們都可以被視為抽象基類,並可以從中派生出不同的子類以實現不同的檔案系統。
由於篇幅原因,這裡只是大概的介紹一下怎樣透過Vnode/Vfs結構來實現檔案系統和訪問檔案。
在Linux中支援的每種檔案系統必須有一個file_system_type結構,此結構的核心是read_super函式,
該函式將讀取檔案系統的超級塊。Linux中支援的所有檔案系統都會被註冊在一條file_system_type結構鏈
中,註冊是在系統初始化時呼叫regsiter_filesystem()完成,如果檔案系統是以模組的方式實現,則是在
呼叫init_module時完成。
當mount某種塊裝置時,將呼叫系統呼叫mount,該呼叫中將會首先檢查該類檔案系統是否註冊在系統種
中,如果註冊了則先給該檔案系統分配一個super_block,並進行初始化,最後呼叫這種檔案系統的
read_super函式來完成super_block結構私有資料的賦值。其中最主要的工作是給super_block的s_ops賦值
,s_ops是一個函式向量表,由檔案系統各自實現了一組操作。
struct super_operations {
void (*read_inode) (struct inode *);
void (*write_inode) (struct inode *);
void (*put_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*statfs) (struct super_block *, struct statfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
};
由於這組操作中定義了檔案系統中對於inode的操作,所以是之後對於檔案系統中檔案所有操作的基礎
。
在給super_block的s_ops賦值後,再給該檔案系統分配一個vfsmount結構,將該結構註冊到系統維護的
另一條鏈vfsmntlist中,所有mount上的檔案系統都在該鏈中有一項。在umount時,則從鏈中刪除這一項並
且釋放超級塊。
對於一個已經mount的檔案系統中任何檔案的操作首先應該以產生一個inode例項,即根據檔案系統的類
型生成一個屬於該檔案系統的記憶體i節點。這首先呼叫檔案定位函式lookup_dentry查詢目錄快取看是否使用
過該檔案,如果還沒有則快取中找不到,於是需要的i接點則依次呼叫路徑上的所有目錄I接點的lookup函式
,在lookup函式中會呼叫iget函式,該函式中最終呼叫超級塊的s_ops->read_inode讀取目標檔案的磁碟I節
點(這一步再往下就是由裝置驅動完成了,透過呼叫驅動程式的read函式讀取磁碟I節點),read_inode函
數的主要功能是初始化inode的一些私有資料(比如資料儲存位置,檔案大小等等)以及給inode_operations
函式開關表賦值,最終該inode被繫結在一個目錄快取結構dentry中返回。
在獲得了檔案的inode之後,對於該檔案的其它一切操作都有了根基。因為可以從inode 獲得檔案操作
函式開關表file_operatoins,該開關表裡給出了標準的檔案I/O介面的實現,包括read, write, lseek,
mmap, ioctl等等。這些函式入口將是所有關於檔案的系統呼叫請求的最終處理入口,透過這些函式入口會
向儲存該檔案的硬裝置驅動發出請求並且由驅動程式返回資料。當然這中間還會牽涉到一些關於buffer的管
理問題,這裡就不贅述了。
透過講述這些,我們應該明白了為什麼可以使用統一的系統呼叫介面來訪問不同檔案系統型別的檔案了
:因為在檔案系統的實現一層,都把低層的差異遮蔽了,使用者可見的只是高層可見的一致的系統呼叫介面。
5.與module相關的系統呼叫
Linux中提供了一種動態載入或解除安裝核心元件的機制——模組。透過這種機制Linux使用者可以為自己可以
保持一個儘量小的核心映像檔案,另外,往核心中載入和解除安裝模組不需要重新編譯整個核心以及引導機器。
可以透過一定的命令或者呼叫在一個執行的系統中載入模組,在不需要時解除安裝模組。模組可以完成許多功能
,比如檔案系統、裝置驅動,系統支援的執行檔案格式,甚至系統呼叫和中斷處理都可以用模組來更新。
Linux中提供了往系統中新增和解除安裝模組的介面,create_module(),init_module (), delete_module
(),這些系統呼叫通常不是直接為程式設計師使用的,它們僅僅是為實現一些系統命令而提供的介面,如
insmod, rmmod,(在使用這些系統呼叫前必須先載入目標檔案到使用者程式的地址空間,這必須由目標檔案
格式所特定的庫函式(如:libobj.a中的一些函式)來完成)。
Linux的核心中維護了一個module_list列表,每個被載入到核心中的模組都在其中佔有一項,系統呼叫
create_module()就是在該列表裡註冊某個指定的模組,而init_module則是使用模組目標檔案內容的對映來
初始化核心中註冊的該模組,並且呼叫該模組的初始化函式,初始化函式通常完成一些特定的初始化操作,
比如檔案系統的初始化函式就是在作業系統中註冊該檔案系統。delete_module則是從系統中解除安裝一個模組
,其主要工作是從module_list中刪除該模組對應的module結構並且呼叫該模組的cleanup函式解除安裝其它私有
資訊。
Back
Linux中怎樣編譯和定製核心
1.編譯核心前注意的事項
檢查系統上其它資源是否符合新核心的要求。在linux/Document目錄下有一個叫Changes的檔案,裡面
列舉了當前核心版本所需要的其它軟體的版本號,
- Kernel modutils 2.1.121 ; insmod -V
- Gnu C 2.7.2.3 ; gcc --version
- Binutils 2.8.1.0.23 ; ld -v
- Linux libc5 C Library 5.4.46 ; ls -l /lib/libc*
- Linux libc6 C Library 2.0.7pre6 ; ls -l /lib/libc*
- Dynamic Linker (ld.so) 1.9.9 ; ldd --version or ldd -v
- Linux C++ Library 2.7.2.8 ; ls -l /usr/lib/libg++.so.*
. . . . . .
其中最後一項是列舉該軟體版本號的命令,如果不符合要求先給相應軟體升級,這一步通常可以忽略。
2.配置核心
使用make config或者make menuconfig, make xconfig配置新核心。其中包括選擇塊裝置驅動程式、網
絡選項、網路裝置支援、檔案系統等等,使用者可以根據自己的需求來進行功能配置。每個選項至少有“y”
和“n”兩種選擇,選擇“y”表示把相應的支援編譯進核心,選“n”表示不提供這種支援,還有的有第三
種選擇“m”,則表示把該支援編譯成可載入模組,即前面提到的module,怎樣編譯和安裝模組在後面會介
紹。
這裡,順便講述一下如何在核心中增加自己的功能支援。
假如我們現在需要在自己的核心中加入一個檔案系統tfile,在完成了檔案系統的程式碼後,在linux/fs
下建立一個tfile目錄,把原始檔複製到該目錄下,然後修改linux/fs下的Makefile,把對應該檔案系統的
目標檔案加入目標檔案列表中,最後修改linux/fs/Config.in檔案,加入
bool 'tfile fs support' CONFIG_TFILE_FS或者
tristate ‘tfile fs support' CONFIG_TFILE_FS
這樣在Make menuconfig時在filesystem選單下就可以看到
< > tfile fs support一項了
3.編譯核心
在配置好核心後就是編譯核心了,在編譯之前首先應該執行make dep命令建立好依賴關係,該命令將會
修改linux中每個子目錄下的.depend檔案,該檔案包含了該目錄下每個目標檔案所需要的標頭檔案(絕對路徑
的方式列舉)。
然後就是使用make bzImage命令來編譯核心了。該命令執行結束後將會在linux/arch/asm/boot/產生一
個名叫bzImage的映像檔案。
4.使用新核心引導
把前面編譯產生的映像檔案複製到/boot目錄下(也可以直接建立一個符號連線,這樣可以省去每次編
譯後的複製工作),這裡暫且命名為vmlinuz-new,那麼再修改/etc/lilo.conf,在其中增加這麼幾條:
image = /boot/vmlinuz-new
root = /dev/hda1
label = new
read-only
並且執行lilo命令,那麼系統在啟動時就可以選用新核心引導了。
5.編譯模組和使用模組
在linux/目錄下執行make modules編譯模組,然後使用命令make modules_install來安裝模組(所有的可加
載模組的目標檔案會被複製到/lib/modules/2.2.12/),這樣之後就可以透過執行insmod 〈模組名〉和
rmmod〈模組名〉命令來載入或解除安裝功能模組了。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-947291/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Linux核心模組程式設計--系統呼叫(轉)Linux程式設計
- 在Linux中新增新的系統呼叫(轉)Linux
- Linux系統呼叫原理Linux
- linux系統呼叫getoptLinux
- Linux系統呼叫列表Linux
- Linux核心分析--系統呼叫實現程式碼分析(轉)Linux
- 柳大的Linux講義·基礎篇(2)Linux檔案系統的inodeLinux
- Linux系統呼叫過程分析Linux
- MINIX系統呼叫EXIT分析 (轉)
- 呼叫系統螢幕保護 (轉)
- 3.系統呼叫跳轉流程
- 柳大的Linux講義·基礎篇(1)磁碟與檔案系統Linux
- Linux作業系統分析 | 深入理解系統呼叫Linux作業系統
- Linux系統呼叫機制淺析Linux
- iOS呼叫系統功能與跳轉到系統設定iOS
- 講敘Linux系統之Shell程式設計基礎知識(轉)Linux程式設計
- 線上環境 Linux 系統呼叫追蹤Linux
- Linux 下系統呼叫的三種方法Linux
- linux系統呼叫第一篇Linux
- 拯救Linux系統(轉)Linux
- 講一下Linux 或Unix下怎樣修改系統時間(轉)Linux
- 在 Linux 上用 strace 來理解系統呼叫Linux
- linux軟中段和系統呼叫深入研究Linux
- 為Linux-3.10.1核心新增系統呼叫Linux
- Linux系統程式設計(七)檔案許可權系統呼叫Linux程式設計
- linux系統日常管理複習題講解Linux
- dup()系統呼叫
- Windows 系統呼叫Windows
- Linux檔案系統 (轉)Linux
- Linux 系統管理(上)(轉)Linux
- Linux 系統管理(中)(轉)Linux
- Linux 系統管理(下)(轉)Linux
- linux系統防DDOS(轉)Linux
- DirectShow系列講座之一——DirectShow系統概述 (轉)
- C程式函式呼叫&系統呼叫C程式函式
- Linux系統呼叫詳解(實現機制分析)Linux
- Go runtime 排程器精講(九):系統呼叫引起的搶佔Go
- Linux檔案系統df、du、fsck命令講解Linux