Linux內(nèi)核代碼追蹤:如何“分裂”出一個新進程的?
在生活中,我們經(jīng)常會進行文件復制操作,比如將一份重要的文檔復制到多個文件夾,以方便在不同場景下使用,每個復制后的文件都擁有獨立的存儲空間,但內(nèi)容最初與原文件一致。在生物學領域,克隆技術也是一種復制,克隆羊多莉就是通過復制母體的遺傳物質(zhì)誕生,擁有和母體幾乎相同的基因。而在 Linux 操作系統(tǒng)中,也存在類似的 “復制” 概念,那就是進程復制,其中fork函數(shù)便是實現(xiàn)進程復制的核心,它如同一個神奇的 “分身術”,讓一個進程能夠創(chuàng)建出與自身幾乎一模一樣的子進程 ,它們是如何實現(xiàn)的。
我們主要聊聊從glibc庫進入內(nèi)核,再從內(nèi)核出來的情景。為了方便期間,我們的硬件平臺為arm,linux內(nèi)核為3.18.3,glibc庫版本為2.20,可從http://ftp.gnu.org/gnu/glibc/下載源碼。接下來,讓我們一起深入探索。
一、Glibc到kernel
我們設定硬件平臺為arm,glibc庫版本為2.20,因為不同的CPU體系結(jié)構中,glibc庫通過系統(tǒng)調(diào)用進入kernel庫的方法是不一樣的。當glibc準備進入kernel時,流程如下:
/* glibc最后會調(diào)用到一個INLINE_SYSCALL宏,參數(shù)如下 */
INLINE_SYSCALL (clone, 5, CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid);
/* INLINE_SYSCALL的宏定義如下,可以看出在INLINE_SYSCALL宏中又使用到了INTERNAL_SYSCALL宏,而INTERNAL_SYSCALL宏最終會調(diào)用INTERNAL_SYSCALL_RAW */
#define INLINE_SYSCALL(name, nr, args...) \
({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \
_sys_result = (unsigned int) -1; \
} \
(int) _sys_result; })
/* 為了方便大家理解,將此宏寫為偽代碼形式 */
int INLINE_SYSCALL (name, nr, args...)
{
unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args);
if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) {
__set_error (INTERNAL_SYSCALL_ERRNO (_sys_result, ));
_sys_result = (unsigned int) -1;
}
return (int)_sys_result;
}
/* 這里我們不需要看INTERNAL_SYSCALL宏,只需要看其最終調(diào)用的INTERNAL_SYSCALL_RAW宏,需要注意的是,INTERNAL_SYSCALL調(diào)用INTERNAL_SYSCALL_RAW時,通過SYS_ify(name)宏將name轉(zhuǎn)為了系統(tǒng)調(diào)用號
* name: 120(通過SYS_ify(name)宏已經(jīng)將clone轉(zhuǎn)為了系統(tǒng)調(diào)用號120)
* err: NULL
* nr: 5
* args[0]: CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
* args[1]: NULL
* args[2]: NULL
* args[3]: NULL
* args[4]: &THREAD_SELF->tid
*/
# define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \
({ \
register int _a1 asm ("r0"), _nr asm ("r7"); \
LOAD_ARGS_##nr (args) \
_nr = name; \
asm volatile ("swi 0x0 @ syscall " #name \
: "=r" (_a1) \
: "r" (_nr) ASM_ARGS_##nr \
: "memory"); \
_a1; })
#endif
INTERNAL_SYSCALL_RAW實現(xiàn)的結(jié)果就是將args[0]存到了r0...args[4]存到了r4中,并將name(120)綁定到r7寄存器。然后通過swi 0x0指令進行了軟中斷。0x0是一個24位的立即數(shù),用于軟中斷執(zhí)行程序判斷執(zhí)行什么操作。當執(zhí)行這條指令時,CPU會跳轉(zhuǎn)至中斷向量表的軟中斷指令處,執(zhí)行該處保存的調(diào)用函數(shù),而在函數(shù)中會根據(jù)swi后面的24位立即數(shù)(在我們的例子中是0x0)執(zhí)行不同操作。在這時候CPU已經(jīng)處于保護模式,陷入內(nèi)核中?,F(xiàn)在進入到linux內(nèi)核中后,具體看此時內(nèi)核是怎么操作的吧。
/* 源文件地址: 內(nèi)核目錄/arch/arm/kernel/entry-common.S */
ENTRY(vector_swi)
/*
* 保存現(xiàn)場
*/
#ifdef CONFIG_CPU_V7M
v7m_exception_entry
#else
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ 將r0~r12保存到棧中
ARM( add r8, sp, #S_PC )
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
THUMB( mov r8, sp )
THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr
mrs r8, spsr @ called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
#endif
zero_fp
alignment_trap r10, ip, __cr_alignment
enable_irq
ct_user_exit
get_thread_info tsk
/*
* 以下代碼根據(jù)不同arm體系結(jié)構獲取系統(tǒng)調(diào)用號
*/
#if defined(CONFIG_OABI_COMPAT)
/*
* 如果內(nèi)核配置了OABI兼容選項,會先判斷是否為THUMB,以下為THUMB情況(我們分析的時候可以忽略這段,一般情況是不走這一段的)
*/
#ifdef CONFIG_ARM_THUMB
tst r8, #PSR_T_BIT
movne r10, #0 @ no thumb OABI emulation
USER( ldreq r10, [lr, #-4] ) @ get SWI instruction
#else
USER( ldr r10, [lr, #-4] ) @ get SWI instruction
#endif
ARM_BE8(rev r10, r10) @ little endian instruction
#elif defined(CONFIG_AEABI)
/*
* 我們主要看這里,EABI將系統(tǒng)調(diào)用號保存在r7中
*/
#elif defined(CONFIG_ARM_THUMB)
/* 先判斷是否為THUMB模式 */
tst r8, #PSR_T_BIT
addne scno, r7, #__NR_SYSCALL_BASE
USER( ldreq scno, [lr, #-4] )
#else
/* EABI模式 */
USER( ldr scno, [lr, #-4] ) @ 獲取系統(tǒng)調(diào)用號
#endif
adr tbl, sys_call_table @ tbl為r8,這里是將sys_call_table的地址(相對于此指令的偏移量)存入r8
#if defined(CONFIG_OABI_COMPAT)
/*
* 在EABI體系中,如果swi跟著的立即數(shù)為0,這段代碼不做處理,而如果是old abi體系,則根據(jù)系統(tǒng)調(diào)用號調(diào)用old abi體系的系統(tǒng)調(diào)用表(sys_oabi_call_table)
* 其實說白了,在EABI體系中,系統(tǒng)調(diào)用時使用swi 0x0進行軟中斷,r7寄存器保存系統(tǒng)調(diào)用號
* 而old abi體系中,是通過swi (系統(tǒng)調(diào)用號|magic)進行調(diào)用的
*/
bics r10, r10, #0xff000000
eorne scno, r10, #__NR_OABI_SYSCALL_BASE
ldrne tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
bic scno, scno, #0xff000000
eor scno, scno, #__NR_SYSCALL_BASE
#endif
local_restart:
ldr r10, [tsk, #TI_FLAGS] @ 檢查系統(tǒng)調(diào)用跟蹤
stmdb {r4, r5} @ 將第5和第6個參數(shù)壓入棧
tst r10, #_TIF_SYSCALL_WORK @ 判斷是否在跟蹤系統(tǒng)調(diào)用
bne __sys_trace
cmp scno, #NR_syscalls @ 檢測系統(tǒng)調(diào)用號是否在范圍內(nèi),NR_syscalls保存系統(tǒng)調(diào)用總數(shù)
adr lr, BSYM(ret_fast_syscall) @ 將返回地址保存到lr寄存器中,lr寄存器是用于函數(shù)返回的。
ldrcc pc, [tbl, scno, lsl #2] @ 調(diào)用相應系統(tǒng)調(diào)用例程,tbl(r8)保存著系統(tǒng)調(diào)用表(sys_call_table)地址,scno(r7)保存著系統(tǒng)調(diào)用號120,這里就轉(zhuǎn)到相應的處理例程上了。
add r1, sp, #S_OFF
2: cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back
bcs arm_syscall
mov why, #0 @ no longer a real syscall
b sys_ni_syscall @ not private func
#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
/*
* We failed to handle a fault trying to access the page
* containing the swi instruction, but we're not really in a
* position to return -EFAULT. Instead, return back to the
* instruction and re-enter the user fault handling path trying
* to page it in. This will likely result in sending SEGV to the
* current task.
*/
9001:
sub lr, lr, #4
str lr, [sp, #S_PC]
b ret_fast_syscall
#endif
ENDPROC(vector_swi) @ 返回
好的,終于跳轉(zhuǎn)到了系統(tǒng)調(diào)用表,現(xiàn)在我們看看系統(tǒng)調(diào)用表是怎么樣的一個形式
/* 文件地址: linux內(nèi)核目錄/arch/arm/kernel/calls.S */
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork)
CALL(sys_read)
CALL(sys_write)
/* 5 */ CALL(sys_open)
CALL(sys_close)
CALL(sys_ni_syscall) /* was sys_waitpid */
CALL(sys_creat)
CALL(sys_link)
/* 10 */ CALL(sys_unlink)
CALL(sys_execve)
CALL(sys_chdir)
CALL(OBSOLETE(sys_time)) /* used by libc4 */
CALL(sys_mknod)
/* 15 */ CALL(sys_chmod)
CALL(sys_lchown16)
CALL(sys_ni_syscall) /* was sys_break */
CALL(sys_ni_syscall) /* was sys_stat */
CALL(sys_lseek)
/* 20 */ CALL(sys_getpid)
CALL(sys_mount)
CALL(OBSOLETE(sys_oldumount)) /* used by libc4 */
CALL(sys_setuid16)
CALL(sys_getuid16)
/* 25 */ CALL(OBSOLETE(sys_stime))
CALL(sys_ptrace)
CALL(OBSOLETE(sys_alarm)) /* used by libc4 */
CALL(sys_ni_syscall) /* was sys_fstat */
CALL(sys_pause)
......................
......................
/* 120 */ CALL(sys_clone) /* 120在此,之前傳進來的系統(tǒng)調(diào)用號120進入內(nèi)核后會到這 */
CALL(sys_setdomainname)
CALL(sys_newuname)
CALL(sys_ni_syscall) /* modify_ldt */
CALL(sys_adjtimex)
/* 125 */ CALL(sys_mprotect)
CALL(sys_sigprocmask)
CALL(sys_ni_syscall) /* was sys_create_module */
CALL(sys_init_module)
CALL(sys_delete_module)
......................
......................
/* 375 */ CALL(sys_setns)
CALL(sys_process_vm_readv)
CALL(sys_process_vm_writev)
CALL(sys_kcmp)
CALL(sys_finit_module)
/* 380 */ CALL(sys_sched_setattr)
CALL(sys_sched_getattr)
CALL(sys_renameat2)
CALL(sys_seccomp)
CALL(sys_getrandom)
/* 385 */ CALL(sys_memfd_create)
CALL(sys_bpf)
#ifndef syscalls_counted
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
#define syscalls_counted
#endif
.rept syscalls_padding
CALL(sys_ni_syscall)
.endr
CALL為一個宏,而我們使用的那一行CALL(sys_clone)配合ldrcc pc,[tbl,scno,lsl #2]使用的結(jié)果就是把sys_clone的地址放入pc寄存器。具體我們仔細分析一下,首先先看看CALL宏展開,然后把CALL代入ldrcc,結(jié)果就很清晰了
/* CALL(x)宏展開 */
#define CALL(x) .equ NR_syscalls,NR_syscalls+1
#include "calls.S"
.ifne NR_syscalls - __NR_syscalls
.error "__NR_syscalls is not equal to the size of the syscall table"
.endif
/* 主要是后面這一段,
* 上面一段主要用于統(tǒng)計系統(tǒng)調(diào)用數(shù)量,并將數(shù)量保存到NR_syscalls中,具體實現(xiàn)說明可以參考http://www.tuicool.com/articles/QFj6zq
*/
#undef CALL
/* 其實就是生成一個數(shù)為x,相當于.long sys_clone,因為sys_clone是函數(shù)名,所以.long生成的是sys_clone函數(shù)名對應的地址 */
#define CALL(x) .long x
#ifdef CONFIG_FUNCTION_TRACER
/* 配合ldrcc一起看,原來ldrcc是這樣 */
ldrcc pc, [tbl, scno, lsl #2]
/* 把CALL(x)代入ldrcc,最后是這樣 */
ldrcc pc, sys_clone(函數(shù)地址)
清楚的看出來,ldrcc最后是將sys_clone的函數(shù)地址存入了pc寄存器,而sys_clone函數(shù)內(nèi)核是怎么定義的呢,如下:
/* 文件地址: linux內(nèi)核目錄/kernel/Fork.c */
/* 以下代碼根據(jù)不同的內(nèi)核配置定義了不同的clone函數(shù)
* 其最終都調(diào)用的do_fork函數(shù),我們先看看SYSCALL_DEFINE是怎么實現(xiàn)的吧,實現(xiàn)在此代碼片段后面
*/
#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);
}
/************************************************
* 我是代碼分界線
************************************************/
/* 文件地址: linux內(nèi)核目錄/include/linux.h */
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx
可以看出系統(tǒng)調(diào)用是使用SYSCALL_DEFINEx進行定義的,以我們的例子,實際上最后clone函數(shù)被定義為:
/* 展開前 */
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
/* 應用層默認fork參數(shù)(CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid) */
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
/* 展開后 */
asmlinkage long sys_clone (unsigned long clone_flags, unsigned long newsp, int __user * parent_tidptr, int __user * child_tidptr, int tls_val)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
終于看到最后系統(tǒng)會調(diào)用do_fork函數(shù)進行操作,接下來我們看看do_fork函數(shù)
/* 應用層的fork最后會通過sys_clone系統(tǒng)調(diào)用調(diào)用到此函數(shù) */
/* 應用層默認fork參數(shù)(CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid)
* clone_flags: CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
* stack_start: NULL
* stack_size: NULL
* parent_tidptr: NULL
* child_tidptr: &THREAD_SELF->tid
* pid: NULL
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/* 判斷是否進行跟蹤 */
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;
}
/* 調(diào)用copy_process進行初始化,返回初始化好的struct task_struct結(jié)構體,當我們調(diào)用fork時返回兩次的原因也是在這個函數(shù)當中,下回分析 */
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
if (!IS_ERR(p)) {
/* 創(chuàng)建成功 */
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
/* 獲取子進程PID */
pid = get_task_pid(p, PIDTYPE_PID);
/* 返回子進程pid所屬的命名空間所看到的局部PID */
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
/* 將新進程加入到CPU的運行隊列中 */
wake_up_new_task(p);
/* 跟蹤才會用到 */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
/* 如果是vfork調(diào)用,則在此等待vfork的進程結(jié)束 */
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
/* 創(chuàng)建失敗 */
nr = PTR_ERR(p);
}
/* 返回新進程PID(新進程在這會返回0) */
return nr;
}
在do_fork函數(shù)中,首先會根據(jù)clone_flags判斷是否對父進程進行了跟蹤(調(diào)試使用),如果進行了函數(shù)跟蹤(還需要判斷是否對子進程進行跟蹤),之后調(diào)用copy_process(do_fork的核心函數(shù),之后的文章會對它進行分析),在copy_process中會對子進程的許多結(jié)構體和參數(shù)進行初始化(同時在fork正常情況中為什么會返回兩次也是在此函數(shù)中實現(xiàn)的),do_fork最后就判斷是否是通過vfork創(chuàng)建,如果是vfork創(chuàng)建,則會使父進程阻塞直到子進程結(jié)束釋放所占內(nèi)存空間后才繼續(xù)執(zhí)行,最后do_fork子進程pid。
到這里,整個系統(tǒng)調(diào)用的入口就分析完了,其實整個流程也不算很復雜:應用層通過swi軟中斷進入內(nèi)核---->通過系統(tǒng)調(diào)用表選定目標系統(tǒng)調(diào)用--->執(zhí)行系統(tǒng)調(diào)用--->返回。
二、fork的基礎概念
2.1 fork 是什么
在 Linux 系統(tǒng)中,fork是一個系統(tǒng)調(diào)用,用于創(chuàng)建一個新的進程,這個新進程被稱為子進程,而調(diào)用fork的進程則是父進程 。fork函數(shù)就像是一把神奇的 “叉子”,將一個進程 “分叉” 成兩個,這兩個進程(父進程和子進程)從fork調(diào)用之后的代碼開始,各自獨立執(zhí)行,就像兩條從同一節(jié)點出發(fā)的不同路徑,后續(xù)的走向可能截然不同 。例如,一個負責數(shù)據(jù)處理的父進程,調(diào)用fork后,子進程可以繼承父進程的數(shù)據(jù)讀取部分,然后去執(zhí)行數(shù)據(jù)分析,而父進程繼續(xù)進行數(shù)據(jù)的收集工作 ,兩者相互協(xié)作又互不干擾。
2.2 fork 函數(shù)返回值的奧秘
fork函數(shù)的一個獨特之處在于它 “一次調(diào)用,兩次返回” 。當fork被調(diào)用后,操作系統(tǒng)會創(chuàng)建出子進程,然后在父進程和子進程中分別返回不同的值 。在父進程中,fork返回子進程的進程 ID(PID,是一個大于 0 的整數(shù)),這個 ID 就像是子進程的 “身份證號”,父進程可以通過它來識別和管理子進程 ;而在子進程中,fork返回 0,就好像在告訴子進程:“你是新創(chuàng)建的子進程” 。
我們通過一段簡單的 C 語言代碼來直觀感受一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
// 調(diào)用fork函數(shù)
pid = fork();
if (pid < 0) {
// fork失敗
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子進程
printf("I am the child process, my pid is %d, and my parent's pid is %d\n", getpid(), getppid());
} else {
// 父進程
printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
}
return 0;
}
在上述代碼中,fork函數(shù)執(zhí)行后,父進程會打印出自己的 PID 以及子進程的 PID,而子進程會打印出自己的 PID 和父進程的 PID 。通過返回值的不同,父進程和子進程能夠清晰地知道自己的 “身份”,進而執(zhí)行不同的代碼邏輯 。
三、fork工作原理
3.1進程的關鍵數(shù)據(jù)結(jié)構
在深入探討fork原理之前,我們需要先了解一些與進程密切相關的數(shù)據(jù)結(jié)構,它們是理解fork實現(xiàn)的關鍵 。
task_struct:這是進程描述符,就像是進程的 “身份證”,每個進程在內(nèi)核中都有一個對應的task_struct結(jié)構體 。它記錄了進程的所有相關信息,包括進程 ID(PID)、進程狀態(tài)(是運行態(tài)、就緒態(tài)還是阻塞態(tài)等)、內(nèi)存映射區(qū)域、文件描述符表、信號處理函數(shù)表等 。例如,通過task_struct中的 PID,系統(tǒng)可以唯一標識和區(qū)分不同的進程,就像每個人的身份證號是獨一無二的 ;而進程狀態(tài)信息則幫助內(nèi)核決定該進程何時可以獲得 CPU 資源,是馬上運行,還是需要等待某些條件滿足 。
mm_struct:進程內(nèi)存管理描述符,主要管理每個進程的虛擬內(nèi)存和物理內(nèi)存 。它包含了虛擬內(nèi)存區(qū)域的信息,以及內(nèi)存映射的相關設置等 。比如,當進程申請內(nèi)存時,mm_struct會參與管理內(nèi)存的分配,決定從哪里分配虛擬內(nèi)存,以及如何與物理內(nèi)存進行映射 。它就像是一個內(nèi)存管家,統(tǒng)籌著進程內(nèi)存的使用 。
vm_area_struct:虛擬內(nèi)存描述符,用于描述一個進程的虛擬內(nèi)存區(qū)域,包括起始和結(jié)束地址、訪問權限(是可讀、可寫還是可執(zhí)行)、映射的物理頁框號等信息 。每個vm_area_struct對應著進程虛擬內(nèi)存中的一個連續(xù)區(qū)域 。例如,進程的代碼段、數(shù)據(jù)段、堆、棧等在虛擬內(nèi)存中都有各自對應的vm_area_struct,通過它可以清晰地了解每個內(nèi)存區(qū)域的屬性和范圍 。
這些數(shù)據(jù)結(jié)構相互關聯(lián),共同構成了進程在系統(tǒng)中的完整描述 。task_struct包含了指向mm_struct的指針,通過它可以訪問到進程的內(nèi)存管理信息 ;而mm_struct中又包含了指向vm_area_struct鏈表的指針,用于管理進程的各個虛擬內(nèi)存區(qū)域 。它們之間的關系緊密,就像一個復雜的機器,各個零件協(xié)同工作,保證進程的正常運行 。
3.2fork 的詳細執(zhí)行步驟
當進程調(diào)用fork函數(shù)時,背后會發(fā)生一系列復雜而有序的操作 ,下面我們來詳細剖析:
①進入內(nèi)核態(tài)
進程在用戶態(tài)調(diào)用fork時,會通過軟件中斷(在 x86 架構中,通常是int 0x80或sysenter指令)進入內(nèi)核態(tài) 。這就像是從普通的街道進入了 “核心區(qū)域”,擁有了更高的權限 。
進入內(nèi)核態(tài)后,系統(tǒng)會找到sys_fork()系統(tǒng)調(diào)用處理函數(shù),開始處理fork請求 。這個過程就好比是一個市民向政府部門提交申請,政府部門收到申請后,安排專門的工作人員(sys_fork()函數(shù))來處理 。
②獲取 PID 與創(chuàng)建描述符
內(nèi)核首先會獲取一個可用的 PID,這個 PID 將作為新創(chuàng)建子進程的身份標識 。PID 的分配就像是給新出生的寶寶辦理身份證,每個 PID 在系統(tǒng)中都是唯一的 。接著,內(nèi)核調(diào)用copy_process()函數(shù),為子進程分配和初始化一個全新的task_struct 。在這個過程中,copy_process()會從父進程的task_struct復制大部分內(nèi)容,包括文件系統(tǒng)相關數(shù)據(jù)(如打開的文件描述符表,這樣子進程就可以繼承父進程打開的文件)、信號處理函數(shù)表(使得子進程能像父進程一樣響應各種信號)、命名空間、進程狀態(tài)等 。
同時,它會為新進程設置一些初始狀態(tài),比如將新進程的狀態(tài)設置為TASK_UNINTERRUPTIBLE(不可中斷睡眠狀態(tài)),這就像是新員工入職后,先被安排在一個 “待命” 的狀態(tài) ;還會為新進程分配一個獨立的內(nèi)核棧,用于內(nèi)核態(tài)下的函數(shù)調(diào)用和數(shù)據(jù)存儲 ,并初始化計時器、信號等數(shù)據(jù)結(jié)構 。
③復制內(nèi)存映射區(qū)域
在copy_process()中,會調(diào)用dup_mmap()函數(shù)來復制父進程的內(nèi)存映射區(qū)域 。dup_mmap()會仔細遍歷父進程的所有vm_area_struct,并為子進程創(chuàng)建相應的內(nèi)存映射區(qū)域 。但此時,父子進程只是簡單地共享同一組頁表項,實際的物理內(nèi)存頁還未復制 。
這就好比兩個房間(父子進程)共享了同一份房間布局圖(頁表項),但房間里的實際物品(物理內(nèi)存頁)還沒有復制 。這樣做的好處是可以快速創(chuàng)建子進程,避免了大量物理內(nèi)存的復制開銷 。
④寫時復制(COW)設置
復制完vm_area_struct后,dup_mmap()會調(diào)用pud_mkwrite等函數(shù),將父子進程共享的所有頁表項都標記為只讀(設置頁表項的權限位為非可寫) 。這是寫時復制機制的關鍵一步,當父子進程中有一方試圖寫入共享的內(nèi)存頁時,CPU 會觸發(fā)頁保護異常 。
例如,父進程和子進程一開始共享某一內(nèi)存頁,當子進程想要修改這個內(nèi)存頁時,由于頁表項是只讀的,就會引發(fā)異常,從而觸發(fā)內(nèi)核的異常處理程序執(zhí)行寫時復制操作 ,就像是原本共同使用一份文件的兩人,當其中一人想要修改文件時,系統(tǒng)會為他復制一份獨立的文件副本 。
⑤其他設置
在copy_process()中,還會進行一些其他重要的設置 。比如復制父進程的信號處理程序表,確保子進程也能正確響應不同的信號,就像孩子繼承了父母應對各種情況的能力 ;為子進程設置SIGCHLD信號的默認處理程序,以便父進程能夠捕獲子進程的結(jié)束信號,這就像是給子進程和父進程之間建立了一個特殊的 “通訊渠道”,用于傳遞子進程結(jié)束的消息 。如果新創(chuàng)建的進程是一個內(nèi)核線程,copy_process()會進行一些額外的設置,如禁止內(nèi)核線程加載執(zhí)行用戶空間代碼、禁止訪問用戶態(tài)內(nèi)存等,這是為了保證內(nèi)核線程的安全性和穩(wěn)定性 。
此外,copy_process()會復制父進程的調(diào)度策略、優(yōu)先級等相關信息,并為子進程分配新的運行時統(tǒng)計數(shù)據(jù)結(jié)構,用于 CPU 調(diào)度 ,就像為子進程制定了一份專屬的 “工作安排表” 。最后,新創(chuàng)建的子進程會被加入相應的進程鏈表中,如任務隊列、反饋優(yōu)先級鏈表等,以便內(nèi)核進行進程調(diào)度和管理,這就像是將新員工加入到公司的組織架構中,方便進行工作安排和管理 。
⑥寫時復制異常處理
當子進程對共享內(nèi)存區(qū)域進行寫操作而發(fā)生頁保護異常時,寫時復制異常處理程序do_cow_fault()就會發(fā)揮作用 。它的主要工作包括為發(fā)生寫操作的內(nèi)存頁分配新的內(nèi)核頁框(物理內(nèi)存頁),就像是為需要修改文件的人分配一個新的文件存放空間 ;將原有的內(nèi)存頁內(nèi)容復制到新的頁框中,保證數(shù)據(jù)的一致性 ;修改相應的頁表項,使其指向新分配的物理內(nèi)存頁框,并設置為可寫,這樣子進程就可以在自己獨立的內(nèi)存頁上進行寫操作了 ;
同時,在原有的物理內(nèi)存頁上設置寫保護,避免不必要的復制 。通過這一系列操作,父子進程最終會擁有各自獨立的物理內(nèi)存副本,從而可以進行自身的數(shù)據(jù)寫入而不會相互影響 。
⑦執(zhí)行切換和系統(tǒng)調(diào)用返回
最后,內(nèi)核會決定父進程和子進程的執(zhí)行順序 。一般情況下,內(nèi)核會先讓子進程執(zhí)行,因為子進程的執(zhí)行狀態(tài)被設置為TASK_UNINTERRUPTIBLE 。在子進程執(zhí)行時,會執(zhí)行一些額外的初始化工作,如清理上下文、設置執(zhí)行計數(shù)器等 。
fork系統(tǒng)調(diào)用在父子進程中的返回值不同,在子進程中,fork返回0,就像是在告訴子進程 “你是新創(chuàng)建的,現(xiàn)在可以開始你的獨立旅程了” ;在父進程中,fork返回新創(chuàng)建子進程的PID,父進程可以通過這個PID來識別和管理子進程 。通過這種不同的返回值,父子進程可以區(qū)分不同的執(zhí)行路徑,各自執(zhí)行自己的代碼邏輯 。
3.3copy_process源碼分析
/* 代碼目錄:linux源碼/kernel/Fork.c */
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
/* CLONE_FS 不能與 CLONE_NEWNS 或 CLONE_NEWUSER 同時設置 */
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);
/* 創(chuàng)建線程時線程之間要共享信號處理函數(shù) */
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* 父子進程共享信號處理函數(shù)時必須共享內(nèi)存地址空間
* 這就是為什么書上寫的fork出來的父子進程有其獨立的信號處理函數(shù),因為他們的內(nèi)存地址空間不同
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/*
* 防止參數(shù)init進程的兄弟進程
* 只有init進程的 signal->flags & SIGNAL_UNKILLABLE 為真
* 因為當進程退出時實際上是成為了僵尸進程(zombie),而要通過init進程將它回收,而如果此進程為init的兄弟進程,則沒辦法將其回收
*/
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
/* 如果新的進程將會有新的用戶空間或者pid,則不能讓它共享父進程的線程組或者信號處理或者父進程 */
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);
}
/* 附加安全檢查 */
retval = security_task_create(clone_flags);
if (retval)
goto fork_out;
retval = -ENOMEM;
/* 為新進程分配struct task_struct內(nèi)存和內(nèi)核棧內(nèi)存 */
p = dup_task_struct(current);
if (!p)
goto fork_out;
/* ftrace是用于內(nèi)核性能分析和跟蹤的 */
ftrace_graph_init_task(p);
/* futex初始化,其用于SYSTEM V IPC,具體可見 http://blog.chinaunix.net/uid-7295895-id-3011238.html */
rt_mutex_init_task(p);
#ifdef CONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
retval = -EAGAIN;
/* 檢查 tsk->signal->rlim[RLIMIT_NPROC].rlim_cur是否小于等于用戶所擁有的進程數(shù),rlim結(jié)構體表示相關資源的最大值 */
if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) {
/* INIT_USER是root權限。檢查父進程是否有root權限 */
if (p->real_cred->user != INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
current->flags &= ~PF_NPROC_EXCEEDED;
/* 將父進程的cred復制到子進程的real_cred和cred。struct cred用于安全操作的結(jié)構 */
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
retval = -EAGAIN;
/* 進程數(shù)量是否超出系統(tǒng)允許最大進程數(shù)量,最大進程數(shù)量跟內(nèi)存有關,一般原則是所有的進程內(nèi)核棧(默認8K)加起來不超過總內(nèi)存的1/8,可通過/proc/sys/kernel/threads-max改寫此值 */
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
/* 如果實現(xiàn)新進程的執(zhí)行域和可執(zhí)行格式的內(nèi)核函數(shù)都包含在內(nèi)核模塊中,則遞增其使用計數(shù) */
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
/* 清除 PF_SUPERPRIV(表示進程使用了超級用戶權限) 和 PF_WQ_WORKER(使用了工作隊列) */
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
/* 設置 PF_FORKNOEXEC 表明此子進程還沒有進行 execve() 系統(tǒng)調(diào)用 */
p->flags |= PF_FORKNOEXEC;
/* 初始化子進程的子進程鏈表和兄弟進程鏈表為空 */
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
/* 見 http://www.ibm.com/developerworks/cn/linux/l-rcu/ */
rcu_copy_process(p);
p->vfork_done = NULL;
/* 初始化分配鎖,此鎖用于保護分配內(nèi)存,文件,文件系統(tǒng)等操作 */
spin_lock_init(&p->alloc_lock);
/* 信號列表初始化,此列表保存被掛起的信號 */
init_sigpending(&p->pending);
/* 代碼執(zhí)行時間變量都置為0 */
p->utime = p->stime = p->gtime = 0;
p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
seqlock_init(&p->vtime_seqlock);
p->vtime_snap = 0;
p->vtime_snap_whence = VTIME_SLEEPING;
#endif
#if defined(SPLIT_RSS_COUNTING)
memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endif
/* 此變量一般用于epoll和select,從父進程復制過來 */
p->default_timer_slack_ns = current->timer_slack_ns;
/* 初始化進程IO計數(shù)結(jié)構 */
task_io_accounting_init(&p->ioac);
acct_clear_integrals(p);
/* 初始化cputime_expires結(jié)構 */
posix_cpu_timers_init(p);
/* 設置進程創(chuàng)建時間 */
p->start_time = ktime_get_ns();
p->real_start_time = ktime_get_boot_ns();
/* io_context 和 audit_context 置空 */
p->io_context = NULL;
p->audit_context = NULL;
/* 如果創(chuàng)建的是線程,因為需要修改到當前進程的描述符,會先上鎖 */
if (clone_flags & CLONE_THREAD)
threadgroup_change_begin(current);
cgroup_fork(p);
#ifdef CONFIG_NUMA
p->mempolicy = mpol_dup(p->mempolicy);
if (IS_ERR(p->mempolicy)) {
retval = PTR_ERR(p->mempolicy);
p->mempolicy = NULL;
goto bad_fork_cleanup_threadgroup_lock;
}
#endif
#ifdef CONFIG_CPUSETS
p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
p->cpuset_slab_spread_rotor = NUMA_NO_NODE;
seqcount_init(&p->mems_allowed_seq);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
p->irq_events = 0;
p->hardirqs_enabled = 0;
p->hardirq_enable_ip = 0;
p->hardirq_enable_event = 0;
p->hardirq_disable_ip = _THIS_IP_;
p->hardirq_disable_event = 0;
p->softirqs_enabled = 1;
p->softirq_enable_ip = _THIS_IP_;
p->softirq_enable_event = 0;
p->softirq_disable_ip = 0;
p->softirq_disable_event = 0;
p->hardirq_context = 0;
p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
p->lockdep_depth = 0; /* no locks held yet */
p->curr_chain_key = 0;
p->lockdep_recursion = 0;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
p->sequential_io = 0;
p->sequential_io_avg = 0;
#endif
/* 初始化子進程的調(diào)度優(yōu)先級和策略,在此并沒有將此進程加入到運行隊列,在copy_process返回之后加入 */
retval = sched_fork(clone_flags, p);
if (retval)
goto bad_fork_cleanup_policy;
/* perf event是一個性能調(diào)優(yōu)工具,具體見 http://blog.sina.com.cn/s/blog_98822316010122ex.html */
retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
goto bad_fork_cleanup_perf;
/* 初始化 p->sysvshm.shm_clist 鏈表頭 */
shm_init_task(p);
/* copy_semundo, copy_files, copy_fs, copy_sighand, copy_signal, copy_mm, copy_namespaces, copy_io都是根據(jù)clone_flags從父進程做相應的復制 */
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
/* 判斷是否設置 CLONE_SIGHAND ,如果是(線程必須為是),增加父進行的sighand引用計數(shù),如果否(創(chuàng)建的必定是子進程),將父線程的sighand_struct復制到子進程中 */
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
/* 如果創(chuàng)建的是線程,直接返回0,如果創(chuàng)建的是進程,則會將父進程的信號屏蔽和安排復制到子進程中 */
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
/*
* 如果是進程,則將父進程的mm_struct結(jié)構復制到子進程中,然后修改當中屬于子進程有別于父進程的信息(如頁目錄)
* 如果是線程,則將子線程的mm指針和active_mm指針都指向父進程的mm指針所指結(jié)構。
*/
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
/*
* 初始化子進程內(nèi)核棧和thread_struct結(jié)構體
* 當進程切換時,進程的硬件上下文一般保存于三個地方: tss_struct(保存進程內(nèi)核棧地址,I/O許可權限位),thread_struct(大部分非通用寄存器),進程內(nèi)核棧(通用寄存器)
* copy_thread函數(shù)會將父進程的thread_struct和內(nèi)核棧數(shù)據(jù)復制到子進程中,并將子進程的返回值置為0(x86返回值保存在eax中,arm保存在r0中,即把eax或者r0所在的內(nèi)核棧數(shù)據(jù)置為0)
* copy_thread函數(shù)還會將子進程的eip寄存器值設置為ret_from_fork()的地址,即當子進程首次被調(diào)用就立即執(zhí)行系統(tǒng)調(diào)用clone返回。
* 所以應用層調(diào)用fork()函數(shù)后,子進程返回0,父進程返回子進程ID(返回子進程ID在之后代碼中會實現(xiàn))
*/
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io;
/* 判斷是不是init進程 */
if (pid != &init_struct_pid) {
retval = -ENOMEM;
/* 分配pid */
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
}
/* 如果設置了CLONE_CHILD_SETTID則將task_struct中的set_child_tid指向用戶空間的child_tidptr,否則置空 */
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
/* 如果設置了CLONE_CHILD_CLEARTID則將task_struct中的clear_child_tid指向用戶空間的child_tidptr,否則置空 */
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;
#ifdef CONFIG_BLOCK
p->plug = NULL;
#endif
#ifdef CONFIG_FUTEX
p->robust_list = NULL;
#ifdef CONFIG_COMPAT
p->compat_robust_list = NULL;
#endif
INIT_LIST_HEAD(&p->pi_state_list);
p->pi_state_cache = NULL;
#endif
/*
* 如果共享VM或者vfork創(chuàng)建,信號棧清空
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = 0;
/*
* 系統(tǒng)調(diào)用跟蹤時應該禁止單步執(zhí)行
*/
user_disable_single_step(p);
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
clear_all_latency_tracing(p);
/* 將子進程的PID設置為分配的PID在全局namespace中分配的值,在不同namespace中進程的PID不同,而p->pid保存的是全局的namespace中所分配的PID */
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
/* 創(chuàng)建的是線程 */
p->exit_signal = -1;
/* 線程組的所有線程的group_leader都一致 */
p->group_leader = current->group_leader;
/* 線程組的所有線程的tgid都一致,使用getpid返回的就是tgid */
p->tgid = current->tgid;
} else {
/* 創(chuàng)建的是子進程 */
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p;
/* tgid與pid一致,所以當創(chuàng)建子線程時,tgid與主線程的一致 */
p->tgid = p->pid;
}
/* 初始化頁框中臟頁數(shù)量為0 */
p->nr_dirtied = 0;
/* 初始化臟頁數(shù)量臨界值,當臟頁數(shù)量到達臨界值時,會調(diào)用balance_dirty_pages()將臟頁寫入磁盤 */
p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
/* 將臟頁寫入磁盤的開始時間 */
p->dirty_paused_when = 0;
p->pdeath_signal = 0;
/* 初始化線程組鏈表為空 */
INIT_LIST_HEAD(&p->thread_group);
p->task_works = NULL;
/* 到此系統(tǒng)中已經(jīng)存在此進程(線程),但是它還不能夠執(zhí)行,需要等待父進程對其處理,這里會上鎖 */
write_lock_irq(&tasklist_lock);
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
/* 創(chuàng)建的是兄弟進程或者相同線程組線程 */
/* 其父進程為父進程的父進程 */
p->real_parent = current->real_parent;
/* 其父進程執(zhí)行域為父進程的父進程執(zhí)行域 */
p->parent_exec_id = current->parent_exec_id;
} else {
/* 創(chuàng)建的是子進程 */
/* 父進程為父進程 */
p->real_parent = current;
/* 父進程的執(zhí)行域為父進程的執(zhí)行域 */
p->parent_exec_id = current->self_exec_id;
}
/* 當前進程信號處理上鎖,這里應該是禁止了信號處理 */
spin_lock(¤t->sighand->siglock);
/*
* seccomp與系統(tǒng)安全有關,具體見 http://note.sdo.com/u/634687868481358385/NoteContent/M5cEN~kkf9BFnM4og00239
*/
copy_seccomp(p);
/*
* 在fork之前,進程組和會話信號都需要送到父親結(jié)點,而在fork之后,這些信號需要送到父親和孩子結(jié)點。
* 如果我們在將新進程添加到進程組的過程中出現(xiàn)一個信號,而這個掛起信號會導致當前進程退出(current),我們的子進程就不能夠被kill或者退出了
* 所以這里要檢測父進程有沒有信號被掛起。
*/
recalc_sigpending();
if (signal_pending(current)) {
/* 包含有掛起進程,錯誤 */
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_free_pid;
}
if (likely(p->pid)) {
/* 如果子進程需要跟蹤,就將 current->parent 賦值給 tsk->parent ,并將子進程插入調(diào)試程序的跟蹤鏈表中 */
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
/* p->pids[PIDTYPE_PID].pid = pid; */
init_task_pid(p, PIDTYPE_PID, pid);
/* 如果是子進程(其實就是判斷 p->exit_signal 是否大于等于0,創(chuàng)建的是線程的話,exit_signal的值為-1) */
if (thread_group_leader(p)) {
/* p->pids[PIDTYPE_PGID].pid = current->group_leader->pids[PIDTYPE_PGID].pid; PGID為進程組ID,所以直接復制父進程的pgid */
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
/* p->pids[PIDTYPE_SID].pid = current->group_leader->pids[PIDTYPE_SID].pid; SID為會話組ID,當沒有使用setsid()時,子進程的sid與父進程一致 */
init_task_pid(p, PIDTYPE_SID, task_session(current));
/* return pid->numbers[pid->level].nr == 1; 判斷新進程是否處于一個新創(chuàng)建的namespace中(新進程所在的新namespace中的pid會為1,以此判斷) */
if (is_child_reaper(pid)) {
/* 將當前namespace的init進程設置為此新進程 */
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
p->signal->leader_pid = pid;
p->signal->tty = tty_kref_get(current->signal->tty);
/* 將此進程添加到父進程的子進程鏈表 */
list_add_tail(&p->sibling, &p->real_parent->children);
/* 將此進程task_struct加入到task鏈表中 */
list_add_tail_rcu(&p->tasks, &init_task.tasks);
/* 將新進程描述符的pgid結(jié)構插入pgid_hash */
attach_pid(p, PIDTYPE_PGID);
/* 將新進程描述符的sid結(jié)構插入sid_hash */
attach_pid(p, PIDTYPE_SID);
/* 當前cpu進程數(shù)量加1 */
__this_cpu_inc(process_counts);
} else {
/* 創(chuàng)建的是線程,這里的處理導致了線程會共享信號 */
current->signal->nr_threads++;
atomic_inc(¤t->signal->live);
atomic_inc(¤t->signal->sigcnt);
/* 將新線程的thread_group結(jié)點加入到線程組的領頭線程的thread_group鏈表中 */
list_add_tail_rcu(&p->thread_group,
&p->group_leader->thread_group);
/* 將新線程的thread_node結(jié)點加入的新線程的signal->thread_head中 */
list_add_tail_rcu(&p->thread_node,
&p->signal->thread_head);
}
/* 將新進程描述符的pid結(jié)構插入pid_hash */
attach_pid(p, PIDTYPE_PID);
/* 當前系統(tǒng)進程數(shù)加1 */
nr_threads++;
}
/* 已創(chuàng)建的進程數(shù)量加1 */
total_forks++;
/* 釋放當前進程信號處理鎖 */
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
/* 釋放tasklist_lock鎖 */
write_unlock_irq(&tasklist_lock);
/* 將新進程與proc文件系統(tǒng)進行關聯(lián) */
proc_fork_connector(p);
cgroup_post_fork(p);
/* 如果創(chuàng)建的是線程,釋放此鎖 */
if (clone_flags & CLONE_THREAD)
threadgroup_change_end(current);
perf_event_fork(p);
trace_task_newtask(p, clone_flags);
uprobe_copy_process(p, clone_flags);
/* 返回新進程的task_struct結(jié)構 */
return p;
/* 以下為執(zhí)行期間的錯誤處理 */
bad_fork_free_pid:
if (pid != &init_struct_pid)
free_pid(pid);
bad_fork_cleanup_io:
if (p->io_context)
exit_io_context(p);
bad_fork_cleanup_namespaces:
exit_task_namespaces(p);
bad_fork_cleanup_mm:
if (p->mm)
mmput(p->mm);
bad_fork_cleanup_signal:
if (!(clone_flags & CLONE_THREAD))
free_signal_struct(p->signal);
bad_fork_cleanup_sighand:
__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
exit_fs(p); /* blocking */
bad_fork_cleanup_files:
exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
exit_sem(p);
bad_fork_cleanup_audit:
audit_free(p);
bad_fork_cleanup_perf:
perf_event_free_task(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
mpol_put(p->mempolicy);
bad_fork_cleanup_threadgroup_lock:
#endif
if (clone_flags & CLONE_THREAD)
threadgroup_change_end(current);
delayacct_tsk_free(p);
module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:
atomic_dec(&p->cred->user->processes);
exit_creds(p);
bad_fork_free:
free_task(p);
fork_out:
return ERR_PTR(retval);
}
四、fork應用實例與技巧
4.1簡單示例代碼分析
下面我們通過一個簡單的 C 語言示例代碼,來更直觀地了解fork在實際編程中的運用 。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
int num = 10;
// 調(diào)用fork函數(shù)
pid = fork();
if (pid < 0) {
// fork失敗
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子進程
num = num * 2;
printf("I am the child process, num is %d, my pid is %d\n", num, getpid());
} else {
// 父進程
num = num + 5;
printf("I am the parent process, num is %d, my child's pid is %d\n", num, pid);
}
return 0;
}
在這段代碼中,我們首先定義了一個變量num并初始化為 10 。然后調(diào)用fork函數(shù)創(chuàng)建子進程 。如果fork返回值小于 0,表示創(chuàng)建子進程失敗,通過perror函數(shù)輸出錯誤信息并返回 1 。如果返回值為 0,說明當前是子進程,子進程將num乘以 2,然后打印出自己的進程 ID 和修改后的num值 。
如果返回值大于 0,說明當前是父進程,父進程將num加上 5,并打印出自己的信息以及子進程的 PID 。通過這個簡單的例子,我們可以看到父子進程雖然最初共享相同的變量值,但在后續(xù)的執(zhí)行過程中,它們可以獨立地對變量進行修改,互不影響 ,這充分展示了fork創(chuàng)建獨立執(zhí)行路徑的特性 。
4.2解決實際問題場景
網(wǎng)絡服務器場景:在網(wǎng)絡服務器中,fork發(fā)揮著至關重要的作用 。當一個服務器接收到客戶端的連接請求時,它可以調(diào)用fork創(chuàng)建一個子進程來專門處理這個客戶端的請求 。這樣,父進程就可以繼續(xù)監(jiān)聽其他客戶端的連接,從而實現(xiàn)并發(fā)處理多個客戶端請求的功能 。
例如,一個 Web 服務器,當有用戶訪問網(wǎng)頁時,服務器通過fork創(chuàng)建子進程,子進程負責處理用戶的頁面請求,如解析 HTTP 請求、讀取網(wǎng)頁文件、生成響應內(nèi)容等,而父進程則繼續(xù)等待新的用戶連接,大大提高了服務器的處理效率和響應速度 ,能夠同時為多個用戶提供服務 。
數(shù)據(jù)分析場景:在處理大規(guī)模數(shù)據(jù)分析任務時,fork也能派上用場 。假設我們有一個龐大的數(shù)據(jù)集需要進行復雜的統(tǒng)計分析,如計算平均值、方差等 。我們可以利用fork創(chuàng)建多個子進程,每個子進程負責處理數(shù)據(jù)集的一部分 。
比如,將一個包含 100 萬條數(shù)據(jù)的文件分成 10 個子部分,每個子進程處理 10 萬條數(shù)據(jù),最后父進程收集各個子進程的計算結(jié)果并進行匯總,從而加快整個數(shù)據(jù)分析的速度,充分利用多核 CPU 的計算資源,提高數(shù)據(jù)分析的效率 。
4.3注意事項與常見問題
資源競爭問題:在使用fork時,由于父子進程共享部分資源(如打開的文件描述符),可能會出現(xiàn)資源競爭的情況 。例如,父子進程同時對同一個文件進行寫操作,可能會導致文件內(nèi)容混亂 。為了避免這種情況,可以使用文件鎖機制,如flock函數(shù),在進行文件操作前先獲取文件鎖,確保同一時間只有一個進程能夠?qū)ξ募M行寫操作 。
另外,在多線程程序中調(diào)用fork要格外小心,因為多線程程序中每個線程都有自己的棧和寄存器狀態(tài),調(diào)用fork時,子進程會繼承父進程的所有線程,這可能會導致復雜的狀態(tài)不一致性和資源競爭問題,所以通常建議避免在多線程程序中調(diào)用fork,如果確實需要創(chuàng)建新進程,可以考慮使用exec函數(shù)族 。
子進程退出處理:子進程退出時,如果父進程沒有及時處理,子進程就會變成僵尸進程,占用系統(tǒng)資源 。為了避免產(chǎn)生僵尸進程,父進程可以調(diào)用wait或waitpid函數(shù)來等待子進程結(jié)束,并獲取子進程的退出狀態(tài) 。wait函數(shù)會阻塞父進程,直到有子進程結(jié)束;而waitpid函數(shù)則更加靈活,可以指定等待特定的子進程,并且可以設置非阻塞模式 。例如:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid, wpid;
int status;
pid = fork();
if (pid == -1) {
perror("fork error");
return 1;
} else if (pid == 0) {
// 子進程
sleep(2);
printf("Child process is exiting\n");
return 3;
} else {
// 父進程
wpid = waitpid(pid, &status, 0);
if (wpid == -1) {
perror("waitpid error");
return 1;
}
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child was terminated by signal %d\n", WTERMSIG(status));
}
}
return 0;
}
在上述代碼中,父進程通過waitpid等待子進程結(jié)束,并通過WIFEXITED和WEXITSTATUS宏來判斷子進程是否正常退出以及獲取退出狀態(tài) ,這樣就可以有效地避免僵尸進程的產(chǎn)生 。
五、對比與拓展
5.1 fork與vfork的對比
在 Linux 進程創(chuàng)建中,除了fork,還有一個與之類似的系統(tǒng)調(diào)用vfork ,它們在功能上有相似之處,但也存在著諸多重要的區(qū)別 。
創(chuàng)建進程與地址空間共享:fork創(chuàng)建子進程時,會為子進程復制父進程的地址空間,包括代碼段、數(shù)據(jù)段、堆和棧等 。雖然在復制時采用了寫時復制(COW)技術,即最初父子進程共享物理內(nèi)存頁,只有當有寫操作發(fā)生時才為子進程分配獨立的物理內(nèi)存頁,但從本質(zhì)上來說,子進程擁有自己獨立的虛擬地址空間 ,后續(xù)的寫操作會使父子進程的數(shù)據(jù)相互獨立 。而vfork創(chuàng)建的子進程則直接與父進程共享地址空間 ,子進程對數(shù)據(jù)的修改會直接反映在父進程中 ,它們就像在同一間屋子里活動,所有的物品(數(shù)據(jù))都是共享的 。
執(zhí)行順序:fork創(chuàng)建的父子進程執(zhí)行順序是不確定的 ,這取決于內(nèi)核的調(diào)度算法 。有可能父進程先執(zhí)行,也有可能子進程先執(zhí)行 。而vfork則保證子進程先運行 ,在子進程調(diào)用exec函數(shù)族(用于執(zhí)行另一個程序,替換當前進程的內(nèi)存映像)或exit(用于終止進程)之前,父進程會被阻塞,處于等待狀態(tài) ,只有當子進程執(zhí)行了這兩個操作之一后,父進程才有可能被調(diào)度運行 。
適用場景:由于fork創(chuàng)建的子進程擁有獨立的地址空間,適合用于需要父子進程并發(fā)執(zhí)行且相互獨立工作的場景 ,比如前面提到的網(wǎng)絡服務器中處理多個客戶端請求,每個子進程獨立處理自己的任務,互不干擾 。而vfork由于共享地址空間且保證子進程先運行的特性,適用于子進程創(chuàng)建后立即要執(zhí)行exec函數(shù)族去執(zhí)行另一個程序的場景 ,這樣可以避免不必要的地址空間復制開銷 ,提高效率 。例如,當一個程序需要啟動另一個程序時,可以使用vfork創(chuàng)建子進程,然后子進程調(diào)用exec函數(shù)族來加載并運行新程序 。
5.2 fork在不同Linux版本中的優(yōu)化
隨著 Linux 操作系統(tǒng)的不斷發(fā)展和演進,fork在不同版本中也經(jīng)歷了一系列的優(yōu)化改進 ,以提升性能和資源管理效率 。
早期版本:在早期的Linux版本中,fork采用的是相對簡單直接的復制方式 。當調(diào)用fork時,會將父進程的整個地址空間完整地復制給子進程 ,包括所有的內(nèi)存頁面 。這種方式雖然實現(xiàn)簡單,但效率較低,因為大量的內(nèi)存復制操作會消耗較多的時間和系統(tǒng)資源 ,尤其是在父進程內(nèi)存占用較大時,fork的開銷會非常明顯 。
寫時復制(COW)技術引入:為了提高fork的效率,Linux 內(nèi)核引入了寫時復制(COW)技術 。從2.0版本開始,fork創(chuàng)建子進程時不再立即復制物理內(nèi)存頁,而是讓父子進程共享同一組頁表項,指向相同的物理內(nèi)存頁 。只有當父子進程中有一方試圖對共享內(nèi)存頁進行寫操作時,才會觸發(fā)寫時復制機制 ,為執(zhí)行寫操作的進程分配新的物理內(nèi)存頁,并將原內(nèi)存頁內(nèi)容復制到新頁中 。這種優(yōu)化大大減少了fork時的內(nèi)存復制開銷,加快了子進程的創(chuàng)建速度 ,同時也節(jié)省了內(nèi)存資源 。
后續(xù)版本優(yōu)化:在后續(xù)的 Linux 版本中,對fork的優(yōu)化還在繼續(xù) 。例如,在進程調(diào)度方面,內(nèi)核不斷改進調(diào)度算法,使得fork創(chuàng)建的父子進程能夠更合理地分配 CPU資源 ,提高整體的并發(fā)執(zhí)行效率 。在內(nèi)存管理方面,進一步優(yōu)化了頁表的管理和更新機制 ,減少了寫時復制過程中的開銷 。此外,還針對多處理器系統(tǒng)進行了優(yōu)化,提高了fork在多核環(huán)境下的性能 ,使得父子進程能夠更好地利用多核 CPU 的計算資源 。