Linux內核代碼追蹤:如何“分裂”出一個新進程的?
在生活中,我們經常會進行文件復制操作,比如將一份重要的文檔復制到多個文件夾,以方便在不同場景下使用,每個復制后的文件都擁有獨立的存儲空間,但內容最初與原文件一致。在生物學領域,克隆技術也是一種復制,克隆羊多莉就是通過復制母體的遺傳物質誕生,擁有和母體幾乎相同的基因。而在 Linux 操作系統中,也存在類似的 “復制” 概念,那就是進程復制,其中fork函數便是實現進程復制的核心,它如同一個神奇的 “分身術”,讓一個進程能夠創建出與自身幾乎一模一樣的子進程 ,它們是如何實現的。
我們主要聊聊從glibc庫進入內核,再從內核出來的情景。為了方便期間,我們的硬件平臺為arm,linux內核為3.18.3,glibc庫版本為2.20,可從http://ftp.gnu.org/gnu/glibc/下載源碼。接下來,讓我們一起深入探索。
一、Glibc到kernel
我們設定硬件平臺為arm,glibc庫版本為2.20,因為不同的CPU體系結構中,glibc庫通過系統調用進入kernel庫的方法是不一樣的。當glibc準備進入kernel時,流程如下:
/* glibc最后會調用到一個INLINE_SYSCALL宏,參數如下 */
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宏最終會調用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宏,只需要看其最終調用的INTERNAL_SYSCALL_RAW宏,需要注意的是,INTERNAL_SYSCALL調用INTERNAL_SYSCALL_RAW時,通過SYS_ify(name)宏將name轉為了系統調用號
* name: 120(通過SYS_ify(name)宏已經將clone轉為了系統調用號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實現的結果就是將args[0]存到了r0...args[4]存到了r4中,并將name(120)綁定到r7寄存器。然后通過swi 0x0指令進行了軟中斷。0x0是一個24位的立即數,用于軟中斷執行程序判斷執行什么操作。當執行這條指令時,CPU會跳轉至中斷向量表的軟中斷指令處,執行該處保存的調用函數,而在函數中會根據swi后面的24位立即數(在我們的例子中是0x0)執行不同操作。在這時候CPU已經處于保護模式,陷入內核中。現在進入到linux內核中后,具體看此時內核是怎么操作的吧。
/* 源文件地址: 內核目錄/arch/arm/kernel/entry-common.S */
ENTRY(vector_swi)
/*
* 保存現場
*/
#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
/*
* 以下代碼根據不同arm體系結構獲取系統調用號
*/
#if defined(CONFIG_OABI_COMPAT)
/*
* 如果內核配置了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將系統調用號保存在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] ) @ 獲取系統調用號
#endif
adr tbl, sys_call_table @ tbl為r8,這里是將sys_call_table的地址(相對于此指令的偏移量)存入r8
#if defined(CONFIG_OABI_COMPAT)
/*
* 在EABI體系中,如果swi跟著的立即數為0,這段代碼不做處理,而如果是old abi體系,則根據系統調用號調用old abi體系的系統調用表(sys_oabi_call_table)
* 其實說白了,在EABI體系中,系統調用時使用swi 0x0進行軟中斷,r7寄存器保存系統調用號
* 而old abi體系中,是通過swi (系統調用號|magic)進行調用的
*/
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] @ 檢查系統調用跟蹤
stmdb {r4, r5} @ 將第5和第6個參數壓入棧
tst r10, #_TIF_SYSCALL_WORK @ 判斷是否在跟蹤系統調用
bne __sys_trace
cmp scno, #NR_syscalls @ 檢測系統調用號是否在范圍內,NR_syscalls保存系統調用總數
adr lr, BSYM(ret_fast_syscall) @ 將返回地址保存到lr寄存器中,lr寄存器是用于函數返回的。
ldrcc pc, [tbl, scno, lsl #2] @ 調用相應系統調用例程,tbl(r8)保存著系統調用表(sys_call_table)地址,scno(r7)保存著系統調用號120,這里就轉到相應的處理例程上了。
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) @ 返回
好的,終于跳轉到了系統調用表,現在我們看看系統調用表是怎么樣的一個形式
/* 文件地址: linux內核目錄/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在此,之前傳進來的系統調用號120進入內核后會到這 */
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]使用的結果就是把sys_clone的地址放入pc寄存器。具體我們仔細分析一下,首先先看看CALL宏展開,然后把CALL代入ldrcc,結果就很清晰了
/* 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
/* 主要是后面這一段,
* 上面一段主要用于統計系統調用數量,并將數量保存到NR_syscalls中,具體實現說明可以參考http://www.tuicool.com/articles/QFj6zq
*/
#undef CALL
/* 其實就是生成一個數為x,相當于.long sys_clone,因為sys_clone是函數名,所以.long生成的是sys_clone函數名對應的地址 */
#define CALL(x) .long x
#ifdef CONFIG_FUNCTION_TRACER
/* 配合ldrcc一起看,原來ldrcc是這樣 */
ldrcc pc, [tbl, scno, lsl #2]
/* 把CALL(x)代入ldrcc,最后是這樣 */
ldrcc pc, sys_clone(函數地址)
清楚的看出來,ldrcc最后是將sys_clone的函數地址存入了pc寄存器,而sys_clone函數內核是怎么定義的呢,如下:
/* 文件地址: linux內核目錄/kernel/Fork.c */
/* 以下代碼根據不同的內核配置定義了不同的clone函數
* 其最終都調用的do_fork函數,我們先看看SYSCALL_DEFINE是怎么實現的吧,實現在此代碼片段后面
*/
#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內核目錄/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
可以看出系統調用是使用SYSCALL_DEFINEx進行定義的,以我們的例子,實際上最后clone函數被定義為:
/* 展開前 */
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
/* 應用層默認fork參數(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);
}
終于看到最后系統會調用do_fork函數進行操作,接下來我們看看do_fork函數
/* 應用層的fork最后會通過sys_clone系統調用調用到此函數 */
/* 應用層默認fork參數(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;
}
/* 調用copy_process進行初始化,返回初始化好的struct task_struct結構體,當我們調用fork時返回兩次的原因也是在這個函數當中,下回分析 */
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
if (!IS_ERR(p)) {
/* 創建成功 */
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調用,則在此等待vfork的進程結束 */
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
/* 創建失敗 */
nr = PTR_ERR(p);
}
/* 返回新進程PID(新進程在這會返回0) */
return nr;
}
在do_fork函數中,首先會根據clone_flags判斷是否對父進程進行了跟蹤(調試使用),如果進行了函數跟蹤(還需要判斷是否對子進程進行跟蹤),之后調用copy_process(do_fork的核心函數,之后的文章會對它進行分析),在copy_process中會對子進程的許多結構體和參數進行初始化(同時在fork正常情況中為什么會返回兩次也是在此函數中實現的),do_fork最后就判斷是否是通過vfork創建,如果是vfork創建,則會使父進程阻塞直到子進程結束釋放所占內存空間后才繼續執行,最后do_fork子進程pid。
到這里,整個系統調用的入口就分析完了,其實整個流程也不算很復雜:應用層通過swi軟中斷進入內核---->通過系統調用表選定目標系統調用--->執行系統調用--->返回。
二、fork的基礎概念
2.1 fork 是什么
在 Linux 系統中,fork是一個系統調用,用于創建一個新的進程,這個新進程被稱為子進程,而調用fork的進程則是父進程 。fork函數就像是一把神奇的 “叉子”,將一個進程 “分叉” 成兩個,這兩個進程(父進程和子進程)從fork調用之后的代碼開始,各自獨立執行,就像兩條從同一節點出發的不同路徑,后續的走向可能截然不同 。例如,一個負責數據處理的父進程,調用fork后,子進程可以繼承父進程的數據讀取部分,然后去執行數據分析,而父進程繼續進行數據的收集工作 ,兩者相互協作又互不干擾。
2.2 fork 函數返回值的奧秘
fork函數的一個獨特之處在于它 “一次調用,兩次返回” 。當fork被調用后,操作系統會創建出子進程,然后在父進程和子進程中分別返回不同的值 。在父進程中,fork返回子進程的進程 ID(PID,是一個大于 0 的整數),這個 ID 就像是子進程的 “身份證號”,父進程可以通過它來識別和管理子進程 ;而在子進程中,fork返回 0,就好像在告訴子進程:“你是新創建的子進程” 。
我們通過一段簡單的 C 語言代碼來直觀感受一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
// 調用fork函數
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函數執行后,父進程會打印出自己的 PID 以及子進程的 PID,而子進程會打印出自己的 PID 和父進程的 PID 。通過返回值的不同,父進程和子進程能夠清晰地知道自己的 “身份”,進而執行不同的代碼邏輯 。
三、fork工作原理
3.1進程的關鍵數據結構
在深入探討fork原理之前,我們需要先了解一些與進程密切相關的數據結構,它們是理解fork實現的關鍵 。
task_struct:這是進程描述符,就像是進程的 “身份證”,每個進程在內核中都有一個對應的task_struct結構體 。它記錄了進程的所有相關信息,包括進程 ID(PID)、進程狀態(是運行態、就緒態還是阻塞態等)、內存映射區域、文件描述符表、信號處理函數表等 。例如,通過task_struct中的 PID,系統可以唯一標識和區分不同的進程,就像每個人的身份證號是獨一無二的 ;而進程狀態信息則幫助內核決定該進程何時可以獲得 CPU 資源,是馬上運行,還是需要等待某些條件滿足 。
mm_struct:進程內存管理描述符,主要管理每個進程的虛擬內存和物理內存 。它包含了虛擬內存區域的信息,以及內存映射的相關設置等 。比如,當進程申請內存時,mm_struct會參與管理內存的分配,決定從哪里分配虛擬內存,以及如何與物理內存進行映射 。它就像是一個內存管家,統籌著進程內存的使用 。
vm_area_struct:虛擬內存描述符,用于描述一個進程的虛擬內存區域,包括起始和結束地址、訪問權限(是可讀、可寫還是可執行)、映射的物理頁框號等信息 。每個vm_area_struct對應著進程虛擬內存中的一個連續區域 。例如,進程的代碼段、數據段、堆、棧等在虛擬內存中都有各自對應的vm_area_struct,通過它可以清晰地了解每個內存區域的屬性和范圍 。
這些數據結構相互關聯,共同構成了進程在系統中的完整描述 。task_struct包含了指向mm_struct的指針,通過它可以訪問到進程的內存管理信息 ;而mm_struct中又包含了指向vm_area_struct鏈表的指針,用于管理進程的各個虛擬內存區域 。它們之間的關系緊密,就像一個復雜的機器,各個零件協同工作,保證進程的正常運行 。
3.2fork 的詳細執行步驟
當進程調用fork函數時,背后會發生一系列復雜而有序的操作 ,下面我們來詳細剖析:
①進入內核態
進程在用戶態調用fork時,會通過軟件中斷(在 x86 架構中,通常是int 0x80或sysenter指令)進入內核態 。這就像是從普通的街道進入了 “核心區域”,擁有了更高的權限 。
進入內核態后,系統會找到sys_fork()系統調用處理函數,開始處理fork請求 。這個過程就好比是一個市民向政府部門提交申請,政府部門收到申請后,安排專門的工作人員(sys_fork()函數)來處理 。
②獲取 PID 與創建描述符
內核首先會獲取一個可用的 PID,這個 PID 將作為新創建子進程的身份標識 。PID 的分配就像是給新出生的寶寶辦理身份證,每個 PID 在系統中都是唯一的 。接著,內核調用copy_process()函數,為子進程分配和初始化一個全新的task_struct 。在這個過程中,copy_process()會從父進程的task_struct復制大部分內容,包括文件系統相關數據(如打開的文件描述符表,這樣子進程就可以繼承父進程打開的文件)、信號處理函數表(使得子進程能像父進程一樣響應各種信號)、命名空間、進程狀態等 。
同時,它會為新進程設置一些初始狀態,比如將新進程的狀態設置為TASK_UNINTERRUPTIBLE(不可中斷睡眠狀態),這就像是新員工入職后,先被安排在一個 “待命” 的狀態 ;還會為新進程分配一個獨立的內核棧,用于內核態下的函數調用和數據存儲 ,并初始化計時器、信號等數據結構 。
③復制內存映射區域
在copy_process()中,會調用dup_mmap()函數來復制父進程的內存映射區域 。dup_mmap()會仔細遍歷父進程的所有vm_area_struct,并為子進程創建相應的內存映射區域 。但此時,父子進程只是簡單地共享同一組頁表項,實際的物理內存頁還未復制 。
這就好比兩個房間(父子進程)共享了同一份房間布局圖(頁表項),但房間里的實際物品(物理內存頁)還沒有復制 。這樣做的好處是可以快速創建子進程,避免了大量物理內存的復制開銷 。
④寫時復制(COW)設置
復制完vm_area_struct后,dup_mmap()會調用pud_mkwrite等函數,將父子進程共享的所有頁表項都標記為只讀(設置頁表項的權限位為非可寫) 。這是寫時復制機制的關鍵一步,當父子進程中有一方試圖寫入共享的內存頁時,CPU 會觸發頁保護異常 。
例如,父進程和子進程一開始共享某一內存頁,當子進程想要修改這個內存頁時,由于頁表項是只讀的,就會引發異常,從而觸發內核的異常處理程序執行寫時復制操作 ,就像是原本共同使用一份文件的兩人,當其中一人想要修改文件時,系統會為他復制一份獨立的文件副本 。
⑤其他設置
在copy_process()中,還會進行一些其他重要的設置 。比如復制父進程的信號處理程序表,確保子進程也能正確響應不同的信號,就像孩子繼承了父母應對各種情況的能力 ;為子進程設置SIGCHLD信號的默認處理程序,以便父進程能夠捕獲子進程的結束信號,這就像是給子進程和父進程之間建立了一個特殊的 “通訊渠道”,用于傳遞子進程結束的消息 。如果新創建的進程是一個內核線程,copy_process()會進行一些額外的設置,如禁止內核線程加載執行用戶空間代碼、禁止訪問用戶態內存等,這是為了保證內核線程的安全性和穩定性 。
此外,copy_process()會復制父進程的調度策略、優先級等相關信息,并為子進程分配新的運行時統計數據結構,用于 CPU 調度 ,就像為子進程制定了一份專屬的 “工作安排表” 。最后,新創建的子進程會被加入相應的進程鏈表中,如任務隊列、反饋優先級鏈表等,以便內核進行進程調度和管理,這就像是將新員工加入到公司的組織架構中,方便進行工作安排和管理 。
⑥寫時復制異常處理
當子進程對共享內存區域進行寫操作而發生頁保護異常時,寫時復制異常處理程序do_cow_fault()就會發揮作用 。它的主要工作包括為發生寫操作的內存頁分配新的內核頁框(物理內存頁),就像是為需要修改文件的人分配一個新的文件存放空間 ;將原有的內存頁內容復制到新的頁框中,保證數據的一致性 ;修改相應的頁表項,使其指向新分配的物理內存頁框,并設置為可寫,這樣子進程就可以在自己獨立的內存頁上進行寫操作了 ;
同時,在原有的物理內存頁上設置寫保護,避免不必要的復制 。通過這一系列操作,父子進程最終會擁有各自獨立的物理內存副本,從而可以進行自身的數據寫入而不會相互影響 。
⑦執行切換和系統調用返回
最后,內核會決定父進程和子進程的執行順序 。一般情況下,內核會先讓子進程執行,因為子進程的執行狀態被設置為TASK_UNINTERRUPTIBLE 。在子進程執行時,會執行一些額外的初始化工作,如清理上下文、設置執行計數器等 。
fork系統調用在父子進程中的返回值不同,在子進程中,fork返回0,就像是在告訴子進程 “你是新創建的,現在可以開始你的獨立旅程了” ;在父進程中,fork返回新創建子進程的PID,父進程可以通過這個PID來識別和管理子進程 。通過這種不同的返回值,父子進程可以區分不同的執行路徑,各自執行自己的代碼邏輯 。
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);
/* 創建線程時線程之間要共享信號處理函數 */
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* 父子進程共享信號處理函數時必須共享內存地址空間
* 這就是為什么書上寫的fork出來的父子進程有其獨立的信號處理函數,因為他們的內存地址空間不同
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/*
* 防止參數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內存和內核棧內存 */
p = dup_task_struct(current);
if (!p)
goto fork_out;
/* ftrace是用于內核性能分析和跟蹤的 */
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是否小于等于用戶所擁有的進程數,rlim結構體表示相關資源的最大值 */
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用于安全操作的結構 */
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
retval = -EAGAIN;
/* 進程數量是否超出系統允許最大進程數量,最大進程數量跟內存有關,一般原則是所有的進程內核棧(默認8K)加起來不超過總內存的1/8,可通過/proc/sys/kernel/threads-max改寫此值 */
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
/* 如果實現新進程的執行域和可執行格式的內核函數都包含在內核模塊中,則遞增其使用計數 */
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() 系統調用 */
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;
/* 初始化分配鎖,此鎖用于保護分配內存,文件,文件系統等操作 */
spin_lock_init(&p->alloc_lock);
/* 信號列表初始化,此列表保存被掛起的信號 */
init_sigpending(&p->pending);
/* 代碼執行時間變量都置為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計數結構 */
task_io_accounting_init(&p->ioac);
acct_clear_integrals(p);
/* 初始化cputime_expires結構 */
posix_cpu_timers_init(p);
/* 設置進程創建時間 */
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;
/* 如果創建的是線程,因為需要修改到當前進程的描述符,會先上鎖 */
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
/* 初始化子進程的調度優先級和策略,在此并沒有將此進程加入到運行隊列,在copy_process返回之后加入 */
retval = sched_fork(clone_flags, p);
if (retval)
goto bad_fork_cleanup_policy;
/* perf event是一個性能調優工具,具體見 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都是根據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引用計數,如果否(創建的必定是子進程),將父線程的sighand_struct復制到子進程中 */
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
/* 如果創建的是線程,直接返回0,如果創建的是進程,則會將父進程的信號屏蔽和安排復制到子進程中 */
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
/*
* 如果是進程,則將父進程的mm_struct結構復制到子進程中,然后修改當中屬于子進程有別于父進程的信息(如頁目錄)
* 如果是線程,則將子線程的mm指針和active_mm指針都指向父進程的mm指針所指結構。
*/
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;
/*
* 初始化子進程內核棧和thread_struct結構體
* 當進程切換時,進程的硬件上下文一般保存于三個地方: tss_struct(保存進程內核棧地址,I/O許可權限位),thread_struct(大部分非通用寄存器),進程內核棧(通用寄存器)
* copy_thread函數會將父進程的thread_struct和內核棧數據復制到子進程中,并將子進程的返回值置為0(x86返回值保存在eax中,arm保存在r0中,即把eax或者r0所在的內核棧數據置為0)
* copy_thread函數還會將子進程的eip寄存器值設置為ret_from_fork()的地址,即當子進程首次被調用就立即執行系統調用clone返回。
* 所以應用層調用fork()函數后,子進程返回0,父進程返回子進程ID(返回子進程ID在之后代碼中會實現)
*/
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創建,信號棧清空
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = 0;
/*
* 系統調用跟蹤時應該禁止單步執行
*/
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) {
/* 創建的是線程 */
p->exit_signal = -1;
/* 線程組的所有線程的group_leader都一致 */
p->group_leader = current->group_leader;
/* 線程組的所有線程的tgid都一致,使用getpid返回的就是tgid */
p->tgid = current->tgid;
} else {
/* 創建的是子進程 */
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一致,所以當創建子線程時,tgid與主線程的一致 */
p->tgid = p->pid;
}
/* 初始化頁框中臟頁數量為0 */
p->nr_dirtied = 0;
/* 初始化臟頁數量臨界值,當臟頁數量到達臨界值時,會調用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;
/* 到此系統中已經存在此進程(線程),但是它還不能夠執行,需要等待父進程對其處理,這里會上鎖 */
write_lock_irq(&tasklist_lock);
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
/* 創建的是兄弟進程或者相同線程組線程 */
/* 其父進程為父進程的父進程 */
p->real_parent = current->real_parent;
/* 其父進程執行域為父進程的父進程執行域 */
p->parent_exec_id = current->parent_exec_id;
} else {
/* 創建的是子進程 */
/* 父進程為父進程 */
p->real_parent = current;
/* 父進程的執行域為父進程的執行域 */
p->parent_exec_id = current->self_exec_id;
}
/* 當前進程信號處理上鎖,這里應該是禁止了信號處理 */
spin_lock(¤t->sighand->siglock);
/*
* seccomp與系統安全有關,具體見 http://note.sdo.com/u/634687868481358385/NoteContent/M5cEN~kkf9BFnM4og00239
*/
copy_seccomp(p);
/*
* 在fork之前,進程組和會話信號都需要送到父親結點,而在fork之后,這些信號需要送到父親和孩子結點。
* 如果我們在將新進程添加到進程組的過程中出現一個信號,而這個掛起信號會導致當前進程退出(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 ,并將子進程插入調試程序的跟蹤鏈表中 */
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,創建的是線程的話,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; 判斷新進程是否處于一個新創建的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結構插入pgid_hash */
attach_pid(p, PIDTYPE_PGID);
/* 將新進程描述符的sid結構插入sid_hash */
attach_pid(p, PIDTYPE_SID);
/* 當前cpu進程數量加1 */
__this_cpu_inc(process_counts);
} else {
/* 創建的是線程,這里的處理導致了線程會共享信號 */
current->signal->nr_threads++;
atomic_inc(¤t->signal->live);
atomic_inc(¤t->signal->sigcnt);
/* 將新線程的thread_group結點加入到線程組的領頭線程的thread_group鏈表中 */
list_add_tail_rcu(&p->thread_group,
&p->group_leader->thread_group);
/* 將新線程的thread_node結點加入的新線程的signal->thread_head中 */
list_add_tail_rcu(&p->thread_node,
&p->signal->thread_head);
}
/* 將新進程描述符的pid結構插入pid_hash */
attach_pid(p, PIDTYPE_PID);
/* 當前系統進程數加1 */
nr_threads++;
}
/* 已創建的進程數量加1 */
total_forks++;
/* 釋放當前進程信號處理鎖 */
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
/* 釋放tasklist_lock鎖 */
write_unlock_irq(&tasklist_lock);
/* 將新進程與proc文件系統進行關聯 */
proc_fork_connector(p);
cgroup_post_fork(p);
/* 如果創建的是線程,釋放此鎖 */
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結構 */
return p;
/* 以下為執行期間的錯誤處理 */
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;
// 調用fork函數
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 。然后調用fork函數創建子進程 。如果fork返回值小于 0,表示創建子進程失敗,通過perror函數輸出錯誤信息并返回 1 。如果返回值為 0,說明當前是子進程,子進程將num乘以 2,然后打印出自己的進程 ID 和修改后的num值 。
如果返回值大于 0,說明當前是父進程,父進程將num加上 5,并打印出自己的信息以及子進程的 PID 。通過這個簡單的例子,我們可以看到父子進程雖然最初共享相同的變量值,但在后續的執行過程中,它們可以獨立地對變量進行修改,互不影響 ,這充分展示了fork創建獨立執行路徑的特性 。
4.2解決實際問題場景
網絡服務器場景:在網絡服務器中,fork發揮著至關重要的作用 。當一個服務器接收到客戶端的連接請求時,它可以調用fork創建一個子進程來專門處理這個客戶端的請求 。這樣,父進程就可以繼續監聽其他客戶端的連接,從而實現并發處理多個客戶端請求的功能 。
例如,一個 Web 服務器,當有用戶訪問網頁時,服務器通過fork創建子進程,子進程負責處理用戶的頁面請求,如解析 HTTP 請求、讀取網頁文件、生成響應內容等,而父進程則繼續等待新的用戶連接,大大提高了服務器的處理效率和響應速度 ,能夠同時為多個用戶提供服務 。
數據分析場景:在處理大規模數據分析任務時,fork也能派上用場 。假設我們有一個龐大的數據集需要進行復雜的統計分析,如計算平均值、方差等 。我們可以利用fork創建多個子進程,每個子進程負責處理數據集的一部分 。
比如,將一個包含 100 萬條數據的文件分成 10 個子部分,每個子進程處理 10 萬條數據,最后父進程收集各個子進程的計算結果并進行匯總,從而加快整個數據分析的速度,充分利用多核 CPU 的計算資源,提高數據分析的效率 。
4.3注意事項與常見問題
資源競爭問題:在使用fork時,由于父子進程共享部分資源(如打開的文件描述符),可能會出現資源競爭的情況 。例如,父子進程同時對同一個文件進行寫操作,可能會導致文件內容混亂 。為了避免這種情況,可以使用文件鎖機制,如flock函數,在進行文件操作前先獲取文件鎖,確保同一時間只有一個進程能夠對文件進行寫操作 。
另外,在多線程程序中調用fork要格外小心,因為多線程程序中每個線程都有自己的棧和寄存器狀態,調用fork時,子進程會繼承父進程的所有線程,這可能會導致復雜的狀態不一致性和資源競爭問題,所以通常建議避免在多線程程序中調用fork,如果確實需要創建新進程,可以考慮使用exec函數族 。
子進程退出處理:子進程退出時,如果父進程沒有及時處理,子進程就會變成僵尸進程,占用系統資源 。為了避免產生僵尸進程,父進程可以調用wait或waitpid函數來等待子進程結束,并獲取子進程的退出狀態 。wait函數會阻塞父進程,直到有子進程結束;而waitpid函數則更加靈活,可以指定等待特定的子進程,并且可以設置非阻塞模式 。例如:
#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等待子進程結束,并通過WIFEXITED和WEXITSTATUS宏來判斷子進程是否正常退出以及獲取退出狀態 ,這樣就可以有效地避免僵尸進程的產生 。
五、對比與拓展
5.1 fork與vfork的對比
在 Linux 進程創建中,除了fork,還有一個與之類似的系統調用vfork ,它們在功能上有相似之處,但也存在著諸多重要的區別 。
創建進程與地址空間共享:fork創建子進程時,會為子進程復制父進程的地址空間,包括代碼段、數據段、堆和棧等 。雖然在復制時采用了寫時復制(COW)技術,即最初父子進程共享物理內存頁,只有當有寫操作發生時才為子進程分配獨立的物理內存頁,但從本質上來說,子進程擁有自己獨立的虛擬地址空間 ,后續的寫操作會使父子進程的數據相互獨立 。而vfork創建的子進程則直接與父進程共享地址空間 ,子進程對數據的修改會直接反映在父進程中 ,它們就像在同一間屋子里活動,所有的物品(數據)都是共享的 。
執行順序:fork創建的父子進程執行順序是不確定的 ,這取決于內核的調度算法 。有可能父進程先執行,也有可能子進程先執行 。而vfork則保證子進程先運行 ,在子進程調用exec函數族(用于執行另一個程序,替換當前進程的內存映像)或exit(用于終止進程)之前,父進程會被阻塞,處于等待狀態 ,只有當子進程執行了這兩個操作之一后,父進程才有可能被調度運行 。
適用場景:由于fork創建的子進程擁有獨立的地址空間,適合用于需要父子進程并發執行且相互獨立工作的場景 ,比如前面提到的網絡服務器中處理多個客戶端請求,每個子進程獨立處理自己的任務,互不干擾 。而vfork由于共享地址空間且保證子進程先運行的特性,適用于子進程創建后立即要執行exec函數族去執行另一個程序的場景 ,這樣可以避免不必要的地址空間復制開銷 ,提高效率 。例如,當一個程序需要啟動另一個程序時,可以使用vfork創建子進程,然后子進程調用exec函數族來加載并運行新程序 。
5.2 fork在不同Linux版本中的優化
隨著 Linux 操作系統的不斷發展和演進,fork在不同版本中也經歷了一系列的優化改進 ,以提升性能和資源管理效率 。
早期版本:在早期的Linux版本中,fork采用的是相對簡單直接的復制方式 。當調用fork時,會將父進程的整個地址空間完整地復制給子進程 ,包括所有的內存頁面 。這種方式雖然實現簡單,但效率較低,因為大量的內存復制操作會消耗較多的時間和系統資源 ,尤其是在父進程內存占用較大時,fork的開銷會非常明顯 。
寫時復制(COW)技術引入:為了提高fork的效率,Linux 內核引入了寫時復制(COW)技術 。從2.0版本開始,fork創建子進程時不再立即復制物理內存頁,而是讓父子進程共享同一組頁表項,指向相同的物理內存頁 。只有當父子進程中有一方試圖對共享內存頁進行寫操作時,才會觸發寫時復制機制 ,為執行寫操作的進程分配新的物理內存頁,并將原內存頁內容復制到新頁中 。這種優化大大減少了fork時的內存復制開銷,加快了子進程的創建速度 ,同時也節省了內存資源 。
后續版本優化:在后續的 Linux 版本中,對fork的優化還在繼續 。例如,在進程調度方面,內核不斷改進調度算法,使得fork創建的父子進程能夠更合理地分配 CPU資源 ,提高整體的并發執行效率 。在內存管理方面,進一步優化了頁表的管理和更新機制 ,減少了寫時復制過程中的開銷 。此外,還針對多處理器系統進行了優化,提高了fork在多核環境下的性能 ,使得父子進程能夠更好地利用多核 CPU 的計算資源 。