进程的创建——fork函数
1. 進程的信息
- 進程的結(jié)構(gòu)
在Linux中,一切皆文件,進程也是保存在內(nèi)存中的一個實例,下圖描述了進程的結(jié)構(gòu):
- 堆棧:保存局部變量
- 數(shù)據(jù)段:一般存放全局變量和靜態(tài)變量
- 代碼段:存儲進程的代碼文件
- TSS狀態(tài)段:進程做切換時,需要保存進程現(xiàn)場以便恢復(fù),一般存儲一些寄存器的值。
- task_struct : 進程的結(jié)構(gòu)體描述符,用來描述一個進程的屬性,這是一種面向?qū)ο蟮木幊趟枷?#xff0c;task_struct的結(jié)構(gòu)大致如下:
通過管理進程對應(yīng)的task_struct,可以完成進程的相關(guān)操作。
- GDT和LDT
操作系統(tǒng)在保護模式下,內(nèi)存管理分為分段模式和分頁模式。分段模式下內(nèi)存的尋址為「段基址:偏移地址」。對一個段的描述包括以下三個方面:【Base Address,Limit,Access】,他們加在一起被放在一個64bit長的數(shù)據(jù)結(jié)構(gòu)中,被稱為段描述符。因此需要用64bit的寄存器去存儲段描述符,但是操作系統(tǒng)的段基址寄存器只能存儲16bit的數(shù)據(jù),因此無法直接存儲64bit的段描述符,為了解決這個問題,操作系統(tǒng)將段描述符存放在一個全局的數(shù)組中,而段寄存器直接存儲對應(yīng)的段描述符對應(yīng)的下標(biāo),這個全局的數(shù)組叫做GDT
由于GDT也需要直接存在內(nèi)存中,所以操作系統(tǒng)用GDTR寄存器來存儲GDT的基地址,因此尋 址的過程為:
1. 通過GDTR寄存器找到GDT的基地址 2. 通過段寄存器找到段描述符的索引 3. 通過GDT基地址+索引從GDT數(shù)組中找到段描述符 4. 通過段描述符基地址+偏移地址找到線性地址至于LDT,本質(zhì)上和GDT是類似的,但也有不一樣的地方,LDT本身也是一段內(nèi)存,因此需要段描述符去描述它,它的段描述符存在GDT中,而LDT有LDTR寄存器,LDTR并不存儲LDT的段基址,而是一個段選擇子,是LDT的索引。
用一張圖來詮釋GDT和LDT的尋址過程:
- 進程的狀態(tài)
進程一共有五種狀態(tài),分別是:
-
TASK_RUNNING 運行狀態(tài)
運行狀態(tài)表示正在運行,只有這個狀態(tài)的進程才能被執(zhí)行
-
TASK_INTERRUPTIBLE 可中斷睡眠狀態(tài)
當(dāng)一個進程處于可中斷睡眠狀態(tài)時,它不會占用cpu資源,但是它可以響應(yīng)中斷或者信號。如socket等待連接建立時,它是睡眠的,但是連接一旦建立就會被喚醒。這種狀態(tài)就是阻塞狀態(tài)。
-
TASK_UNINTERRUPTIBLE 不可中斷睡眠狀態(tài)
和TASK_INTERRUPTIBLE不同,處于TASK_UNINTERRUPTIBLE狀態(tài)的進程無法被中斷或者信號喚醒,假設(shè)一個進程是TASK_UNINTERRUPTIBLE狀態(tài),你會驚奇的發(fā)現(xiàn)通過kill -9無法殺死該進程,因為它無法響應(yīng)異步信號。這種狀態(tài)很少見,一般發(fā)生在內(nèi)核態(tài)程序中,如讀取某個設(shè)備的文件,需要通過read系統(tǒng)調(diào)用通過驅(qū)動操作硬件設(shè)備讀取,這個過程是無法被中斷的。
-
TASK_ZOMBIE 僵死狀態(tài)
處于TASK_ZOMBIE狀態(tài)的進程并不代表著進程已經(jīng)被銷毀,此時除了task_struct,進程占有的所有資源將被釋放,之所以不釋放task_struct是因為task_struct保存著一些統(tǒng)計信息,其父進程可能需要這些信息。
-
TASK_STOPPED
不保留task_struct,進程資源全部被釋放
2. 系統(tǒng)初始化——main函數(shù)
上面大致介紹了和進程相關(guān)的一些信息說明,本節(jié)將從kernel的main.c方法開始,分析進程的創(chuàng)建過程.
Linux的main.c文件,是Linux開機時內(nèi)核初始化函數(shù),在初始化的過程中,內(nèi)核將創(chuàng)建系統(tǒng)的第一個進程:0號進程,0號進程不做任何操作,也不能被終止(除非系統(tǒng)異常或者關(guān)機),以后創(chuàng)建的每一個進程都是0號進程的子孫進程。
main.c的入口是main()函數(shù),main()函數(shù)的主要實現(xiàn):
void main(void){ROOT_DEV = ORIG_ROOT_DEV;drive_info = DRIVE_INFO; //省略了一段內(nèi)存初始化操作mem_init(main_memory_start,memory_end); // 主內(nèi)存區(qū)初始化trap_init(); // 陷阱門(硬件中斷向量)初始化blk_dev_init(); // 塊設(shè)備初始化chr_dev_init(); // 字符設(shè)備初始化tty_init(); // tty初始化time_init(); // 設(shè)置開機啟動時間 startup_timesched_init(); // 調(diào)度程序初始化(加載任務(wù)0的tr,ldtr)buffer_init(buffer_memory_end); // 緩沖管理初始化,建內(nèi)存鏈表等。hd_init(); // 硬盤初始化floppy_init(); // 軟驅(qū)初始化sti(); // 所有初始化工作都做完了,開啟中斷// 下面過程通過在堆棧中設(shè)置的參數(shù),利用中斷返回指令啟動任務(wù)0執(zhí)行。move_to_user_mode(); // 移到用戶模式下執(zhí)行if (!fork()) { init(); // 在新建的子進程(任務(wù)1)中執(zhí)行。}for(;;) pause(); }我們來看一看和進程管理相關(guān)的兩個初始化過程:
time_init()函數(shù)主要從CMOS管中讀取一個實時時鐘的年月日時分秒等信息并保存起來,并且通過kernel_mktime()函數(shù)來計算一個startup_time作為系統(tǒng)的開機時間,而kernel_time()函數(shù)就是根據(jù)從CMOS讀出的信息計算出1970年1月1日到現(xiàn)在的一個時間。
分析這個函數(shù)主要想介紹一下內(nèi)核一個很重要的時間概念:jiffies(系統(tǒng)滴答)
jiffies是系統(tǒng)的脈搏,或者說是系統(tǒng)的節(jié)拍。在內(nèi)核中,系統(tǒng)會以一定的頻率發(fā)生定時中斷,也就是說,某個進程正在運行,運行一段時間后系統(tǒng)會暫停這個進程運行,然后切換到另一個進程運行,jiffies決定了系統(tǒng)發(fā)生中斷的頻率,因此它和進程的調(diào)度息息相關(guān)。
首先來看一個變量task[],它的定義為:
struct task_struct * task[NR_TASKS] = {&(init_task.task), }在sched.c文件中,定義了一個task數(shù)組,這個數(shù)組的類型為task_struct結(jié)構(gòu)體,它的最大容量NR_TASKS=64,它的初始值也就是task[0] = init_task.task。
這個task數(shù)組的意義是:
task是一個保存進程結(jié)構(gòu)體的數(shù)組,最大容量為64,task[0]的位置保存了0號進程task_struct
再回到sched_init()的代碼,首先定義了一個desc_struct指針,然后為0號進程設(shè)置了它的ldt段和tss段,ldt段是由數(shù)據(jù)段和代碼段構(gòu)成的。然后從1開始遍歷task數(shù)組,將每個槽設(shè)置為null,并將其gdt設(shè)置為空,由于是從1開始遍歷,因此處于index=0的0號進程不會被置空。可見,sched_init()函數(shù)創(chuàng)建了0號進程。
3. 進程的創(chuàng)建——fork()函數(shù)
進行一系列初始化后,執(zhí)行了這句代碼:
move_to_user_mode();這個函數(shù)是將當(dāng)前模式由內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)。
內(nèi)核態(tài):不可搶占的
用戶態(tài):可搶占,可以進行調(diào)度的
也就是說,上述所有的初始化操作都是在內(nèi)核態(tài)執(zhí)行的,這么做的目的是,內(nèi)核初始化過程是不能被中斷的,在內(nèi)核態(tài)運行可以保證這一點。
切換到用戶態(tài)以后,便開始創(chuàng)建進程了:
if (!fork()) { init(); }這里的fork()函數(shù),就是linux創(chuàng)建進程的函數(shù),進入這個函數(shù),它的聲明為:
static inline _syscall0(int,fork)syscall是系統(tǒng)調(diào)用函數(shù),就是內(nèi)核自己實現(xiàn)的一些函數(shù),如 read,open,chmod等,這些函數(shù)可以直接提供給開發(fā)人員調(diào)用,而調(diào)用的過程需要切換到內(nèi)核態(tài)進行,因為函數(shù)調(diào)用過程中不允許被中斷。這個調(diào)用過程稱為系統(tǒng)調(diào)用。
_syscall0的函數(shù)定義如下:
#define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_##name)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \ }把參數(shù)替換成fork以后,是這個樣子:
#define _syscall0(type,fork) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_FORK)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \ }這是一段匯編代碼,這段代碼的執(zhí)行過程是這樣的:
set_system_gate(0x80,&system_call),因此產(chǎn)生0x80中斷以后會調(diào)用system_call,system_call在system_call.s中定義,
首先將各寄存器入棧,然后調(diào)用了關(guān)鍵的一個函數(shù):
call sys_call_table(,%eax,4)sys_call_table是一個數(shù)組,在sys.h中定義,它保存著所有系統(tǒng)調(diào)用的函數(shù)名,%eax就是eax寄存器的值,前面提到過0x80中斷產(chǎn)生之前將_NR_FORK=2加入到了eax寄存器中,因此調(diào)用的就是sys_call_table[2],也就是sys_fork函數(shù):
sys_fork:call find_empty_processtestl %eax,%eax js 1fpush %gspushl %esipushl %edipushl %ebppushl %eaxcall copy_processaddl $20,%esp 1: ret這是一段匯編代碼,首先調(diào)用了fork.c文件中的find_empty_process函數(shù),目的在于從task進程數(shù)組中找到一個空的槽用于保存要創(chuàng)建的進程的task_struct,這個函數(shù)會返回進程的pid。
testl %eax,%eax 指令作用是將call find_empty_process函數(shù)的返回值保存到eax寄存器中。隨后進行了一系列寄存器數(shù)的壓棧,最后調(diào)用了copy_process()函數(shù):
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,long ebx,long ecx,long edx,long fs,long es,long ds,long eip,long cs,long eflags,long esp,long ss) {struct task_struct *p;int i;struct file *f;p = (struct task_struct *) get_free_page();if (!p)return -EAGAIN;task[nr] = p;*p = *current; p->state = TASK_UNINTERRUPTIBLE;p->pid = last_pid; // 新進程號。也由find_empty_process()得到。p->father = current->pid; // 設(shè)置父進程p->counter = p->priority; // 運行時間片值p->signal = 0; // 信號位圖置0p->alarm = 0; // 報警定時值(滴答數(shù))p->leader = 0; p->utime = p->stime = 0; // 用戶態(tài)時間和和心態(tài)運行時間p->cutime = p->cstime = 0; // 子進程用戶態(tài)和和心態(tài)運行時間p->start_time = jiffies; // 進程開始運行時間(當(dāng)前時間滴答數(shù))p->tss.back_link = 0;p->tss.esp0 = PAGE_SIZE + (long) p; // 任務(wù)內(nèi)核態(tài)棧指針。p->tss.ss0 = 0x10; // 內(nèi)核態(tài)棧的段選擇符(與內(nèi)核數(shù)據(jù)段相同)p->tss.eip = eip; // 指令代碼指針p->tss.eflags = eflags; // 標(biāo)志寄存器p->tss.eax = 0; // 這是當(dāng)fork()返回時新進程會返回0的原因所在p->tss.es = es & 0xffff; // 段寄存器僅16位有效p->tss.ldt = _LDT(nr); // 任務(wù)局部表描述符的選擇符(LDT描述符在GDT中)p->tss.trace_bitmap = 0x80000000; // 高16位有效if (copy_mem(nr,p)) {task[nr] = NULL;free_page((long) p);return -EAGAIN;}set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));p->state = TASK_RUNNING; /* do this last, just in case */return last_pid; }這個函數(shù)非常長,省略了一些無關(guān)代碼,主要有以下幾個重要步驟:
*current指向當(dāng)前進程,也就是調(diào)用fork函數(shù)的進程,本過程中就是0號進程,*p指向要創(chuàng)建的進程,本過程中就是1號進程。*p=*current,這不就是將0號進程的task_struct直接賦值給了1號進程嗎?原來進程的創(chuàng)建第一步都是先把它的父進程拿來拷貝一份。
這么做的目的是當(dāng)前進程既不能處理信號,也無法參與調(diào)度。
要創(chuàng)建的進程p是通過拷貝父進程task_struct而來,但是作為一個進程,它需要有自己特定的屬性,因此需要對其特定的屬性進行賦值:
大部分屬性都容易看懂,但是有兩個地方卻暗藏玄機:
p->tss.eax = 0; p->tss.eip = eip;看似很常規(guī)的兩行代碼: 將子進程的eax寄存器設(shè)置為0,將子進程eip寄存器設(shè)置為父進程的eip寄存器值。
eax寄存器存儲著函數(shù)的返回值,而eip寄存器,存儲著cpu要去讀取的下一行指令代碼的位置,那尋根溯源一下,父進程下一行代碼是哪一行呢?
事實上,我們正在分析的fork系統(tǒng)調(diào)用的代碼并不屬于父進程執(zhí)行的代碼,它屬于內(nèi)核態(tài)程序,真正父進程執(zhí)行的代碼應(yīng)該是它產(chǎn)生軟中斷而調(diào)用fork系統(tǒng)調(diào)用的下一行,也就是這個:
#define _syscall0(type,fork) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_FORK)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \中的這一行
if (__res >= 0)這也就意味著,當(dāng)子進程開始運行的時候,會從這一行開始執(zhí)行。
進程創(chuàng)建到這里,已經(jīng)為要創(chuàng)建的進程創(chuàng)建了task_struct,并完成了task_struct初始化,也設(shè)置了進程的ldt段和tss段,那么這個進程已經(jīng)可以開始運行并可以參與調(diào)度了,因此將進程設(shè)置為就緒狀態(tài)。
就是返回子進程的id,這里返回的是一號進程的id 1。
copy_process()函數(shù)返回了,sys_fork也就返回了,返回的值就是copy_process函數(shù)的返回值,這里就是一號進程的id=1,然后就返回到system_call執(zhí)行,將各寄存器值出棧,然后0x80中斷就返回了,將切換到用戶態(tài)繼續(xù)執(zhí)行0號進程的代碼,_syscall0將繼續(xù)往下執(zhí)行
#define _syscall0(type,fork) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80"\: "=a" (__res) \: "0" (__NR_FORK)); \ if (__res >= 0) \return (type) __res; \ errno = -__res; \ return -1; \ }前面提到過_res的值就是eax,當(dāng)前0號進程eax值就是sys_fork調(diào)用的copy_process()的返回值,前面提到了copy_process()返回的是子進程1號進程的進程id,也就是1。因此if條件符合,_syscall0返回1。
到這里fork()函數(shù)執(zhí)行完畢并返回了1,回到fork調(diào)用的地方
if (!fork()) {init(); } for(;;) pause();由于fork()返回了1,所以這里if條件不符合,往下走到一段死循環(huán),循環(huán)里調(diào)用了pause()函數(shù),點開pause函數(shù)的聲明:
static inline _syscall0(int,pause)啊這。。又是這玩意,看到這里立馬就懂了,pause()函數(shù)也是一個系統(tǒng)調(diào)用,省去中間系統(tǒng)調(diào)用的過程,直接來到pause調(diào)用的函數(shù):
int sys_pause(void) {current->state = TASK_INTERRUPTIBLE;schedule();return 0; }與前面fork系統(tǒng)調(diào)用不同的是,pause系統(tǒng)調(diào)用是c語言實現(xiàn)的。pause先將進程狀態(tài)設(shè)置為可中斷睡眠狀態(tài),然后進行了一次schedule()也就是進行了一次進程調(diào)度,由于當(dāng)前只有0號和1號兩個進程,所以進程調(diào)度的結(jié)果肯定是由0號進程切換到1號進程,至于進程的調(diào)度和進程的切換,后面會有詳細(xì)介紹這里就不展開了。
現(xiàn)在正在運行的是1號進程,cpu就會找到gdt找到1號進程的ldt,就會讀取1號進程的eip寄存器去讀取指令。現(xiàn)在重點來了,我們在介紹0號進程fork1號進程的時候提示過,當(dāng)時0號進程將自己的eip寄存器值賦給了1號進程,所以1號進程eip寄存器存儲的下一行代碼是:
if (__res >= 0)_res是eax寄存器的值,而fork的時候0號進程將1號進程的eax寄存器值得設(shè)置為了0
p->tss.eax = 0;因此if條件也是符合的,就將_res 返回了,返回到哪里了呢?返回的肯定是fork()函數(shù)被調(diào)用的地方,也就是:
if (!fork()) {init(); } for(;;) pause();看到這你可能有點懵逼了,怎么又到這來了,0號進程不是已經(jīng)執(zhí)行過一次了嗎,又來。。
但是和之前不同的是,這里的fork()返回的_res值是0,是符合if條件的,然后會執(zhí)行init()。。
這就是fork()函數(shù)很神秘的地方,它實現(xiàn)了一個函數(shù)"return了兩次",一次是父進程返回,一次是子進程返回。
至于init()函數(shù),里面涉及了一些shell初始化,tty0初始化,輸入輸出設(shè)備初始化操作,跟進程的創(chuàng)建沒有太大的關(guān)系,就不繼續(xù)展開了。
4. 總結(jié)
本文以0號進程創(chuàng)建1號進程為例,分析了fork()函數(shù)詳細(xì)的過程,有以下:
總結(jié)
以上是生活随笔為你收集整理的进程的创建——fork函数的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TCP协议如何保证可靠传输
- 下一篇: 【数学建模】第一讲-层次分析法