unilm 联合对齐-翻译实战指南:基于 fairseq 的 Jointly Learning to Align and Translate(Garg et al., 2019)完整训练与评估流程
2026/9/13 4:02:32
【Linux】从 fork 到进程终止:写时拷贝细节与常见退出方式
Linux 进程创建(fork)与终止(exit/kill)是操作系统中最核心、最常被考察的机制之一。
本文重点讲解fork 的写时拷贝(Copy-On-Write, COW)实现细节,以及进程终止的各种方式(正常/异常/强制),结合内核视角、常见陷阱与生产实践。
fork()是 POSIX 标准中创建新进程的系统调用,返回值:
最关键的一点:子进程是父进程的几乎完整副本(包括代码段、数据段、堆、栈、打开的文件描述符、信号处理、环境变量等)。
传统实现(很早的 Unix)会直接复制整个地址空间 →极其昂贵(内存拷贝 + 时间)。
Linux(从很早版本开始)采用COW机制:
fork 时:
读操作:直接访问共享的物理页面 →零拷贝,速度极快
第一次写操作(任一进程):
一句话总结:
fork 后父子共享物理内存,直到其中一方写时才真正复制该页。
#include<stdio.h>#include<unistd.h>#include<sys/wait.h>intglobal_var=100;// 数据段intmain(){intstack_var=200;// 栈pid_tpid=fork();if(pid==0){// 子进程printf("子进程: global=%d, stack=%d\n",global_var,stack_var);global_var=999;// 写 → 触发 COW(该页被复制)stack_var=888;printf("子进程修改后: global=%d, stack=%d\n",global_var,stack_var);}else{// 父进程wait(NULL);printf("父进程: global=%d, stack=%d\n",global_var,stack_var);// 父进程看到的仍是 100 和 200(COW 后子进程修改的是自己的副本)}return0;}输出:
子进程: global=100, stack=200 子进程修改后: global=999, stack=888 父进程: global=100, stack=200结论:父子进程修改的是各自独立的副本,不互相影响。
优点:
代价 / 副作用:
生产建议:
Linux 进程退出有多种路径,影响退出码、资源释放、信号处理、atexit/on_exit等清理函数是否执行。
| 方式 | 系统调用/信号 | 是否调用 atexit() | 是否发 SIGCHLD | 是否可捕获 | 典型退出码 | 备注 / 适用场景 |
|---|---|---|---|---|---|---|
| main 返回 | exit() | 是 | 是 | — | 返回值 | 最正常退出 |
| exit() / _exit() | exit_group() / exit | 是 / 否 | 是 | — | 参数 | exit 调用 atexit,_exit 不调用 |
| pthread_exit() | — | 否(仅线程) | 否 | — | — | 仅退出当前线程,主线程仍存活 |
| return from main | exit() | 是 | 是 | — | 返回值 | 等价于 exit() |
| SIGTERM (kill -15) | — | 是(若 handler 调用 exit) | 是 | 可捕获 | 通常 143 | 优雅终止(默认行为) |
| SIGKILL (kill -9) | — | 否 | 是 | 不可捕获 | 通常 137 | 强制杀死(不可拦截) |
| SIGABRT (abort()) | — | 否(核心转储) | 是 | 可捕获 | 通常 134 | 断言失败等 |
| 段错误等异常 | SIGSEGV 等 | 否 | 是 | 可捕获 | 通常 139 | 核心转储 |
关键区别:
退出码约定(Shell 中 $?):
#include<stdio.h>#include<stdlib.h>#include<signal.h>#include<unistd.h>staticvoidcleanup(void){printf("atexit 清理:关闭文件、释放资源...\n");}staticvoidsigterm_handler(intsig){printf("收到 SIGTERM,进行优雅退出...\n");// 可以做:保存状态、通知其他进程、关闭连接等exit(EXIT_SUCCESS);// 或 _exit(0) 看需求}intmain(){atexit(cleanup);signal(SIGTERM,sigterm_handler);// signal(SIGINT, sigterm_handler); // Ctrl+Cprintf("进程运行中... PID=%d\n",getpid());while(1){sleep(1);}return0;}测试:
# 终端1./a.out# 终端2kill-TERM<pid># 优雅退出,调用 atexitkill-9<pid># 强制杀死,不调用 atexit想继续深入哪个方向?
A. vfork() vs fork() vs clone() 区别
B. 僵尸进程、孤儿进程的产生与处理
C. 多线程信号分发与 pthread_kill
D. 内核视角:task_struct → do_exit 流程
E. 生产事故案例(SIGKILL 导致数据损坏)
告诉我字母,我们继续深挖!