一、进程等待
1.1 进程等待必要性
- 子进程退出后,若父进程不管不顾,可能会产生 “僵尸进程”,进而造成内存泄漏。
- 进程一旦变为僵尸状态,即使使用
kill -9
也无法将其杀死,因为无法杀死一个已死的进程。 - 父进程需要了解子进程的任务完成情况,比如子进程运行结束后结果是否正确,是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源并获取其退出信息。
1.2 进程等待的方法
wait()
函数
wait()
函数用于阻塞等待子进程的结束,并回收其资源。以下是一个简单的示例代码:
#include <sys/wait.h>
#include <stdio.h>
int main() {
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程执行任务
exit(42); // 子进程退出码42
} else {
int status;
pid_t terminated_pid = wait(&status); // 阻塞等待
if (WIFEXITED(status)) {
printf("子进程 %d 退出码: %d\n", terminated_pid, WEXITSTATUS(status));
}
}
return 0;
}
waitpid()
函数
waitpid()
函数的原型为 pid_t waitpid(pid_t pid, int *status, int options);
,它可以更灵活地等待指定子进程的结束。
1.3 获取子进程 status
wait
和 waitpid
都有一个 status
参数,这是一个输出型参数,由操作系统填充。若传递 NULL
,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数将子进程的退出信息反馈给父进程。
status
不能简单地当作整型来看待,可将其视为位图,具体细节可参考下面的图片(只研究 status
低 16 比特位):
状态位 | 说明 |
---|---|
低8位(0-7) | 子进程退出码 (正常终止时有效) |
第8位(8-15) | 信号编号 (被信号终止时有效) |
其他标志位 | 通过宏检测状态类型(如WIFEXITED) |
1.4 阻塞和非阻塞
核心结论
本质区别:
阻塞(Blocking):调用者线程暂停执行,直到操作完成(如数据到达、资源就绪)。
非阻塞(Non-blocking):调用立即返回,无论操作是否完成,需通过轮询或事件通知获取结果。
选择依据:
阻塞:适合简单逻辑、单任务场景,代码直观但资源利用率低。
非阻塞:适合高并发、实时响应需求,需配合多路复用(如
epoll
)或异步通知(如回调)
深度解析
运行机制对比
特性 | 阻塞模式 | 非阻塞模式 |
---|---|---|
线程状态 | 挂起(Sleeping) | 持续运行(Running) |
CPU 占用 | 低(等待时不消耗 CPU) | 高(需轮询检查状态) |
响应延迟 | 操作完成后立即响应 | 需主动检测或等待通知 |
代码复杂度 | 低(线性执行) | 高(需处理中间状态和错误码) |
典型应用场景
以下是阻塞和非阻塞模式的典型应用代码示例:
阻塞模式示例:
#include <stdio.h>
#include <fcntl.h>
int main() {
int fd = open("file.txt", O_RDONLY);
char buf[1024];
read(fd, buf, sizeof(buf)); // 阻塞直到数据就绪
printf("Data: %s\n", buf);
return 0;
}
非阻塞模式示例:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
char data[BUFFER_SIZE] = "Hello, Server!";
int len = strlen(data);
while (1) {
if (send(sockfd, data, len, MSG_DONTWAIT) == -1) {
if (errno == EAGAIN) {
usleep(1000); // 数据未就绪,短暂等待后重试
continue;
}
}
break;
}
close(sockfd);
return 0;
}
二、进程程序替换
2.1 替换原理
使用 fork
创建子进程后,子进程执行的是与父进程相同的程序(但可能执行不同的代码分支)。若子进程想执行一个全新的程序,可通过进程的程序替换来实现。当进程调用一种 exec
函数时,该进程的用户空间代码和数据会完全被新程序替换,并从新程序的启动例程开始执行。调用 exec
并不会创建新进程,因此调用前后该进程的 ID 不会改变。
2.2 替换函数
函数名 | 参数传递方式 | PATH 搜索 | 环境变量 | 典型用途 |
---|---|---|---|---|
execl |
参数列表(可变参数) | 否 | 继承当前环境 | 已知绝对路径的固定参数调用 |
execv |
参数数组(char *[] ) |
否 | 继承当前环境 | 动态构建参数的固定路径调用 |
execlp |
参数列表 | 是 | 继承当前环境 | 执行 PATH 中的命令(如 Shell) |
execvp |
参数数组 | 是 | 继承当前环境 | 动态执行 PATH 中的命令 |
execle |
参数列表 | 否 | 自定义环境变量 | 需严格控制环境的场景 |
execvpe |
参数数组 | 是 | 自定义环境变量 | 动态参数 + 自定义环境 |
2.3 函数解释
这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。
如果调⽤出错则返回-1
所以exec函数只有出错的返回值⽽没有成功的返回值
2.4 命名理解
l(list)
:表示参数采用列表形式。v(vector)
:参数使用数组。p(path)
:有p
则自动搜索环境变量PATH
。e(env)
:表示自己维护环境变量。
以下是调用示例:
#include <unistd.h>
#include <stdio.h>
int main() {
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带 p 的,可以使用环境变量 PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带 e 的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带 p 的,可以使用环境变量 PATH,无需写全路径
execvp("ps", argv);
// 带 e 的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
// 如果 exec 调用失败,会执行到这里
perror("exec 调用失败");
return 1;
}