Linux 系统中,进程的启动和退出

在 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//maps

输出:显示进程的内存映射(代码段、堆、栈、共享库等)。

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() 性能,通过动态链接支持模块化程序,通过信号机制处理异步事件。


梅花是什么季节(梅花是什么季节开的?揭秘冬季花中仙子的绽放时刻)
银行从业人员职称是什么 ?