進(jìn)程的創(chuàng )建過(guò)程
------基于Linux0.11源碼分析
1. 背景
進(jìn)程的創(chuàng )建過(guò)程無(wú)疑是最重要的操作系統處理過(guò)程之一,很多書(shū)和教材上說(shuō)的最多的還是一些原理的部分,忽略了很多細節。比如,子進(jìn)程復制父進(jìn)程所擁有的資源,或者子進(jìn)程和父進(jìn)程共享相同的物理頁(yè)面,擁有自己的地址空間,子進(jìn)程創(chuàng )建后接受統一調度執行等等。
原理性的書(shū)籍更多地關(guān)注了進(jìn)程創(chuàng )建過(guò)程中各個(gè)關(guān)鍵部分的功能,但由于過(guò)于抽象,很難理解,因此如果自己能夠實(shí)際操作,實(shí)踐這個(gè)過(guò)程就很重要,可以讓那些看起來(lái)抽象的概念變的現實(shí)而容易理解,比如所謂的父進(jìn)程的資源,父進(jìn)程所擁有的物理頁(yè)面,甚至父進(jìn)程的地址空間等等,這些抽象的概念其實(shí)只要實(shí)際操作一次就更能有感性的認識。本人參考Linux0.11源代碼實(shí)踐了創(chuàng )建進(jìn)程和調度,這個(gè)過(guò)程獲益匪淺,這里把主要的學(xué)習成果結合實(shí)踐總結一下。
2. 0號進(jìn)程
子進(jìn)程的創(chuàng )建是基于父進(jìn)程的,因此一直追溯上去,總有一個(gè)進(jìn)程是原始的,即沒(méi)有父進(jìn)程的。這個(gè)進(jìn)程在Linux中的進(jìn)程號是0,也就是傳說(shuō)中的0號進(jìn)程(可惜很多理論書(shū)上對這個(gè)重要的進(jìn)程只字不提)。
如果說(shuō)子進(jìn)程可以通過(guò)規范的創(chuàng )建進(jìn)程的函數(如:fork())基于父進(jìn)程復制創(chuàng )建,那么0號進(jìn)程并沒(méi)有可以復制和參考的對象,也就是說(shuō)0號進(jìn)程擁有的所有信息和資源都是強制設置的,不是復制的,這個(gè)過(guò)程我稱(chēng)為手工設置,也就是說(shuō)0號進(jìn)程是“純手工打造”,這是操作系統中“最原始”的一個(gè)進(jìn)程,它是一個(gè)模子,后面的任何進(jìn)程都是基于0號進(jìn)程生成的。
手工打造0號進(jìn)程最主要包括兩個(gè)部分:創(chuàng )建進(jìn)程0運行時(shí)所需的所有信息,即填充0號進(jìn)程,讓它充滿(mǎn)“血肉”;二是調度0號進(jìn)程的執行,即讓它“動(dòng)”起來(lái),只有動(dòng)起來(lái),才是真正意義上的進(jìn)程,因為進(jìn)程本身實(shí)際上是個(gè)動(dòng)態(tài)的概念。
不同的操作系統或者同一個(gè)操作系統的不同版本進(jìn)程信息的內涵可能會(huì )有些細微的差距,但大體上關(guān)鍵的部分和邏輯是沒(méi)有什么不同的,我這里只是基于Linux0.11的實(shí)現來(lái)描述進(jìn)程創(chuàng )建的關(guān)鍵步驟和關(guān)鍵細節。
1)填充0號進(jìn)程信息
進(jìn)程包括的內容非常復雜,但總的來(lái)說(shuō)進(jìn)程的信息都是由進(jìn)程的描述符引導標識的,因此填充0號進(jìn)程的過(guò)程邏輯上是以填充其描述符為牽引完成的(也有書(shū)將進(jìn)程描述符稱(chēng)為進(jìn)程控制塊)。下面是Linux0.11版進(jìn)程的描述符信息結構體:
struct task_struct {
long state,counter,priority, signal;
struct sigaction sigaction[32];
long blocked;
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,father,pgrp,session,leader;
unsigned short uid,euid,suid,gid,egid,sgid;
long alarm;
long utime,stime,cutime,cstime,start_time;
unsigned short used_math;
int tty;
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];
struct desc_struct ldt[3];
struct tss_struct tss;
};
可以看到進(jìn)程描述符里的信息很多,大體上有幾部分:
a. 進(jìn)程的運行信息,如進(jìn)程的當前狀態(tài)(state),進(jìn)程的各種時(shí)間片消耗記錄(utime、stime等),進(jìn)程的信號(signal)和優(yōu)先級(priority)等。
b. 進(jìn)程的基本創(chuàng )建信息,如進(jìn)程號(pid),進(jìn)程的創(chuàng )建用戶(hù)(uid)等。
c. 進(jìn)程的資源類(lèi)信息,如使用的tty自設備號(tty),文件根目錄i節點(diǎn)結構(root)等。
d. 進(jìn)程執行和切換CPU需要使用的關(guān)鍵信息:局部描述符表(LDT)、任務(wù)狀態(tài)段(TSS)信息。
這些信息并不是在進(jìn)程創(chuàng )建的時(shí)候就全部確定的,大部分只是暫時(shí)賦一個(gè)初值,在運行的時(shí)候會(huì )動(dòng)態(tài)更改,也有一些是要在進(jìn)程運行前設置好的,才能保證進(jìn)程被正確地執行起來(lái)。實(shí)際上,我們最需要填充的信息是那些使得操作系統可以順利切換到0號進(jìn)程的信息,最重要的顯然是進(jìn)程的LDT和TSS信息。TSS是CPU在切換任務(wù)時(shí)需要使用的信息,而LDT是局部描述符表,0號進(jìn)程是第一個(gè)運行在用戶(hù)態(tài)的進(jìn)程,需要使用自己的LDT。TSS和LDT是保證不同進(jìn)程之間相互隔離的重要機制。
實(shí)際上還有一個(gè)重要的信息不是放在進(jìn)程本身的描述符里的,而是放在全局描述符表GDT中,因為所有的進(jìn)程是由操作系統統一管理的,因此操作系統至少要保持對它們的索引,這種索引性質(zhì)的信息放在操作系統內核的GDT中。對于Linux0.11來(lái)說(shuō),每個(gè)進(jìn)程都有一個(gè)LDT和一個(gè)TSS描述符,而Linux2.4之后是每個(gè)CPU一個(gè)TSS描述符并存儲在GDT中,而不是每個(gè)進(jìn)程一個(gè)。當然這種區別會(huì )造成進(jìn)程創(chuàng )建和切換過(guò)程中一些細節上的差異,但本質(zhì)的部分和任務(wù)的切換過(guò)程并沒(méi)有任何不同。
下面是Linux0.11手動(dòng)填充進(jìn)程0的進(jìn)程描述符信息的宏:
#define INIT_TASK \
{ 0,15,15, \
0,{{},},0, \
0,0,0,0,0,0, \
0,-1,0,0,0, \
0,0,0,0,0,0, \
0,0,0,0,0,0, \
0, \
-1,0022,NULL,NULL,NULL,0, \
{NULL,}, \
{ \
{0,0}, \
{0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \
}, \
{ 0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, {} \
}, \
}
除了填充進(jìn)程描述符的信息外,還需要在GDT中設置相關(guān)的項,即進(jìn)程0的LDT和TSS選擇符,這個(gè)工作是在sched_init()里完成的:
void sched_init(void){
...
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
...
ltr(0);
lldt(0);
}
可以看到,在進(jìn)程0的TSS和LDT描述符信息設置到GDT中后,立刻設置了TR寄存器和LDTR寄存器,為即將運行0號進(jìn)程作準備。
2)運行0號進(jìn)程
進(jìn)程0是運行在用戶(hù)態(tài)下的進(jìn)程,因此就意味著(zhù)進(jìn)程0的運行過(guò)程實(shí)際上是一個(gè)從0級特權級到3級特權級切換的過(guò)程,使用的是CPU指令iret,模擬了中斷調用的返回過(guò)程,具體執行過(guò)程由move_to_user_mode完成:
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \
"pushl %%eax\n\t" \
"pushfl\n\t" \
"pushl $0x0f\n\t" \
"pushl $1f\n\t" \
"iret\n" \
"1:\tmovl $0x17,%%eax\n\t" \
...)
這個(gè)宏將進(jìn)程0執行時(shí)的ss,esp,eflags.cs,eip信息全部壓棧,待到執行iret指令時(shí),CPU將這幾項信息從棧中彈出加載到相應的寄存器中,這樣就實(shí)現了進(jìn)程0的啟動(dòng)執行。從這里也可以看出,進(jìn)程0剛開(kāi)始執行時(shí)幾個(gè)關(guān)鍵寄存器的信息也是在其運行前事先設定好的,從進(jìn)程描述符信息到執行信息均是人為設置,因此我稱(chēng)之為“純手工打造的進(jìn)程”。
3. 子進(jìn)程的創(chuàng )建
有了0號進(jìn)程這個(gè)原始的進(jìn)程,再來(lái)看子進(jìn)程的創(chuàng )建就比較容易理解一些。除了0號進(jìn)程外,其余的進(jìn)程均使用系統調用fork()完成,其具體工作由內核態(tài)的_sys_fork實(shí)現:
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
可以看到,一個(gè)進(jìn)程的創(chuàng )建主要有兩個(gè)步驟:一是找到一個(gè)空閑進(jìn)程資源(find_empty_process),Linux0.11來(lái)說(shuō)可以同時(shí)運行的進(jìn)程數目是64個(gè),是有限的,因此需要先得到一個(gè)空閑的進(jìn)程表中的一項用來(lái)索引即將創(chuàng )建的進(jìn)程信息;第二個(gè)主要步驟就是復制(copy_process),這個(gè)函數具體來(lái)實(shí)現子進(jìn)程基于父進(jìn)程的復制創(chuàng )建。
主要包括的步驟和內容是:
1) 為新進(jìn)程在內存中分配一個(gè)物理頁(yè),將新進(jìn)程的描述符信息填充在該頁(yè)的開(kāi)頭,并設置新進(jìn)程的描述符里各項信息;
2) 拷貝父進(jìn)程的頁(yè)表,使得它們共同指向相同的物理頁(yè),同時(shí)將父進(jìn)程的各個(gè)頁(yè)表屬性改為只讀,這樣將來(lái)可以使用寫(xiě)時(shí)復制機制。
3) 在GDT中設置該進(jìn)程項的TSS和LDT選擇符。
Linux0.11版本子進(jìn)程內容的設置主要內容就是這些,當然不同版本會(huì )有不同,在改進(jìn)執行性能上也會(huì )有改進(jìn),但這個(gè)版本體現出來(lái)的最基本創(chuàng )建過(guò)程基本上反映了操作系統創(chuàng )建進(jìn)程的主要過(guò)程。
4. 子進(jìn)程的運行
子進(jìn)程在創(chuàng )建好后并不能立即執行,至少需要一次調度,而這個(gè)調度到子進(jìn)程的運行過(guò)程就完全不需要像進(jìn)程0那樣人為在棧上設置信息然后用iret方式,而是執行的任務(wù)的切換過(guò)程。不考慮進(jìn)程調度的各個(gè)算法和選擇細節,最終負責完成切換操作的函數如下:
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
"je 1f\n\t" \
"movw %%dx,%1\n\t" \
"xchgl %%ecx,_current\n\t" \
"ljmp %0\n\t" \
"cmpl %%ecx,_last_task_used_math\n\t" \
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
最終的切換執行了一個(gè)ljmp操作,它的操作數是一個(gè)任務(wù)描述符,這會(huì )導致CPU執行一次任務(wù)切換,根據新進(jìn)程的TSS信息將相關(guān)信息加載進(jìn)cs,eip,eflags,ss,esp寄存器開(kāi)始執行新的代碼。當然由于先前拷貝的父進(jìn)程的相關(guān)頁(yè)面被設置為只讀,子進(jìn)程第一次執行到該頁(yè)面時(shí)會(huì )觸發(fā)頁(yè)保護的異常,這時(shí)會(huì )觸發(fā)寫(xiě)時(shí)復制操作,為子進(jìn)程分配自己的相應頁(yè)面。
符:任務(wù)(task)和進(jìn)程(process)的區別
任務(wù)和進(jìn)程很容易被人混淆,甚至在Linux中進(jìn)程描述符結構體也是用task_struct表示,而不是process,這更讓人有的時(shí)候搞不清楚。我個(gè)人認為,其實(shí)任務(wù)的概念更底層,可以認為是基于CPU的角度來(lái)考慮的,進(jìn)程所處的層次更高一些,應當可以認為是操作系統一級的概念。
任務(wù)關(guān)注點(diǎn)是一組程序操作,這組操作實(shí)現了某個(gè)功能,它最終會(huì )涉及到指令級別,我們說(shuō)任務(wù)的切換最終需要關(guān)注的還是CPU的相關(guān)指令。
進(jìn)程的概念通常是指程序的執行,是動(dòng)態(tài)的過(guò)程。進(jìn)程除了包含其要運行的程序之外,還包括運行時(shí)的諸多信息,如運行時(shí)間,信號等等。