進程是一個指令執行流及其執行環境,其執行環境是一個系統資源的集合,這些資源在Linux中被抽
象成各種資料對象:進程式控制制塊、虛存空間、檔案系統,檔案I/O、訊號處理函數。所以建立一個進程的
過程就是這些資料對象的建立過程。
在調用系統調用fork建立一個進程時,子進程只是完全複製父進程的資源,這樣得到的子進程獨立於
父進程,具有良好的並發性,但是二者之間的通訊需要通過專門的通訊機制,如:pipe,fifo,System V
IPC機制等,另外通過fork建立子進程系統開銷很大,需要將上面描述的每種資源都複製一個副本。這樣
看來,fork是一個開銷十分大的系統調用,這些開銷並不是所有的情況下都是必須的,比如某進程fork出
一個子進程後,其子進程僅僅是為了調用exec執行另一個執行檔案,那麼在fork過程中對於虛存空間的複
制將是一個多餘的過程(由於Linux中是採取了copy-on-write技術,所以這一步驟的所做的工作只是虛存
管理部分的複製以及頁表的建立,而並沒有包括物理也面的拷貝);另外,有時一個進程中具有幾個獨立
的計算單元,可以在相同的地址空間上基本無衝突進行運算,但是為了把這些計算單元分配到不同的處理
器上,需要建立幾個子進程,然後各個子進程分別計算最後通過一定的進程間通訊和同步機制把計算結果
匯總,這樣做往往有許多格外的開銷,而且這種開銷有時足以抵消並行計算帶來的好處。
這說明了把計算單元抽象到進程上是不充分的,這也就是許多系統中都引入了線程的概念的原因。在
講述線程前首先介紹以下vfork系統調用,vfork系統調用不同於fork,用vfork建立的子進程共用地址空
間,也就是說子進程完全運行在父進程的地址空間上,子進程對虛擬位址空間任何資料的修改同樣為父進
程所見。但是用vfork建立子進程後,父進程會被阻塞直到子進程調用exec或exit。這樣的好處是在子進
程被建立後僅僅是為了調用exec執行另一個程式時,因為它就不會對父進程的地址空間有任何引用,所以
對地址空間的複製是多餘的,通過vfork可以減少不必要的開銷。
在Linux中, fork和vfork都是調用同一個核心函數
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)
其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PID,CLONE_VFORK
等等標誌位,任何一位被置1了則表明建立的子進程和父進程共用該位對應的資源。所以在vfork的實現中
,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,這表示子進程和父進程共用地址空間,同時
do_fork會檢查CLONE_VFORK,如果該位被置1了,子進程會把父進程的地址空間鎖住,直到子進程退出或
執行exec時才釋放該鎖。
在講述clone系統調用前先簡單介紹線程的一些概念。
線程是在進程的基礎上進一步的抽象,也就是說一個進程分為兩個部分:線程集合和資源集合。線程
是進程中的一個動態對象,它應該是一組獨立的指令流,進程中的所有線程將共用進程裡的資源。但是線
程應該有自己的私人對象:比如程式計數器、堆棧和寄存器上下文。
線程分為三種類型:
核心線程、輕量級進程和使用者線程。
核心線程:
它的建立和撤消是由核心的內部需求來決定的,用來負責執行一個指定的函數,一個核心線程不需要
和一個使用者進程聯絡起來。它共用核心的本文段核全域資料,具有自己的核心堆棧。它能夠單獨的被調度
並且使用標準的核心同步機制,可以被單獨的分配到一個處理器上運行。核心線程的調度由於不需要經過
態的轉換並進行地址空間的重新對應,因此在核心線程間做環境切換比在進程間做環境切換快得多。
輕量級進程:
輕量級進程是核心支援的使用者線程,它在一個單獨的進程中提供多線程式控制制。這些輕量級進程被單獨
的調度,可以在多個處理器上運行,每一個輕量級進程都被綁定在一個核心線程上,而且在它的生命週期
這種綁定都是有效。輕量級進程被獨立調度並且共用地址空間和進程中的其它資源,但是每個LWP都應
該有自己的程式計數器、寄存器集合、核心棧和使用者棧。
使用者線程:
使用者線程是通過線程庫實現的。它們可以在沒有核心參與下建立、釋放和管理。線程庫提供了同步和
調度的方法。這樣進程可以使用大量的線程而不消耗核心資源,而且省去大量的系統開銷。使用者線程的實
現是可能的,因為使用者線程的上下文可以在沒有核心幹預的情況下儲存和恢複。每個使用者線程都可以有自
己的使用者堆棧,一塊用來儲存使用者級寄存器上下文以及如訊號屏蔽等狀態資訊的記憶體區。庫通過儲存當前
線程的堆棧和寄存器內容載入新調度線程的那些內容來實現使用者線程之間的調度和環境切換。
核心仍然負責進程的切換,因為只有核心具有修改記憶體管理寄存器的權力。使用者線程不是真正的調度
實體,核心對它們一無所知,而只是調度使用者線程下的進程或者輕量級進程,這些進程再通過線程庫函數
來調度它們的線程。當一個進程被搶佔時,它的所有使用者線程都被搶佔,當一個使用者線程被阻塞時,它會
阻塞下面的輕量級進程,如果進程只有一個輕量級進程,則它的所有使用者線程都會被阻塞。
明確了這些概念後,來講述Linux的線程和clone系統調用。
在許多實現了MT的作業系統中(如:Solaris,Digital Unix等), 線程和進程通過兩種資料結構來
抽象表示: 進程表項和線程表項,一個進程表項可以指向若干個線程表項, 調度器在進程的時間片內再
調度線程。 但是在Linux中沒有做這種區分, 而是統一使用task_struct來管理所有進程/線程,只是
線程與線程之間的資源是共用的,這些資源可是是前面提到過的:虛存、檔案系統、檔案I/O以及訊號處
理函數甚至PID中的幾種。
也就是說Linux中,每個線程都有一個task_struct,所以線程和進程可以使用同一調度器調度。其實
Linux核心中,輕量級進程和進程沒有質上的差別,因為Linux中進程的概念已經被抽象成了計算狀態加資
源的集合,這些資源在進程間可以共用。如果一個task獨佔所有的資源,則是一個HWP,如果一個task和
其它task共用部分資源,則是LWP。
clone系統調用就是一個建立輕量級進程的系統調用:
int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);
其中fn是輕量級進程所執行的過程,stack是輕量級進程所使用的堆棧,flags可以是前面提到的
CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的組合。Clone 和fork,vfork在實現時
都是調用核心函數do_fork。
do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);
和fork、vfork不同的是,fork時clone_flag = SIGCHLD;
vfork時clone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD;
而在clone中,clone_flag由使用者給出。
下面給出一個使用clone的例子。
Void * func(int arg)
{
. . . . . .
}
int main()
{
int clone_flag, arg;
. . . . . .
clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |
CLONE_FILES;
stack = (char *)malloc(STACK_FRAME);
stack += STACK_FRAME;
retval = clone((void *)func, stack, clone_flag, arg);
. . . . . .
}
看起來clone的用法和pthread_create有些相似,兩者的最根本的差別在於clone是建立一個LWP,對
核心是可見的,由核心調度,而pthread_create通常只是建立一個使用者線程,對核心是不可見的,由線程
庫調度。
linux的pthread_create最終調用clone,pthread_create調用clone,並把開闢一個stack作為參數
thread 建立, 同步,銷毀等由線程庫負責,