Linux0.11內核--fork進程分析

来源:http://www.cnblogs.com/joey-hua/archive/2016/06/19/5597818.html
-Advertisement-
Play Games

【版權所有,轉載請註明出處。出處: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函數分析結束。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 因為有萬惡的 IE 存在,所以當Web項目初始化併進入開發階段時。 如果是項目經理,需要很明確的知道客戶將會用什麼瀏覽器來訪問系統。 明確知道限定瀏覽器的情況下,你才能從容的讓手下的封裝必要的前端組件。 本篇文章試圖從常見的上傳方式和組件進行分析,僅局限與前端,至於後端需依據業務複雜度,自行拿捏實現 ...
  • 首先要加入類庫GDataXMLNode和JSON 解析本地文件Students.txt <students> <student> <name>湯姆 </name> <age>20</age> <phone>13049640144</phone> </student> <student> <name> ...
  • 在iOS學習23之事件處理中,小編詳細的介紹了事件處理,在這裡小編敘述一下它的相關原理 1、UITouch對象 在觸摸事件的處理方法中都會有一個存放著UITouch對象的集合,這個參數有什麼用呢? (1)UITouch 對象的簡介 當用戶用一根手指觸摸屏幕時,會創建一個與手指相關聯的 UITouch ...
  • 誤解一:安卓是iOS的後輩 不知不覺,安卓已經成為了世界上最流行的移動智能系統,就市場占有率來看,安卓甚至要高於引領了智能機和平板電腦革命的iOS。安卓的紅火深遠地影響了IT行業,全球最大的社交網路Facebook甚至倡議員工棄用iOS改換安卓手機以更深入地瞭解用戶體驗 但是,流行總伴隨著流言,安卓 ...
  • 【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5598451.html 】 在上一篇的fork函數中,首先一上來就調用get_free_page為新任務的數據結構申請一頁記憶體,在memory.c中: 上面有幾個指令比較陌生,先介紹repne s ...
  • WWDC 2016 大會之後,蘋果公司發佈了四個全新平臺:iOS,macOS,watchOS 和 tvOS。並且在此之後,蘋果應用商店審核條款也同時進行了更新——貌似不算進行了更新,簡直就是重寫!上個版本的 30 個章節被修改成了 5 大章節,但原版英文版字數從 5000 多個英文單詞增加到了 60 ...
  • 一.繼承關係 二.概述 TAB的容器。這個對象包含兩個子元素: 三.常用方法 四.三個內部類 TabHost.TabSpec tab(標簽)有一個indicator,content後臺tag.例如: 1.indicator 有三個重載的方法可以設置標簽的名字和圖案。返回值都是TabHost.TabS ...
  • 便利的初始化view以及設置tag值 效果 源碼 https://github.com/YouXianMing/iOS-Project-Examples 中的 SetRect 細節 需要實現協議(用NSMapTable的strongToWeakObjectsMapTable來作為存儲string - ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...