Linux下系統(tǒng)調(diào)用的實現(xiàn)
Linux中的系統(tǒng)調(diào)用
Linux中怎樣編譯和定制內(nèi)核
操作系統(tǒng)是從硬件抽象出來的虛擬機(jī),在該虛擬機(jī)上用戶可以運行應(yīng)用程序。它負(fù)責(zé)直接與硬件交互,向用戶程序提供公共服務(wù),并使它們同硬件特性隔離。因為程序不應(yīng)該依賴于下層的硬件,只有這樣應(yīng)用程序才能很方便的在各種不同的Unix系統(tǒng)之間移動。系統(tǒng)調(diào)用是Unix/Linux操作系統(tǒng)向用戶程序提供支持的接口,通過這些接口應(yīng)用程序向操作系統(tǒng)請求服務(wù),控制轉(zhuǎn)向操作系統(tǒng),而操作系統(tǒng)在完成服務(wù)后,將控制和結(jié)果返回給用戶程序。
一個Unix/Linux系統(tǒng)分為三個層次:用戶、核心以及硬件。
系統(tǒng)調(diào)用接口看起來和C程序中的普通函數(shù)調(diào)用很相似,它們通常是通過庫把這些函數(shù)調(diào)用映射成進(jìn)入操作系統(tǒng)所需要的原語。
這些操作原語只是提供一個基本功能集,而通過庫對這些操作的引用和封裝,可以形成豐富而且強大的系統(tǒng)調(diào)用庫。這里體現(xiàn)了機(jī)制與策略相分離的編程思想——系統(tǒng)調(diào)用只是提供訪問核心的基本機(jī)制,而策略是通過系統(tǒng)調(diào)用庫來體現(xiàn)。
例:execv, execl, execlv, opendir , readdir...
運行模式(運行態(tài)):
一種計算機(jī)硬件要運行Unix/Linux系統(tǒng),至少需要提供兩種運行模式:高優(yōu)先級的核心模式和低優(yōu)先級的用戶模式。
實際上許多計算機(jī)都有兩種以上的執(zhí)行模式。如:intel 80x86體系結(jié)構(gòu)就有四層執(zhí)行特權(quán),內(nèi)層特權(quán)最高。Unix只需要兩層即可以了:核心運行在高優(yōu)先級,稱之為核心態(tài);其它外圍軟件包括shell,編輯程序,Xwindow等等都是在低優(yōu)先級運行,稱之為用戶態(tài)。之所以采取不同的執(zhí)行模式主要原因時為了保護(hù),由于用戶進(jìn)程在較低的特權(quán)級上運行,它們將不能意外或故意的破壞其它進(jìn)程或內(nèi)核。程序造成的破壞會被局部化而不影響系統(tǒng)中其它活動或者進(jìn)程。當(dāng)用戶進(jìn)程需要完成特權(quán)模式下才能完成的某些功能時,必須嚴(yán)格按照系統(tǒng)調(diào)用提供接口才能進(jìn)入特權(quán)模式,然后執(zhí)行調(diào)用所提供的有限功能。
每種運行態(tài)都應(yīng)該有自己的堆棧。在Linux中,分為用戶棧和核心棧。用戶棧包括在用戶態(tài)執(zhí)行時函數(shù)調(diào)用的參數(shù)、局部變量和其它數(shù)據(jù)結(jié)構(gòu)。有些系統(tǒng)中專門為全局中斷處理提供了中斷棧,但是x86中并沒有中斷棧,中斷在當(dāng)前進(jìn)程的核心棧中處理。
地址空間:
采用特權(quán)模式進(jìn)行保護(hù)的根本目的是對地址空間的保護(hù),用戶進(jìn)程不應(yīng)該能夠訪問所有的地址空間:只有通過系統(tǒng)調(diào)用這種受嚴(yán)格限制的接口,進(jìn)程才能進(jìn)入核心態(tài)并訪問到受保護(hù)的那一部分地址空間的數(shù)據(jù),這一部分通常是留給操作系統(tǒng)使用。另外,進(jìn)程與進(jìn)程之間的地址空間也不應(yīng)該隨便互訪。這樣,就需要提供一種機(jī)制來在一片物理內(nèi)存上實現(xiàn)同一進(jìn)程不同地址空間上的保護(hù),以及不同進(jìn)程之間地址空間的保護(hù)。
Unix/Linux中通過虛存管理機(jī)制很好的實現(xiàn)了這種保護(hù),在虛存系統(tǒng)中,進(jìn)程所使用的地址不直接對應(yīng)物理的存儲單元。每個進(jìn)程都有自己的虛存空間,每個進(jìn)程有自己的虛擬地址空間,對虛擬地址的引用通過地址轉(zhuǎn)換機(jī)制轉(zhuǎn)換成為物理地址的引用。正因為所有進(jìn)程共享物理內(nèi)存資源,所以必須通過一定的方法來保護(hù)這種共享資源,通過虛存系統(tǒng)很好的實現(xiàn)了這種保護(hù):每個進(jìn)程的地址空間通過地址轉(zhuǎn)換機(jī)制映射到不同的物理存儲頁面上,這樣就保證了進(jìn)程只能訪問自己的地址空間所對應(yīng)的頁面而不能訪問或修改其它進(jìn)程的地址空間對應(yīng)的頁面。
虛擬地址空間分為兩個部分:用戶空間和系統(tǒng)空間。在用戶模式下只能訪問用戶空間而在核心模式下可以訪問系統(tǒng)空間和用戶空間。系統(tǒng)空間在每個進(jìn)程的虛擬地址空間中都是固定的,而且由于系統(tǒng)中只有一個內(nèi)核實例在運行,因此所有進(jìn)程都映射到單一內(nèi)核地址空間。內(nèi)核中維護(hù)全局?jǐn)?shù)據(jù)結(jié)構(gòu)和每個進(jìn)程的一些對象信息,后者包括的信息使得內(nèi)核可以訪問任何進(jìn)程的地址空間。通過地址轉(zhuǎn)換機(jī)制進(jìn)程可以直接訪問當(dāng)前進(jìn)程的地址空間(通過MMU),而通過一些特殊的方法也可以訪問到其它進(jìn)程的地址空間。
盡管所有進(jìn)程都共享內(nèi)核,但是系統(tǒng)空間是受保護(hù)的,進(jìn)程在用戶態(tài)無法訪問。進(jìn)程如果需要訪問內(nèi)核,則必須通過系統(tǒng)調(diào)用接口。進(jìn)程調(diào)用一個系統(tǒng)調(diào)用時,通過執(zhí)行一組特殊的指令(這個指令是與平臺相關(guān)的,每種系統(tǒng)都提供了專門的trap命令,基于x86的Linux中是使用int 指令)使系統(tǒng)進(jìn)入內(nèi)核態(tài),并將控制權(quán)交給內(nèi)核,由內(nèi)核替代進(jìn)程完成操作。當(dāng)系統(tǒng)調(diào)用完成后,內(nèi)核執(zhí)行另一組特征指令將系統(tǒng)返回到用戶態(tài),控制權(quán)返回給進(jìn)程。
上下文:
一個進(jìn)程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統(tǒng)級上下文。
用戶級上下文:正文、數(shù)據(jù)、用戶棧以及共享存儲區(qū);
寄存器上下文:程序寄存器(IP),即CPU將執(zhí)行的下條指令地址,處理機(jī)狀態(tài)寄存器(EFLAGS),棧指針,通用寄存器;
系統(tǒng)級上下文:進(jìn)程表項(proc結(jié)構(gòu))和U區(qū),在Linux中這兩個部分被合成task_struct,區(qū)表及頁表(mm_struct , vm_area_struct, pgd, pmd, pte等),核心棧等。
全部的上下文信息組成了一個進(jìn)程的運行環(huán)境。當(dāng)發(fā)生進(jìn)程調(diào)度時,必須對全部上下文信息進(jìn)行切換,新調(diào)度的進(jìn)程才能運行。進(jìn)程就是上下文的集合的一個抽象概念。
系統(tǒng)調(diào)用可以看作是一個所有Unix/Linux進(jìn)程共享的子程序庫,但是它是在特權(quán)方式下運行,可以存取核心數(shù)據(jù)結(jié)構(gòu)和它所支持的用戶級數(shù)據(jù)。系統(tǒng)調(diào)用的主要功能是使用戶可以使用操作系統(tǒng)提供的有關(guān)設(shè)備管理、文件系統(tǒng)、進(jìn)程控制進(jìn)程通訊以及存儲管理方面的功能,而不必要了解操作系統(tǒng)的內(nèi)部結(jié)構(gòu)和有關(guān)硬件的細(xì)節(jié)問題,從而減輕用戶負(fù)擔(dān)和保護(hù)系統(tǒng)以及提高資源利用率。
系統(tǒng)調(diào)用分為兩個部分:與文件子系統(tǒng)交互的和進(jìn)程子系統(tǒng)交互的兩個部分。其中和文件子系統(tǒng)交互的部分進(jìn)一步由可以包括與設(shè)備文件的交互和與普通文件的交互的系統(tǒng)調(diào)用(open, close, ioctl, create, unlink, . . . );與進(jìn)程相關(guān)的系統(tǒng)調(diào)用又包括進(jìn)程控制系統(tǒng)調(diào)用(fork, exit, getpid, . . . ),進(jìn)程間通訊,存儲管理,進(jìn)程調(diào)度等方面的系統(tǒng)調(diào)用。
在每種平臺上,都有特定的指令可以使進(jìn)程的執(zhí)行由用戶態(tài)轉(zhuǎn)換為核心態(tài),這種指令稱作操作系統(tǒng)陷入(operating system trap)。進(jìn)程通過執(zhí)行陷入指令后,便可以在核心態(tài)運行系統(tǒng)調(diào)用代碼。
在Linux中是通過軟中斷來實現(xiàn)這種陷入的,在x86平臺上,這條指令是int 0x80。也就是說在Linux中,系統(tǒng)調(diào)用的接口是一個中斷處理函數(shù)的特例。具體怎樣通過中斷處理函數(shù)來實現(xiàn)系統(tǒng)調(diào)用的入口將在后面詳細(xì)介紹。
這樣,就需要在系統(tǒng)啟動時,對INT 0x80進(jìn)行一定的初始化,下面將描述其過程:
1.使用匯編子程序setup_idt(linux/arch/i386/kernel/head.S)初始化idt表(中斷描述符表),這時所有的入口函數(shù)偏移地址都被設(shè)為ignore_int
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea SYMBOL_NAME(idt_table),%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
ret
selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1);
2.Start_kernel()(linux/init/main.c)調(diào)用trap_init()(linux/arch/i386/kernel/trap.c)函數(shù)設(shè)置中斷描述符表。在該函數(shù)里,實際上是通過調(diào)用函數(shù)set_system_gate(SYSCALL_VECTOR,&system_call)來完成該項的設(shè)置的。其中的SYSCALL_VECTOR就是0x80,而system_call則是一個匯編子函數(shù),它即是中斷0x80的處理函數(shù),主要完成兩項工作:a. 寄存器上下文的保存;b. 跳轉(zhuǎn)到系統(tǒng)調(diào)用處理函數(shù)。在后面會詳細(xì)介紹這些內(nèi)容。
set_system_gate()是在linux/arch/i386/kernel/trap.S中定義的,在該文件中還定義了幾個類似的函數(shù)set_intr_gate(), set_trap_gate, set_call_gate()。這些函數(shù)都調(diào)用了同一個匯編子函數(shù)__set_gate(),該函數(shù)的作用是設(shè)置門描述符。IDT中的每一項都是一個門描述符。
#define _set_gate(gate_addr,type,dpl,addr)
set_gate(idt_table+n,15,3,addr);
門描述符的作用是用于控制轉(zhuǎn)移,其中會包括選擇子,這里總是為__KERNEL_CS(指向GDT中的一項段描述符)、入口函數(shù)偏移地址、門訪問特權(quán)級(DPL)以及類型標(biāo)識(TYPE)。Set_system_gate的DPL為3,表示從特權(quán)級3(最低特權(quán)級)也可以訪問該門,type為15,表示為386中斷門。)
1.系統(tǒng)調(diào)用處理函數(shù)的函數(shù)名的約定
函數(shù)名都以“sys_”開頭,后面跟該系統(tǒng)調(diào)用的名字。例如,系統(tǒng)調(diào)用fork()的處理函數(shù)名是sys_fork()。
asmlinkage int sys_fork(struct pt_regs regs);
(補充關(guān)于asmlinkage的說明)
2.系統(tǒng)調(diào)用號(System Call Number)
核心中為每個系統(tǒng)調(diào)用定義了一個唯一的編號,這個編號的定義在linux/include/asm/unistd.h中,編號的定義方式如下所示:
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
. . . . . .
用戶在調(diào)用一個系統(tǒng)調(diào)用時,系統(tǒng)調(diào)用號號作為參數(shù)傳遞給中斷0x80,而該標(biāo)號實際上是后面將要提到的系統(tǒng)調(diào)用表(sys_call_table)的下標(biāo),通過該值可以找到相映系統(tǒng)調(diào)用的處理函數(shù)地址。
3.系統(tǒng)調(diào)用表
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
. . . . . .
C.系統(tǒng)調(diào)用函數(shù)接口是如何轉(zhuǎn)化為陷入命令
我們還是以x86為例說明:
由于陷入指令是一條特殊指令,而且依賴與操作系統(tǒng)實現(xiàn)的平臺,如在x86中,這條指令是int 0x80,這顯然不是用戶在編程時應(yīng)該使用的語句,因為這將使得用戶程序難于移植。所以在操作系統(tǒng)的上層需要實現(xiàn)一個對應(yīng)的系統(tǒng)調(diào)用庫,每個系統(tǒng)調(diào)用都在該庫中包含了一個入口點(如我們看到的fork, open, close等等),這些函數(shù)對程序員是可見的,而這些庫函數(shù)的工作是以對應(yīng)系統(tǒng)調(diào)用號作為參數(shù),執(zhí)行陷入指令int 0x80,以陷入核心執(zhí)行真正的系統(tǒng)調(diào)用處理函數(shù)。當(dāng)一個進(jìn)程調(diào)用一個特定的系統(tǒng)調(diào)用庫的入口點,正如同它調(diào)用任何函數(shù)一樣,對于庫函數(shù)也要創(chuàng)建一個棧幀。而當(dāng)進(jìn)程執(zhí)行陷入指令時,它將處理機(jī)狀態(tài)轉(zhuǎn)換到核心態(tài),并且在核心棧執(zhí)行核心代碼。
這里給出一個示例(linux/include/asm/unistd.h):
#define _syscallN(type, name, type1, arg1, type2, arg2, . . . ) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
. . . . . .
__syscall_return(type,__res); \
}
在執(zhí)行一個系統(tǒng)調(diào)用庫中定義的系統(tǒng)調(diào)用入口函數(shù)時,實際執(zhí)行的是類似如上的一段代碼。這里牽涉到一些gcc的嵌入式匯編語言,不做詳細(xì)的介紹,只簡單說明其意義:
其中__NR_##name是系統(tǒng)調(diào)用號,如name == ioctl,則為__NR_ioctl,它將被放在寄存器eax中作為參數(shù)傳遞給中斷0x80的處理函數(shù)。而系統(tǒng)調(diào)用的其它參數(shù)arg1, arg2, …則依次被放入ebx, ecx, . . .等通用寄存器中,并作為系統(tǒng)調(diào)用處理函數(shù)的參數(shù),這些參數(shù)是怎樣傳入核心的將會在后面介紹。
下面將示例說明:
int func1()
{
int fd, retval;
fd = open(filename, ……);
……
ioctl(fd, cmd, arg);
. . .
}
func2()
{
int fd, retval;
fd = open(filename, ……);
……
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_ioctl),\
"b"(fd),\
"c"(cmd),\
"d"(arg));
}
這兩個函數(shù)在Linux/x86上運行的結(jié)果應(yīng)該是一樣的。
若干個庫函數(shù)可以映射到同一個系統(tǒng)調(diào)用入口點。系統(tǒng)調(diào)用入口點對每個系統(tǒng)調(diào)用定義其真正的語法和語義,但庫函數(shù)通常提供一個更方便的接口。如系統(tǒng)調(diào)用exec有集中不同的調(diào)用方式:execl, execle,等,它們實際上只是同一系統(tǒng)調(diào)用的不同接口而已。對于這些調(diào)用,它們的庫函數(shù)對它們各自的參數(shù)加以處理,來實現(xiàn)各自的特點,但是最終都被映射到同一個核心入口點。
D.系統(tǒng)調(diào)用陷入內(nèi)核后作何初始化處理
在這一部分,我們將介紹INT 0x80的處理函數(shù)system_call。
思考一下就會發(fā)現(xiàn),在調(diào)用前和調(diào)用后執(zhí)行態(tài)完全不相同:前者是在用戶棧上執(zhí)行用戶態(tài)程序,后者在核心棧上執(zhí)行核心態(tài)代碼。那么,為了保證在核心內(nèi)部執(zhí)行完系統(tǒng)調(diào)用后能夠返回調(diào)用點繼續(xù)執(zhí)行用戶代碼,必須在進(jìn)入核心態(tài)時保存時往核心中壓入一個上下文層;在從核心返回時會彈出一個上下文層,這樣用戶進(jìn)程就可以繼續(xù)運行。
那么,這些上下文信息是怎樣被保存的,被保存的又是那些上下文信息呢?這里仍以x86為例說明。
在執(zhí)行INT指令時,實際完成了以下幾條操作:
1.由于INT指令發(fā)生了不同優(yōu)先級之間的控制轉(zhuǎn)移,所以首先從TSS(任務(wù)狀態(tài)段)中獲取高優(yōu)先級的核心堆棧信息(SS和ESP);2.把低優(yōu)先級堆棧信息(SS和ESP)保留到高優(yōu)先級堆棧(即核心棧)中;
3.把EFLAGS,外層CS,EIP推入高優(yōu)先級堆棧(核心棧)中。
4.通過IDT加載CS,EIP(控制轉(zhuǎn)移至中斷處理函數(shù))
#define SAVE_ALL \
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;
ENTRY(system_call)
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
在這里所做的所有工作是:
1.保存EAX寄存器,因為在SAVE_ALL中保存的EAX寄存器會被調(diào)用的返回值所覆蓋;
2.調(diào)用SAVE_ALL保存寄存器上下文;
3.判斷當(dāng)前調(diào)用是否是合法系統(tǒng)調(diào)用(EAX是系統(tǒng)調(diào)用號,它應(yīng)該小于NR_syscalls);
4.如果設(shè)置了PF_TRACESYS標(biāo)志,則跳轉(zhuǎn)到syscall_trace,在那里將會把當(dāng)前進(jìn)程掛起并向其父進(jìn)程發(fā)送SIGTRAP,這主要是為了設(shè) 置調(diào)試斷點而設(shè)計的;
5.如果沒有設(shè)置PF_TRACESYS標(biāo)志,則跳轉(zhuǎn)到該系統(tǒng)調(diào)用的處理函數(shù)入口。這里是以EAX(即前面提到的系統(tǒng)調(diào)用號)作為偏移,在系 統(tǒng)調(diào)用表sys_call_table中查找處理函數(shù)入口地址,并跳轉(zhuǎn)到該入口地址。
movl %esp, reg; \
andl $-8192, reg;
其作用是取得當(dāng)前進(jìn)程的task_struct結(jié)構(gòu)的指針返回到reg中,因為在Linux中核心棧的位置是task_struct之后的兩個頁面處(8192bytes),所以此處把棧指針與-8192則得到的是task_struct結(jié)構(gòu)指針,而task_struct中偏移為4的位置是成員flags,在這里指令testb $0x20,flags(%ebx)檢測的就是task_struct->flags。
正如前面提到的,SAVE_ALL是系統(tǒng)調(diào)用參數(shù)的傳入過程,當(dāng)執(zhí)行完SAVE_ALL并且再由CALL指令調(diào)用其處理函數(shù)時,堆棧的結(jié)構(gòu)應(yīng)該如上圖所示。這時的堆棧結(jié)構(gòu)看起來和執(zhí)行一個普通帶參數(shù)的函數(shù)調(diào)用是一樣的,參數(shù)在堆棧中對應(yīng)的順序是(arg1, ebx),(arg2, ecx),(arg3, edx). . . . . .,這正是SAVE_ALL壓棧的反順序,這些參數(shù)正是用戶在使用系統(tǒng)調(diào)用時試圖傳送給核心的參數(shù)。下面是在核心的調(diào)用處理函數(shù)中使用參數(shù)的兩種典型方法:
asmlinkage int sys_fork(struct pt_regs regs);
asmlinkage int sys_open(const char * filename, int flags, int mode);
在sys_fork中,把整個堆棧中的內(nèi)容視為一個struct pt_regs類型的參數(shù),該參數(shù)的結(jié)構(gòu)和堆棧的結(jié)構(gòu)是一致的,所以可以使用堆棧中的全部信息。而在sys_open中參數(shù)filename, flags, mode正好對應(yīng)與堆棧中的ebx, ecx, edx的位置,而這些寄存器正是用戶在通過C庫調(diào)用系統(tǒng)調(diào)用時給這些參數(shù)指定的寄存器。
__asm__ __volatile__(\
"int $0x80\n\t"\
:"=a"(retval)\
:"0"(__NR_open),\
"b"(filename),\
"c"(flags),\
"d"(mode));
3.核心如何使用用戶空間的參數(shù)
ENTRY(gdt_table)
.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 */
.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版的內(nèi)核中SAVE_ALL宏定義還有這樣幾條語句:
"movl $" STR(KERNEL_DS) ",%edx\n\t" \
"mov %dx,%ds\n\t" \
"mov %dx,%es\n\t" \
"movl $" STR(USER_DS) ",%edx\n\t" \
"mov %dx,%fs\n\t" \
"movl $0,%edx\n\t" \
1.判斷有沒有軟中斷,如果有則跳轉(zhuǎn)到軟中斷處理;
2.判斷當(dāng)前進(jìn)程是否需要重新調(diào)度,如果需要則跳轉(zhuǎn)到調(diào)度處理;
3.如果當(dāng)前進(jìn)程有掛起的信號還沒有處理,則跳轉(zhuǎn)到信號處理;
4.使用用RESTORE_ALL來彈出所有被SAVE_ALL壓入核心棧的內(nèi)容并且使用iret返回用戶態(tài)。
F.實例介紹
這里實現(xiàn)的系統(tǒng)調(diào)用hello僅僅是在控制臺上打印一條語句,沒有任何功能。
1.修改linux/include/i386/unistd.h,在里面增加一條語句:
. . . . . .
asmlinkage int sys_hello(char * str)
{
printk(“My syscall: hello, I know what you say to me: %s ! \n”, 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,里面至少應(yīng)包括這樣幾條語句:
#ifdef __KERNEL
#else
inline _syscall1(int, hello, char *, str);
#endif
這樣就可以使用系統(tǒng)調(diào)用hello了
進(jìn)程是一個指令執(zhí)行流及其執(zhí)行環(huán)境,其執(zhí)行環(huán)境是一個系統(tǒng)資源的集合,這些資源在Linux中被抽象成各種數(shù)據(jù)對象:進(jìn)程控制塊、虛存空間、文件系統(tǒng),文件I/O、信號處理函數(shù)。所以創(chuàng)建一個進(jìn)程的過程就是這些數(shù)據(jù)對象的創(chuàng)建過程。
在調(diào)用系統(tǒng)調(diào)用fork創(chuàng)建一個進(jìn)程時,子進(jìn)程只是完全復(fù)制父進(jìn)程的資源,這樣得到的子進(jìn)程獨立于父進(jìn)程,具有良好的并發(fā)性,但是二者之間的通訊需要通過專門的通訊機(jī)制,如:pipe,fifo,System V IPC機(jī)制等,另外通過fork創(chuàng)建子進(jìn)程系統(tǒng)開銷很大,需要將上面描述的每種資源都復(fù)制一個副本。這樣看來,fork是一個開銷十分大的系統(tǒng)調(diào)用,這些開銷并不是所有的情況下都是必須的,比如某進(jìn)程fork出一個子進(jìn)程后,其子進(jìn)程僅僅是為了調(diào)用exec執(zhí)行另一個執(zhí)行文件,那么在fork過程中對于虛存空間的復(fù)制將是一個多余的過程(由于Linux中是采取了copy-on-write技術(shù),所以這一步驟的所做的工作只是虛存管理部分的復(fù)制以及頁表的創(chuàng)建,而并沒有包括物理也面的拷貝);另外,有時一個進(jìn)程中具有幾個獨立的計算單元,可以在相同的地址空間上基本無沖突進(jìn)行運算,但是為了把這些計算單元分配到不同的處理器上,需要創(chuàng)建幾個子進(jìn)程,然后各個子進(jìn)程分別計算最后通過一定的進(jìn)程間通訊和同步機(jī)制把計算結(jié)果匯總,這樣做往往有許多格外的開銷,而且這種開銷有時足以抵消并行計算帶來的好處。
這說明了把計算單元抽象到進(jìn)程上是不充分的,這也就是許多系統(tǒng)中都引入了線程的概念的原因。在講述線程前首先介紹以下vfork系統(tǒng)調(diào)用,vfork系統(tǒng)調(diào)用不同于fork,用vfork創(chuàng)建的子進(jìn)程共享地址空間,也就是說子進(jìn)程完全運行在父進(jìn)程的地址空間上,子進(jìn)程對虛擬地址空間任何數(shù)據(jù)的修改同樣為父進(jìn)程所見。但是用vfork創(chuàng)建子進(jìn)程后,父進(jìn)程會被阻塞直到子進(jìn)程調(diào)用exec或exit。這樣的好處是在子進(jìn)程被創(chuàng)建后僅僅是為了調(diào)用exec執(zhí)行另一個程序時,因為它就不會對父進(jìn)程的地址空間有任何引用,所以對地址空間的復(fù)制是多余的,通過vfork可以減少不必要的開銷。
在Linux中, fork和vfork都是調(diào)用同一個核心函數(shù)
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等等標(biāo)志位,任何一位被置1了則表明創(chuàng)建的子進(jìn)程和父進(jìn)程共享該位對應(yīng)的資源。所以在vfork的實現(xiàn)中,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,這表示子進(jìn)程和父進(jìn)程共享地址空間,同時do_fork會檢查CLONE_VFORK,如果該位被置1了,子進(jìn)程會把父進(jìn)程的地址空間鎖住,直到子進(jìn)程退出或執(zhí)行exec時才釋放該鎖。
在講述clone系統(tǒng)調(diào)用前先簡單介紹線程的一些概念。
線程是在進(jìn)程的基礎(chǔ)上進(jìn)一步的抽象,也就是說一個進(jìn)程分為兩個部分:線程集合和資源集合。線程是進(jìn)程中的一個動態(tài)對象,它應(yīng)該是一組獨立的指令流,進(jìn)程中的所有線程將共享進(jìn)程里的資源。但是線程應(yīng)該有自己的私有對象:比如程序計數(shù)器、堆棧和寄存器上下文。
線程分為三種類型:
內(nèi)核線程、輕量級進(jìn)程和用戶線程。
內(nèi)核線程:
它的創(chuàng)建和撤消是由內(nèi)核的內(nèi)部需求來決定的,用來負(fù)責(zé)執(zhí)行一個指定的函數(shù),一個內(nèi)核線程不需要和一個用戶進(jìn)程聯(lián)系起來。它共享內(nèi)核的正文段核全局?jǐn)?shù)據(jù),具有自己的內(nèi)核堆棧。它能夠單獨的被調(diào)度并且使用標(biāo)準(zhǔn)的內(nèi)核同步機(jī)制,可以被單獨的分配到一個處理器上運行。內(nèi)核線程的調(diào)度由于不需要經(jīng)過態(tài)的轉(zhuǎn)換并進(jìn)行地址空間的重新映射,因此在內(nèi)核線程間做上下文切換比在進(jìn)程間做上下文切換快得多。
輕量級進(jìn)程:
輕量級進(jìn)程是核心支持的用戶線程,它在一個單獨的進(jìn)程中提供多線程控制。這些輕量級進(jìn)程被單獨的調(diào)度,可以在多個處理器上運行,每一個輕量級進(jìn)程都被綁定在一個內(nèi)核線程上,而且在它的生命周期這種綁定都是有效的。輕量級進(jìn)程被獨立調(diào)度并且共享地址空間和進(jìn)程中的其它資源,但是每個LWP都應(yīng)該有自己的程序計數(shù)器、寄存器集合、核心棧和用戶棧。
用戶線程:
用戶線程是通過線程庫實現(xiàn)的。它們可以在沒有內(nèi)核參與下創(chuàng)建、釋放和管理。線程庫提供了同步和調(diào)度的方法。這樣進(jìn)程可以使用大量的線程而不消耗內(nèi)核資源,而且省去大量的系統(tǒng)開銷。用戶線程的實現(xiàn)是可能的,因為用戶線程的上下文可以在沒有內(nèi)核干預(yù)的情況下保存和恢復(fù)。每個用戶線程都可以有自己的用戶堆棧,一塊用來保存用戶級寄存器上下文以及如信號屏蔽等狀態(tài)信息的內(nèi)存區(qū)。庫通過保存當(dāng)前線程的堆棧和寄存器內(nèi)容載入新調(diào)度線程的那些內(nèi)容來實現(xiàn)用戶線程之間的調(diào)度和上下文切換。
內(nèi)核仍然負(fù)責(zé)進(jìn)程的切換,因為只有內(nèi)核具有修改內(nèi)存管理寄存器的權(quán)力。用戶線程不是真正的調(diào)度實體,內(nèi)核對它們一無所知,而只是調(diào)度用戶線程下的進(jìn)程或者輕量級進(jìn)程,這些進(jìn)程再通過線程庫函數(shù)來調(diào)度它們的線程。當(dāng)一個進(jìn)程被搶占時,它的所有用戶線程都被搶占,當(dāng)一個用戶線程被阻塞時,它會阻塞下面的輕量級進(jìn)程,如果進(jìn)程只有一個輕量級進(jìn)程,則它的所有用戶線程都會被阻塞。
明確了這些概念后,來講述Linux的線程和clone系統(tǒng)調(diào)用。
在許多實現(xiàn)了MT的操作系統(tǒng)中(如:Solaris,Digital Unix等), 線程和進(jìn)程通過兩種數(shù)據(jù)結(jié)構(gòu)來抽象表示: 進(jìn)程表項和線程表項,一個進(jìn)程表項可以指向若干個線程表項, 調(diào)度器在進(jìn)程的時間片內(nèi)再調(diào)度線程。 但是在Linux中沒有做這種區(qū)分, 而是統(tǒng)一使用task_struct來管理所有進(jìn)程/線程,只是線程與線程之間的資源是共享的,這些資源可是是前面提到過的:虛存、文件系統(tǒng)、文件I/O以及信號處理函數(shù)甚至PID中的幾種。
clone系統(tǒng)調(diào)用就是一個創(chuàng)建輕量級進(jìn)程的系統(tǒng)調(diào)用:
int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);
其中fn是輕量級進(jìn)程所執(zhí)行的過程,stack是輕量級進(jìn)程所使用的堆棧,flags可以是前面提到的CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的組合。Clone 和fork,vfork在實現(xiàn)時都是調(diào)用核心函數(shù)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是創(chuàng)建一個LWP,對核心是可見的,由核心調(diào)度,而pthread_create通常只是創(chuàng)建一個用戶線程,對核心是不可見的,由線程庫調(diào)度。
Nanosleep & sleep
sleep和nanosleep都是使進(jìn)程睡眠一段時間后被喚醒,但是二者的實現(xiàn)完全不同。
Linux中并沒有提供系統(tǒng)調(diào)用sleep,sleep是在庫函數(shù)中實現(xiàn)的,它是通過調(diào)用alarm來設(shè)定報警時間,調(diào)用sigsuspend將進(jìn)程掛起在信號SIGALARM上,sleep只能精確到秒級上。
nanosleep則是Linux中的系統(tǒng)調(diào)用,它是使用定時器來實現(xiàn)的,該調(diào)用使調(diào)用進(jìn)程睡眠,并往定時器隊列上加入一個time_list型定時器,time_list結(jié)構(gòu)里包括喚醒時間以及喚醒后執(zhí)行的函數(shù),通過nanosleep加入的定時器的執(zhí)行函數(shù)僅僅完成喚醒當(dāng)前進(jìn)程的功能。系統(tǒng)通過一定的機(jī)制定時檢查這些隊列(比如通過系統(tǒng)調(diào)用陷入核心后,從核心返回用戶態(tài)前,要檢查當(dāng)前進(jìn)程的時間片是否已經(jīng)耗盡,如果是則調(diào)用schedule()函數(shù)重新調(diào)度,該函數(shù)中就會檢查定時器隊列,另外慢中斷返回前也會做此檢查),如果定時時間已超過,則執(zhí)行定時器指定的函數(shù)喚醒調(diào)用進(jìn)程。當(dāng)然,由于系統(tǒng)時間片可能丟失,所以nanosleep精度也不是很高。
alarm也是通過定時器實現(xiàn)的,但是其精度只精確到秒級,另外,它設(shè)置的定時器執(zhí)行函數(shù)是在指定時間向當(dāng)前進(jìn)程發(fā)送SIGALRM信號。
2.存儲相關(guān)的系統(tǒng)調(diào)用
在講述文件映射的概念時,不可避免的要牽涉到虛存(SVR 4的VM)。實際上,文件映射是虛存的中心概念,文件映射一方面給用戶提供了一組措施,似的用戶將文件映射到自己地址空間的某個部分,使用簡單的內(nèi)存訪問指令讀寫文件;另一方面,它也可以用于內(nèi)核的基本組織模式,在這種模式種,內(nèi)核將整個地址空間視為諸如文件之類的一組不同對象的映射。
Unix中的傳統(tǒng)文件訪問方式是,首先用open系統(tǒng)調(diào)用打開文件,然后使用read,write以及lseek等調(diào)用進(jìn)行順序或者隨即的I/O。這種方式是非常低效的,每一次I/O操作都需要一次系統(tǒng)調(diào)用。另外,如果若干個進(jìn)程訪問同一個文件,每個進(jìn)程都要在自己的地址空間維護(hù)一個副本,浪費了內(nèi)存空間。而如果能夠通過一定的機(jī)制將頁面映射到進(jìn)程的地址空間中,也就是說首先通過簡單的產(chǎn)生某些內(nèi)存管理數(shù)據(jù)結(jié)構(gòu)完成映射的創(chuàng)建。當(dāng)進(jìn)程訪問頁面時產(chǎn)生一個缺頁中斷,內(nèi)核將頁面讀入內(nèi)存并且更新頁表指向該頁面。而且這種方式非常方便于同一副本的共享。
下面給出以上兩種方式的對比圖:
VM是面向?qū)ο蟮姆椒ㄔO(shè)計的,這里的對象是指內(nèi)存對象:內(nèi)存對象是一個軟件抽象的概念,它描述內(nèi)存區(qū)與后備存儲之間的映射。系統(tǒng)可以使用多種類型的后備存儲,比如交換空間,本地或者遠(yuǎn)程文件以及幀緩存等等。VM系統(tǒng)對它們統(tǒng)一處理,采用同一操作集操作,比如讀取頁面或者回寫頁面等。每種不同的后備存儲都可以用不同的方法實現(xiàn)這些操作。這樣,系統(tǒng)定義了一套統(tǒng)一的接口,每種后備存儲給出自己的實現(xiàn)方法。
這樣,進(jìn)程的地址空間就被視為一組映射到不同數(shù)據(jù)對象上的的映射組成。所有的有效地址就是那些映射到數(shù)據(jù)對象上的地址。這些對象為映射它的頁面提供了持久性的后備存儲。映射使得用戶可以直接尋址這些對象。
值得提出的是,VM體系結(jié)構(gòu)獨立于Unix系統(tǒng),所有的Unix系統(tǒng)語義,如正文,數(shù)據(jù)及堆棧區(qū)都可以建構(gòu)在基本VM系統(tǒng)之上。同時,VM體系結(jié)構(gòu)也是獨立于存儲管理的,存儲管理是由操作系統(tǒng)實施的,如:究竟采取什么樣的對換和請求調(diào)頁算法,究竟是采取分段還是分頁機(jī)制進(jìn)行存儲管理,究竟是如何將虛擬地址轉(zhuǎn)換成為物理地址等等(Linux中是一種叫Three Level Page Table的機(jī)制),這些都與內(nèi)存對象的概念無關(guān)。
下面介紹Linux中VM的實現(xiàn)。
如下圖所示,一個進(jìn)程應(yīng)該包括一個mm_struct(memory manage struct),該結(jié)構(gòu)是進(jìn)程虛擬地址空間的抽象描述,里面包括了進(jìn)程虛擬空間的一些管理信息:start_code, end_code, start_data, end_data, start_brk, end_brk等等信息。另外,也有一個指向進(jìn)程虛存區(qū)表(vm_area_struct :virtual memory area)的指針,該鏈?zhǔn)前凑仗摂M地址的增長順序排列的。
struct vm_area_struct {
/*公共的,與vma類型無關(guān)的 */
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;
/* 與類型相關(guān)的 */
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 . . . . . .
Mmap系統(tǒng)調(diào)用的實現(xiàn)過程是:
1.先通過文件系統(tǒng)定位要映射的文件;
2.權(quán)限檢查,映射的權(quán)限不會超過文件打開的方式,也就是說如果文件是以只讀方式打開,那么則不允許建立一個可寫映射;
3.創(chuàng)建一個vma對象,并對之進(jìn)行初始化;
4.調(diào)用映射文件的mmap函數(shù),其主要工作是給vm_ops向量表賦值;
5.把該vma鏈入該進(jìn)程的vma鏈表中,如果可以和前后的vma合并則合并;
6.如果是要求VM_LOCKED(映射區(qū)不被換出)方式映射,則發(fā)出缺頁請求,把映射頁面讀入內(nèi)存中;
該調(diào)用可以看作是mmap的一個逆過程。它將進(jìn)程中從start開始length長度的一段區(qū)域的映射關(guān)閉,如果該區(qū)域不是恰好對應(yīng)一個vma,則有可能會分割幾個或幾個vma。
Msync(void * start, size_t length, int flags) :
把映射區(qū)域的修改回寫到后備存儲中。因為munmap時并不保證頁面回寫,如果不調(diào)用msync,那么有可能在munmap后丟失對映射區(qū)的修改。其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE,MS_SYNC要求回寫完成后才返回,MS_ASYNC發(fā)出回寫請求后立即返回,MS_INVALIDATE使用回寫的內(nèi)容更新該文件的其它映射。
該系統(tǒng)調(diào)用是通過調(diào)用映射文件的sync函數(shù)來完成工作的。
brk(void * end_data_segement):
將進(jìn)程的數(shù)據(jù)段擴(kuò)展到end_data_segement指定的地址,該系統(tǒng)調(diào)用和mmap的實現(xiàn)方式十分相似,同樣是產(chǎn)生一個vma,然后指定其屬性。不過在此之前需要做一些合法性檢查,比如該地址是否大于mm->end_code,end_data_segement和mm->brk之間是否還存在其它vma等等。通過brk產(chǎn)生的vma映射的文件為空,這和匿名映射產(chǎn)生的vma相似,關(guān)于匿名映射不做進(jìn)一步介紹。我們使用的庫函數(shù)malloc就是通過brk實現(xiàn)的,通過下面這個例子很容易證實這點:
main()
{
char * m, * n;
int size;
m = (char *)sbrk(0);
printf("sbrk addr = %08lx\n", m);
do {
n = malloc(1024);
printf("malloc addr = %08lx\n", n);
m = (char *)sbrk(0);
}
malloc addr = 08049be0
malloc addr = 08049fe8
malloc addr = 0804a3f0
new sbrk addr = 0804b000
3.進(jìn)程間通信(IPC)
System V IPC包括三種機(jī)制:message(允許進(jìn)程發(fā)送格式化的數(shù)據(jù)流到任意的進(jìn)程)、shared memory(允許進(jìn)程間共享它們虛擬地址空間的部分區(qū)域)和semaphore(允許進(jìn)程間同步的執(zhí)行)。
操作系統(tǒng)核心中為它們分別維護(hù)著一個表,這三個表是系統(tǒng)中所有這三種IPC對象的集合,表的索引是一個數(shù)值ID,進(jìn)程通過這個ID可以查找到需要使用的IPC資源。進(jìn)程每創(chuàng)建一個IPC對象,系統(tǒng)中都會在相應(yīng)的表中增加一項。之后其它進(jìn)程(具有許可權(quán)的進(jìn)程)只要通過該IPC對象的ID則可以引用它。
IPC對象必須使用IPC_RMID命令來顯示的釋放,否則這個對象就處于活動狀態(tài),甚至所有的使用它的進(jìn)程都已經(jīng)終止。這種機(jī)制某些時候十分有用,但是也正因為這種特征,使得操作系統(tǒng)內(nèi)核無法判斷IPC對象是被用戶故意遺留下來供將來其它進(jìn)程使用還是被無意拋棄的。
Linux中只提供了一個系統(tǒng)調(diào)用接口ipc()來完成所有System V IPC操作,我們常使用的是建立在該調(diào)用之上的庫函數(shù)接口。對于這三種IPC,都有很相似的三種調(diào)用:xxxget, (msgsnd, msgrcv)|semopt | (shmat, shmdt), xxxctl
Xxxget:獲取調(diào)用,在系統(tǒng)中申請或者查詢一個IPC資源,返回值是該IPC對象的ID,該調(diào)用類似于文件系統(tǒng)的open, create調(diào)用;
Xxxctl:控制調(diào)用,至少包括三種操作:XXX_RMID(釋放IPC對象), XXX_STAT(查詢狀態(tài)), XXX_SET(設(shè)置狀態(tài)信息);
(msgsnd, msgrcv) | Semopt | (shmat, shmdt)|:操作調(diào)用,這些調(diào)用的功能隨IPC對象的類型不同而有較大差異。
4.文件系統(tǒng)相關(guān)的調(diào)用
文件是用來保存數(shù)據(jù)的,而文件系統(tǒng)則可以讓用戶組織,操縱以及存取不同的文件。內(nèi)核允許用戶通過一個嚴(yán)格定義的過程性接口與文件系統(tǒng)進(jìn)行交互,這個接口對用戶屏蔽了文件系統(tǒng)的細(xì)節(jié),同時指定了所有相關(guān)系統(tǒng)調(diào)用的行為和語義。Linux支持許多中文件系統(tǒng),如ext2,msdos, ntfs, proc, dev, ufs, nfs等等,這些文件系統(tǒng)都實現(xiàn)了相同的接口,因此給應(yīng)用程序提供了一致性的視圖。但每種文件系統(tǒng)在實現(xiàn)時可能對某個方面加以了一定的限制。如:文件名的長度,是否支持所有的文件系統(tǒng)接口調(diào)用。
為了支持多文件系統(tǒng),sun提出了一種vnode/vfs接口,SVR4中將之實現(xiàn)成了一種工業(yè)標(biāo)準(zhǔn)。而Linux作為一種Unix的clone體,自然也實現(xiàn)了這種接口,只是它的接口定義和SVR4的稍有不同。Vnode/Vfs接口的設(shè)計體現(xiàn)了面向?qū)ο蟮乃枷耄?/font>Vfs(虛擬文件系統(tǒng))代表內(nèi)核中的一個文件系統(tǒng),Vnode(虛擬節(jié)點)代表內(nèi)核中的一個文件,它們都可以被視為抽象基類,并可以從中派生出不同的子類以實現(xiàn)不同的文件系統(tǒng)。 由于篇幅原因,這里只是大概的介紹一下怎樣通過Vnode/Vfs結(jié)構(gòu)來實現(xiàn)文件系統(tǒng)和訪問文件。 在Linux中支持的每種文件系統(tǒng)必須有一個file_system_type結(jié)構(gòu),此結(jié)構(gòu)的核心是read_super函數(shù),該函數(shù)將讀取文件系統(tǒng)的超級塊。Linux中支持的所有文件系統(tǒng)都會被注冊在一條file_system_type結(jié)構(gòu)鏈中,注冊是在系統(tǒng)初始化時調(diào)用regsiter_filesystem()完成,如果文件系統(tǒng)是以模塊的方式實現(xiàn),則是在調(diào)用init_module時完成。 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 *); }; 由于這組操作中定義了文件系統(tǒng)中對于inode的操作,所以是之后對于文件系統(tǒng)中文件所有操作的基礎(chǔ)。 在給super_block的s_ops賦值后,再給該文件系統(tǒng)分配一個vfsmount結(jié)構(gòu),將該結(jié)構(gòu)注冊到系統(tǒng)維護(hù)的另一條鏈vfsmntlist中,所有mount上的文件系統(tǒng)都在該鏈中有一項。在umount時,則從鏈中刪除這一項并且釋放超級塊。 對于一個已經(jīng)mount的文件系統(tǒng)中任何文件的操作首先應(yīng)該以產(chǎn)生一個inode實例,即根據(jù)文件系統(tǒng)的類型生成一個屬于該文件系統(tǒng)的內(nèi)存i節(jié)點。這首先調(diào)用文件定位函數(shù)lookup_dentry查找目錄緩存看是否使用過該文件,如果還沒有則緩存中找不到,于是需要的i接點則依次調(diào)用路徑上的所有目錄I接點的lookup函數(shù),在lookup函數(shù)中會調(diào)用iget函數(shù),該函數(shù)中最終調(diào)用超級塊的s_ops->read_inode讀取目標(biāo)文件的磁盤I節(jié)點(這一步再往下就是由設(shè)備驅(qū)動完成了,通過調(diào)用驅(qū)動程序的read函數(shù)讀取磁盤I節(jié)點),read_inode函數(shù)的主要功能是初始化inode的一些私有數(shù)據(jù)(比如數(shù)據(jù)存儲位置,文件大小等等)以及給inode_operations函數(shù)開關(guān)表賦值,最終該inode被綁定在一個目錄緩存結(jié)構(gòu)dentry中返回。 在獲得了文件的inode之后,對于該文件的其它一切操作都有了根基。因為可以從inode 獲得文件操作函數(shù)開關(guān)表file_operatoins,該開關(guān)表里給出了標(biāo)準(zhǔn)的文件I/O接口的實現(xiàn),包括read, write, lseek, mmap, ioctl等等。這些函數(shù)入口將是所有關(guān)于文件的系統(tǒng)調(diào)用請求的最終處理入口,通過這些函數(shù)入口會向存儲該文件的硬設(shè)備驅(qū)動發(fā)出請求并且由驅(qū)動程序返回數(shù)據(jù)。當(dāng)然這中間還會牽涉到一些關(guān)于buffer的管理問題,這里就不贅述了。 通過講述這些,我們應(yīng)該明白了為什么可以使用統(tǒng)一的系統(tǒng)調(diào)用接口來訪問不同文件系統(tǒng)類型的文件了:因為在文件系統(tǒng)的實現(xiàn)一層,都把低層的差異屏蔽了,用戶可見的只是高層可見的一致的系統(tǒng)調(diào)用接口。
當(dāng)mount某種塊設(shè)備時,將調(diào)用系統(tǒng)調(diào)用mount,該調(diào)用中將會首先檢查該類文件系統(tǒng)是否注冊在系統(tǒng)種中,如果注冊了則先給該文件系統(tǒng)分配一個super_block,并進(jìn)行初始化,最后調(diào)用這種文件系統(tǒng)的read_super函數(shù)來完成super_block結(jié)構(gòu)私有數(shù)據(jù)的賦值。其中最主要的工作是給super_block的s_ops賦值,s_ops是一個函數(shù)向量表,由文件系統(tǒng)各自實現(xiàn)了一組操作。
5.與module相關(guān)的系統(tǒng)調(diào)用
Linux中提供了往系統(tǒng)中添加和卸載模塊的接口,create_module(),init_module (), delete_module(),這些系統(tǒng)調(diào)用通常不是直接為程序員使用的,它們僅僅是為實現(xiàn)一些系統(tǒng)命令而提供的接口,如insmod, rmmod,(在使用這些系統(tǒng)調(diào)用前必須先加載目標(biāo)文件到用戶進(jìn)程的地址空間,這必須由目標(biāo)文件格式所特定的庫函數(shù)(如:libobj.a中的一些函數(shù))來完成)。
Linux的核心中維護(hù)了一個module_list列表,每個被加載到核心中的模塊都在其中占有一項,系統(tǒng)調(diào)用create_module()就是在該列表里注冊某個指定的模塊,而init_module則是使用模塊目標(biāo)文件內(nèi)容的映射來初始化核心中注冊的該模塊,并且調(diào)用該模塊的初始化函數(shù),初始化函數(shù)通常完成一些特定的初始化操作,比如文件系統(tǒng)的初始化函數(shù)就是在操作系統(tǒng)中注冊該文件系統(tǒng)。delete_module則是從系統(tǒng)中卸載一個模塊,其主要工作是從module_list中刪除該模塊對應(yīng)的module結(jié)構(gòu)并且調(diào)用該模塊的cleanup函數(shù)卸載其它私有信息。
檢查系統(tǒng)上其它資源是否符合新內(nèi)核的要求。在linux/Document目錄下有一個叫Changes的文件,里面列舉了當(dāng)前內(nèi)核版本所需要的其它軟件的版本號,
- 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.*
. . . . . .
其中最后一項是列舉該軟件版本號的命令,如果不符合要求先給相應(yīng)軟件升級,這一步通常可以忽略。
2.配置內(nèi)核
使用make config或者make menuconfig, make xconfig配置新內(nèi)核。其中包括選擇塊設(shè)備驅(qū)動程序、網(wǎng)絡(luò)選項、網(wǎng)絡(luò)設(shè)備支持、文件系統(tǒng)等等,用戶可以根據(jù)自己的需求來進(jìn)行功能配置。每個選項至少有“y”和“n”兩種選擇,選擇“y”表示把相應(yīng)的支持編譯進(jìn)內(nèi)核,選“n”表示不提供這種支持,還有的有第三種選擇“m”,則表示把該支持編譯成可加載模塊,即前面提到的module,怎樣編譯和安裝模塊在后面會介紹。
這里,順便講述一下如何在內(nèi)核中增加自己的功能支持。
假如我們現(xiàn)在需要在自己的內(nèi)核中加入一個文件系統(tǒng)tfile,在完成了文件系統(tǒng)的代碼后,在linux/fs下建立一個tfile目錄,把源文件拷貝到該目錄下,然后修改linux/fs下的Makefile,把對應(yīng)該文件系統(tǒng)的目標(biāo)文件加入目標(biāo)文件列表中,最后修改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.編譯內(nèi)核
在配置好內(nèi)核后就是編譯內(nèi)核了,在編譯之前首先應(yīng)該執(zhí)行make dep命令建立好依賴關(guān)系,該命令將會修改linux中每個子目錄下的.depend文件,該文件包含了該目錄下每個目標(biāo)文件所需要的頭文件(絕對路徑的方式列舉)。
然后就是使用make bzImage命令來編譯內(nèi)核了。該命令運行結(jié)束后將會在linux/arch/asm/boot/產(chǎn)生一個名叫bzImage的映像文件。
4.使用新內(nèi)核引導(dǎo)
把前面編譯產(chǎn)生的映像文件拷貝到/boot目錄下(也可以直接建立一個符號連接,這樣可以省去每次編譯后的拷貝工作),這里暫且命名為vmlinuz-new,那么再修改/etc/lilo.conf,在其中增加這么幾條:
image = /boot/vmlinuz-new
root = /dev/hda1
label = new
read-only
并且運行lilo命令,那么系統(tǒng)在啟動時就可以選用新內(nèi)核引導(dǎo)了。
5.編譯模塊和使用模塊