行程、輕量級行程和執行緒
行程在教科書中通常定義:行程是程式執行時的一個實體,可以把它看作充分描述程式已經執行到何種程度的資料結構的匯集,
從內核的觀點,行程的目的就是擔當分配系統資源(CPU時間、記憶體等)的物體,
當一個行程被創建時,他幾乎于父行程相同,它接受父行程地址空間的一個(邏輯)拷貝,并從行程創建系統呼叫的下一條指令開始執行于父行程相同的代碼,盡管父子行程可以共享含有程式代碼(正文)的頁,但是他們各自有獨立的資料拷貝(堆疊和堆),因此子行程對一個記憶體單元的修改對父行程是不可見的,
Unix中一個行程由幾個執行緒組成,每個執行緒都代表行程的一個執行流,從內核觀點來看,多執行緒應用程式僅僅是一個普通行程,多執行緒應用程式多個執行流的創建、處理、調度整個都是在用戶態進行的,
Linux使用輕量級行程對多執行緒應用程式提供更好的支持,兩個輕量級行程基本上可以共享一些資源,諸如地址空間、打開的檔案等,只要其中一修改共享資源,另一個就立即查看這種修改,
實作多執行緒應用程式的一個簡單方式就是輕量級行程與每個執行緒關聯起來,保證共享資源的同時,每個執行緒都可以由內核獨立調度,
行程描述符
行程描述符都是task_struct型別結構,它的欄位包含了與一個行程相關的所有資訊,
行程狀態
行程描述符中的state欄位描述了行程當前所處的狀態,
??可運行狀態(TASK_RUNNING):行程要么在CPU執行,要么準備執行,
??可中斷的等待狀態(TASK_INTERRUPTIBLE):行程被掛起(睡眠),直到某個條件為真,產生一個硬體中斷,釋放當前的行程資源,或傳遞一個喚醒的信號,
??不可中斷的等待狀態(TASK_UNINTERRUPTIBLE):與可中斷的等待狀態類似,有個例外信號傳遞不能改變狀態,用的很少,某些情況下(行程必須等待,直到一個不能被中斷的事件發生),例如,當行程打開一個設備檔案,其相應的驅動程式開始探測相應的硬體設備時會用到,在探測完成前,設備驅動程式不能被中斷,否則,硬體設備會處于不可預知的狀態,
??暫停狀態(TASK_STOPPED):行程執行被暫停,當行程收到SIGTOP、SIGTSTP、SIGTTIN或SIGTTOU信號后,進入暫停狀態,
??跟蹤狀態(TASK_TRACED):行程的執行已由debugger程式暫停,當行程被另一個行程監控時,任何信號都可以把這個行程置于該狀態,
還有兩個狀態既可以放在行程描述符的state欄位,又可以放在exit_state欄位中,當行程被終止時,行程的狀態才會變成兩種狀態的一種:
??僵死狀態(EXIT_ZOMBIE):行程的執行被終止,但是父行程還沒有發布wait4()或waitpid()系統呼叫來回傳有關死亡行程的資訊,發布wait()類系統呼叫前,內核不能丟棄包含在死行程描述符中的資料,因為父行程可能還需要它,
??僵死撤銷狀態(EXIT_DEAD):最終狀態,由于父行程剛發出wait4()或waitpid()系統呼叫,因而行程由系統洗掉,為了防止其他執行執行緒在同一個行程上也執行wait()類系統呼叫(這是一種競爭條件),而把行程的狀態由僵死改為僵死撤銷狀態,
行程鏈表
采用雙向鏈表把所有行程的描述符鏈接起來,
圖
每個task_struct結構都包含一個list_head型別的tasks欄位,這個型別的prev和next欄位分別指向前面和后面的task_struct元素,
行程鏈表的頭是init_task描述符,它是所謂的0行程(process0)或swapper行程的行程描述符,init_task的task.prev欄位指向鏈表中最后插入的行程描述符的tasks欄位,
TASK_RUNNING狀態的行程鏈表
當內核尋找一個新行程在CPU上運行時,必須考慮可運行行程,
提高調度程式運行速度的訣竅時建立多個可運行的程式鏈表,每種行程優先權對應一個不同的鏈表,每個task_struct描述符包含一個list_head型別的欄位run_list,用于保存可運行程式的優先級,在多處理器系統中,每個CPU都有自己的運行佇列,即他自己的行程鏈表集,
運行佇列的主要資料結構是組成運行佇列的行程描述符鏈表,由一個單獨的priio_array_t資料結構來實作,
行程間的關系
程式創建具有父子關系,如果一個行程創建多個子行程時,則子行程之間具有兄弟關系,
行程0和行程1是由內核創建的,行程1(init)是所有行程的祖先,
為了加速查找,引入了4個散串列,沒中過型別的PID需要他自己的散串列,
Linux利用雙向鏈表來處理沖突的PID,每一個表項都是由沖突的行程描述符組成的雙向鏈表,
等待佇列
等待佇列由雙向鏈表實作,其元素包括指向行程描述符的指標,等待佇列頭是一個型別為wait_queue_head_t的資料結構,
struct __wait_queue_head{
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
因為等待佇列是由中斷處理函式和主要內核函式修改的,因此必須對其雙向鏈表進行保護以免對其進行訪問,同步是通過等待佇列頭中的lock自旋鎖達到,
等待佇列鏈表中的元素型別為wait_queue_t:
struct __wait_queue{
unsigned int flags;
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
描述符地址存放在task欄位中,task_list欄位包含的是指標,由這個指標把一個元素連接到等待相同事件的行程鏈表中,
有兩種睡眠行程:互斥行程(flags=1)由內核有選擇的喚醒,而非互斥行程(flags=0)總是由內核在事件發生時喚醒,等待訪問臨界資源的行程就是互斥的,等待相關事件的行程時非互斥的,func欄位表示喚醒的方式,
一旦定義了一個元素,必須把它插入等待佇列,add_wait_queue()函式把一個非互斥行程插入等待佇列鏈表的第一個位置,add_wait_queue_exclusive()函式把一個互斥行程插入等待佇列鏈表的最后一個位置,remove_wait_queue()函式從等待佇列鏈表中洗掉一個行程,waitqueue_active()函式檢查一個給定的等待佇列是否為空,
wake_up_locked宏和wake_up宏相類似,僅有的不同是當wait_queue_head_t中的自旋鎖已經被持有時要呼叫wake_up_locked;
例如,wake_up宏等價于下列代碼:
void wake_up(wait_queue_head_t *q){
struct list_head *tmp;
wait_queue_t *curr;
list_for_each(tmp,wait_queue_t,task_list){
curr = list_entry(tmp,wait_queue_t,task_list);
if(curr->func(curr, TASK_INTERRUPTIBLE|TASK_UNINTERRUPTBLE,0,NULL) && curr->flags)
break;
}
}
list_for_each宏掃描雙向鏈表中的所有項,即等待佇列的所有行程,對每一項,list_entry宏都計算wait_queue_t變數對應的地址,這個變數的func欄位存放喚醒函式的地址,它試圖喚醒由等待佇列元素的task欄位標識的行程,如果一個行程已經被有效地喚醒并且行程是互斥的,回圈結束,
因為所有的非互斥行程總是在雙向鏈表的開始位置,而所有的互斥行程在雙向鏈表的尾部,所以函式總是先喚醒非互斥行程然后再喚醒互斥行程,如果有行程存在的話,
行程切換
為了控制行程的執行,內核必須有能力掛起正在CPU上運行的行程,并恢復以前掛起的某個行程的執行,這種行為被稱為行程切換、任務切換或背景關系切換,
硬體背景關系
行程恢復執行前必須裝入暫存器的一組資料稱為硬體背景關系,硬體背景關系是行程可執行背景關系的一個子集,因為可執行背景關系包含行程執行所需要的所有資訊,在Linux中,行程硬體背景關系的一部分存放在TSS段,而剩余部分存放在內核態堆疊中,
prev--切換出的行程的描述符 next--切換進的行程的描述符
我們把行程切換定義為這樣的行為:保存prev硬體上背景關系,用next硬體背景關系代替prev,
執行行程切換
從本質上說,每個行程切換由兩部組成:
1、切換頁全域目錄以安裝一個新的地址空間;
2、切換內核態堆疊和硬體背景關系,因為硬體背景關系提供了內核執行新行程所需要的所有資訊,包含CPU暫存器,
switch_to宏
行程切換的第二步由switch_to宏執行,它是內核中與硬體關系最密切的歷程之一,
他有三個引數,prev、next 和 last,
在任何行程切換中涉及到三個行程而不是兩個行程,假設內核決定暫停行程A而激活行程B,在shedule()函式中,prev指向A的描述符而next指向B的描述符,switch_to宏一旦使A暫停,A的執行流就凍結,隨后,當內核想再次激活行程A,就必須暫停另一個行程C,于是就要用prev指向C而next指向A來執行另一個switch_to宏,
switch_to宏的最后一個引數是輸出引數,它表示宏把行程C的描述符地址寫在記憶體的什么位置了,
創建行程
clone()、fork()及 vfork()系統呼叫
在Linux中,輕量級行程是由名為clone()的函式創建的,
傳統的fork()系統呼叫在Linux中是用clone()實作的,其中clone()的flags引數指定為SIGCHLD信號及所有清0的clone標志,而它的child_stack引數是父行程當前的堆疊指標,因此,父行程和子行程暫時共享一個用戶態堆疊,
要感謝寫時復制機制,通常只要父子行程中有一個試圖去改變,則立即各自的到用戶態堆疊的一份拷貝,
do_fork()函式負責處理clone()、fork()和vfork()系統呼叫,利用輔助函式 copy_process()來創建行程描述符以及子行程所需要的內核資料結構,
do_fork()之后有了處于可運行狀態的完整子行程,但他還沒有實際運行,調度程式決定何時把CPU交給這個子行程,在以后的行程切換中,調度程式將繼續完善子行程,然后,在fork()、vfork()、clone()系統呼叫結束后,新行程將開始執行,系統呼叫的回傳值放在eax暫存器中,回傳給子行程的值是0,回傳給父行程的是子行程的PID,
內核執行緒
現代作業系統將一些重要的任務交給內核執行緒,內核執行緒不受不必要的用戶態背景關系的拖累,這些任務包括重繪磁盤高速快取,交換出不用的頁框,維護網路連接等等,在Linux中,內核執行緒在以下幾個方面不用于普通行程:
??內核執行緒之運行在內核態,而普通行程既可以運行在內核態,也可以運行在用戶態,
??內核執行緒只使用大于PAGE_OFFSET的線性地址空間,普通行程可以用4GB的線性地址空間,
創建一個內核執行緒
kernel_thread()函式創建一個新的內核執行緒,它接受的引數由:所要指向的內核函式的地址(fn)、要傳遞給函式的引數(arg)、一組clone標志(flags),該函式本質以下面方式呼叫do_fork():
do_fork(flags|CLONE_UNTRACED, 0, pregs, 0, NULL, NULL);
CLONE_VM標志避免復制呼叫行程的頁表,CLONE_UNTRACED標志保證不會有任何行程跟蹤,
傳遞給do_fork()的引數pregs表示內核堆疊的地址,copy_thread()函式將從這里找到新執行緒初始化CPU暫存器的值,
行程0
所有行程的祖先叫做行程0,idel行程或因為歷史的淵源叫做swapper行程,它是在Linux的初始化階段從無到有創建的一個內核執行緒,這個祖先行程使用下列靜態分配的資料結構:
??存放在 init_task 變數中的行程描述符,由 INIT_TASK 宏完成對它的初始化
??存放在 init_thread_union 變數中的 thread_info 描述符和內核堆疊,由 INIT_THREAD_INFO 宏完成對它們的初始化
start_kernel()函式初始化內核需要的所有資料結構,激活中斷,創建另一個叫行程1 的內核執行緒(一般叫做init行程):
kernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);
新創建的內核執行緒的PID為1,并于行程0共享每行程所有的內核資料結構,此外,當調度程式選擇到它時,init 行程開始執行init()函式,
在多處理器系統中,每個CPU都有一個行程0,打開電源,計算機的BIOS就啟動某一個CPU,同時禁用其他CPU,運行在CPU 0上的swapper行程初始化內核資料結構,然后激活其他的CPU,并通過copy_process()函式創建另外的swapper行程,把0傳遞給新創建的swapper行程作為它們的新PID,
行程1(init行程)
由行程0創建的內核執行緒執行init()函式,init()依此完成內核初始化,init()呼叫execve()系統呼叫裝入可執行程式init,結果,init內核執行緒變成一個普通行程,且擁有自己的每行程內核資料結構,在系統關閉之前,init行程一直存活,因為它創建和監控在作業系統外層執行的所有行程的活動,
其他內核執行緒
Linux使用很多其他內核執行緒,其中一些在初始化階段創建,一直運行到系統關閉,而其他一些在內核必須執行一個任務時“按需”創建,這種任務在內核的執行背景關系中得到很好的執行,
行程撤銷
行程終止
在Linux2.6中有兩個終止用戶態應用的系統呼叫:
??exit_group()系統呼叫,它終止整個執行緒組,即整個基于多執行緒的應用,do_group_exit()是實作這個系統呼叫的主要內核函式,這是C庫函式應該呼叫的系統呼叫,
??exit()系統呼叫,它終止某一個執行緒,而不管該執行緒所屬執行緒組中的所有其他執行緒,do_exit()是實作這個系統呼叫的主要內核函式,這是被諸如 pthread_exit()的Linux執行緒庫的函式所呼叫的系統呼叫,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551416.html
標籤:C
上一篇:如何將 Spire.Doc for C++ 集成到 C++ 程式中
下一篇:返回列表