進程(下)

參考:進程(上)

三. 進程切換

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。這稱爲進程切換。

3.1 硬件上下文

儘管每個進程可以擁有屬於自己的地址空間,但所有進程必須共享CPU寄存器。因此,在恢復一個進程的執行之前,內核必須確保每個寄存器裝入了掛起進程時的值。
進程恢復執行前必須裝入寄存器的一組數據稱爲硬件上下文。硬件上下文是進程可執行上下文的一個子集,因爲可執行上下文包含進程執行時需要的所有信息。在Linux中,(intel)進程硬件上下文存放在TSS段,而剩餘部分存放在內核態堆棧中。
進程切換隻發生在內核態。在執行進程切換之前,用戶態進程使用的所有寄存器內容都已保存在內核態堆棧上,這也包括SS和ESP這對寄存器的內容。

3.2 任務狀態段(TSS)

TSS是用來存放硬件上下文的。但是linux沒有使用TSS來做硬件上下文的切換。
tss_struct結構描述TSS的格式。init_tss數組爲系統上每個不同的CPU存放一個TSS。在每次進程切換時,內核都更新TSS的某些字段以便相應的CPU控制單元可以安全地檢索到它需要的信息。因此,TSS反映了CPU上當前進程的特權級,但不必爲沒有運行的進程保留TSS。
每個TSS有一個8字節的任務狀態段描述符(TSSD)。由Linux創建的TSSD存放在全局描述符標(GDT)中,GDT的基地址存放在gdtr寄存器中,tr寄存器存放TSSD選擇符。

3.2.1 thread字段

linux中,被替換的進程的硬件上下文沒有保存在TSS中,他們保存在task_struct的thread字段中。這個數據結構包含的字段涉及大部分CPU寄存器,但不包括諸如eax、ebx等通用寄存器,它們保留在堆棧中。

3.3 執行進程切換

進程切換可能只發生在精心定義的點:schedule()函數(後邊會詳細講到)。這裏,我們僅關注內核如何執行一個進程切換:

  • 切換頁全局目錄以安裝一個新的地址空間(後續詳細講解);
  • 切換內核態堆棧和硬件上下文,因爲硬件上下文提供了內核執行新進程所需要的所有信息,包含CPU寄存器。

3.3.1 switch_to宏

硬件上下文的切換,由switch_to宏開始。這個宏跟體系結構相關。這個宏的最後一個參數last用來保存被切換出去的進程的值,宏的prev和next兩個參數分別表示正在執行的進程和即將執行的進程,prev和next都是task_struct指針類型。x86下switch_to步驟如下:

  • 在兩個通用寄存器eax、edx中保存prev和next的值;
  • 保存eflags和ebp寄存器(棧底)到prev內核棧中;
  • 保存esp(棧頂)的內容到prev->thread.esp;
  • 將next->thread.esp裝入esp。此時,內核開始在next的內核棧上操作。這條指令實際完成了從prev到next進程的切換。此時current宏的返回值變指向next;
  • 將prev進程下一條要被執行的指令地址保存到prev->thread.eip(實際被保存的指令併發進程B的指令,而是switch_to宏的切換指令);
  • 將next->thread.eip的值壓入next的內核棧;
  • 執行switch_to函數;
  • 進程prev再次獲得CPU,esp指向prev進程。將eflags和ebp(棧低)壓棧;
  • 將eax的值保存到第三個參數last中。

3.3.2 __switch_to()函數

__switch_to函數也是體系結構相關的。這個函數跟普通函數的參數傳遞區別是:普通函數通過棧傳遞參數,此函數使用switch_to宏保存的eax和edx作爲prev和next的參數,實現方式是通過gcc的擴張編譯器宏
函數執行過程:

  • 有選擇地保存FPU、MMX及XMM寄存器的內容;
  • 將next->thread.esp0裝入本地CPU的TSS的esp0字段;
  • 將next進程使用的線程局部存儲(TLS)段裝入本地CPU的全局描述符表;
  • 將fs,gs寄存器的內容分別存放在prev->thread.fs和prev->thread.gs中(fs/gs/ds/es都是數據段);
  • 如果fs或者gs被prev和next中任一使用(值非0),就將next->thread中存放的fs和gs裝入寄存器fs和gs;
  • 如果next進程正在被調試,則裝入next->thread中的debugreg數組的內容到對應寄存器;
  • 根據需要,更新TSS的I/O bitmap;

四 創建進程

傳統的Unix操作系統以統一的方式對待所有的進程:子進程複製父進程所擁有的資源。 這種方式使進程的創建非常慢且效率低,因爲子進程需要拷貝父進程的整個地址空間。而且,很多時候子進程幾乎不必讀或修改父進程擁有的任何資源。
現代Unix內核通過引入以下機制解決了這個問題:

  • 寫時拷貝技術。允許父子進程讀相同的物理頁。任何一個進程試圖寫一個物理頁時,內核就把這個頁的內容拷貝到一個新的物理頁,並把這個物理頁映射給正在寫的進程。
  • 輕量級進程允許父子進程共享各進程在內核的很多數據結構,如頁表(也就是整個用戶態地址空間)、打開文件表及信號處理。
  • vfork()系統調用創建的進程能共享其父進程的內存地址空間。爲了防止父進程重寫子進程需要的數據,阻塞父進程的執行,直到子進程退出或執行新的程序。

4.1 clone()、fork()及vfork()系統調用

Linux中,輕量級進程是由clone()函數創建的。

int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, void *newtls, pid_t *ctid */ );
fn:新進程執行的函數, 函數的返回值表示子進程的退出代碼。
arg: fn()函數的參數
flags: 控制子進程創建或退出等的某些行爲,例如結束時候發送的信號代碼,子進程創建完是否立即執行,是否共享頁表等。
child_stack:表示把用戶態堆棧指針賦給子進程的esp寄存器。父進程應該總是爲子進程分配新的堆棧。
tls: 表示線程局部存儲段(TLS)數據結構的地址,該結構是爲新輕量級進程定義的。設置flags標誌CLONE_SETTLS有效

傳統的fork() 系統調用在Linux中是用clone()實現的,其中flags參數只設置了SIGCHLD信號,child_stack參數是父進程當前的堆棧指針。當父子進程之一試圖入棧的時候,寫時拷貝機制會產生一份棧的拷貝,之後兩個進程都使用各自獨立的棧。

4.1.1 do_fork()函數

內核中的do_fork()函數負責處理clone()系統調用。

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
clone_flags:即clone()系統調用的flags
stack_start:即clone()系統調用參數child_stack
regs: 指向通用寄存器值的指針,通用寄存器的值是在用戶態切換內核態時保存到內核態堆棧中的。
stack_size:未使用

do_fork()利用輔助函數copy_process()來創建進程描述符以及子進程執行所需要的所有其他內核數據結構。下面是do_fork()執行的主要步驟:

  • 通過查找pid_map_array位圖,爲子進程分配新的PID。
  • 檢查自己是否可被跟蹤,如果可以,並且需要被跟蹤,設置CLONE_PTRACE標誌。
  • 調用copy_process()複製進程描述符。如果所有必須的資源都是可用的,該函數返回剛創建的task_struct描述符的地址。這是創建過程的關鍵步驟,後文詳述。
  • 如果flags設置了CLONE_STOPPED,或者必須跟蹤子進程;則將進程狀態設置成TASK_STOPPED,併爲子進程增加掛起的信號SIGSTOP信號。
  • 如果flags沒有設置CLONE_STOPPED標誌,則調用wake_up_new_task()函數以執行下述操作:
    – 調整父進程和子進程的調度參數,以後詳述
    – 如果子進程將和父進程運行在同一個CPU上,而且父進程和子進程不能共享同一組頁表(flags標誌CLONE_VM未設置),那麼,就把子進程插入父進程運行隊列,插入時,子進程剛好在父進程前面,這樣可以是子進程先於父進程運行。如果子進程刷新其地址空間(使用新的頁表),這樣可以獲得更好的性能。如果讓父進程先運行,那麼寫時複製將會造成不必要的頁面複製。
    – 如果子進程和父進程運行在不同的CPU上,或者父子進程共享頁表,就將子進程插入父進程運行隊列的隊尾。
  • 如果CLONE_STOPPED標誌被設置,就把子進程設置爲TASK_STOPPED狀態。
  • 進程被跟蹤的處理
  • 如果flags設置了CLONE_VFORK標誌,則把父進程插入等待隊列,並掛起父進程直到子進程釋放自己的內存空間(即子進程結束或執行新的程序)。
  • 結束並返回子進程的PID。

4.1.2 copy_process()函數

4.2 內核線程

4.2.1 創建一個內核線程

4.2.2 進程0

4.2.3 進程1

五 撤銷進程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章