fork()是linux的系統調用函數sys_fork()的提供給用戶的接口函數,fork()函數會實現對中斷int 0x80的調用過程並把調用結果返回給用戶程序。linux
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()函數的嵌入彙編定義:this
1 static inline _syscall0(int,fork)
其中_syscall0()是include/linux/sys/unistd.h中的內嵌宏代碼,它以嵌入彙編的形式調用linux的系統調用中斷int 0x80。spa
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的系統調用中斷門。code
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中:orm
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中:blog
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的低兩個字節等於i 3 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 schedule 10 11 system_call: 12 cmpl $nr_system_calls-1,%eax #檢查eax中的功能號是否有效(在給定的範圍內) 13 ja bad_sys_call #跳轉到bad_sys_call,即eax=-1,中斷返回 14 push %ds 15 push %es 16 push %fs 17 pushl %edx 18 pushl %ecx # 將edx,ecx,ebx壓棧,做爲system_call的參數 19 pushl %ebx 20 movl $0x10,%edx # ds,es用於內核數據段 21 mov %dx,%ds 22 mov %dx,%es 23 movl $0x17,%edx # fs用於用戶數據段 24 mov %dx,%fs 25 call sys_call_table(,%eax,4) # 跳轉到對應的系統調用函數中,此處是sys_fork() 26 pushl %eax # 把系統調用的返回值入棧 27 movl current,%eax # 取當前任務數據結構地址->eax 28 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 signals 34 cmpl task,%eax # 判斷當前任務是不是初始任務task0 35 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),%ecx 42 notl %ecx 43 andl %ebx,%ecx 44 bsfl %ecx,%ecx 45 je 3f 46 btrl %ecx,%ebx 47 movl %ebx,signal(%eax) 48 incl %ecx 49 pushl %ecx 50 call do_signal 51 popl %eax 52 3: popl %eax # eax含有系統調用的返回值 53 popl %ebx 54 popl %ecx 55 popl %edx 56 pop %fs 57 pop %es 58 pop %ds 59 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 %eax 10 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 process 28 * information (task[nr]) and sets up the necessary registers. It 29 * 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; //最後返回新進程的pid 98 }
雖然操做系統爲新進程在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。
好累,第一次寫博客,終於完成了,用了將近一天。