Linux进程控制核心知识详解
一、标准I/O缓冲区与进程退出
1.1 缓冲区类型
- 全缓冲:缓冲区满、调用fflush或程序正常退出时刷新。磁盘文件默认使用全缓冲。
- 行缓冲:遇到换行符\n、缓冲区满或调用fflush时刷新。终端标准输出stdout默认行缓冲。
- 无缓冲:数据立即写入设备。标准错误输出stderr默认无缓冲,保证错误信息及时显示。
1.2 exit() vs _exit()
函数对比
|
特性
|
exit()
|
_exit()
|
|
类型
|
C 标准库函数
|
系统调用
|
|
头文件
|
<stdlib.h>
|
<unistd>
|
|
刷新缓冲区
|
✅ 会
|
❌ 不会
|
|
执行清理函数
|
✅ 会
|
❌ 不会
|
|
关闭文件流
|
✅ 会
|
❌ 不会
|
执行流程
exit() 执行流程:用户态:执行 atexit() 注册的函数 → 刷新所有缓冲区 → 关闭文件流↓内核态:调用 _exit() 回收进程资源_exit() 执行流程:直接进入内核态 → 回收进程资源(PCB、文件描述符)→ 返回退出状态给父进程
二、虚拟内存与地址空间
2.1 逻辑地址vs物理地址
- 逻辑地址(虚拟地址):进程运行时操作系统分配的地址,仅在本进程内有效。32位系统每个进程有4GB独立虚拟地址空间。让每个进程都感觉自己独占整个完整的内存地址空间,互不干扰
- 物理地址:内存硬件的实际地址,由内存控制器和CPU使用。
2.2 MMU内存管理单元
- 虚拟地址到物理地址的映射
- 内存访问权限检查
三、进程基础概念
3.1 程序vs进程
- 程序:存储在磁盘上的二进制文件,不占用系统资源,无生命周期。
- 进程:运行中的程序,是操作系统分配资源的基本单位,有独立的地址空间和生命周期。
3.2 常用进程操作命令
- ps aux:查看系统中所有进程的详细信息
- ps ajx:查看进程树,显示进程间的父子关系
- kill -9 pid:发送SIGKILL信号,强制杀死进程,不清理资源
- kill -15 pid:发送SIGTERM信号,优雅终止进程,让进程正常退出,清理资源
- ulimit -a:查看进程资源限制(如最大打开文件数、最大进程数等)
3.3 环境变量
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("HOME: %s\n", getenv("HOME"));
return 0;
}
四、fork函数详解
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
4.1 fork的返回值
- 失败:返回-1
- 成功:父进程返回子进程的PID,子进程返回0
在循环中或代码的不同分支中调用fork函数,会形成 “进程树” 结构:父进程会创建新的子进程,而已存在的子进程也可能创建自己的 “孙子进程”。
4.2 写时复制(COW)机制
int var = 100;
int main() {
pid_t pid = fork();
if (pid == 0) {
var = 1000; // 触发写时复制,子进程有了自己的var副本
printf("Child var: %d\n", var); // 输出1000
} else {
sleep(1); // 等待子进程修改
printf("Parent var: %d\n", var); // 输出100
}
return 0;
}
根本原因就是:进程拥有独立的虚拟地址空间 + 写时复制(COW)机制。
坑点:在父子进程中分别进行var变量地址打印时,会发现两个打印的地址值相同。但是明明每个进程拥有独立的虚拟地址,为什么变量的地址会相同?
每个进程都"认为"自己独占整个地址空间,所以编译器为所有进程生成相同的地址布局。但是内核会为每个进程维护独立的页表,所以相同的虚拟地址映射到不同的物理内存。
eg:
进程A的虚拟地址 0x1234 → 物理内存地址 0xABCD
进程B的虚拟地址 0x1234 → 物理内存地址 0x5678虽然两个进程都看到
0x1234,但它们指向的完全是不同的物理内存单元。一句话总结:虚拟地址是进程私有的"门牌号",相同的门牌号指向不同房子里的房间。
4.3 父子进程资源共享
|
资源类型
|
共享情况
|
说明
|
|
代码段
|
共享
|
只读,无需复制
|
|
数据段/堆/栈
|
写时复制
|
初始共享,修改后独立
|
|
文件描述符表
|
共享
|
指向同一文件表项,共享文件偏移量
|
|
工作目录/根目录
|
继承(可独立修改)
|
子进程可通过chdir切换
|
|
UID/GID/权限
|
继承(可独立修改)
|
可通过setuid变更
|
|
信号处理方式
|
继承(可独立修改)
|
初始一致,后续修改互不影响
|
|
PID/PPID
|
不共享
|
子进程有独立PID
|
|
文件锁
|
不共享
|
子进程需重新申请
|
五、孤儿进程与僵尸进程
5.1 孤儿进程
- 定义:父进程先于子进程退出,子进程成为孤儿进程。
- 处理:孤儿进程会被init进程(或systemd --user进程)领养,由init进程负责回收其资源。
- 特点:孤儿进程不会造成系统资源泄漏。
5.2 僵尸进程
- 定义:子进程先于父进程退出,但父进程没有调用wait()或waitpid()回收子进程的资源,子进程的PCB仍残留在系统中。
- 危害:僵尸进程会占用系统资源(主要是PID),如果大量产生会导致系统无法创建新进程。
- 产生原因:子进程退出时,内核会向父进程发送SIGCHLD信号,但父进程默认忽略该信号,不会主动回收资源。
僵尸进程中,为什么父进程还活着,却不能回收已死的子进程的资源?
子进程结束时,内核会向其父进程发送 SIGCHLD信号,并将子进程的状态信息保留在进程表项中,此时子进程进入僵尸状态。
内核只负责记录状态,不会自动清理子进程资源。因为退出状态可能对父进程有用,必须由父进程主动调用 wait()或 waitpid()来获取状态并回收资源。
默认情况下,父进程忽略 SIGCHLD 信号,如果父进程没有显式设置 SIGCHLD 信号的处理函数,那么即便子进程结束并发送了 SIGCHLD 信号,父进程收到了 SIGCHLD 信号,但因为默认是忽略处理,所以不会执行回收子进程的代码,也不会主动调用 wait()/waitpid() 等回收函数。因此即使收到信号也不会回收子进程。
即便父进程设置了信号处理函数,若函数中未调用 wait() / waitpid(),依然无法回收资源。
六、子进程资源回收
6.1 wait函数
#include <sys/wait.h>
pid_t wait(int *wstatus);
- 功能:阻塞等待任意一个子进程退出,回收其资源。
- 参数:wstatus是传出参数,用于获取子进程的退出状态。
- 返回值:成功返回终止子进程的PID,失败返回-1。
- WIFEXITED(wstatus):如果子进程正常退出,返回真。此时用WEXITSTATUS(wstatus)获取退出状态码。
- WIFSIGNALED(wstatus):如果子进程被信号杀死,返回真。此时用WTERMSIG(wstatus)获取信号编号。
6.2 waitpid函数
pid_t waitpid(pid_t pid, int *wstatus, int options);
- pid:
- pid > 0:等待指定PID的子进程
- pid = -1:等待任意子进程(同wait)
- pid = 0:等待同一进程组的所有子进程
- pid < -1:等待进程组ID为|pid|的任意子进程
- options:
- 0:阻塞等待(同wait)
- WNOHANG:非阻塞等待,如果没有子进程退出立即返回0
- 成功:返回退出子进程的PID
- 设置WNOHANG且无退出子进程:返回0,表示还有子进程在运行,但是暂时没有子进程退出
- 失败:返回-1,本质是系统调用出错,错误码是 ECHILD ,含义就是:当前父进程已经没有任何子进程了,所以直接返回-1结束循环
6.3 回收多个子进程
// 创建5个子进程
for (int i = 0; i < 5; i++) {
if (fork() == 0) {
printf("Child %d\n", getpid());
exit(0);
}
}
// 父进程回收所有子进程
for (int i = 0; i < 5; i++) {
pid_t wpid = wait(NULL);
printf("Reaped: %d\n", wpid);
}
while (1) {
pid_t wpid = waitpid(-1, NULL, WNOHANG);
if (wpid == -1) {
// 没有子进程了,退出循环
break;
} else if (wpid > 0) {
printf("Reaped: %d\n", wpid);
}
// 没有子进程退出,做其他事情
sleep(1);
}
七、exec族函数(进程替换)
7.1 核心作用(必须先懂)
调用exec后,当前进程的代码、数据等被新程序覆盖,PID 不变,但进程的 “内容” 完全变成新程序。从exec族函数开始,原程序下面的代码都不会被执行。
- 成功时不会返回(因为进程已被替换)。
- 失败时返回 -1,并设置 errno(如文件不存在、权限不足等)。因此,exec 后的代码仅在失败时执行,通常用于处理错误。
7.2 最常用的两个exec函数
1. execlp(列表传参,自动找命令)
#include <unistd.h>
int execlp(const char *file,const char arg,.../*(char *) NULL */);
- file: 要执行的程序
- arg:传递给新程序的第一个参数,一般来说,这个参数通常是要执行的程序名本身
- ...:可变参数列表,用于传递其他参数,参数以 NULL 结尾来标识参数列表的结束。
//替换原进程的内容,执行ls -l命令
execlp("ls", "ls", "-l", NULL);
2. execvp(数组传参,自动找命令)
int execvp(const char *file, char *const argv[]);
- 第一个参数:要执行的命令
- 直接写:
"ls""pwd""cat" - 不用写绝对路径!
- 直接写:
- 第二个参数:命令参数数组
- 格式:
{命令名, 参数1, 参数2, ..., NULL} - 必须以 NULL 结尾!
- 格式:
//替换原进程的内容,执行ls -l命令
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
总结
- 标准I/O缓冲区机制和exit/_exit的区别
- 虚拟内存和写时复制机制
- 程序与进程的区别及常用操作命令
- fork函数的工作原理和父子进程资源共享
- 孤儿进程和僵尸进程的产生原因
- wait和waitpid函数回收子进程资源
- exec族函数替换进程内容
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)