OS / Linux / clone、fork、vfork 与 pthread_create 创建线程有何不同
進程是一個指令執行流及其執行環境,其執行環境是一個系統資源的集合,這些資源在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, structpt_regs),代碼如下:
asmlinkage int sys_fork(struct pt_regs regs) {return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL); } asmlinkage int sys_vfork(struct pt_regs regs) {return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL); }?其中 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 系統調用前先簡單介紹線程的一些概念。
線程是在進程的基礎上進一步的抽象,也就是說一個進程分為兩個部分:線程集合和資源集合。線程是進程中的一個動態對象,它應該是一組獨立的指令流,進程中的所有線程將共享進程里的資源。但是線
程應該有自己的私有對象:比如程序計數器、堆棧和寄存器上下文。
線程分為三種類型:內核線程、輕量級進程和用戶線程。
內核線程:
它的創建和撤消是由內核的內部需求來決定的,用來負責執行一個指定的函數,一個內核線程不需要和一個用戶進程聯系起來。它共享內核的正文段核全局數據,具有自己的內核堆棧。它能夠單獨的被調度并且使用標準的內核同步機制,可以被單獨的分配到一個處理器上運行。內核線程的調度由于不需要經過態的轉換并進行地址空間的重新映射,因此在內核線程間做上下文切換比在進程間做上下文切換快得多。
源碼:
// Create a kernel thread /*** 創建一個新的內核線程* fn-要執行的內核函數的地址。* arg-要傳遞給函數的參數* flags-一組clone標志*/ int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) {struct pt_regs regs;memset(®s, 0, sizeof(regs));// 內核棧地址,為其賦初值。// do_fork 將從這里取值來為新線程初始化 CPU。regs.ebx = (unsigned long) fn;regs.edx = (unsigned long) arg;regs.xds = __USER_DS;regs.xes = __USER_DS;regs.orig_eax = -1;/// 把 eip 設置成 kernel_thread_helper,這樣,新線程將執行 fn 函數。如果函數結束,將執行do_exit// fn 的返回值作為 do_exit 的參數。regs.eip = (unsigned long) kernel_thread_helper;regs.xcs = __KERNEL_CS;regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;/* Ok, create the new process.. */// CLONE_VM避免復制調用進程的頁表。由于新的內核線程無論如何都不會訪問用戶態地址空間。// 所以復制只會造成時間和空間的浪費。// CLONE_UNTRACED標志保證內核線程不會被跟蹤,即使調用進程被跟蹤。return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL); }輕量級進程:
輕量級進程是核心支持的用戶線程,它在一個單獨的進程中提供多線程控制。這些輕量級進程被單獨的調度,可以在多個處理器上運行,每一個輕量級進程都被綁定在一個內核線程上,而且在它的生命周期
這種綁定都是有效的。輕量級進程被獨立調度并且共享地址空間和進程中的其它資源,但是每個 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, structpt_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 建立, 同步,銷毀等由線程庫負責。
總結:
創建進程的系統調用函數:
fork:父子各自獨立,
vfork:父子共用一套虛擬地址空間,但是開始的時候會堵塞父進程,直至子進程調用 exit 或者 exec 。一般用于執行 exec 調用。
clone:創建輕量級進程,父子共用部分內容(虛擬地址空間等)
exec:加載可執行文件從而創建進程,父進程消亡。
?
轉載于:https://blog.csdn.net/rock_joker/article/details/72722008
?
(SAW:Game Over!)
總結
以上是生活随笔為你收集整理的OS / Linux / clone、fork、vfork 与 pthread_create 创建线程有何不同的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OS / Linux / 系统阻塞在系统
- 下一篇: 请实现一个函数,将字符串中的空格替换成“