进程等待与进程替换

发布于:2025-02-17 ⋅ 阅读:(139) ⋅ 点赞:(0)

目录

一、进程等待

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 获取子进程的退出状态

  1. wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  2. 如果传递NULL,表示不关心子进程的退出状态信息。
  3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  4. 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):参数逐个列出(execlexeclpexecle

  • v(vector):使用参数数组(execvexecvpexecve

  • p(path):自动搜索PATH环境变量

  • e(env):自定义环境变量

2.3 函数解释

  1. 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  2. 如果调用出错则返回-1。
  3. 所以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)


网站公告

今日签到

点亮在社区的每一天
去签到