1、前言linux
爲何要寫一個關於進程如何建立的文檔?其實用do_fork做爲關鍵字進行索引,你會發現網上的相關文檔數以萬計。做爲一個內核工程師,對進程以及進程相關的內容固然是很是感興趣,可是網上的資料並不能令我很是滿意(也許是我沒有檢索到好的文章),一個簡單的例子以下:shell
static void copy_flags(unsigned long clone_flags, struct task_struct *p)
{
unsigned long new_flags = p->flags; 安全new_flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
new_flags |= PF_FORKNOEXEC;
p->flags = new_flags;
} 網絡
上面的代碼是進程建立過程的一個片斷,網上的解釋通常都是對代碼邏輯的描述:清除PF_SUPERPRIV 和PF_WQ_WORKER這兩個flag的標記,設定PF_FORKNOEXEC標記。坦率的講,這樣的代碼解析沒有任何意義,其實c代碼都已是很是清楚了。固然,也有的文章進行了進一步的分析,例如對PF_SUPERPRIV 被清除進行了這樣的解釋:代表進程是否擁有超級用戶權限的PF_SUPERPRIV標誌被清0。很遺憾,這樣的解釋不能使人信服,由於若是父進程是超級用戶權限,其建立的子進程是要繼承超級用戶權限的。數據結構
正由於如此,我想對linux kernel中進程建立涉及的方方面面的系統知識進行梳理,在個人能力範圍內對進程建立的source code進行逐行解析。一言以蔽之,do_fork的source code只是索引,重要的是與其相關的各個知識點。多線程
因爲進程建立是一個大工程,所以分紅若干的部分。本文是第一部分,主要內容包括:app
一、從用戶空間看進程建立框架
二、系統調用層面看進程建立函數
三、trace的處理性能
四、參數檢查
五、複製thread_info和task_struct
注:本文引用的內核代碼來自3.14版本的linux kernel。
2、用戶空間如何建立進程
應用程序在用戶空間建立進程有兩種場景:
一、建立的子進程和父進程共用一個elf文件。這種狀況,elf文件中的正文段中的部分代碼是父進程和子進程共享,部分代碼是屬於父進程,部分代碼屬於子進程。這種狀況適合於大多數的網絡服務程序。
二、建立的子進程須要加載本身的elf文件。例如shell。
爲了應對這些需求,linux採用了fork then exec兩段式的方式來建立進程。對於場景1,程序直接fork便可,對於場景2,使用fork then exec來應對。本文主要focus在fork操做上,對於exec的操做,在進程加載文檔中描述。
應用程序能夠經過fork系統調用建立進程,該新建立的進程是調用fork進程的子進程。fork以後,一個進程會象細胞分裂那樣變成兩個進程。子進程複製了父進程(也就是調用fork的那個進程)的絕大部分的資源(文件描述符、信號處理、當前工做目錄等),更細節的信息能夠參考後面具體的內核代碼分析。
徹底複製父進程的資源的開銷很是大,特別是對於場景2,全部的開銷都是徹底的沒有任何意義,由於系統load新的elf文件後,會重建text、data等segment。不過,在引入COW(copy-on-write)技術後,fork的開銷其實也不算特別大,大部分的copy都是經過share完成的,主要的開銷集中在複製父進程的頁表上。在某些特定的場合下,若是程序想把複製父進程頁表這一點開銷也節省掉,那麼linux還提供了vfork函數。Vfork和fork是相似的,除了下面兩點:
一、阻塞父進程
二、不復制父進程的頁表
之因此vfork要阻塞父進程是由於vfork後父子進程使用的是徹底相同的memory descriptor, 也就是說使用的是徹底相同的虛擬內存空間, 包括棧也相同。因此兩個進程不能同時運行, 不然棧就亂掉了。因此vfork後, 父進程是阻塞的,直到調用了exec系列函數或者exit函數後。這時候,子進程的mm(old_mm)須要釋放掉,再也不與父進程共用了,這時候就能夠解 除父進程的阻塞狀態。
除了fork和vfork,Linux內核還提供的clone的系統調用接口主要用於線程的建立,這個接口提供了更多的靈活性,可讓用戶指定父進程和子進程(也就是建立的進程)共享的內容。其實經過傳遞不一樣的參數,clone接口能夠實現fork和vfork的功能。更多細節能夠參考後面具體的內核代碼分析
3、系統調用相關代碼分析
fork對應的系統調用代碼以下:
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return(-EINVAL);
#endif
}
#endif
對於fork的實現,在kernel中會使用COW技術,若是沒有MMU的話,也就沒有虛擬地址、頁表這些概念,也就沒法實現COW版本的fork。在這樣的條件下,若是強行實現fork,那麼也只能是:
一、徹底複製。也就是說,內核爲子進程選擇適合的地址空間,而且copy完整的父進程的地址空間到子進程。
二、禁止fork,用vfork+exec來實現fork
上面的代碼已經很清楚了,內核採用了方法2。
vfork對應的系統調用代碼以下:
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#endif
fork和vfork的實現是和CPU architecture相關的(參見source code中的__ARCH_WANT_SYS_FORK和__ARCH_WANT_SYS_VFORK)。在POSIX標準中對vfork描述以下:Applications are recommended to use the fork( ) function instead of this function。也就是說,標準不建議實現vfork,可是linux kernel仍是保留了該系統調用,一方面是有些應用對performance特別敏感,vfork能夠得到一些的性能優點。此外,在沒有MMU支持的CPU上,vfork+exec來能夠用來實現fork。
clone對應的系統調用代碼以下:
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
在我熟悉的平臺上(ARM和X86),clone的實現是上面粗體的定義。不一樣的CPU architecture會有一些區別(例如,參數順序不同,stack的增加方向等),這不是本文的主題,所以暫且略過。
從上面的代碼片斷能夠看出,不管哪個系統調用,最終都是使用了do_fork這個內核函數,後續咱們的分析主要幾種在對這個函數逐行解讀。
4、trace相關的處理
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
Linux的內核提供了ptrace這樣的系統調用,經過它,一個進程(咱們稱之 tracer,例如strace、gdb)能夠觀測和控制另一個進程(被trace的進程,咱們稱之tracee)的執行。一旦Tracer和 tracee創建了跟蹤關係,那麼全部發送給tracee的信號(除SIGKILL)都會彙報給Tracer,以便Tracer能夠控制或者觀測 tracee的執行。例如斷點的操做。Tracer程序通常會提供界面,以便用戶能夠設定一個斷點(當tracee運行到斷點時,會停下來)。當用戶設定 了斷點後,tracer就會保存該位置的指令,而後向該位置寫入SWI __ARM_NR_breakpoint(這種斷點是soft break point,能夠設定無限多個,對於hard break point是和CPU體系結構相關,通常支持2個)。當執行到斷點位置的時候,發生軟中斷,內核會給tracee進程發出SIGTRAP信號,固然這個信號也會被tracer捕獲。對於tracee,當收到信號的時候,不管是什麼信號,甚至是ignor的信號,tracee進程都會中止運行。Tracer進程能夠對tracee進行各類操做,例如觀察tracer的寄存器,觀察變量等等。
在瞭解完上述的背景以後,再來看代碼就比較簡單了。這個代碼塊控制建立進程是否向tracer上報信號,若是須要上報,那麼要上報哪些信號。若是用戶進程 在建立的時候有攜帶CLONE_UNTRACED的flag,那麼該進程則不能被trace。對於內核線程,在建立的時候都會攜帶該flag,這也就意味着,內核線程是沒法被traced,也就不須要上報event給tracer。
5、參數檢查和安全檢查
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
在 2.4.19版本以前,系統中的全部進程都是共享一個mount namespace,在這種狀況下,任何進程經過mount或者umount來改變mount namespace都會反應到其餘的進程中。從2.4.19版本開始,linux提供了per-process的mount namespace機制,也就是說每一個進程都是擁有本身私有的mount namespace(呵呵~~~是否是有點懷念過去簡單而美好的日子了)。
CLONE_NEWNS這個flag就是用來控制在clone的時候,父子進程是否要共享mount namespace的。經過fork建立的進程老是和父進程共享mount namespace的(固然子進程也能夠調用unshare來解除共享)。當調用clone建立進程的時候,能夠有更多的靈活性,能夠經過 CLONE_NEWNS這個flag能夠不和父進程共享mount namespace(注意:子進程的這個private mount namespace仍然用父進程的mount namespace來初始化,只是以後,子進程和父進程的mount namespace就分道揚鑣了,這時候,子進程的mount或者umount的動做將不會影響到父進程)。
CLONE_FS flag是用來控制父子進程是否共享文件系統信息(例如文件系統的root、當前工做目錄等),若是設定了該flag,那麼父子進程共享文件系統信息,如 果不設定該flag,那麼子進程則copy父進程的文件系統信息,以後,子進程調用chroot,chdir,umask來改變文件系統信息將不會影響到 父進程。
在內核中,CLONE_NEWNS和CLONE_FS是排他的。一個進程的文件系統信息在內核中是用struct fs_struct來抽象,這個結構中就有mount namespace的信息,所以若是想共享文件系統信息,其前提條件就是要處於同一個mount namespace中。
if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);
CLONE_NEWUSER這個flag是和user namespace相關的標識,在經過clone函數fork進程的時候,咱們能夠選擇clone以前的user namespace,固然也能夠經過傳遞該標識來建立新的user namespace。user namespace是linux kernel支持虛擬化以後引入的一個機制,能夠容許系統建立不一樣的user namespace(以前系統只有一個user namespace)。user namespace用來管理user ID和group ID的映射。一個user namespace造成一個container,該user namespace的user ID和group ID的權限被限定在container內部。也就是說,某一個user namespace中的root(UID等於0)並不是具有任意的權限,他僅僅是在該user namespace中是privileges的,在該user namespace以外,該user並不是是特權用戶。
CLONE_NEWUSER|CLONE_FS的組合會致使一個系統漏洞,可讓一個普通用戶竊取到root的權限,具體能夠參考下面的鏈接:
http://www.openwall.com/lists/oss-security/2013/03/13/10
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
POSIX規定一個進程內部的多個thread要共享一個PID,可是,在linux kernel中,不管是進程仍是線程,都是會分配一個task struct而且分配一個惟一的PID(這時候,PID其實就是thread ID)。這樣,爲了知足POSIX的線程規定,linux引入了線程組的概念,一個進程中的全部線程所共享的那個PID被稱爲線程組ID,也就是task struct中的tgid成員。所以,在linux kernel中,線程組ID(tgid,thread group id)就是傳統意義的進程ID。對於sys_getpid系統調用,linux內核返回了tgid。對於sys_gettid系統調用,本意是要求返回線 程ID,在linux內核中,返回了task struct的pid成員。一言以蔽之,POSIX的進程ID就是linux中的線程組ID。POSIX的線程ID也就是linux中的pid。
在瞭解了線程組ID和線程ID以後,咱們來看一看CLONE_THREAD這個flag。這個flag被設定的話,則表示被建立的子進程與父進程在一個線程組中。不然會建立一個新的線程組。
若是設定CLONE_SIGHAND這個flag,則表示建立的子進程與父進程共享相同的信號處理(signal handler)表。線程組應該共享signal handler(POSIX規定),所以,當設定了CLONE_THREAD後必須同時設定CLONE_SIGHAND
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
設定了CLONE_SIGHAND表示共享signal handler,前提條件就是要共享地址空間(也就是說必須設定CLONE_VM),不然,沒法共享signal handler。由於若是不共享地址空間,即使是一樣地址的handler,其物理地址都是不同的。
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
CLONE_PARENT這個flag表示新fork的進程想要和建立該進程的cloner擁有一樣的父進程。
SIGNAL_UNKILLABLE這個flag是for init進程的,其餘進程不會設定這個flag。
Linux kernel會靜態定義一個init task,該task的pid是0,被稱做swapper(其實就是idle進程,在系統沒有任何進程可調度的時候會執行該進程)。系統中的全部進程(包括內核線程)由此開始。對於用戶空間進程,內核會首先建立init進程,全部其餘用戶空間的進程都是由init進程派生出來的。所以init進程要負責爲全部用戶空間的進程處理後事(不然會變成僵 屍進程)。可是若是init進程想要建立兄弟進程(其父親是swapper),那麼該進程沒法由init進程回收,其父親swapper進程也不會收養用戶空間建立的init的兄弟進程,這種狀況下,這類進程退出都會變成zombie,所以要杜絕。
if (clone_flags & CLONE_SIGHAND) {
if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
(task_active_pid_ns(current) !=
current->nsproxy->pid_ns_for_children))
return ERR_PTR(-EINVAL);
}
當CLONE_SIGHAND被設定的時候,父子進程應該共享signal disposition table。也就是說,一個進程修改了某一個signal的handler,另一個進程也能夠感知的到。
CLONE_NEWPID這個flag是和PID namespace相關的標識。思路同CLONE_NEWUSER。 這兩個flag是和虛擬化技術相關的。虛擬化技術就須要資源隔離,也就是說,不一樣的虛擬主機(實際上在一臺物理主機上)資源是互相不可見的。所以,linux kernel增長了若干個name space,例如user name space、PID namespace、IPC namespace、uts namespace、network namespace等。以PID namespace爲例,原來的linux kernel中,PID惟一的標識了一個process,在引入PID namespace以後,不一樣的namespace能夠擁有一樣的ID,也就是說,標識一個進程的是PID namespace + PID。
CLONE_NEWUSER設定的時候,就會爲fork的進程建立一個新的user namespace,以便隔離USER ID。linux 系統內的一個進程和某個user namespace內的uid和gid相關。user namespace被實現成樹狀結構,新的user namespace中第一個進程的uid就是0,也就是root用戶。這個進程在這個新的user namespace中有超級權限,可是,在其父user namespace中只是一個普通用戶。
更詳細的解釋TODO。
retval = security_task_create(clone_flags);
if (retval)
goto fork_out;
這一段代碼是和LinuxSecurity Modules相關的。LinuxSecurity Modules是一個安全框架,容許各類安全模型插入到內核。你們熟知的一個計算機安全模型就是selinux。具體這裏就再也不描述。若是本次操做經過了 安全校驗,那麼後續的操做能夠順利進行
6、複製內核棧、thread_info和task_struct
retval = -ENOMEM;
p = dup_task_struct(current);
if (!p)
goto fork_out;
每個用戶空間進程都有一個內核棧和一個用戶空間的棧(對於多線程的進程,應該有多個用戶空間棧和內核棧)。內核棧和thread_info數據結構共同佔用了THREAD_SIZE(通常是2個page)的memory。thread_info數據結構和CPU architecture相關,thread_info數據結構的task 成員指向進程描述符(也就是task struct數據結構)。進程描述符的stack成員指向對應的thread_info數據結構。
dup_task_struct這段代碼主要動做序列包括:
一、分配內核棧和thread_info數據結構所須要的memory(統一分配),分配task sturct須要的memory。
二、設定內核棧和thread_info以及task sturct之間的聯繫
三、將父進程的thread_info和task_struct數據結構的內容徹底copy到子進程的thread_info和task_struct數據結構
四、將task_struct數據結構的usage成員設定爲2。usage成員其實就是一個reference count。之因此被設定爲2,由於fork以後已經存在兩個reference了,一個是本身,另一個是其父進程。