一、進程切換與硬體上下文
1,進程切換:為了控制進程的執行,核心必須有能力掛起正在CPU上啟動並執行進程,並恢複之前被掛起的某個進程,這種能力叫做進程切換或者任務切換或者環境切換。
2,硬體上下文:
儘管每個進程可以擁有屬於自己的地址空間,但是所有進程必須共用CPU寄存器,因此,在恢複一個進程的執行前,核心必須確保每個寄存器裝入了掛起進程時的值
進程恢複執行前必須裝入寄存器的那一組資料成為硬體上下文。
進程切換可以這樣表述:儲存將要切換出的進程的硬體上下文,用將要切換進來的進程的硬體上下文來代替。linux2.6使用軟體進行進程切換。
進程切換隻發生在核心態。
二、switch_to宏
switch_to宏用於進程切換,給定了前一個進程結構體指標prev,以及需要切換到的進程結構體指標next,從prev切換到next.
但是,實際上,switch_to宏有三個參數,除了上面說的兩個參數之外,還有一個last參數.而且使用switch_to宏的時候傳入的prev和last都是同一個值,比如會這麼調用這個宏:
switch_to(prev,next,prev).
考慮一種情境,進程A切換到進程B,因為每個進程的空間是不同的,所以在切換之前,進程A的空間裡prev=A,next=B,last=A.
一段時間之後,需要切換回到進程A,假設當前進程是C,那麼對於C而言prev=C,next=A,last=C.
對比前後兩種情境:
進程A切換前:prev=A,next=B,last=A
進程C切換前:prev=C,next=A,last=C
這時開始從進程C切換到進程A,注意到在切換之前switch_to宏將prev存放到了eax寄存器中,也就是在進程C切換到進程A之前,eax=C
切換之後,很顯然,來到了進程A的空間,因此prev,next,last指標要回到進程A被切換出去之前的指向,因此prev=A,next=B,last=A,而eax的資料保持不變.
在switch_to宏返回之前,將eax寄存器的資料存放到last中,因此,last=eax=C.
此時,也就是進程A被切換回來之後,prev=A,next=B,last=C
從上面的分析可以看出,實際上,prev指向的是進程切換之前被切換走的進程指標,而last指向的是切換之後從哪個進程切換過來的.
兩者的意義並不一樣,只不過是在切換之後原先的prev無用了,可以用於儲存切換之後是從哪個進程切換過來的,所以才會出現調用switch_to宏時prev和last相同的情況.
三、建立進程
建立進程:傳統的unix作業系統以統一的方式對待所有的進程,子進程拷貝父進程所有的全部資源,這種方法使進程的建立效率非常慢。實際上,子進程不必讀或者寫父進程所擁有的全部資源。
現在unix核心使用三種不同的機制來解決這個問題:
1,寫時複製技術允許父子進程讀相同的物理頁。
2,輕量級進程允許父子進程共用進程在核心的很多資料結構,如頁表,開啟檔案表,訊號處理
3,Vfork()系統調用建立的進程能共用父進程的記憶體位址為了防止父進程重寫子進程所需要的資料,要阻塞父進程的執行,一直到子進程退出或者執行一個新的程式為止。
四、Linux中的clone()函數
int clone(int (*fn)(void *), void*child_stack, int flags, void *arg);
這裡fn是函數指標,我們知道進程的4要素,這個就是指向程式的指標,就是所謂的“劇本", child_stack明顯是為子進程分配系統堆棧空間(在linux下系統堆棧空間是2頁面,就是8K的記憶體,其中在這塊記憶體中,低地址上放入了值,這個值就是進程式控制制塊task_struct的值),flags就是標誌用來描述你需要從父進程繼承那些資源, arg就是傳給子進程的參數)。下面是flags可以取的值
標誌 含義
CLONE_PARENT 建立的子進程的父進程是調用者的父進程,新進程與建立它的進程成了“兄弟”而不是“父子”
CLONE_FS 子進程與父進程共用相同的檔案系統,包括root、目前的目錄、umask
CLONE_FILES 子進程與父進程共用相同的檔案描述符(file descriptor)表
CLONE_NEWNS 在新的namespace啟動子進程,namespace描述了進程的檔案hierarchy
CLONE_SIGHAND 子進程與父進程共用相同的訊號處理(signal handler)表
CLONE_PTRACE 若父進程被trace,子進程也被trace
CLONE_VFORK 父進程被掛起,直至子進程釋放虛擬記憶體資源
CLONE_VM 子進程與父進程運行於相同的記憶體空間
CLONE_PID 子進程在建立時PID與父進程一致
CLONE_THREAD Linux2.4中增加以支援POSIX線程標準,子進程與父進程共用相同的線程群
五、do_fork()函數
do_fork()函數利用輔助函數copy_process()來建立進程描述符以及子進程執行所需要的所有其他核心資料結構。下面是do_fork()執行的主要步驟:
通過尋找pidmap_array位元影像,為子進程分配新的PID
檢查父進程的ptrace欄位(current->ptrace):如果它的值不等於0,說明有另外一個進程正在跟蹤父進程,因而,do_fork()檢查debugger程式是否自己想跟蹤子進程(獨立於由父進程指定的CLONE_PTRACE標誌的值)。在這種情況下,如果子進程不是核心線程(CLONE_UNTRACED標誌被清0),那麼do_fork()函數設定CLONE_PTRACE標誌。
調用copy_process()複製進程描述符。如果所有必須的資源都是可用的,該函數返回剛建立的task_struct描述符的地址。這是建立過程的關鍵步驟,將在do_fork()之後描述它。
如果設定了CLONE_STOPPED標誌,或者必須跟蹤子進程,即在p->ptrace中設定了PT_PTRACED標誌,那麼子進程的狀態被設定成TASK_STOPPED,並為子進程增加掛起的SIGSTOP訊號。在另外一個進程(不妨假設是跟蹤進程或是父進程)把子進程的狀態恢複為TASK_RUNNING之前(通常是通過發送SIGCONT訊號),子進程將一直保持TASK_STOPPED狀態。
如果沒有設定CLONE_STOPPED標誌,則調用wake_up_new_task()函數以執行下述操作:
a.調整父進程和子進程的調度參數
b.如果子進程將和父進程運行在同一個CPU上(當核心建立一個新進程時父進程有可能會被轉移到另一個CPU上執行),而且父進程和子進程不能共用同一組頁表(CLONE_VM標誌被清0),那麼,就把子進程插入父進程運行隊列,插入時讓子進程恰好在父進程前面,因此而迫使子進程先於父進程運行。如果子進程重新整理其地址空間,並在建立之後執行新程式,那麼這種簡單的處理會產生較好的效能。而如果我們讓父進程先運行,那麼寫時複製機制將會執行一系列不必要的頁面複製。
c.否則,如果子進程與父進程運行在不同的CPU上,或者父進程和子進程共用同一組頁表(CLONE_VM標誌被置位),就把子進程插入父進程運行隊列的隊尾。
如果CLONE_STOPPED標誌被置位,則把子進程置為TASK_STOPPED狀態。
如果父進程被跟蹤,則把子進程的PID存入current的ptrace_message欄位並調用ptrace_notify()。ptrace_notify()是當前進程停止運行,並向當前進程的父進程發送SIGCHLD訊號。子進程的祖父進程是跟蹤父進程的debugger進程。SIGCHLD訊號通知debugger進程:current已經建立了一個子進程,可以通過尋找current->ptrace_message欄位獲得子進程的PID。
如果設定了CLONE_VFORK標誌,則把父進程插入等待隊列,並掛起父進程直到子進程釋放自己的記憶體位址空間(也就是說,直到子進程結束或執行新的程式)。
結束並返回子進程的PID。
六、copy_process()函數
copy_process()函數完成的工作很有意思:
.調用dup_task_struct()為新進程建立一個核心棧、thread_info結構和task_struct,這些值與當前進程的值相同。此時,子進程和父進程的描述符是完全相同的。
.檢查新建立的這個子進程後,目前使用者所擁有的進程數目沒有超出給他分配的資源的限制。
.現在,子進程著手使自己與父進程區別開來。進程描述符內的許多成員都要被清0或設為初始值。進程描述符的成員值並不是繼承而來的,主要是統計資訊。進程描述符中的大多數資料都是共用的。
.接下來,子進程的狀態被設定為TASK_UNINTERRUPTIBLE以保證它不會投入運行。
.copy_process()調用copy_flag()以更新task_struct的flags成員。表明進程是否擁有超級使用者權限的PF_SUPERPRIV標誌被清0.表明進程還沒有調用exec()函數的PF_FORKNOEXEC標誌被設定。
.調用get_pid()為新進程擷取一個有效PID。
.根據傳遞給clone()的參數標誌,copy_process拷貝或共用開啟的檔案、檔案系統資訊、訊號處理函數、進程地址空間和命名空間等。在一般情況下,這些資源會被給定進程的所有線程共用;否則, 這些資源對每個進程是不同的,因此被拷貝到這裡。
.最後,copy_process()作掃尾工作並返回一個指向子進程的指標。
再回到do_fork()函數,如果copy_process()函數成功返回,新建立的子進程被喚醒並讓其投入運行。核心有意選擇子進程執行。因此一般子進程都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷,如果父進程首先執行的話,有可能會開始向地址空間寫入。