在 Linux 系统中,进程的启动和退出涉及 用户空间(glibc、程序代码) 和 内核空间(系统调用、进程管理) 的协同工作。以下从系统级视角深入解析进程生命周期的完整流程,覆盖内核机制和用户态行为。
一、进程启动(Creation)
1. 创建新进程的两种方式
fork():复制当前进程(父进程),生成子进程(共享代码段,写时复制数据段)。
execve():加载新程序到内存,替换当前进程的代码段和数据段。
2. 进程启动的完整流程
父进程调用 fork() → 内核创建子进程 → 子进程调用 execve() → 内核加载新程序 → 用户态入口 _start → __libc_start_main → main()
(1) fork() 系统调用
内核操作:
复制父进程的 task_struct(进程描述符),生成子进程。
复制内存页表(写时复制,COW)。
分配新的 PID 和内核栈。
返回值:
父进程:返回子进程 PID。
子进程:返回 0。
(2) execve() 系统调用
作用:加载可执行文件(ELF、脚本等)到内存,替换当前进程的地址空间。
内核操作:
解析可执行文件:
检查文件格式(通过 binfmt 模块,如 binfmt_elf)。
读取 ELF 头,验证架构兼容性。
设置新地址空间:
代码段(.text):映射到内存,权限为 R-X。
数据段(.data、.bss):映射到内存,权限为 RW-。
堆(brk)、栈(用户栈)初始化。
构建用户态栈:
压入参数(argv)、环境变量(envp)、辅助向量(auxv)。
辅助向量包含关键信息:AT_ENTRY(程序入口 _start)、AT_PHDR(程序头表地址)等。
设置寄存器状态:
指令指针(RIP/EIP)指向入口地址 _start。
栈指针(RSP/ESP)指向用户栈顶。
(3) 用户态启动流程
入口点 _start(由汇编实现):
从栈中读取 argc、argv、envp。
调用 __libc_start_main。
__libc_start_main(glibc):
初始化线程本地存储(TLS)。
调用全局构造函数(.init_array)。
调用用户 main() 函数。
处理 main() 返回值,调用 exit()。
二、进程退出(Termination)
1. 正常退出
用户态入口:exit()(glibc)或 _exit()(直接系统调用)。
内核操作:
释放进程资源:
关闭所有打开的文件描述符。
释放内存页(代码、数据、堆、栈等)。
释放进程描述符 task_struct。
通知父进程:
发送 SIGCHLD 信号给父进程。
将退出状态码存入进程描述符(供 wait() 读取)。
处理孤儿进程:
若父进程已退出,由 init 进程(PID 1)接管子进程。
2. 异常退出
信号(Signal)触发:如 SIGSEGV(段错误)、SIGKILL(强制终止)。
内核操作:
生成核心转储(若配置允许)。
执行与正常退出类似的资源回收流程。
3. exit() vs _exit()
函数行为
exit()
glibc 函数,执行清理:刷新缓冲区、调用 atexit() 注册的函数、析构函数。
_exit()
系统调用,直接终止进程,跳过用户态清理。
三、关键数据结构与内核机制
1. 进程描述符(task_struct)
存储位置:内核栈底部(x86-64 为 current 宏指向当前进程的 task_struct)。
关键字段:
pid、ppid:进程 ID 和父进程 ID。
mm_struct:内存管理信息(地址空间、页表等)。
files_struct:打开的文件描述符表。
exit_code:退出状态码。
signal:挂起的信号。
2. 地址空间管理(mm_struct)
代码段:只读,映射 ELF 的 .text 节。
数据段:可读写,映射 .data(已初始化全局变量)和 .bss(未初始化全局变量)。
堆:动态内存分配(brk/sbrk 或 malloc)。
栈:用户栈,用于函数调用和局部变量。
3. 进程树与信号
父子关系:通过 fork() 创建的进程形成树状结构。
僵尸进程:子进程退出后,若父进程未调用 wait(),其 task_struct 会残留,直到父进程回收。
SIGCHLD:子进程退出时,内核向父进程发送此信号。
四、高级主题
1. 动态链接(ld.so)
介入时机:在 execve() 加载动态链接程序时,内核将控制权先交给动态链接器。
操作流程:
加载依赖的共享库(.so 文件)。
重定位符号(地址绑定)。
将控制权转交到程序的 _start。
2. 核心转储(Core Dump)
触发条件:进程因信号(如 SIGSEGV)崩溃,且系统配置允许生成核心文件。
文件内容:进程崩溃时的内存映像、寄存器状态等,用于调试。
3. 进程间通信(IPC)与退出
管道、套接字:进程退出时,内核会自动关闭这些资源。
共享内存:需显式释放(通过 shmdt() 或进程终止后由系统回收)。
五、代码示例与工具验证
1. 跟踪进程启动系统调用
strace -f -e trace=execve,fork,clone,execve bash -c "ls"
输出:显示 fork、execve 的调用顺序和参数。
2. 查看进程地址空间
cat /proc/
输出:显示进程的内存映射(代码段、堆、栈、共享库等)。
3. 观察僵尸进程
#include
int main() {
if (fork() == 0) { // 子进程立即退出
return 0;
} else { // 父进程不调用 wait()
sleep(60);
}
return 0;
}
查看僵尸进程:
ps aux | grep Z
六、总结
进程启动:通过 fork + execve 实现,内核负责资源分配和程序加载,用户态从 _start 开始执行。
进程退出:用户态通过 exit() 或 _exit() 触发,内核回收资源并通知父进程。
设计哲学:通过 COW 机制优化 fork() 性能,通过动态链接支持模块化程序,通过信号机制处理异步事件。
梅花是什么季节(梅花是什么季节开的?揭秘冬季花中仙子的绽放时刻)
银行从业人员职称是什么 ?