Linux內核創建一個進程的過程分析
來自: http://blog.csdn.net/xingjiarong/article/details/50920207
不管在什么系統中,所有的任務都是以進程為載體的,所以理解進程的創建對于理解操作系統的原理是非常重要的,本文是我在學習linux內核中所做的筆記,如有錯誤還請大家批評指正。注:我所閱讀的內核版本是0.11。
一、關于PCB
對于一個進程來說,PCB就好像是他的記賬先生,當一個進程被創建時PCB就被分配,然后有關進程的所有信息就全都存儲在PCB中,例如,打開的文件,頁表基址寄存器,進程號等等。在linux中PCB是用結構task_struct來表示的,我們首先來看一下task_struct的組成。
代碼位于linux/include/linux/Sched.h
struct task_struct {long state; //表示進程的狀態,-1表示不可執行,0表示可執行,>0表示停止 long counter;/* 運行時間片,以jiffs遞減計數 */ long priority; /* 運行優先數,開始時,counter = priority,值越大,表示優先數越高,等待時間越長. */ long signal;/* 信號.是一組位圖,每一個bit代表一種信號. */ struct sigaction sigaction[32]; /* 信號響應的數據結構, 對應信號要執行的操作和標志信息 */ long blocked; /* 進程信號屏蔽碼(對應信號位圖) */
/ various fields / int exit_code; / 任務執行停止的退出碼,其父進程會取 / unsigned long start_code,end_code,end_data,brk,start_stack;/ start_code代碼段地址,end_code代碼長度(byte), end_data代碼長度+數據長度(byte),brk總長度(byte),start_stack堆棧段地址 / long pid,father,pgrp,session,leader;/ 進程號,父進程號 ,父進程組號,會話號,會話頭(發起者)/ unsigned short uid,euid,suid;/ 用戶id 號,有效用戶 id 號,保存用戶 id 號/ unsigned short gid,egid,sgid;/ 組標記號 (組id),有效組 id,保存的組id / long alarm;/ 報警定時值 (jiffs數) / long utime,stime,cutime,cstime,start_time;/ 用戶態運行時間 (jiffs數), 系統態運行時間 (jiffs數),子進程用戶態運行時間,子進程系統態運行時間,進程開始運行時刻 / unsigned short used_math;/ 是否使用了協處理器 / / file system info / int tty; / 進程使用tty的子設備號. -1表示設有使用 / unsigned short umask; / 文件創建屬性屏蔽位 / struct m_inode pwd; / 當前工作目錄 i節點結構 / struct m_inode root; / 根目錄i節點結構 / struct m_inode executable;/ 執行文件i節點結構 / unsigned long close_on_exec; / 執行時關閉文件句柄位圖標志. / struct file filp[NR_OPEN]; / 文件結構指針表,最多32項. 表項號即是文件描述符的值 / struct desc_struct ldt[3]; / 任務局部描述符表.0-空,1-cs段,2-Ds和Ss段 / struct tss_struct tss; / 進程的任務狀態段信息結構 / };</pre>
二、進程的創建
系統中的進程是由父進程調用fork()函數來創建的,那么調用fork()函數的時候究竟會發生什么呢?
1、引發0x80中斷
進程1是由進程0通過fork()創建的,其中的fork代碼如下:
init/main.c
#define _syscall0(type,name) / type name(void) / { / long __res; / __asm__ volatile ( "int $0x80" / // 調用系統中斷0x80。 :"=a" (__res) / // 返回值??eax(__res)。 :"0" (__NR_##name)); / // 輸入為系統中斷調用號__NR_name。 if (__res >= 0) / // 如果返回值>=0,則直接返回該值。 return (type) __res; errno = -__res; / // 否則置出錯號,并返回-1。 return -1;}這樣使用int 0x80中斷,調用sys_fork系統調用來創建進程。
2、sys_fork()
_sys_fork: call _find_empty_process # 調用find_empty_process()(kernel/fork.c,135)。 testl %eax,%eax js 1f push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process # 調用C 函數copy_process()(kernel/fork.c,68)。 addl $20,%esp # 丟棄這里所有壓棧內容。 1: ret雖然是一段匯編代碼,但是我們可以很清楚的看到首先調用的是find_empty_process(),然后又調用了copy_process(),而這兩個函數就是fork.c中的函數。下面我們來看一下這兩個函數。
3、find_empty_process()
// 為新進程取得不重復的進程號last_pid,并返回在任務數組中的任務號(數組index)。 int find_empty_process (void)
{
int i;repeat:
if ((++last_pid) < 0)
last_pid = 1;
for (i = 0; i < NR_TASKS; i++)
if (task[i] && task[i]->pid == last_pid)
goto repeat;
for (i = 1; i < NR_TASKS; i++) // 任務0 排除在外。 if (!task[i])
return i;
return -EAGAIN;
} </pre>find_empty_process的作用就是為所要創建的進程分配一個進程號。在內核中用全局變量last_pid來存放系統自開機以來累計的進程數,也將此變量用作新建進程的進程號。內核第一次遍歷task[64],如果&&條件成立說明last_pid已經被別的進程使用了,所以++last_pid,直到獲取到新的進程號。第二次遍歷task[64],獲得第一個空閑的i,也就是任務號。因為在linux0.11中,最多允許同時執行64個進程,所以如果當前的進程已滿,就會返回-EAGAIN。
4、copy_process()
獲得進程號并且將一些寄存器的值壓棧后,開始執行copy_process(),該函數主要負責以下的內容。
- 為子進程創建task_struct,將父進程的task_struct復制給子進程。
- 為子進程的task_struct,tss做個性化設置。
- 為子進程創建第一個頁表,也將父進程的頁表內容賦給這個頁表。
- 子進程共享父進程的文件。
- 設置子進程的GDT項。
- 最后將子進程設置為就緒狀態,使其可以參與進程間的輪轉。
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; // 將新任務結構指針放入任務數組中。 // 其中nr 為任務號,由前面find_empty_process()返回。 p = current; / NOTE! this doesn't copy the supervisor stack /
/ 注意!這樣做不會復制超級用戶的堆棧 / (只復制當前進程內容)。
p->state = TASK_UNINTERRUPTIBLE; // 將新進程的狀態先置為不可中斷等待狀態。 p->pid = last_pid; // 新進程號。由前面調用find_empty_process()得到。 p->father = current->pid; // 設置父進程號。 p->counter = p->priority;
p->signal = 0; // 信號位圖置0。 p->alarm = 0;
p->leader = 0; / process leadership doesn't inherit /
/ 進程的領導權是不能繼承的 /
p->utime = p->stime = 0; // 初始化用戶態時間和核心態時間。 p->cutime = p->cstime = 0; // 初始化子進程用戶態和核心態時間。 p->start_time = jiffies; // 當前滴答數時間。 // 以下設置任務狀態段TSS 所需的數據(參見列表后說明)。 p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p; // 堆棧指針(由于是給任務結構p 分配了1 頁 // 新內存,所以此時esp0 正好指向該頁頂端)。 p->tss.ss0 = 0x10; // 堆棧段選擇符(內核數據段)[??]。 p->tss.eip = eip; // 指令代碼指針。 p->tss.eflags = eflags; // 標志寄存器。 p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff; // 段寄存器僅16 位有效。 p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT (nr); // 該新任務nr 的局部描述符表選擇符(LDT 的描述符在GDT 中)。 p->tss.trace_bitmap = 0x80000000;
// 如果當前任務使用了協處理器,就保存其上下文。 if (last_task_used_math == current)
asm ("clts ; fnsave %0"::"m" (p->tss.i387));
// 設置新任務的代碼和數據段基址、限長并復制頁表。如果出錯(返回值不是0),則復位任務數組中 // 相應項并釋放為該新任務分配的內存頁。 if (copy_mem (nr, p))
{ // 返回不為0 表示出錯。 task[nr] = NULL;
free_page ((long) p);
return -EAGAIN;
}
// 如果父進程中有文件是打開的,則將對應文件的打開次數增1。 for (i = 0; i < NR_OPEN; i++)
if (f = p->filp[i])
f->f_count++;
// 將當前進程(父進程)的pwd, root 和executable 引用次數均增1。 if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
// 在GDT 中設置新任務的TSS 和LDT 描述符項,數據從task 結構中取。 // 在任務切換時,任務寄存器tr 由CPU 自動加載。 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; // 返回新進程號(與任務號是不同的)。 }</pre>進入copy_prossess函數后,調用get_free_page()函數,在主內存申請一個空閑頁面,并將申請到的頁面清0。將這個頁面的指針強制類型轉化成task_struct類型的指針,并掛接在task[nr]上,nr就是在find_empty_process中返回的任務號。
接下來的 *p=*current 將當前進程的指針賦給了子進程的,也就是說子進程繼承了父進程一些重要的屬性,當然這是不夠的,所以接下來的一大堆代碼都是為子進程做個性化設置的。
一般來講,每個進程都要加載屬于自己的代碼、數據,所以copy_process設置子進程的內存地址。通過copy_mem來設置新任務的代碼和數據段基址、限長并復制頁表。
int copy_mem (int nr, struct task_struct *p)
{
unsigned long old_data_base, new_data_base, data_limit;
unsigned long old_code_base, new_code_base, code_limit;code_limit = get_limit (0x0f); // 取局部描述符表中代碼段描述符項中段限長。 data_limit = get_limit (0x17); // 取局部描述符表中數據段描述符項中段限長。 old_code_base = get_base (current->ldt[1]); // 取原代碼段基址。 old_data_base = get_base (current->ldt[2]); // 取原數據段基址。 if (old_data_base != old_code_base) // 0.11 版不支持代碼和數據段分立的情況。 panic ("We don't support separate I&D");
if (data_limit < code_limit) // 如果數據段長度 < 代碼段長度也不對。 panic ("Bad data_limit");
new_data_base = new_code_base = nr 0x4000000; // 新基址=任務號64Mb(任務大小)。 p->start_code = new_code_base;
set_base (p->ldt[1], new_code_base); // 設置代碼段描述符中基址域。 set_base (p->ldt[2], new_data_base); // 設置數據段描述符中基址域。 if (copy_page_tables (old_data_base, new_data_base, data_limit))
{ // 復制代碼和數據段。 free_page_tables (new_data_base, data_limit); // 如果出錯則釋放申請的內存。 return -ENOMEM;
}
return 0;
} </pre>然后是對文件,pwd等資源的修改,接著要設置子進程在GDT中的表項,最后將進程設置為就緒狀態,并返回進程號。
三、創建過程總結
可以將上面繁瑣的創建過程總結為一下的幾步:
1、調用fork()函數引發0x80中斷
2、調用sys_fork
3、通過find_empty_process為新進程分配一個進程號
4、通過copy_process函數使子進程復制父進程的資源,并進行一些個性化設置后,返回進程號。