【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5597818.html 】 據說安卓應用里通過fork子進程的方式可以防止應用被殺,大概原理就是子進程被殺會向父進程發送信號什麼的,就不深究了。 首先fork()函數它是一個系統調用,在sys.h ...
【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5597818.html 】
據說安卓應用里通過fork子進程的方式可以防止應用被殺,大概原理就是子進程被殺會向父進程發送信號什麼的,就不深究了。
首先fork()函數它是一個系統調用,在sys.h中:
extern int sys_fork (); // 創建進程。 (kernel/system_call.s, 208) // 系統調用函數指針表。用於系統調用中斷處理程式(int 0x80),作為跳轉表。 fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, ...}
前面有文章對系統調用做過詳細分析,main.c中的:
static inline _syscall0 (int, fork)
將__NR_fork也就是2和0x80中斷綁定了,剛好對應的是上面數組的sys_fork函數,在system_call.s中:
#### sys_fork()調用,用於創建子進程,是system_call 功能2。原形在include/linux/sys.h 中。 # 首先調用C 函數find_empty_process(),取得一個進程號pid。若返回負數則說明目前任務數組 # 已滿。然後調用copy_process()複製進程。 .align 2 _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來尋找任務數組中還未使用的編號,在fork.c中:
// 為新進程取得不重覆的進程號last_pid,並返回在任務數組中的任務號(數組index)。 int find_empty_process (void) { int i; repeat: // 如果last_pid 增1 後超出其正數表示範圍,則重新從1 開始使用pid 號。 if ((++last_pid) < 0) last_pid = 1; // 在任務數組中搜索剛設置的pid 號是否已經被任何任務使用。如果是則重新獲得一個pid 號。 for (i = 0; i < NR_TASKS; i++) if (task[i] && task[i]->pid == last_pid) goto repeat; // 在任務數組中為新任務尋找一個空閑項,並返回項號。last_pid 是一個全局變數,不用返回。 for (i = 1; i < NR_TASKS; i++) // 任務0 排除在外。 if (!task[i]) return i; // 如果任務數組中64 個項已經被全部占用,則返回出處碼。 return -EAGAIN; }
這個函數比較好理解,接下來看find_empty_process的返回值是保存在eax中,如果為負數則直接跳出sys_fork,否則push一堆指令,作為copy_process的參數,也在fork.c中:
/* * OK,下麵是主要的fork 子程式。它複製系統進程信息(task[n])並且設置必要的寄存器。 * 它還整個地複製數據段。 */ // 複製進程。 // 其中參數nr 是調用find_empty_process()分配的任務數組項號。none 是system_call.s 中調用 // sys_call_table 時壓入堆棧的返回地址。 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 分配了1 頁新記憶體,所以此時esp0 正好指向該頁頂端。ss0:esp0 用於作為程式 // 在內核態執行時的堆棧。 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; // 這是當fork()返回時,新進程會返回0 的原因所在。 p->tss.ecx = ecx; p->tss.edx = edx; p->tss.ebx = ebx; p->tss.esp = esp; // 新進程完全複製了父進程的堆棧內容。因此要求task0 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); // 設置新任務的局部描述符表的選擇符(LDT 描述符在GDT 中)。 p->tss.trace_bitmap = 0x80000000; //(高16 位有效)。 // 如果當前任務使用了協處理器,就保存其上下文。彙編指令clts 用於清除控制寄存器CR0 中的任務 // 已交換(TS)標誌。每當發生任務切換,CPU 都會設置該標誌。該標誌用於管理數學協處理器:如果 // 該標誌置位,那麼每個ESC 指令都會被捕獲。如果協處理器存在標誌也同時置位的話那麼就會捕獲 // WAIT 指令。因此,如果任務切換髮生在一個ESC 指令開始執行之後,則協處理器中的內容就可能需 // 要在執行新的ESC 指令之前保存起來。錯誤處理句柄會保存協處理器的內容並複位TS 標誌。 // 指令fnsave 用於把協處理器的所有狀態保存到目的操作數指定的記憶體區域中(tss.i387)。 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; // 返回新進程號(與任務號是不同的)。 }
這裡有問題需要註意一下,為什麼copy_process有那麼多參數,而sys_fork才push了5個寄存器,這是因為根據系統調用機制,調用sys_fork之前是先調用的system_call函數,已經往棧壓入了一堆寄存器,這就對應上了。
首先為新任務數據結構分配記憶體(註意這裡是數據結構不是任務本身),get_free_page放在後面的記憶體管理文章分析,fork函數和記憶體管理memory.c是息息相關的。這裡只要知道這個函數是獲取到主記憶體區的一頁空閑頁面並返回這個頁面的地址。
接下來的比較好理解,複製當前進程的進程描述符到新任務中,並對各個屬性重新賦值。這裡值得註意的是p->father = current->pid表示新任務的父進程就是當前進程。
接下來設置esp0指向剛新分配的頁記憶體的頂端,ss0為內核數據段選擇子,因為內核數據段描述符中的基址為0,所以ss0:esp0用作程式在內核態執行時的堆棧。
接下來p->tss.ldt = _LDT (nr);設置ldt的索引號,也就是LDT在GDT中的選擇子。
下麵是最關鍵的函數copy_mem:
// 設置新任務的代碼和數據段基址、限長並複製頁表。 // nr 為新任務號;p 是新任務數據結構的指針。 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"); // 創建中新進程線上性地址空間中的基地址等於64MB * 其任務號。 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; }
首先取局部描述符表(LDT自身的描述符表)中代碼和數據段描述符中的限長,在sched.h中:
// 取段選擇符segment 的段長值。 // %0 - 存放段長值(位元組數);%1 - 段選擇符segment。 #define get_limit(segment) ({ \ unsigned long __limit; \ __asm__( "lsll %1,%0\n\tincl %0": "=r" (__limit): "r" (segment)); \ __limit;})
因為在進程描述符結構中有個
struct desc_struct ldt[3];// struct desc_struct ldt[3] 本任務的局部表描述符。0-空,1-代碼段cs,2-數據和堆棧段ds&ss。
這表示的是LDT描述符表自身,第一個描述符為空,第二個描述符也就是8-15位元組是代碼段,又因為描述符的0-15位是段限長,所以取的是0x0f,然後第三個描述符也就是16-23位元組是數據段,所以取0x17.
接下來是取當前進程的ldt的代碼段的基地址:
// 從地址addr 處描述符中取段基地址。功能與_set_base()正好相反。 // edx - 存放基地址(__base);%1 - 地址addr 偏移2;%2 - 地址addr 偏移4;%3 - addr 偏移7。 #define _get_base(addr) ({\ unsigned long __base; \ __asm__( "movb %3,%%dh\n\t" \ // 取[addr+7]處基址高16 位的高8 位(位31-24)??dh。 "movb %2,%%dl\n\t" \ // 取[addr+4]處基址高16 位的低8 位(位23-16)??dl。 "shll $16,%%edx\n\t" \ // 基地址高16 位移到edx 中高16 位處。 "movw %1,%%dx" \ // 取[addr+2]處基址低16 位(位15-0)??dx。 :"=d" (__base) \ // 從而edx 中含有32 位的段基地址。 :"m" (*((addr) + 2)), "m" (*((addr) + 4)), "m" (*((addr) + 7))); __base; } ) // 取局部描述符表中ldt 所指段描述符中的基地址。 #define get_base(ldt) _get_base( ((char *)&(ldt)) )
current->ldt[1]為當前進程的ldt的代碼段描述符項的內容,所以這裡就不難理解了,就是從描述符項的內容中提取基地址。
接下來設置新進程的線性地址的基地址,linus給每個程式(進程)劃分了64MB的虛擬記憶體空間,所以新基址就是任務號*64MB。
再接著就是往新進程的LDT表中的段描述符設置基地址了,原理類似。
copy_page_tables和free_page_tables放到後面一篇講解。
最後面是設置新任務的TSS和LDT描述符項,在進程調度的初始化中講解過。
最後返回新進程號。
至此fork函數分析結束。