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; }) #endif1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.

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 were 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) @ 返回1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.

好的,终于跳转到了系统调用表,现在我们看看系统调用表是怎么样的一个形式

复制
/* 文件地址: 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) .endr1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.

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(函数地址)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.

清楚的看出来,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_DEFINEx1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.

可以看出系统调用是使用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); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

终于看到最后系统会调用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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.

在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 parents pid is %d\n", getpid(), getppid()); } else { // 父进程 printf("I am the parent process, my pid is %d, and my childs pid is %d\n", getpid(), pid); } return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.

在上述代码中,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); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.139.140.141.142.143.144.145.146.147.148.149.150.151.152.153.154.155.156.157.158.159.160.161.162.163.164.165.166.167.168.169.170.171.172.173.174.175.176.177.178.179.180.181.182.183.184.185.186.187.188.189.190.191.192.193.194.195.196.197.198.199.200.201.202.203.204.205.206.207.208.209.210.211.212.213.214.215.216.217.218.219.220.221.222.223.224.225.226.227.228.229.230.231.232.233.234.235.236.237.238.239.240.241.242.243.244.245.246.247.248.249.250.251.252.253.254.255.256.257.258.259.260.261.262.263.264.265.266.267.268.269.270.271.272.273.274.275.276.277.278.279.280.281.282.283.284.285.286.287.288.289.290.291.292.293.294.295.296.297.298.299.300.301.302.303.304.305.306.307.308.309.310.311.312.313.314.315.316.317.318.319.320.321.322.323.324.325.326.327.328.329.330.331.332.333.334.335.336.337.338.339.340.341.342.343.344.345.346.347.348.349.350.351.352.353.354.355.356.357.358.359.360.361.362.363.364.365.366.367.368.369.370.371.372.373.374.375.376.377.378.379.380.381.382.383.384.385.386.387.388.389.390.391.392.393.394.395.396.397.398.399.400.401.402.403.404.405.406.407.408.409.410.411.412.413.414.415.416.417.418.419.420.421.422.423.424.425.426.427.428.429.430.431.432.433.434.435.436.437.438.439.440.441.442.443.444.445.446.447.448.449.450.451.452.453.454.455.456.457.458.459.460.461.462.463.464.465.466.467.468.469.470.471.472.473.474.475.476.477.478.479.

四、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 childs pid is %d\n", num, pid); } return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.

在这段代码中,我们首先定义了一个变量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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.

在上述代码中,父进程通过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 的计算资源 。

THE END
本站服务器由亿华云赞助提供-企业级高防云服务器