进程等待与进程替换
目录
一、进程等待
1.1 为什么要等待子进程?
1.2 等待的两种方式
1.2.1 wait函数
1.2.2 waitpid函数
1.3 获取子进程的退出状态
1.4 示例代码
阻塞式等待(同步)
非阻塞等待(异步)
二、进程替换
2.1 什么是进程替换?
2.2 exec 系列函数
2.3 函数解释
2.4 示例代码
2.5 重要特性
一、进程等待
1.1 为什么要等待子进程?
在 Linux 中,父进程创建子进程后,子进程会独立运行。如果子进程退出后,父进程不采取任何措施,子进程会变成僵尸进程。僵尸进程虽然占用的资源很少,但会浪费系统资源(如进程表项),并且无法被杀死(因为已经“死”了)。为了避免这种情况,父进程需要通过进程等待的方式回收子进程的资源,并获取子进程的退出信息。
此外,父进程通常需要知道子进程的任务完成情况,例如子进程是否正常退出、退出状态码是多少等。这些信息可以通过进程等待来获取。
想象你请了一位临时工(子进程)来完成工作,完成后你需要验收工作成果并结算工资。如果放任不管,这个临时工就会变成"僵尸"赖在系统中,这就是僵尸进程。僵尸进程会导致:
内存泄漏:占用系统进程表资源
信息丢失:无法获取子进程执行结果
无法清除:连kill -9都无法终止僵尸进程
1.2 等待的两种方式
Linux 提供了两种主要方法来实现进程等待:wait
和 waitpid
。
1.2.1 wait函数
wait
是一种简单的进程等待方法,父进程调用它后会阻塞,直到子进程退出。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值:成功时返回子进程的 PID,失败时返回 -1。
参数:用于获取子进程的退出状态。如果不关心子进程的退出状态,可以传入
NULL
。
1.2.2 waitpid函数
waitpid
是更灵活的进程等待方法,它可以指定等待的子进程,并支持非阻塞等待。
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 如果有符合条件的子进程退出,返回子进程的 PID。
- 如果设置了
WNOHANG
选项且没有子进程退出,返回 0。- 如果出错,返回 -1。
参数:
pid:
- Pid=-1,等待任意一个子进程。与wait等效。
- Pid>0.等待其进程ID与pid相等的子进程。
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
1.3 获取子进程的退出状态
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
关键宏定义:
WIFEXITED(status) // 是否正常退出 WEXITSTATUS(status) // 获取退出码 WIFSIGNALED(status) // 是否被信号终止 WTERMSIG(status) // 获取终止信号
1.4 示例代码
阻塞式等待(同步)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork error");
return 1;
}
else if (pid == 0)
{
// 子进程
printf("Child is running, PID: %d\n", getpid());
sleep(5);
exit(257);
}
else
{
// 父进程
int status = 0;
pid_t ret = waitpid(-1, &status, 0); // 阻塞等待
if (WIFEXITED(status))
{
printf("Child exited with code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
非阻塞等待(异步)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork error");
return 1;
}
else if (pid == 0)
{
// 子进程
printf("Child is running, PID: %d\n", getpid());
sleep(5);
exit(1);
}
else
{
// 父进程
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG); // 非阻塞等待
if (ret == 0)
{
printf("Child is still running...\n");
sleep(1);
}
} while (ret == 0);
if (WIFEXITED(status))
{
printf("Child exited with code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
二、进程替换
2.1 什么是进程替换?
进程替换是指子进程通过调用 exec
系列函数,将自己的用户空间代码和数据完全替换为另一个程序的内容,并从新程序的启动例程开始执行。调用 exec
并不会创建新进程,因此进程 ID 不会改变。
-
不创建新进程:保持原PID不变
-
完全替换:代码段、数据段、堆栈都被替换
-
执行流程:从新程序的main函数开始执行
2.2 exec
系列函数
Linux 提供了六种 exec
函数,它们的功能类似,但参数形式不同:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数名 | 参数格式 | 搜索PATH | 环境变量 |
---|---|---|---|
execl | 列表 | 否 | 继承 |
execlp | 列表 | 是 | 继承 |
execle | 列表 | 否 | 自定义 |
execv | 数组 | 否 | 继承 |
execvp | 数组 | 是 | 继承 |
execve | 数组 | 否 | 自定义 |
记忆口诀:
-
l(list):参数逐个列出(
execl
,execlp
,execle
) -
v(vector):使用参数数组(
execv
,execvp
,execve
) -
p(path):自动搜索PATH环境变量
-
e(env):自定义环境变量
2.3 函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1。
- 所以exec函数只有出错的返回值而没有成功的返回值。
2.4 示例代码
#include <unistd.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);
exit(0);
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示:
2.5 重要特性
-
成功无返回:替换成功后不再执行原程序代码
-
失败返回-1:可通过errno查看错误原因
-
文件描述符:默认保持打开(除非设置FD_CLOEXEC)