基于 Linux 6.4-rc1 源码(commit ac9a78681b92),深入剖析进程创建的内核实现。
进程是操作系统最核心的抽象之一。每次你敲下一条 shell 命令,背后都会发生 fork + exec 这一对经典舞步。然而 “fork 复制父进程、exec 替换镜像” 这句话远未揭示全貌:写时复制如何让 fork 变得极其廉价?clone 与 fork 共享同一条代码路径吗?exec 如何安全地销毁旧地址空间并跳进新程序?本文从 struct task_struct 出发,沿着 fork → clone → exec → exit 的脉络,逐函数拆解内核实现。
一、task_struct:进程的完整画像
每个进程(或线程)在内核中对应一个 struct task_struct,定义于 include/linux/sched.h。它既是调度的基本单元,也是内核追踪进程一切状态的”档案袋”。以下精选最关键的字段并加以注释。
1.1 进程状态
1 2 3 4 5 6 7 8 9
| #define TASK_RUNNING 0x00000000 #define TASK_INTERRUPTIBLE 0x00000001 #define TASK_UNINTERRUPTIBLE 0x00000002 #define __TASK_STOPPED 0x00000004 #define __TASK_TRACED 0x00000008 #define EXIT_DEAD 0x00000010 #define EXIT_ZOMBIE 0x00000020 #define TASK_DEAD 0x00000080
|
task_struct 中保存状态的字段为:
注意前缀双下划线——内核要求通过 READ_ONCE()/WRITE_ONCE() 或专用宏访问它,以防止编译器优化导致的竞态。
1.2 调度相关字段
1 2 3 4 5 6 7 8 9 10
| int prio; int static_prio; int normal_prio; unsigned int rt_priority;
struct sched_entity se; struct sched_rt_entity rt; struct sched_dl_entity dl; const struct sched_class *sched_class;
|
sched_class 是一个虚函数表指针,实现了策略模式:CFS、RT、Deadline 各自实现 enqueue_task、dequeue_task、pick_next_task 等接口,调度器核心代码通过 sched_class 统一调用,无需 if/else 判断。
1.3 进程标识与亲缘关系
1 2 3 4 5 6 7 8 9
| pid_t pid; pid_t tgid;
struct task_struct __rcu *real_parent; struct task_struct __rcu *parent; struct list_head children; struct list_head sibling; struct task_struct *group_leader;
|
pid vs tgid:POSIX 要求同一进程内所有线程共享 PID,这在内核中通过 tgid 实现。getpid() 返回 tgid,gettid() 返回 pid。group_leader 永远指向主线程。
1.4 内存管理
1 2 3
| struct mm_struct *mm; struct mm_struct *active_mm;
|
内核线程 mm == NULL,但运行时 CPU 的 TLB 仍需要一个 mm,所以借用上一个用户进程的 active_mm,这就是 lazy TLB 优化的来源。
1.5 文件系统与文件描述符
1 2 3
| struct fs_struct *fs; struct files_struct *files;
|
fs_struct 和 files_struct 各自有引用计数,clone(CLONE_FS) 或 clone(CLONE_FILES) 时父子进程共享同一个实例而非复制。
1.6 信号处理
1 2 3 4 5
| struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked; struct sigpending pending;
|
1.7 凭证与命名空间
1 2 3 4 5 6
| const struct cred __rcu *real_cred; const struct cred __rcu *cred;
struct nsproxy *nsproxy;
|
cred 采用写时复制(COW):setuid() 会分配新的 cred 对象,而非修改现有的,这使得 RCU 读者可以无锁访问。
二、fork() 系统调用:从用户态到 kernel_clone
2.1 调用链全景
1 2 3 4 5 6 7 8 9
| 用户态: fork() ↓ glibc syscall wrapper 内核态: sys_fork (SYSCALL_DEFINE0) ↓ kernel_clone(args) ↓ copy_process(pid, trace, node, args) ← 核心:创建子进程描述符 ↓ wake_up_new_task(p) ← 将子进程加入运行队列
|
2.2 sys_fork 的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| SYSCALL_DEFINE0(fork) { #ifdef CONFIG_MMU struct kernel_clone_args args = { .exit_signal = SIGCHLD, };
return kernel_clone(&args); #else return -EINVAL; #endif }
|
fork 不传任何 clone flags,这意味着子进程拥有独立的 mm、文件描述符表、信号处理器、命名空间——一个完整的独立进程。
2.3 kernel_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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| pid_t kernel_clone(struct kernel_clone_args *args) { u64 clone_flags = args->flags; struct completion vfork; struct pid *pid; struct task_struct *p; pid_t nr;
if (!(clone_flags & CLONE_UNTRACED)) { if (clone_flags & CLONE_VFORK) trace = PTRACE_EVENT_VFORK; else if (args->exit_signal != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; }
p = copy_process(NULL, trace, NUMA_NO_NODE, args); if (IS_ERR(p)) return PTR_ERR(p);
pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid);
if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); }
wake_up_new_task(p);
if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); }
put_pid(pid); return nr; }
|
2.4 copy_process:子进程的诞生
copy_process 是整个 fork 路径最复杂的函数(约 600 行),它按照严格顺序完成以下工作:
第一步:合法性检查
1 2 3 4 5 6 7 8
|
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL);
|
这揭示了线程的必要条件:线程 = 共享信号处理器 = 共享地址空间,三者缺一不可。
第二步:dup_task_struct——克隆描述符与内核栈
1 2
| p = dup_task_struct(current, node);
|
dup_task_struct 的实现(kernel/fork.c: 1101–1190):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static struct task_struct *dup_task_struct(struct task_struct *orig, int node) { struct task_struct *tsk;
tsk = alloc_task_struct_node(node); if (!tsk) return NULL;
err = arch_dup_task_struct(tsk, orig); if (err) goto free_tsk;
err = alloc_thread_stack_node(tsk, node); if (err) goto free_tsk;
setup_thread_stack(tsk, orig); clear_tsk_need_resched(tsk); set_task_stack_end_magic(tsk);
#ifdef CONFIG_STACKPROTECTOR tsk->stack_canary = get_random_canary(); #endif refcount_set(&tsk->usage, 1); return tsk; }
|
关键点:此时 task_struct 是父进程的完整拷贝,但内核栈是全新分配的——否则父子进程会使用同一个内核栈,调度时会互相破坏。
第三步:各子系统的选择性复制
1 2 3 4 5 6 7 8 9 10 11
| retval = copy_files(clone_flags, p, args->no_files); retval = copy_fs(clone_flags, p); retval = copy_sighand(clone_flags, p); retval = copy_signal(clone_flags, p); retval = copy_mm(clone_flags, p); retval = copy_namespaces(clone_flags, p); retval = copy_thread(p, args);
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid, args->set_tid_size);
|
copy_mm(kernel/fork.c: 1714)的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static int copy_mm(unsigned long clone_flags, struct task_struct *tsk) { struct mm_struct *mm, *oldmm = current->mm;
if (!oldmm) return 0;
if (clone_flags & CLONE_VM) { mmget(oldmm); mm = oldmm; } else { mm = dup_mm(tsk, current->mm); if (!mm) return -ENOMEM; }
tsk->mm = mm; tsk->active_mm = mm; return 0; }
|
copy_files(kernel/fork.c: 1772):
1 2 3 4 5 6 7 8 9 10 11 12 13
| static int copy_files(unsigned long clone_flags, struct task_struct *tsk, int no_files) { struct files_struct *oldf = current->files;
if (clone_flags & CLONE_FILES) { atomic_inc(&oldf->count); goto out; }
newf = dup_fd(oldf, NR_OPEN_MAX, &error); tsk->files = newf; ... }
|
第四步:设置 pid/tgid/group_leader
1 2 3 4 5 6 7 8 9
| p->pid = pid_nr(pid); if (clone_flags & CLONE_THREAD) { p->group_leader = current->group_leader; p->tgid = current->tgid; } else { p->group_leader = p; p->tgid = p->pid; }
|
第五步:加入进程树
1 2 3 4 5 6 7
| if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { p->real_parent = current->real_parent; } else { p->real_parent = current; p->exit_signal = args->exit_signal; }
|
2.5 wake_up_new_task:子进程入队
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void wake_up_new_task(struct task_struct *p) { struct rq_flags rf; struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, rf.flags); WRITE_ONCE(p->__state, TASK_RUNNING);
#ifdef CONFIG_SMP __set_task_cpu(p, select_task_rq(p, task_cpu(p), WF_FORK)); #endif rq = __task_rq_lock(p, &rf); update_rq_clock(rq); post_init_entity_util_avg(p);
activate_task(rq, p, ENQUEUE_NOCLOCK); trace_sched_wakeup_new(p); check_preempt_curr(rq, p, WF_FORK);
task_rq_unlock(rq, p, &rf); }
|
至此,子进程已就绪,等待调度器选中它上 CPU 运行。
三、clone() 与线程创建
3.1 clone flags 语义
所有标志定义于 include/uapi/linux/sched.h:
| 标志 |
含义 |
CLONE_VM |
共享地址空间(mm_struct),线程的必要条件 |
CLONE_FS |
共享文件系统信息(cwd、root、umask) |
CLONE_FILES |
共享文件描述符表 |
CLONE_SIGHAND |
共享信号处理函数表,CLONE_VM 的前提 |
CLONE_THREAD |
加入同一线程组(共享 tgid),CLONE_SIGHAND 的前提 |
CLONE_NEWNS |
新建 Mount 命名空间(容器隔离的基础) |
CLONE_NEWPID |
新建 PID 命名空间(容器内 PID 从 1 开始) |
CLONE_VFORK |
父进程挂起,直到子进程 exec 或 exit |
CLONE_PIDFD |
在父进程返回一个 pidfd(进程文件描述符) |
3.2 pthread_create 的内核路径
glibc 中 pthread_create 最终调用:
1 2 3 4
| clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, child_stack, &parent_tid, tls, &child_tid);
|
这些 flags 让内核:
- 共享地址空间(VM)
- 共享文件和信号处理
- 加入同一线程组(THREAD)
- 设置 TLS 段(SETTLS)
- 在子线程退出时通过
CLONE_CHILD_CLEARTID 向 futex 地址写 0,唤醒等待 pthread_join 的线程
3.3 sys_clone 的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, int __user *, parent_tidptr, int __user *, child_tidptr, unsigned long, tls) { struct kernel_clone_args args = { .flags = (lower_32_bits(clone_flags) & ~CSIGNAL), .pidfd = parent_tidptr, .child_tid = child_tidptr, .parent_tid = parent_tidptr, .exit_signal = (lower_32_bits(clone_flags) & CSIGNAL), .stack = newsp, .tls = tls, };
return kernel_clone(&args); }
|
fork、vfork、clone 在内核中最终都调用 kernel_clone,差异仅在传入的 kernel_clone_args 结构体。这是 Linux 内核”一个实现,多个接口”的典型范式。
3.4 copy_thread:fork 返回 0 的秘密
子进程在 fork 后从 ret_from_fork 开始运行,但为何 fork() 在子进程中返回 0?答案在 x86 的 copy_thread:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) { struct pt_regs *childregs = task_pt_regs(p); struct fork_frame *fork_frame = container_of(childregs, struct fork_frame, regs); struct inactive_task_frame *frame = &fork_frame->frame;
frame->ret_addr = (unsigned long) ret_from_fork; p->thread.sp = (unsigned long) fork_frame;
*childregs = *current_pt_regs();
childregs->ax = 0;
if (sp) childregs->sp = sp; ... }
|
父进程的 rax 由 kernel_clone 返回子进程 PID,子进程的 rax 被强制置 0。当子进程被调度到 CPU 上,它从 ret_from_fork 恢复执行,iret 时 rax=0 作为系统调用返回值送回用户态——这就是 fork() 在子进程中返回 0 的完整机制。
四、exec() 系统调用:变身新程序
4.1 调用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 用户态: execve(path, argv, envp) ↓ sys_execve (SYSCALL_DEFINE3) ↓ do_execve → do_execveat_common ↓ alloc_bprm + bprm_mm_init ← 分配 linux_binprm,创建新 mm ↓ bprm_execve ↓ exec_binprm → search_binary_handler ← 遍历 binfmt 链表找加载器 ↓ load_elf_binary (fs/binfmt_elf.c) ← ELF 加载器 ↓ begin_new_exec ← 不可回头点:替换旧进程上下文 ↓ start_thread (arch) ← 设置入口地址,跳入新程序
|
4.2 do_execveat_common:准备 bprm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags) { struct linux_binprm *bprm;
bprm = alloc_bprm(fd, filename);
bprm->argc = count(argv, MAX_ARG_STRINGS); bprm->envc = count(envp, MAX_ARG_STRINGS);
bprm_stack_limits(bprm);
copy_string_kernel(bprm->filename, bprm); copy_strings(bprm->envc, envp, bprm); copy_strings(bprm->argc, argv, bprm);
retval = bprm_execve(bprm, fd, filename, flags); ... }
|
4.3 bprm_mm_init:提前准备新地址空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| static int bprm_mm_init(struct linux_binprm *bprm) { struct mm_struct *mm = NULL;
bprm->mm = mm = mm_alloc(); if (!mm) goto err;
task_lock(current->group_leader); bprm->rlim_stack = current->signal->rlim[RLIMIT_STACK]; task_unlock(current->group_leader);
err = __bprm_mm_init(bprm); ... }
|
注意此时旧的 mm 仍然有效——新 mm 在 begin_new_exec 之前不会替换旧的,这样如果 load_elf_binary 失败,进程可以安全回滚。
4.4 search_binary_handler:寻找加载器
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
| static int search_binary_handler(struct linux_binprm *bprm) { struct linux_binfmt *fmt;
retval = prepare_binprm(bprm); retval = security_bprm_check(bprm);
read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock);
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock); put_binfmt(fmt); if (bprm->point_of_no_return || retval != -ENOEXEC) { read_unlock(&binfmt_lock); return retval; } } ... }
|
binfmt 链表按注册顺序排列,典型格式有 ELF(binfmt_elf)、脚本(binfmt_script,处理 #!)、misc(binfmt_misc,处理 .jar/.py 等)。
4.5 load_elf_binary:ELF 加载详解
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
| static int load_elf_binary(struct linux_binprm *bprm) { struct elfhdr *elf_ex = (struct elfhdr *)bprm->buf;
if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0) goto out; if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN) goto out;
elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) { if (elf_ppnt->p_type != PT_INTERP) continue; elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); elf_read(bprm->file, elf_interpreter, ...); interpreter = open_exec(elf_interpreter); break; }
retval = begin_new_exec(bprm);
for (i = 0, elf_ppnt = elf_phdata; i < elf_ex->e_phnum; i++, elf_ppnt++) { if (elf_ppnt->p_type == PT_LOAD) { ... } }
if (interpreter) { elf_entry = load_elf_interp(interp_elf_ex, interpreter, ...); } ... }
|
ELF 加载后的栈布局(从高地址到低地址):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 高地址 ┌─────────────────────────────────┐ │ NULL(栈结束标记) │ │ envp 字符串数据 │ │ argv 字符串数据 │ │ filename 字符串 │ │ AT_NULL(auxv 结束) │ │ 辅助向量 auxv(AT_PHDR/AT_ENTRY/│ │ AT_PAGESZ/AT_RANDOM…) │ │ NULL(envp 结束) │ │ envp[n-1] ... envp[0] │ │ NULL(argv 结束) │ │ argv[argc-1] ... argv[0] │ │ argc │ ← rsp 初始指向此处 └─────────────────────────────────┘ 低地址
|
auxv(辅助向量)向 ld.so 传递内核信息:AT_PHDR 指向程序头表在内存中的位置,AT_ENTRY 是程序真实入口(ld.so 完成重定位后跳转到此),AT_RANDOM 是 16 字节随机数用于 PIE/ASLR 种子。
4.6 begin_new_exec:不可回头点
begin_new_exec(fs/exec.c: 1244)是 exec 过程的分水岭,在此之后:
- 调用
exec_mmap(bprm->mm) 将旧 mm 替换为新 mm,旧地址空间被销毁
- 调用
flush_signal_handlers() 将所有非 SIG_IGN 的信号处理器重置为 SIG_DFL
- 关闭所有标有
O_CLOEXEC 的文件描述符
- 更新进程名(
comm)为新程序的 basename
- 若此时发生错误,进程将以 SIGSEGV 终止(因为旧环境已消失)
五、进程退出:do_exit 与僵尸进程
5.1 do_exit 的退出流程
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
| void __noreturn do_exit(long code) { struct task_struct *tsk = current;
exit_signals(tsk);
group_dead = atomic_dec_and_test(&tsk->signal->live);
tsk->exit_code = code;
exit_mm(); exit_sem(tsk); exit_shm(tsk); exit_files(tsk); exit_fs(tsk); exit_task_namespaces(tsk); exit_thread(tsk);
perf_event_exit_task(tsk); cgroup_exit(tsk);
exit_notify(tsk, group_dead);
do_task_dead(); }
|
5.2 exit_mm:地址空间的释放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static void exit_mm(void) { struct mm_struct *mm = current->mm;
exit_mm_release(current, mm); if (!mm) return;
sync_mm_rss(mm); mmap_read_lock(mm); mmgrab_lazy_tlb(mm);
task_lock(current); current->mm = NULL; enter_lazy_tlb(mm, current); task_unlock(current);
mmap_read_unlock(mm); mm_update_next_owner(mm); mmput(mm); }
|
mmput() 只是递减引用计数。若进程有共享同一 mm 的线程(线程组),mm 不会被立刻释放,直到最后一个线程退出。
5.3 exit_notify:从 RUNNING 到 ZOMBIE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
write_lock_irq(&tasklist_lock); forget_original_parent(tsk, &dead);
tsk->exit_state = EXIT_ZOMBIE;
if (thread_group_leader(tsk)) { autoreap = do_notify_parent(tsk, tsk->exit_signal); }
if (autoreap) { tsk->exit_state = EXIT_DEAD; } write_unlock_irq(&tasklist_lock);
|
进程在 exit_state = EXIT_ZOMBIE 后:task_struct 仍然存在于内存中,保留 PID、退出码等信息,等待父进程调用 wait() 来回收。这就是”僵尸进程”。
5.4 wait4 与 do_wait:父进程回收子进程
1 2 3 4 5 6 7 8 9 10 11 12
| SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr, int, options, struct rusage __user *, ru) { struct rusage r; long err = kernel_wait4(upid, stat_addr, options, ru ? &r : NULL); if (err > 0) { if (ru && copy_to_user(ru, &r, sizeof(struct rusage))) return -EFAULT; } return err; }
|
kernel_wait4 → do_wait → wait_consider_task:扫描子进程列表,找到 EXIT_ZOMBIE 状态的子进程,收集退出码后调用 release_task() 释放 task_struct,PID 归还 pidmap。
5.5 孤儿进程:find_new_reaper
当父进程先于子进程退出,子进程成为孤儿:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static struct task_struct *find_new_reaper(struct task_struct *father, struct task_struct *child_reaper) { thread = find_alive_thread(father); if (thread) return thread;
if (father->signal->has_child_subreaper) { for (reaper = father->real_parent; ...; reaper = reaper->real_parent) { if (reaper->signal->is_child_subreaper) { thread = find_alive_thread(reaper); if (thread) return thread; } } }
return child_reaper; }
|
这就是为什么容器内 PID 1 必须正确处理 SIGCHLD 并调用 wait()——否则容器内所有孤儿进程都会成为其僵尸子进程,逐渐耗尽内核资源。
六、写时复制(COW):fork 廉价的秘密
6.1 COW 的实现原理
fork 时调用 dup_mm → dup_mmap,后者遍历父进程的所有 VMA(虚拟内存区域),将每个页表项拷贝到子进程,但同时将父子两边的 PTE 都标记为只读并清除 dirty bit。
当任一方尝试写入,CPU 触发缺页异常(Protection Fault),内核的 do_wp_page 函数被调用:
- 检查页面的
_mapcount(被多少 PTE 引用)
- 若
_mapcount == 1(只有自己引用),直接将 PTE 改回可写(”破坏单映射”)
- 若
_mapcount > 1,分配新物理页,拷贝内容,更新 PTE 为新页并设为可写
这样,只有真正被写入的页面才会发生物理复制,大量只读的代码页、rodata 页永远不会被复制。
6.2 fork + exec 的极致优化:vfork
如果紧接着 fork 就要 exec,COW 的页表复制本身也是浪费,vfork 为此而生:
1 2 3 4 5 6 7 8 9
| SYSCALL_DEFINE0(vfork) { struct kernel_clone_args args = { .flags = CLONE_VFORK | CLONE_VM, .exit_signal = SIGCHLD, }; return kernel_clone(&args); }
|
CLONE_VM:父子共享同一 mm,不复制页表
CLONE_VFORK:父进程阻塞在 wait_for_vfork_done() 直到子进程调用 exec 或 _exit
vfork 的约束极为严格:子进程只能调用 exec 系列或 _exit,不得修改任何全局状态,也不得 return 出创建它的函数——因为父子共享栈和 mm,任何写操作都会污染父进程。
6.3 为何 fork + exec 几乎不复制内存
即使不用 vfork,普通 fork 后立即 exec,实际代价也很小:
fork 时仅复制页表结构(4级页表的高层节点),并将所有 PTE 标记为 COW 只读
exec 调用 begin_new_exec → exec_mmap,直接将新 mm 替换旧 mm,mmput(old_mm) 递减引用计数后释放旧页表,所有 COW 只读标记都随之消失
- 整个过程:父进程没有任何页面被真正复制,唯一的开销是页表遍历和清理
这就是为什么 shell 每秒可以创建数千个子进程,而内存用量并不会暴增。
七、诊断方法
7.1 /proc/PID/status:进程状态全览
1 2 3 4 5 6 7 8 9 10 11
| $ cat /proc/1234/status Name: nginx State: S (sleeping) Tgid: 1234 Pid: 1234 PPid: 1 Threads: 4 VmRSS: 12488 kB VmSize: 458752 kB SigBlk: 0000000000000000 SigCgt: 0000000180014a03
|
7.2 /proc/PID/task/:查看所有线程
1 2 3 4 5 6
| $ ls /proc/1234/task/ 1234 1235 1236 1237
$ cat /proc/1234/task/1235/status | grep -E "^(Pid|Tgid):" Tgid: 1234 Pid: 1235
|
7.3 pstree -p:可视化进程树
1 2 3 4 5 6
| $ pstree -p 1 systemd(1)─┬─sshd(892)───sshd(1041)───bash(1042)───pstree(2300) ├─nginx(1234)─┬─nginx(1235) │ ├─nginx(1236) │ └─nginx(1237) └─...
|
7.4 strace -f:追踪进程创建系统调用
1 2 3 4
| $ strace -f -e trace=fork,clone,clone3,execve ls /tmp execve("/usr/bin/ls", ["ls", "/tmp"], 0x7ffd.../* 40 vars */) = 0
|
7.5 bpftrace:动态追踪 copy_process
1 2 3 4 5 6 7
| $ bpftrace -e ' kretprobe:copy_process /retval != 0/ { printf("fork: ppid=%d pcomm=%s cpid=%d\n", pid, comm, ((struct task_struct *)retval)->pid); }'
|
7.6 perf stat:量化进程创建开销
1 2 3 4 5 6 7 8 9
| $ perf stat -e task-clock,context-switches,cpu-migrations,page-faults \ -r 5 bash -c 'for i in $(seq 100); do /bin/true; done'
Performance counter stats for 'bash -c ...' (5 runs):
312.45 msec task-clock 850 context-switches 2 cpu-migrations 14230 page-faults
|
7.7 /proc/PID/maps:观察 COW 页面状态
1 2 3 4 5 6
| $ cat /proc/1234/maps | head -5 55a3c2000000-55a3c2001000 r--p 00000000 fd:01 123456 /usr/bin/nginx 55a3c2001000-55a3c2100000 r-xp 00001000 fd:01 123456 /usr/bin/nginx 55a3c2100000-55a3c2120000 r--p 00100000 fd:01 123456 /usr/bin/nginx 55a3c2121000-55a3c2122000 rw-p 00120000 fd:01 123456 /usr/bin/nginx 7f8a00000000-7f8a00020000 rw-p 00000000 00:00 0 [heap]
|
r--p 和 r-xp 中的 p(private)表示这是写时复制映射,fork 后父子进程共享这些物理页,直到写入为止。
总结
| 操作 |
内核入口 |
共享资源 |
代价 |
fork() |
kernel_clone({.exit_signal=SIGCHLD}) |
无(COW 页表) |
页表复制 + 子进程入队 |
vfork() |
`kernel_clone({CLONE_VFORK |
CLONE_VM})` |
mm 完全共享 |
pthread_create |
`clone(CLONE_VM |
CLONE_THREAD |
…)` |
execve() |
do_execveat_common |
无(全新 mm) |
ELF 加载 + 页表建立 |
从 fork 时 dup_task_struct 的内核栈分配,到 copy_thread 中 childregs->ax = 0 让子进程”看到” 返回值 0,再到 exec 路径上 begin_new_exec 的不可回头设计,以及 exit_zombie 状态下 task_struct 对父进程的等待——这条完整的进程生命周期展现了 Linux 内核在正确性、性能和可观测性之间精妙的平衡。
参考资料
include/linux/sched.h — struct task_struct 定义
kernel/fork.c — fork/clone/vfork 实现
fs/exec.c — execve 实现
fs/binfmt_elf.c — ELF 加载器
kernel/exit.c — 进程退出
kernel/sched/core.c — wake_up_new_task
arch/x86/kernel/process.c — x86 copy_thread
include/uapi/linux/sched.h — CLONE_* 标志定义