fork()是linux的系統調用函數sys_fork()的提供給用戶的接口函數,fork()函數會實現對中斷int 0x80的調用過程并把調用結果返回給用戶程序。
fork()的函數定義是在init/main.c中(這一點我感到奇怪,因為大多數系統調用的接口函數都會單獨封裝成一個.c文件,然后在里面進行嵌入匯編展開執行int 0x80中斷從而執行相應的系統調用,如/lib/close.c中:
1 #define __LIBRARY__2 #include <unistd.h>3 4 _syscall1(int,close,int,fd)
但fork()函數確實在mai.c中進行嵌入匯編展開定義的,呃,可能是我目前還沒有完全理解這一部分,但這就是我目前的認識)
以下是init/main.c中fork()函數的嵌入匯編定義:
1 static inline _syscall0(int,fork)
其中_syscall0()是include/linux/sys/unistd.h中的內嵌宏代碼,它以嵌入匯編的形式調用linux的系統調用中斷int 0x80。
1 #define _syscall0(type,name) / 2 type name(void) / 3 { / 4 long __res; / 5 __asm__ volatile ("int $0x80" / 6 : "=a" (__res) / 7 : "0" (__NR_##name)); / 8 if (__res >= 0) / 9 return (type) __res; /10 errno = -__res; /11 return -1; /12 }
對其進行宏展開,即得到fork()函數的代碼:
1 int fork(void) 2 { 3 long __res; 4 __asm__ volatile ("int $0x80" 5 : "=a" (__res) //eax保存的是int 0x80中斷調用的返回值 6 : "0" (__NR_##fork)); //同時eax也是int 0x80中斷調用的系統調用功能號 7 if (__res >= 0) 8 return (type) __res; //返回int 0x80的返回值作為fork()函數的返回值 9 errno = -__res; 10 return -1; 11 }
理解這個函數的關鍵,就在于理解系統調用中斷int 0x80。
在main.c進行初始化時,設置好了int 0x80的系統調用中斷門。
1 void main(void) 2 { 3 ...... 4 sched_init(); //在sched_init()中設置了系統調用中斷int 0x80的中斷門 5 ...... 6 move_to_user_mode(); 7 if (!fork()) { /* we count on this going ok */ 8 init(); 9 }10 for(;;) pause();11 }
sched_init()定義在kernel/sched.c中:
1 void sched_init(void)2 {3 ......4 set_system_gate(0x80,&system_call); //在IDT中設置系統調用中斷int 0x80的描述符5 }
其中,set_system_gate(0x80,&system_call)的宏定義在文件include/asm/system.h中:
1 #define _set_gate(gate_addr,type,dpl,addr) / 2 __asm__ ("movw %%dx,%%ax/n/t" / 3 "movw %0,%%dx/n/t" / 4 "movl %%eax,%1/n/t" / 5 "movl %%edx,%2" / 6 : / 7 : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), / 8 "o" (*((char *) (gate_addr))), / 9 "o" (*(4+(char *) (gate_addr))), /10 "d" ((char *) (addr)),"a" (0x00080000))11 ......12 #define set_system_gate(n,addr) /13 _set_gate(&idt[n],15,3,addr)
其作用也就是填寫IDT中int 0x80的中斷描述符,中斷描述符的格式如下:
輸入參數:%0: i(立即數) = 0x8000(0b1000,0000,0000,0000)
| (dpl<<13)(0b0110,0000,0000,0000)
| (type<<8)(0b0000,1111,0000,0000)
= 0b1110,1111,0000,0000
?。ㄟ@里我有個疑問,按照編號%0指的是輸出寄存器,雖然這里并沒有用到輸出寄存器,但%0是否依然指的是輸出寄存器?)
o(內存變量) =%2: (高四位) *(4+&idt[0x80])
%1: (低四位)*(&idt[0x80])
%3: d(edx,32位)=&system_callcall
%4: a(eax,32位)=0x0008,0000(0b0000,0000,0000,1000,0000,0000,0000,0000)
匯編語句的執行過程:
1 movw %%dx,%%ax //ax=dx,即eax的低兩個字節等于edx的低兩個字節,也就是&system_call的低兩個字節2 movw %0,%%dx //dx=i(0b1110,1111,0000,0000),即edx的低兩個字節等于i3 movl %%eax,%1 //*(&idt[0x80]) = 0000,0000,0000,1000 &system_call(低兩個字節)4 movl %%edx,%2 //*(4+&idt[0x80]) = &system_call(高兩個字節) 1110,1111,0000,0000
這樣,IDT表中的int 0x80的中斷門描述符就填寫好了,如下所示:
(這里我還存在一個疑問,就是int 0x80中斷門描述符的高四位中的第8位填充的是1,但表中要求是寫0)
int 0x80的中斷調用是一個有趣的過程:
首先應用程序通過庫函數fork()調用系統調用sys_fork(),由于應用程序運行在特權級3,是不能訪問內核代碼的中斷處理函數system_call()以及system_call()要進一步調用的具體系統調用函數sys_fork(),所以在int 0x80初始化在填寫IDT表中int 0x80的描述符時,將其DPL置為3,這樣應用程序得以進入內核,而在跳轉到中斷處理函數system_call()時,將對應的選擇符置為0000,0000,0000,1000,即cs=0b0000,0000,0000,1000,表示訪問特權級為0、使用全局描述符表GDT中的第2個段描述符項(內核代碼段),即訪問的基地址是內核代碼段,偏移地址是system_call()的代碼,使其又變為以最高特權級0訪問system_call()函數,這樣就完成了從應用程序到內核代碼段的轉移。
并且在執行int 0x80中斷時,會發生堆棧的切換,即從用戶棧切換到用戶的內核堆棧。具體過程是:
處理器從當前執行任務的TSS段中得到中斷處理過程使用的用戶內核堆棧的段選擇符和棧指針(例如tss.ss0、tss.esp0)。然后將應用程序的用戶棧的段選擇符和棧指針壓棧,接著將EFLAGS、CS、Eip的值也壓棧,而此刻EIP的值就是fork()函數中嵌入匯編int 0x80后的下一條指令的地址,即指令:
5 : "=a" (__res)
1 int fork(void) 2 { 3 long __res; 4 __asm__ volatile ("int $0x80" 5 : "=a" (__res) //eax保存的是int 0x80中斷調用的返回值 6 : "0" (__NR_##fork)); //同時eax也是int 0x80中斷調用的系統調用功能號 7 if (__res >= 0) 8 return (type) __res; //返回int 0x80的返回值作為fork()函數的返回值 9 errno = -__res; 10 return -1; 11 }這一點對于理解為什么fork()函數返回時子進程的返回值是0非常關鍵。接下來我們要關注下system_call的執行過程。system_call函數在kernel/system_call.s中:
1 nr_system_calls = 72 2 3 bad_sys_call: 4 movl $-1,%eax 5 iret 6 7 reschedule: 8 pushl $ret_from_sys_call 9 jmp schedule10 11 system_call:12 cmpl $nr_system_calls-1,%eax #檢查eax中的功能號是否有效(在給定的范圍內)13 ja bad_sys_call #跳轉到bad_sys_call,即eax=-1,中斷返回14 push %ds15 push %es16 push %fs17 pushl %edx18 pushl %ecx # 將edx,ecx,ebx壓棧,作為system_call的參數19 pushl %ebx20 movl $0x10,%edx # ds,es用于內核數據段21 mov %dx,%ds22 mov %dx,%es23 movl $0x17,%edx # fs用于用戶數據段24 mov %dx,%fs25 call sys_call_table(,%eax,4) # 跳轉到對應的系統調用函數中,此處是sys_fork()26 pushl %eax # 把系統調用的返回值入棧27 movl current,%eax # 取當前任務數據結構地址->eax28 cmpl $0,state(%eax) # 判斷當前任務的狀態29 jne reschedule # 不在就緒狀態(state != 0)就去執行調度程序schedule()30 cmpl $0,counter(%eax) # 判斷當前任務時間片是否已用完31 je reschedule # 時間片已用完(counter = 0)也去執行調度程序schedule()32 ret_from_sys_call: # 執行完調度程序schedule()返回或沒有執行調度程序直接到該處33 movl current,%eax # task[0] cannot have signals34 cmpl task,%eax # 判斷當前任務是否是初始任務task035 je 3f # 如果是不必對其進行信號量方面的處理,直接退出中斷36 cmpw $0x0f,CS(%esp) # 判斷調用程序是否是用戶任務37 jne 3f # 如果不是,直接退出中斷38 cmpw $0x17,OLDSS(%esp) # 判斷是否為用戶代碼段的選擇符39 jne 3f # 如果不是,則說明是某個中斷服務程序跳轉到這里,直接退出中斷40 movl signal(%eax),%ebx # 處理當前用戶任務中的信號41 movl blocked(%eax),%ecx42 notl %ecx43 andl %ebx,%ecx44 bsfl %ecx,%ecx45 je 3f46 btrl %ecx,%ebx47 movl %ebx,signal(%eax)48 incl %ecx49 pushl %ecx50 call do_signal51 popl %eax52 3: popl %eax # eax含有系統調用的返回值53 popl %ebx54 popl %ecx55 popl %edx56 pop %fs57 pop %es58 pop %ds59 iret # 中斷返回
這里說明了系統調用int 0x80的中斷處理過程。每次執行完對應的系統調用,操作系統都會檢查本次調用進程的狀態。如果由于上面的系統調用操作或其他情況而使進程的狀態從執行態變成了其他狀態,或者由于進程的時間片已經用完,則調用進程調度函數schedule()。schedule()也是個有趣的函數,schedule()會從就緒隊列中選擇一個就緒進程,將此就緒進程與當前進程執行狀態切換,而跳轉到新的進程中去(即選中的就緒進程),只有當schedule()執行進程切換,再次切換回當前進程時,此次的中斷調用int 0x80才會繼續返回執行,進行信號處理,并中斷返回。對于schedule()函數的理解,也是理解為什么fork()函數父子進程返回值不同的關鍵點。
接下來看一下系統調用sys_fork(),它定義在kernel/system_call.s中:
1 sys_fork: 2 call find_empty_PRocess # 調用find_empty_process() 3 testl %eax,%eax # 在eax中返回進程號pid。若返回負數則退出 4 js 1f 5 push %gs 6 pushl %esi 7 pushl %edi 8 pushl %ebp 9 pushl %eax10 call copy_process # 調用c函數 copy_process()11 addl $20,%esp # 丟棄這里所有壓棧內容12 1: ret
其中,find_empty_process()和copy_process()在kernel/fork.c中定義:
1 int copy_mem(int nr,struct task_struct * p) 2 { 3 unsigned long old_data_base,new_data_base,data_limit; 4 unsigned long old_code_base,new_code_base,code_limit; 5 6 code_limit=get_limit(0x0f); 7 data_limit=get_limit(0x17); 8 old_code_base = get_base(current->ldt[1]); 9 old_data_base = get_base(current->ldt[2]);10 if (old_data_base != old_code_base)11 panic("We don't support separate I&D");12 if (data_limit < code_limit)13 panic("Bad data_limit");14 new_data_base = new_code_base = nr * 0x4000000;15 p->start_code = new_code_base;16 set_base(p->ldt[1],new_code_base);17 set_base(p->ldt[2],new_data_base);18 if (copy_page_tables(old_data_base,new_data_base,data_limit)) { //復制當前進程(父進程)的頁目錄表項和頁表項作為子進程的頁目錄表項和頁表項,則子進程共享父進程的內存頁面19 printk("free_page_tables: from copy_mem/n");20 free_page_tables(new_data_base,data_limit);21 return -ENOMEM;22 }23 return 0;24 }25 26 /*27 * Ok, this is the main fork-routine. It copies the system process28 * information (task[nr]) and sets up the necessary registers. It29 * also copies the data segment in it's entirety.30 */31 int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,32 long ebx,long ecx,long edx,33 long fs,long es,long ds,34 long eip,long cs,long eflags,long esp,long ss) //該函數的參數是進入系統調用中斷處理過程(system_call.s)開始,直到sys_fork()和調用copy_process()前時逐步壓入棧的各寄存器的值,所以新建子進程的狀態會保持為父進程即將進入中斷過程前的狀態35 {36 struct task_struct *p;37 int i;38 struct file *f;39 40 p = (struct task_struct *) get_free_page();41 if (!p)42 return -EAGAIN;43 task[nr] = p;44 *p = *current; /* NOTE! this doesn't copy the supervisor stack */45 p->state = TASK_UNINTERRUPTIBLE; //先將新進程的狀態置為不可中斷等待狀態,以防止內核調度其執行46 p->pid = last_pid; //新進程號,由find_empty_process()得到47 p->father = current->pid;48 p->counter = p->priority;49 p->signal = 0;50 p->alarm = 0;51 p->leader = 0; /* process leadership doesn't inherit */52 p->utime = p->stime = 0;53 p->cutime = p->cstime = 0;54 p->start_time = jiffies;55 p->tss.back_link = 0;56 p->tss.esp0 = PAGE_SIZE + (long) p; //任務內核棧指針指向系統給任務結構p分配的1頁新內存的頂端57 p->tss.ss0 = 0x10; //內核態棧的段選擇符(與內核數據段相同)58 p->tss.eip = eip;59 p->tss.eflags = eflags;60 p->tss.eax = 0; //這是當fork()返回時新進程會返回0的原因所在61 p->tss.ecx = ecx;62 p->tss.edx = edx;63 p->tss.ebx = ebx;64 p->tss.esp = esp;65 p->tss.ebp = ebp;66 p->tss.esi = esi;67 p->tss.edi = edi;68 p->tss.es = es & 0xffff;69 p->tss.cs = cs & 0xffff;70 p->tss.ss = ss & 0xffff;71 p->tss.ds = ds & 0xffff;72 p->tss.fs = fs & 0xffff;73 p->tss.gs = gs & 0xffff;74 p->tss.ldt = _LDT(nr);75 p->tss.trace_bitmap = 0x80000000;76 if (last_task_used_math == current)77 __asm__("clts ; fnsave %0"::"m" (p->tss.i387));78 if (copy_mem(nr,p)) {79 task[nr] = NULL;80 free_page((long) p);81 return -EAGAIN;82 }83 for (i=0; i<NR_OPEN;i++)84 if ((f=p->filp[i]))85 f->f_count++;86 if (current->pwd)87 current->pwd->i_count++;88 if (current->root)89 current->root->i_count++;90 if (current->executable)91 current->executable->i_count++;92 //在GDT表中設置新任務TSS段和LDT段描述符項93 set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));94 set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));95 p->state = TASK_RUNNING; /* do this last, just in case */96 //最后才將新任務置成就緒態,以防萬一97 return last_pid; //最后返回新進程的pid98 }
雖然操作系統為新進程在GDT表中設置了它的TSS段和LDT段的描述符項,也為其在線性地址空間設置了它的頁目錄項和頁表項,但由于其頁目錄項和頁表項是復制父進程的,所以內核并不會立刻為新進程分配代碼和數據內存頁。新進程將與父進程共同使用父進程已有的代碼和數據內存頁面。只有當以后執行過程中如果其中有一個進程以寫方式訪問內存時被訪問的內存頁面才會在寫操作前被復制到新申請的內存頁面中。而此后父進程和子進程就各有擁有其獨立的頁面。
這里我們可以看到,對于父進程來說,當它使用接口函數fork()引發系統調用,到進入系統調用中斷int 0x80執行相應的系統調用中斷處理過程(system_call.s)以及調用對應的系統調用函數(sys_fork()),再到可能被schedule()函數調度讓出CPU使用權,到最后重新得到CPU使用權從int 0x80中斷返回,父進程的返回值就是新建子進程的pid。而子進程當被schedule()函數調度獲得CPU的使用權后,它會繼續執行int 0x80下面的那條指令,即:
5 : "=a" (__res)又由于已經將子進程TSS中的eax置為0,所以當子進程被切換入運行態時,將會把子進程TSS段的各寄存器的值作為CPU此時各寄存器的值,然后執行標號5的指令,將eax=0作為中斷調用的返回值返回到fork()函數結尾處,所以對于子進程來說,它的返回值是0。
好累,第一次寫博客,終于完成了,用了將近一天。
新聞熱點
疑難解答