前言
当你在终端输入 ./hello 运行一个程序时,Linux 内核做了哪些事情?一个进程是如何从无到有被创建出来的?本文深入剖析 Linux 进程启动的完整链路,从 fork 系统调用到程序真正执行,揭示进程管理背后的内核秘密。
进程启动全链路
一、进程描述符
1.1 task_struct 结构
Linux 内核通过 task_struct 描述一个进程(或线程)。这是内核中最核心的数据结构之一。
// Linux 内核进程描述符(简化)// 源码: https://github.com/torvalds/linux/blob/master/include/linux/sched.hstruct task_struct { unsigned int state; // 进程状态 int prio; // 动态优先级 int static_prio; // 静态优先级 pid_t pid; // 进程 ID pid_t tgid; // 线程组 ID struct mm_struct *mm; // 内存描述符 struct files_struct *files; // 打开的文件 struct signal_struct *signal; // 信号 struct sched_entity se; // 调度实体 const struct sched_class *sched_class; // 调度类 struct thread_info thread_info; // 线程信息 // ... 几百个字段};1.2 进程状态
| 状态 | 说明 |
|---|---|
| TASK_RUNNING | 正在运行或就绪等待 CPU |
| TASK_INTERRUPTIBLE | 可中断睡眠,可被信号唤醒 |
| TASK_UNINTERRUPTIBLE | 不可中断睡眠,只能等事件就绪 |
| TASK_STOPPED | 停止运行 |
| EXIT_ZOMBIE | 僵尸状态,等待父进程回收 |
| EXIT_DEAD | 最终状态,资源已回收 |
1.3 进程在内核中的组织
每个进程在内核中有两块核心内存:
+-------------------+ 低地址| 用户态栈 |+-------------------+| ... |+-------------------+| 用户空间 | 0x0000000000000000 ~ 0x00007FFFFFFFFFFF| (代码/数据/堆/栈) |+-------------------+ 0x00007FFFFFFFFFFF (TASK_SIZE)| 不可访问区 |+-------------------+| 内核空间 | 0xFFFF800000000000 (x86_64)| +---------------+ || | 内核栈 (8KB) | || | thread_info | || +---------------+ || | task_struct | || | (slab 分配) | || +---------------+ |+-------------------+二、fork 系统调用
2.1 fork 执行流程
2.2 写时复制 (Copy-on-Write)
fork 时并不真正复制父进程的内存,而是让父子进程共享同一块物理内存,页表标记为只读。只有当某一方尝试写入时,才触发缺页异常,内核此时才真正复制该页。
fork 后的内存布局:
父进程页表 物理内存 子进程页表+----------+ +----------+ +----------+| PTE (RO) |------->| Page A |<-------| PTE (RO) |+----------+ +----------+ +----------+| PTE (RO) |------->| Page B |<-------| PTE (RO) |+----------+ +----------+ +----------+| PTE (RO) |------->| Page C |<-------| PTE (RO) |+----------+ +----------+ +----------+
父进程写入 Page B 后:
父进程页表 物理内存 子进程页表+----------+ +----------+ +----------+| PTE (RW) |------->| Page A |<-------| PTE (RO) |+----------+ +----------+ +----------+| PTE (RW) |---+ | Page B |<-------| PTE (RO) |+----------+ +--->| Page B' | +----------+| PTE (RW) |------->| Page C |<-------| PTE (RO) |+----------+ +----------+ +----------+2.3 fork 内核源码
// fork 核心实现// 源码: https://github.com/torvalds/linux/blob/master/kernel/fork.c
// sys_fork 的实际实现SYSCALL_DEFINE0(fork){ return kernel_clone(clone_args);}
// 更通用的 clone 实现static __latent_entropy struct task_struct *copy_process( struct pid *pid, int trace, int node, struct kernel_clone_args *args){ struct task_struct *p;
// 1. 分配 task_struct p = dup_task_struct(current, node); if (!p) return ERR_PTR(-ENOMEM);
// 2. 复制进程上下文 retval = copy_creds(p, clone_flags); // 复制凭证 retval = copy_mm(clone_flags, p); // 复制内存空间 (COW) retval = copy_namespaces(clone_flags, p); // 复制命名空间 retval = copy_files(clone_flags, p); // 复制文件描述符 retval = copy_fs(clone_flags, p); // 复制 fs 信息 retval = copy_sighand(clone_flags, p); // 复制信号处理 retval = copy_signal(clone_flags, p); // 复制信号 retval = copy_thread(p, args); // 复制线程上下文
// 3. 设置 PID p->pid = pid_nr(pid);
// 4. 初始化调度信息 retval = sched_fork(clone_flags, p);
return p;}2.4 clone 的灵活创建
Linux 的 fork、vfork、pthread_create 底层都调用 clone 系统调用,通过参数控制共享程度:
// 不同创建方式的 clone flags// fork: clone(SIGCHLD, 0)// vfork: clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)// thread: clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...)
// 创建线程 (共享地址空间、文件、信号处理)clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, stack_ptr);
// 创建进程 (什么都不共享)clone(SIGCHLD, 0);| clone flag | 作用 | fork | vfork | pthread |
|---|---|---|---|---|
| CLONE_VM | 共享内存空间 | 否 | 是 | 是 |
| CLONE_FS | 共享 fs_info | 否 | 否 | 是 |
| CLONE_FILES | 共享文件描述符表 | 否 | 否 | 是 |
| CLONE_SIGHAND | 共享信号处理 | 否 | 否 | 是 |
| CLONE_VFORK | 父进程等待子进程 exec | 否 | 是 | 否 |
| CLONE_THREAD | 放入同一线程组 | 否 | 否 | 是 |
三、exec 系统调用
3.1 exec 执行流程
3.2 ELF 文件格式
ELF (Executable and Linkable Format) 是 Linux 下可执行文件和共享库的标准格式。
ELF 文件结构:
+-------------------+| ELF Header | 魔数: 7f 45 4c 46 (DEL E L F)| e_type: ET_EXEC | 类型: 可执行文件| e_entry: 0x400xxx | 入口点地址| e_phoff: 64 | Program Header 偏移| e_shoff: xxx | Section Header 偏移+-------------------+| Program Header | 描述运行时的段 (Segment)| PT_LOAD (.text) | 可加载段: 代码| PT_LOAD (.data) | 可加载段: 数据| PT_INTERP | 动态链接器路径| PT_DYNAMIC | 动态链接信息| PT_STACK | 栈标志+-------------------+| .text | 代码段 (机器指令)| .rodata | 只读数据 (字符串常量等)| .data | 已初始化数据| .bss | 未初始化数据 (不占文件空间)| .dynamic | 动态链接信息| .dynsym | 动态符号表| .strtab | 字符串表+-------------------+| Section Header | 描述链接时的节 (Section)+-------------------+# 查看 ELF 文件信息readelf -h ./hello # ELF Headerreadelf -l ./hello # Program Headersreadelf -S ./hello # Section Headersreadelf -d ./hello # Dynamic Sectionfile ./hello # 文件类型objdump -d ./hello # 反汇编3.3 exec 内核源码
// execve 系统调用入口// 源码: https://github.com/torvalds/linux/blob/master/fs/exec.c
SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp){ return do_execve(getname(filename), argv, envp);}
// exec 核心实现static int exec_binprm(struct linux_binprm *bprm){ // 搜索注册的二进制格式处理器 // ELF -> load_elf_binary() // 脚本 -> load_script() // a.out -> load_aout_binary() ret = search_binary_handler(bprm);
return ret;}
// ELF 加载器// 源码: https://github.com/torvalds/linux/blob/master/fs/binfmt_elf.cstatic int load_elf_binary(struct linux_binprm *bprm){ // 1. 读取并验证 ELF Header retval = elf_read(&loc->elf_ex, sizeof(loc->elf_ex), bprm);
// 2. 找到 PT_INTERP 段,获取动态链接器路径 for (i = 0; i < loc->elf_ex.e_phnum; i++) { if (elf_ppnt->p_type == PT_INTERP) { // 读取 ld.so 路径 retval = elf_read(&elf_interpreter, ...); } }
// 3. 释放旧地址空间 retval = begin_new_exec(bprm); // flush_old_exec 内部
// 4. 映射各 PT_LOAD 段 for (i = 0; i < loc->elf_ex.e_phnum; i++) { if (elf_ppnt->p_type != PT_LOAD) continue; // mmap 映射到进程地址空间 elf_map(bprm->file, load_addr + elf_ppnt->p_vaddr, elf_ppnt, elf_prot, elf_flags, total_size); }
// 5. 设置入口点 if (elf_interpreter) { // 动态链接程序: 入口 = ld.so elf_entry = load_elf_interp(&loc->interp_elf_ex, ...); } else { // 静态链接程序: 入口 = e_entry elf_entry = loc->elf_ex.e_entry; }
return 0;}四、动态链接
4.1 动态链接流程
4.2 GOT 与 PLT
GOT (Global Offset Table):存储外部符号的实际地址。PLT (Procedure Linkage Table):跳转桩代码,实现延迟绑定。
延迟绑定 (Lazy Binding) 机制:
第一次调用 printf:
程序代码 PLT GOT+------------------+ +------------------+ +------------------+| call printf@plt |-->| printf@plt: | | [printf@got] || | | jmp *[printf@got]|-->| 0x0 (未解析) || | | push reloc_index| +------------------+| | | jmp PLT[0] | |+------------------+ +------------------+ v ld.so 动态解析 printf 的实际地址 填入 GOT 表
第二次调用 printf:
程序代码 PLT GOT+------------------+ +------------------+ +------------------+| call printf@plt |-->| printf@plt: | | [printf@got] || | | jmp *[printf@got]|-->| printf 实际地址 |---> printf()+------------------+ +------------------+ +------------------+# 查看 GOT/PLT 信息objdump -d -j .plt ./hello # 查看 PLTobjdump -d -j .got ./hello # 查看 GOTreadelf -r ./hello # 查看重定位条目readelf --dyn-syms ./hello # 查看动态符号
# 查看动态依赖ldd ./hello# 输出:# linux-vdso.so.1 (0x00007ff...)# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6# /lib64/ld-linux-x86-64.so.24.3 动态链接器源码
// glibc 动态链接器入口// 源码: https://github.com/bminor/glibc/blob/master/elf/rtld.c
// ld.so 的真正入口 (_start → _dl_start → _dl_start_final)void _dl_start_final(void *arg){ // 1. 自身重定位 (自举) // 2. 加载依赖库 // 3. 执行重定位 // 4. 初始化各库 // 5. 跳转到程序入口 _dl_init(main_map, argc, argv, envp); entry = _dl_sysdep_start(main_map, ...); // 跳转到程序 _start}五、进程内存布局
5.1 完整内存映射
进程虚拟地址空间布局 (x86_64):
高地址 0xFFFFFFFFFFFFFFFF+-------------------+| 内核空间 | 不可访问 (用户态)| (所有进程共享) |+-------------------+ 0x00007FFFFFFFFFFF (TASK_SIZE)| || 栈 (Stack) | ↓ 向低地址增长| |+-------------------+| ... |+-------------------+| 内存映射区 (mmap) | 文件映射、共享库| ld.so / libc.so | ↑ 向高地址增长+-------------------+| ... |+-------------------+| 堆 (Heap) | ↑ 向高地址增长 (brk/sbrk)| |+-------------------+| BSS 段 | 未初始化的全局/静态变量 (零填充)+-------------------+| Data 段 | 已初始化的全局/静态变量+-------------------+| Text 段 | 代码 (只读、可执行)+-------------------+| ELF Header 等 |+-------------------+ 0x400000 (典型加载地址)| 不可访问区 |+-------------------+ 0x0低地址5.2 查看进程内存映射
# 查看进程内存映射cat /proc/<pid>/maps# 或pmap -x <pid>
# 示例输出:# 地址范围 权限 偏移 设备 inode 路径# 00400000-00401000 r--p 00000000 08:01 12345 /home/user/hello# 00401000-00402000 r-xp 00001000 08:01 12345 /home/user/hello (.text)# 00402000-00403000 r--p 00002000 08:01 12345 /home/user/hello (.rodata)# 00403000-00404000 rw-p 00003000 08:01 12345 /home/user/hello (.data)# 7f0000000-7f0020000 r-xp 00000000 ... /lib/x86_64/libc.so.6# 7ffff7a00000-... rw-p ... 堆# 7ffffffde000-... rw-p ... 栈
# 查看进程详细信息cat /proc/<pid>/statuscat /proc/<pid>/statm # 内存使用 (页数)5.3 内核中的内存管理
// 进程内存描述符// 源码: https://github.com/torvalds/linux/blob/master/include/linux/mm_types.hstruct mm_struct { pgd_t *pgd; // 页全局目录 unsigned long start_code, end_code; // 代码段 unsigned long start_data, end_data; // 数据段 unsigned long start_brk, brk; // 堆 unsigned long start_stack; // 栈 unsigned long mmap_base; // mmap 区基址 struct vm_area_struct *mmap; // VMA 链表 struct rb_root mm_rb; // VMA 红黑树 // ...};
// 虚拟内存区域struct vm_area_struct { unsigned long vm_start; // 起始地址 unsigned long vm_end; // 结束地址 struct vm_area_struct *vm_next; // 链表 struct rb_node vm_rb; // 红黑树节点 struct mm_struct *vm_mm; // 所属 mm_struct unsigned long vm_flags; // 权限标志 const struct vm_operations_struct *vm_ops; // 操作函数};六、文件描述符
6.1 文件描述符继承
fork 后子进程继承父进程的文件描述符表,共享文件偏移量。
6.2 CLOEXEC 标志
exec 替换进程映像时,默认继承所有打开的文件描述符。FD_CLOEXEC 标志可以让 fd 在 exec 时自动关闭。
// 设置 CLOEXEC// 源码: https://github.com/torvalds/linux/blob/master/fs/file.c
// 打开文件时设置 (原子操作)int fd = open("file.txt", O_RDONLY | O_CLOEXEC);
// 或在已有的 fd 上设置fcntl(fd, F_SETFD, FD_CLOEXEC);
// 为什么需要 CLOEXEC?// 1. 安全: 防止子进程意外访问父进程的文件// 2. 资源泄漏: exec 后 fd 不再使用但不关闭// 3. 多线程安全: 传统 SETFD + exec 之间有竞态条件// pipe + CLOEXEC 示例int pipefd[2];pipe2(pipefd, O_CLOEXEC); // 原子设置 CLOEXEC
// fork 后子进程if (fork() == 0) { // 关闭读端 close(pipefd[0]); // 重定向 stdout 到管道写端 // pipefd[1] 没有 CLOEXEC,可以 exec 后使用 dup2(pipefd[1], STDOUT_FILENO); execlp("ls", "ls", NULL);}6.3 标准 I/O 重定向
Shell 中管道和重定向的本质就是操作文件描述符:
# 重定向原理# ls > output.txt 等价于:# 1. fork# 2. 子进程中: open("output.txt") → 得到 fd 3# 3. dup2(3, 1) → fd 1 (stdout) 指向 output.txt# 4. close(3)# 5. exec("ls")// 标准重定向实现void redirect_stdout(const char *filename) { int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644); dup2(fd, STDOUT_FILENO); // STDOUT_FILENO = 1 close(fd);}
// 管道实现void pipe_example() { int pipefd[2]; pipe(pipefd);
if (fork() == 0) { close(pipefd[0]); // 关闭读端 dup2(pipefd[1], STDOUT_FILENO); close(pipefd[1]); execlp("ls", "ls", NULL); }
close(pipefd[1]); // 关闭写端 dup2(pipefd[0], STDIN_FILENO); close(pipefd[0]); execlp("wc", "wc", "-l", NULL);}七、进程调度
7.1 CFS (完全公平调度器)
CFS 是 Linux 默认的进程调度器,核心思想是给每个进程公平的 CPU 时间。
// CFS 调度器核心数据结构// 源码: https://github.com/torvalds/linux/blob/master/kernel/sched/fair.c
// 调度实体struct sched_entity { struct load_weight load; // 权重 struct rb_node run_node; // 红黑树节点 unsigned int on_rq; // 是否在运行队列 u64 vruntime; // 虚拟运行时间 u64 sum_exec_runtime; // 累计执行时间};
// CFS 运行队列 (红黑树,按 vruntime 排序)struct cfs_rq { struct rb_root_cached tasks_timeline; // 红黑树根 struct sched_entity *curr; // 当前运行实体 unsigned int nr_running; // 运行中进程数 u64 min_vruntime; // 最小 vruntime};CFS 红黑树 (按 vruntime 排序):
vruntime=50 / \ vruntime=20 vruntime=80 / \ / \ vr=10 vr=35 vr=65 vr=120 ↑ 下一个调度的进程 (vruntime 最小)7.2 调度策略
| 调度策略 | 类型 | 优先级范围 | 说明 |
|---|---|---|---|
| SCHED_NORMAL | 普通进程 | 100-139 | CFS 调度,nice 值映射 |
| SCHED_BATCH | 普通进程 | 100-139 | 批处理,不交互 |
| SCHED_IDLE | 普通进程 | 最低 | 极低优先级 |
| SCHED_FIFO | 实时进程 | 0-99 | 先进先出,不主动让出 |
| SCHED_RR | 实时进程 | 0-99 | 时间片轮转 |
| SCHED_DEADLINE | 实时进程 | - | 截止时间调度 (EDF) |
# 查看进程调度策略和优先级chrt -p <pid>
# 设置进程调度策略chrt -f 10 ./realtime_app # SCHED_FIFO, 优先级 10chrt -r 50 ./realtime_app # SCHED_RR, 优先级 50chrt -o 0 ./normal_app # SCHED_NORMAL
# nice 值 (影响普通进程优先级)nice -n 10 ./low_priority_app # nice 值 +10renice -n -5 -p <pid> # 提高优先级7.3 进程调度流程
八、进程终止
8.1 exit 流程
8.2 exit 内核源码
// 进程退出实现// 源码: https://github.com/torvalds/linux/blob/master/kernel/exit.c
void __noreturn do_exit(long code){ struct task_struct *tsk = current;
// 1. 检查退出合法性 if (unlikely(in_interrupt())) panic("Aiee, killing interrupt handler!");
// 2. 设置退出码 tsk->exit_code = code;
// 3. 释放资源 exit_signals(tsk); // 退出信号处理 exit_mm(tsk); // 释放内存空间 exit_sem(tsk); // 释放信号量 exit_shm(tsk); // 释放共享内存 exit_files(tsk); // 关闭文件描述符 exit_fs(tsk); // 释放 fs 信息 exit_task_namespaces(tsk); // 释放命名空间
// 4. 设置僵尸状态 tsk->state = TASK_DEAD; tsk->exit_state = EXIT_ZOMBIE;
// 5. 通知父进程 do_notify_parent(tsk, tsk->exit_signal);
// 6. 重新调度 do_task_dead();}8.3 僵尸进程与孤儿进程
僵尸进程 (Zombie): 子进程已退出,但父进程未调用 waitpid 回收 task_struct 仍保留在内核中,占用 PID 资源
+-----------+ +-----------+ | 父进程 | | 子进程 (Z) | | 活跃 | SIGCHLD | task_struct| | 没有wait |<---------| 保留 | +-----------+ +-----------+
危害: PID 耗尽,无法创建新进程 解决: 父进程调用 wait/waitpid 或 kill 父进程让 init 回收
孤儿进程 (Orphan): 父进程先于子进程退出 子进程被 init (PID 1) 或 subreaper 接管
+-----------+ | 父进程 | exit +-----------+ ↓ init (PID 1) 接管 +-----------+ | 子进程 | | 继续运行 | +-----------+# 查找僵尸进程ps aux | grep 'Z'# 或ps -eo pid,ppid,stat,cmd | grep 'Z'
# 消除僵尸进程:# 1. 让父进程调用 wait()# 2. kill 父进程,init 接管并回收# 3. 使用 SIGCHLD 信号处理 (SA_NOCLDSTOP|SA_NOCLDWAIT)
# 设置 subreaper (PR_SET_CHILD_SUBREAPER)# systemd / Docker 常用此机制prctl(PR_SET_CHILD_SUBREAPER, 1);8.4 waitpid 内核实现
// waitpid 系统调用// 源码: https://github.com/torvalds/linux/blob/master/kernel/exit.c
SYSCALL_DEFINE4(wait4, pid_t, pid, int __user *, stat_addr, int, options, struct rusage __user *, ru){ return kernel_wait4(pid, stat_addr, options, ru);}
static long kernel_wait4(pid_t pid, int __user *stat_addr, int options, struct rusage __user *ru){ struct wait_opts wo;
// 遍历子进程 for_each_thread(current, p) { // 检查子进程状态 if (p->exit_state == EXIT_ZOMBIE) { // 找到僵尸子进程 // 读取退出码 // 释放 task_struct // 返回子进程 PID } }
// 如果设置了 WNOHANG,立即返回 if (options & WNOHANG) return 0;
// 否则阻塞等待子进程退出 return do_wait_thread(wo);}九、实战:跟踪进程启动
9.1 strace 跟踪
# 跟踪进程启动全过程strace -f -o trace.log ./hello
# 关键输出分析# -f: 跟踪子进程# -o: 输出到文件
# 典型 strace 输出:execve("./hello", ["./hello"], 0x7ffd... /* 50 vars */) = 0brk(NULL) = 0x5600000mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...access("/etc/ld.so.preload", R_OK) = -1 ENOENTopenat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_size=89012, ...}) = 0mmap(NULL, 89012, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f...close(3) = 0openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3read(3, "\x7fELF\x02\x01\x01\x03", 832) = 832mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...mmap(NULL, 18325696, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f...mprotect(0x7f...24000, 2097152, PROT_NONE) = 0mmap(0x7f...42000, 1552384, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x24000) = 0x7f...mmap(0x7f...99000, 303104, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x199000) = 0x7f...close(3) = 0mprotect(0x7f...99000, 24576, PROT_READ) = 0set_tid_address(0x7f...) = 12345set_robust_list(0x7f..., 24) = 0rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN}, ...) = 0write(1, "Hello, World!\n", 14) = 14exit_group(0) = ?+++ exited with 0 +++9.2 ltrace 跟踪库调用
# 跟踪库函数调用ltrace ./hello
# 典型输出:__libc_start_main(0x560c..., 1, 0x7ffd..., 0x560c... <incomplete ...>puts("Hello, World!") = 14+++ exited (status 0) +++9.3 /proc 文件系统查看
# 启动一个后台进程./hello &PID=$!
# 查看进程信息cat /proc/$PID/status | head -20# Name: hello# State: S (sleeping)# Tgid: 12345# Pid: 12345# PPid: 1234# Uid: 1000 1000 1000 1000# Gid: 1000 1000 1000 1000
# 查看内存映射cat /proc/$PID/maps
# 查看命令行cat /proc/$PID/cmdline | tr '\0' ' '
# 查看打开的文件ls -la /proc/$PID/fd/9.4 从 fork 到 exec 的 Shell 模拟
// 简化版 Shell: fork + exec + wait// 模拟 Shell 执行命令的过程#include <stdio.h>#include <unistd.h>#include <sys/wait.h>
int main() { char *argv[] = {"/bin/ls", "-la", "/tmp", NULL}; char *envp[] = {NULL};
pid_t pid = fork(); // 1. fork 创建子进程
if (pid == 0) { // 子进程 // 2. exec 替换进程映像 execve("/bin/ls", argv, envp); // 如果 exec 成功,下面的代码不会执行 perror("execve failed"); _exit(1); } else if (pid > 0) { // 父进程 int status; // 3. wait 等待子进程 waitpid(pid, &status, 0); printf("Child exited with status: %d\n", WEXITSTATUS(status)); } else { perror("fork failed"); }
return 0;}十、从 _start 到 main
10.1 C 程序的启动序列
exec 加载程序后,入口点并不是 main,而是 _start:
; _start 的汇编实现 (x86_64); 源码: https://github.com/bminor/glibc/blob/master/sysdeps/x86_64/start.S
_start: ; 清空 rbp xor ebp, ebp
; 提取 argc (栈顶) mov r9, rsp ; 保存栈指针 (用于对齐) pop rsi ; argc → rsi mov rdx, rsp ; argv → rdx
; 对齐栈 and rsp, -16
; 调用 __libc_start_main ; 参数: main, argc, argv, init, fini, rtld_fini, stack_end call __libc_start_main
; 如果 __libc_start_main 返回 (不应该) hlt10.2 初始化顺序
C 程序初始化和终止的完整顺序:
初始化:1. 内核映射 ELF 段2. ld.so 加载动态库,执行重定位3. 各 .init 段按依赖顺序执行4. .init_array 中的函数指针(按注册顺序)5. C++ 全局对象构造函数6. main()
终止:7. main() 返回 / exit() 调用8. atexit() 注册的函数(按相反顺序)9. .fini_array 中的函数指针10. C++ 全局对象析构函数11. 各 .fini 段12. _exit() 系统调用常见问题
Q1: fork 和 vfork 有什么区别?
vfork 创建子进程时共享父进程的地址空间(不执行 COW),子进程必须立即调用 exec 或 _exit,不能修改数据或返回。vfork 保证子进程先运行,避免了 COW 的开销,适合 fork 后立即 exec 的场景。现代 Linux 中 fork 已经足够高效(得益于 COW),vfork 的优势不大,不推荐使用。
Q2: exec 会继承哪些属性?
exec 替换进程映像,但部分属性会保留:PID 和 PPID、进程优先级 (nice)、文件描述符(没有 CLOEXEC 的)、当前工作目录、根目录、环境变量(除非显式传入新 envp)、信号掩码、资源限制 (rlimit)。不保留的:代码和数据(被替换)、信号处理函数(恢复为默认)、内存映射(munmap)、atexit 注册的函数。
Q3: 为什么需要 COW?
如果没有 COW,fork 时必须复制父进程的所有内存页。假设父进程使用了 1GB 内存,fork 就需要额外分配 1GB 并复制数据,即使子进程根本不会修改这些数据。COW 让父子进程先共享物理页,只在真正写入时才复制被修改的页。实践表明,fork 后大多数页面都不会被修改。
Q4: 僵尸进程有什么危害?
僵尸进程已经释放了大部分资源,只保留 task_struct 和少量内核栈信息。但它们占用了 PID 资源。Linux 的 PID 数量有限(默认 32768),如果大量僵尸进程不被回收,会导致无法创建新进程。解决办法是确保父进程正确调用 wait/waitpid,或者使用 SIGCHLD 信号的 SA_NOCLDWAIT 标志。
Q5: 如何调试进程启动问题?
使用 strace -f 跟踪系统调用,可以看到 fork、exec、mmap 等完整过程。如果 exec 失败,strace 会显示具体错误码。ltrace 可以跟踪库函数调用。LD_DEBUG 环境变量可以打印动态链接的详细信息:LD_DEBUG=all ./hello。对于更底层的问题,可以使用 ftrace 或 perf 跟踪内核函数调用。
参考资料
- Linux 内核源码
- fork.c 源码
- exec.c 源码
- exit.c 源码
- binfmt_elf.c 源码
- CFS 调度器
- glibc 启动代码
- glibc 动态链接器
- ELF 格式规范
- 《深入理解 Linux 内核》(Daniel P. Bovet)
- 《Linux 内核设计与实现》(Robert Love)
- 《程序员的自我修养: 链接、装载与库》(俞甲子)
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






