process credentials(二)
一、前言
為什么要寫一個關于進程如何創建的文檔?其實用do_fork作為關鍵字進行索引,你會發現網上的相關文檔數以萬計。作為一個內核工程師,對進程以及進程相關的內容當然是非常感興趣,但是網上的資料并不能令我非常滿意(也許是我沒有檢索到好的文章),一個簡單的例子如下:
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只是索引,重要的是與其相關的各個知識點。
由于進程創建是一個大工程,因此分成若干的部分。本文是第一部分,主要內容包括:
1、從用戶空間看進程創建
2、系統調用層面看進程創建
3、trace的處理
4、參數檢查
5、復制thread_info和task_struct
注:本文引用的內核代碼來自3.14版本的linux kernel。
?
二、用戶空間如何創建進程
應用程序在用戶空間創建進程有兩種場景:
1、創建的子進程和父進程共用一個elf文件。這種情況,elf文件中的正文段中的部分代碼是父進程和子進程共享,部分代碼是屬于父進程,部分代碼屬于子進程。這種情況適合于大多數的網絡服務程序。
2、創建的子進程需要加載自己的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是類似的,除了下面兩點:
1、阻塞父進程
2、不復制父進程的頁表
之所以vfork要阻塞父進程是因為vfork后父子進程使用的是完全相同的memory descriptor, 也就是說使用的是完全相同的虛擬內存空間, 包括棧也相同。所以兩個進程不能同時運行, 否則棧就亂掉了。所以vfork后, 父進程是阻塞的,直到調用了exec系列函數或者exit函數后。這時候,子進程的mm(old_mm)需要釋放掉,不再與父進程共用了,這時候就可以解 除父進程的阻塞狀態。
除了fork和vfork,Linux內核還提供的clone的系統調用接口主要用于線程的創建,這個接口提供了更多的靈活性,可以讓用戶指定父進程和子進程(也就是創建的進程)共享的內容。其實通過傳遞不同的參數,clone接口可以實現fork和vfork的功能。更多細節可以參考后面具體的內核代碼分析
?
三、系統調用相關代碼分析
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,那么也只能是:
1、完全復制。也就是說,內核為子進程選擇適合的地址空間,并且copy完整的父進程的地址空間到子進程。
2、禁止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這個內核函數,后續我們的分析主要幾種在對這個函數逐行解讀。
四、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。
五、參數檢查和安全檢查
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。具體這里就不再描述。如果本次操作通過了 安全校驗,那么后續的操作可以順利進行
六、復制內核棧、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這段代碼主要動作序列包括:
1、分配內核棧和thread_info數據結構所需要的memory(統一分配),分配task sturct需要的memory。
2、設定內核棧和thread_info以及task sturct之間的聯系
3、將父進程的thread_info和task_struct數據結構的內容完全copy到子進程的thread_info和task_struct數據結構
4、將task_struct數據結構的usage成員設定為2。usage成員其實就是一個reference count。之所以被設定為2,因為fork之后已經存在兩個reference了,一個是自己,另外一個是其父進程。
轉載于:https://www.cnblogs.com/alantu2018/p/8447458.html
總結
以上是生活随笔為你收集整理的process credentials(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 香港政府活用无人机,正式应用到调研检测领
- 下一篇: ssm(三)