Linux下系統(tǒng)調(diào)用的實現(xiàn)
系統(tǒng)調(diào)用可以看作是一個所有Unix/Linux進程共享的子程序庫,但是它是在特權(quán)方式下運行,可以存取核心數(shù)據(jù)結(jié)構(gòu)和它所支持的用戶級數(shù)據(jù)。系統(tǒng)調(diào)用的主要功能是使用戶可以使用操作系統(tǒng)提供的有關(guān)設(shè)備管理、文件系統(tǒng)、進程控制進程通訊以及存儲管理方面的功能,而不必要了解操作系統(tǒng)的內(nèi)部結(jié)構(gòu)和有關(guān)硬件的細節(jié)問題,從而減輕用戶負擔和保護系統(tǒng)以及提高資源利用率。 系統(tǒng)調(diào)用分為兩個部分:與文件子系統(tǒng)交互的和進程子系統(tǒng)交互的兩個部分。其中和文件子系統(tǒng)交互的部分進一步由可以包括與設(shè)備文件的交互和與普通文件的交互的系統(tǒng)調(diào)用(open, close, ioctl, create, unlink, . . . );與進程相關(guān)的系統(tǒng)調(diào)用又包括進程控制系統(tǒng)調(diào)用(fork, exit, getpid, . . . ),進程間通訊,存儲管理,進程調(diào)度等方面的系統(tǒng)調(diào)用。 (以i386為例說明) A.在Linux中系統(tǒng)調(diào)用是怎樣陷入核心的? 在每種平臺上,都有特定的指令可以使進程的執(zhí)行由用戶態(tài)轉(zhuǎn)換為核心態(tài),這種指令稱作操作系統(tǒng)陷入(operating system trap)。進程通過執(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)用的入口將在后面詳細介紹。 這樣,就需要在系統(tǒng)啟動時,對INT 0x80進行一定的初始化,下面將描述其過程: 1.使用匯編子程序setup_idt(linux/arch/i386/kernel/head.S)初始化idt表(中斷描述符表),這時所有的入口函數(shù)偏移地址都被設(shè)為ignore_int ( setup_idt: 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ù)。在后面會詳細介紹這些內(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)以及類型標識(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的說明) 核心中為每個系統(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,而該標號實際上是后面將要提到的系統(tǒng)調(diào)用表(sys_call_table)的下標,通過該值可以找到相映系統(tǒng)調(diào)用的處理函數(shù)地址。 ENTRY(sys_call_table) .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) .long SYMBOL_NAME(sys_read) .long SYMBOL_NAME(sys_write) . . . . . . 如前面提到的,系統(tǒng)調(diào)用是通過一條陷入指令進入核心態(tài),然后根據(jù)傳給核心的系統(tǒng)調(diào)用號為索引在系統(tǒng)調(diào)用表中找到相映的處理函數(shù)入口地址。這里將詳細介紹這一過程。 我們還是以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ù)。當一個進程調(diào)用一個特定的系統(tǒng)調(diào)用庫的入口點,正如同它調(diào)用任何函數(shù)一樣,對于庫函數(shù)也要創(chuàng)建一個棧幀。而當進程執(zhí)行陷入指令時,它將處理機狀態(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的嵌入式匯編語言,不做詳細的介紹,只簡單說明其意義: 其中__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í)行用戶代碼,必須在進入核心態(tài)時保存時往核心中壓入一個上下文層;在從核心返回時會彈出一個上下文層,這樣用戶進程就可以繼續(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)先級堆棧(即核心棧)中; #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.GET_CURRENT宏 #define GET_CURRENT(reg) \ movl %esp, reg; \ andl $-8192, reg; 其作用是取得當前進程的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ù)的傳入過程,當執(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)); 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" \ 調(diào)用返回的過程要做的工作比其響應(yīng)過程要多一些,這些工作幾乎是每次從核心態(tài)返回用戶態(tài)都需要做的,這里將簡要的說明: 1.判斷有沒有軟中斷,如果有則跳轉(zhuǎn)到軟中斷處理; F.實例介紹 這里實現(xiàn)的系統(tǒng)調(diào)用hello僅僅是在控制臺上打印一條語句,沒有任何功能。 1.修改linux/include/i386/unistd.h,在里面增加一條語句: #define __NR_hello ???(這個數(shù)字可能因為核心版本不同而不同) 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 ! \n”, str); return 0; } ENTRY(sys_call_table) . . . . . . .long SYMBOL_NAME(sys_hello) 并且修改: .rept NR_syscalls-??? /* ??? = ??? +1 */ .long SYMBOL_NAME(sys_ni_syscall)
#ifdef __KERNEL #else inline _syscall1(int, hello, char *, str); #endif 這樣就可以使用系統(tǒng)調(diào)用hello了
Fork & vfork & clone 進程是一個指令執(zhí)行流及其執(zhí)行環(huán)境,其執(zhí)行環(huán)境是一個系統(tǒng)資源的集合,這些資源在Linux中被抽象成各種數(shù)據(jù)對象:進程控制塊、虛存空間、文件系統(tǒng),文件I/O、信號處理函數(shù)。所以創(chuàng)建一個進程的過程就是這些數(shù)據(jù)對象的創(chuàng)建過程。 在調(diào)用系統(tǒng)調(diào)用fork創(chuàng)建一個進程時,子進程只是完全復(fù)制父進程的資源,這樣得到的子進程獨立于父進程,具有良好的并發(fā)性,但是二者之間的通訊需要通過專門的通訊機制,如:pipe,fifo,System V IPC機制等,另外通過fork創(chuàng)建子進程系統(tǒng)開銷很大,需要將上面描述的每種資源都復(fù)制一個副本。這樣看來,fork是一個開銷十分大的系統(tǒng)調(diào)用,這些開銷并不是所有的情況下都是必須的,比如某進程fork出一個子進程后,其子進程僅僅是為了調(diào)用exec執(zhí)行另一個執(zhí)行文件,那么在fork過程中對于虛存空間的復(fù)制將是一個多余的過程(由于Linux中是采取了copy-on-write技術(shù),所以這一步驟的所做的工作只是虛存管理部分的復(fù)制以及頁表的創(chuàng)建,而并沒有包括物理也面的拷貝);另外,有時一個進程中具有幾個獨立的計算單元,可以在相同的地址空間上基本無沖突進行運算,但是為了把這些計算單元分配到不同的處理器上,需要創(chuàng)建幾個子進程,然后各個子進程分別計算最后通過一定的進程間通訊和同步機制把計算結(jié)果匯總,這樣做往往有許多格外的開銷,而且這種開銷有時足以抵消并行計算帶來的好處。 這說明了把計算單元抽象到進程上是不充分的,這也就是許多系統(tǒng)中都引入了線程的概念的原因。在講述線程前首先介紹以下vfork系統(tǒng)調(diào)用,vfork系統(tǒng)調(diào)用不同于fork,用vfork創(chuàng)建的子進程共享地址空間,也就是說子進程完全運行在父進程的地址空間上,子進程對虛擬地址空間任何數(shù)據(jù)的修改同樣為父進程所見。但是用vfork創(chuàng)建子進程后,父進程會被阻塞直到子進程調(diào)用exec或exit。這樣的好處是在子進程被創(chuàng)建后僅僅是為了調(diào)用exec執(zhí)行另一個程序時,因為它就不會對父進程的地址空間有任何引用,所以對地址空間的復(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等等標志位,任何一位被置1了則表明創(chuàng)建的子進程和父進程共享該位對應(yīng)的資源。所以在vfork的實現(xiàn)中,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,這表示子進程和父進程共享地址空間,同時do_fork會檢查CLONE_VFORK,如果該位被置1了,子進程會把父進程的地址空間鎖住,直到子進程退出或執(zhí)行exec時才釋放該鎖。
在講述clone系統(tǒng)調(diào)用前先簡單介紹線程的一些概念。 線程是在進程的基礎(chǔ)上進一步的抽象,也就是說一個進程分為兩個部分:線程集合和資源集合。線程是進程中的一個動態(tài)對象,它應(yīng)該是一組獨立的指令流,進程中的所有線程將共享進程里的資源。但是線程應(yīng)該有自己的私有對象:比如程序計數(shù)器、堆棧和寄存器上下文。 線程分為三種類型: 內(nèi)核線程、輕量級進程和用戶線程。 內(nèi)核線程: 它的創(chuàng)建和撤消是由內(nèi)核的內(nèi)部需求來決定的,用來負責執(zhí)行一個指定的函數(shù),一個內(nèi)核線程不需要和一個用戶進程聯(lián)系起來。它共享內(nèi)核的正文段核全局數(shù)據(jù),具有自己的內(nèi)核堆棧。它能夠單獨的被調(diào)度并且使用標準的內(nèi)核同步機制,可以被單獨的分配到一個處理器上運行。內(nèi)核線程的調(diào)度由于不需要經(jīng)過態(tài)的轉(zhuǎn)換并進行地址空間的重新映射,因此在內(nèi)核線程間做上下文切換比在進程間做上下文切換快得多。 輕量級進程: 輕量級進程是核心支持的用戶線程,它在一個單獨的進程中提供多線程控制。這些輕量級進程被單獨的調(diào)度,可以在多個處理器上運行,每一個輕量級進程都被綁定在一個內(nèi)核線程上,而且在它的生命周期這種綁定都是有效的。輕量級進程被獨立調(diào)度并且共享地址空間和進程中的其它資源,但是每個LWP都應(yīng)該有自己的程序計數(shù)器、寄存器集合、核心棧和用戶棧。 用戶線程: 用戶線程是通過線程庫實現(xiàn)的。它們可以在沒有內(nèi)核參與下創(chuàng)建、釋放和管理。線程庫提供了同步和調(diào)度的方法。這樣進程可以使用大量的線程而不消耗內(nèi)核資源,而且省去大量的系統(tǒng)開銷。用戶線程的實現(xiàn)是可能的,因為用戶線程的上下文可以在沒有內(nèi)核干預(yù)的情況下保存和恢復(fù)。每個用戶線程都可以有自己的用戶堆棧,一塊用來保存用戶級寄存器上下文以及如信號屏蔽等狀態(tài)信息的內(nèi)存區(qū)。庫通過保存當前線程的堆棧和寄存器內(nèi)容載入新調(diào)度線程的那些內(nèi)容來實現(xiàn)用戶線程之間的調(diào)度和上下文切換。 內(nèi)核仍然負責進程的切換,因為只有內(nèi)核具有修改內(nèi)存管理寄存器的權(quán)力。用戶線程不是真正的調(diào)度實體,內(nèi)核對它們一無所知,而只是調(diào)度用戶線程下的進程或者輕量級進程,這些進程再通過線程庫函數(shù)來調(diào)度它們的線程。當一個進程被搶占時,它的所有用戶線程都被搶占,當一個用戶線程被阻塞時,它會阻塞下面的輕量級進程,如果進程只有一個輕量級進程,則它的所有用戶線程都會被阻塞。
明確了這些概念后,來講述Linux的線程和clone系統(tǒng)調(diào)用。 在許多實現(xiàn)了MT的操作系統(tǒng)中(如:Solaris,Digital Unix等), 線程和進程通過兩種數(shù)據(jù)結(jié)構(gòu)來抽象表示: 進程表項和線程表項,一個進程表項可以指向若干個線程表項, 調(diào)度器在進程的時間片內(nèi)再調(diào)度線程。 但是在Linux中沒有做這種區(qū)分, 而是統(tǒng)一使用task_struct來管理所有進程/線程,只是線程與線程之間的資源是共享的,這些資源可是是前面提到過的:虛存、文件系統(tǒng)、文件I/O以及信號處理函數(shù)甚至PID中的幾種。
clone系統(tǒng)調(diào)用就是一個創(chuàng)建輕量級進程的系統(tǒng)調(diào)用: int clone(int (*fn)(void * arg), void *stack, int flags, void * arg); 其中fn是輕量級進程所執(zhí)行的過程,stack是輕量級進程所使用的堆棧,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都是使進程睡眠一段時間后被喚醒,但是二者的實現(xiàn)完全不同。 Linux中并沒有提供系統(tǒng)調(diào)用sleep,sleep是在庫函數(shù)中實現(xiàn)的,它是通過調(diào)用alarm來設(shè)定報警時間,調(diào)用sigsuspend將進程掛起在信號SIGALARM上,sleep只能精確到秒級上。 nanosleep則是Linux中的系統(tǒng)調(diào)用,它是使用定時器來實現(xiàn)的,該調(diào)用使調(diào)用進程睡眠,并往定時器隊列上加入一個time_list型定時器,time_list結(jié)構(gòu)里包括喚醒時間以及喚醒后執(zhí)行的函數(shù),通過nanosleep加入的定時器的執(zhí)行函數(shù)僅僅完成喚醒當前進程的功能。系統(tǒng)通過一定的機制定時檢查這些隊列(比如通過系統(tǒng)調(diào)用陷入核心后,從核心返回用戶態(tài)前,要檢查當前進程的時間片是否已經(jīng)耗盡,如果是則調(diào)用schedule()函數(shù)重新調(diào)度,該函數(shù)中就會檢查定時器隊列,另外慢中斷返回前也會做此檢查),如果定時時間已超過,則執(zhí)行定時器指定的函數(shù)喚醒調(diào)用進程。當然,由于系統(tǒng)時間片可能丟失,所以nanosleep精度也不是很高。 alarm也是通過定時器實現(xiàn)的,但是其精度只精確到秒級,另外,它設(shè)置的定時器執(zhí)行函數(shù)是在指定時間向當前進程發(fā)送SIGALRM信號。 在講述文件映射的概念時,不可避免的要牽涉到虛存(SVR 4的VM)。實際上,文件映射是虛存的中心概念,文件映射一方面給用戶提供了一組措施,似的用戶將文件映射到自己地址空間的某個部分,使用簡單的內(nèi)存訪問指令讀寫文件;另一方面,它也可以用于內(nèi)核的基本組織模式,在這種模式種,內(nèi)核將整個地址空間視為諸如文件之類的一組不同對象的映射。 Unix中的傳統(tǒng)文件訪問方式是,首先用open系統(tǒng)調(diào)用打開文件,然后使用read,write以及lseek等調(diào)用進行順序或者隨即的I/O。這種方式是非常低效的,每一次I/O操作都需要一次系統(tǒng)調(diào)用。另外,如果若干個進程訪問同一個文件,每個進程都要在自己的地址空間維護一個副本,浪費了內(nèi)存空間。而如果能夠通過一定的機制將頁面映射到進程的地址空間中,也就是說首先通過簡單的產(chǎn)生某些內(nèi)存管理數(shù)據(jù)結(jié)構(gòu)完成映射的創(chuàng)建。當進程訪問頁面時產(chǎn)生一個缺頁中斷,內(nèi)核將頁面讀入內(nèi)存并且更新頁表指向該頁面。而且這種方式非常方便于同一副本的共享。 下面給出以上兩種方式的對比圖: VM是面向?qū)ο蟮姆椒ㄔO(shè)計的,這里的對象是指內(nèi)存對象:內(nèi)存對象是一個軟件抽象的概念,它描述內(nèi)存區(qū)與后備存儲之間的映射。系統(tǒng)可以使用多種類型的后備存儲,比如交換空間,本地或者遠程文件以及幀緩存等等。VM系統(tǒng)對它們統(tǒng)一處理,采用同一操作集操作,比如讀取頁面或者回寫頁面等。每種不同的后備存儲都可以用不同的方法實現(xiàn)這些操作。這樣,系統(tǒng)定義了一套統(tǒng)一的接口,每種后備存儲給出自己的實現(xià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)頁算法,究竟是采取分段還是分頁機制進行存儲管理,究竟是如何將虛擬地址轉(zhuǎn)換成為物理地址等等(Linux中是一種叫Three Level Page Table的機制),這些都與內(nèi)存對象的概念無關(guān)。 下面介紹Linux中VM的實現(xiàn)。 如下圖所示,一個進程應(yīng)該包括一個mm_struct(memory manage struct),該結(jié)構(gòu)是進程虛擬地址空間的抽象描述,里面包括了進程虛擬空間的一些管理信息:start_code, end_code, start_data, end_data, start_brk, end_brk等等信息。另外,也有一個指向進程虛存區(qū)表(vm_area_struct :virtual memory area)的指針,該鏈是按照虛擬地址的增長順序排列的。
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)定位要映射的文件; 該調(diào)用可以看作是mmap的一個逆過程。它將進程中從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): 將進程的數(shù)據(jù)段擴展到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)于匿名映射不做進一步介紹。我們使用的庫函數(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.進程間通信(IPC) 進程間通訊可以通過很多種機制,包括signal, pipe, fifo, System V IPC, 以及socket等等,前幾種概念都比較好理解,這里著重介紹關(guān)于System V IPC。 System V IPC包括三種機制:message(允許進程發(fā)送格式化的數(shù)據(jù)流到任意的進程)、shared memory(允許進程間共享它們虛擬地址空間的部分區(qū)域)和semaphore(允許進程間同步的執(zhí)行)。 操作系統(tǒng)核心中為它們分別維護著一個表,這三個表是系統(tǒng)中所有這三種IPC對象的集合,表的索引是一個數(shù)值ID,進程通過這個ID可以查找到需要使用的IPC資源。進程每創(chuàng)建一個IPC對象,系統(tǒng)中都會在相應(yīng)的表中增加一項。之后其它進程(具有許可權(quán)的進程)只要通過該IPC對象的ID則可以引用它。 IPC對象必須使用IPC_RMID命令來顯示的釋放,否則這個對象就處于活動狀態(tài),甚至所有的使用它的進程都已經(jīng)終止。這種機制某些時候十分有用,但是也正因為這種特征,使得操作系統(tǒng)內(nèi)核無法判斷IPC對象是被用戶故意遺留下來供將來其它進程使用還是被無意拋棄的。 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)核允許用戶通過一個嚴格定義的過程性接口與文件系統(tǒng)進行交互,這個接口對用戶屏蔽了文件系統(tǒng)的細節(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è)標準。而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)維護的另一條鏈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讀取目標文件的磁盤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)表里給出了標準的文件I/O接口的實現(xiàn),包括read, write, lseek, mmap, ioctl等等。這些函數(shù)入口將是所有關(guān)于文件的系統(tǒng)調(diào)用請求的最終處理入口,通過這些函數(shù)入口會向存儲該文件的硬設(shè)備驅(qū)動發(fā)出請求并且由驅(qū)動程序返回數(shù)據(jù)。當然這中間還會牽涉到一些關(guān)于buffer的管理問題,這里就不贅述了。
通過講述這些,我們應(yīng)該明白了為什么可以使用統(tǒng)一的系統(tǒng)調(diào)用接口來訪問不同文件系統(tǒng)類型的文件了:因為在文件系統(tǒng)的實現(xià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)用前必須先加載目標文件到用戶進程的地址空間,這必須由目標文件格式所特定的庫函數(shù)(如:libobj.a中的一些函數(shù))來完成)。 Linux的核心中維護了一個module_list列表,每個被加載到核心中的模塊都在其中占有一項,系統(tǒng)調(diào)用create_module()就是在該列表里注冊某個指定的模塊,而init_module則是使用模塊目標文件內(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的文件,里面列舉了當前內(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ù)自己的需求來進行功能配置。每個選項至少有“y”和“n”兩種選擇,選擇“y”表示把相應(yīng)的支持編譯進內(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)的目標文件加入目標文件列表中,最后修改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文件,該文件包含了該目錄下每個目標文件所需要的頭文件(絕對路徑的方式列舉)。 然后就是使用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.編譯模塊和使用模塊
|
|