【Linux】进程退出、等待与程序替换

1. 进程退出场景与退出码

1.1 进程退出的三种场景

  1. 代码运行完毕,结果正确

  2. 代码运行完毕,结果不正确(如逻辑错误)

  3. 代码异常终止(如段错误、被信号杀死)

1.2 退出码

  • 作用:用于表示程序的执行情况,父进程通过退出码了解子进程执行结果。

  • 存储位置 :进程退出码存储在task_struct结构体中。

  • 查看方式

c 复制代码
./program      # 运行程序
echo $?        # 查看最近一次执行的程序的退出码
  • 取值范围:通常为0-255,其中:

    • 0:表示成功(结果正确)

    • 非0:表示失败或异常,不同的值代表不同的错误原因

  • main函数返回值main函数的返回值就是进程的退出码。

1.3 进程退出方式

  1. main函数返回return语句使进程正常退出。

  2. exit()函数 :在任何地方调用exit()都会使进程立即退出。

  3. _exit()系统调用:系统级退出函数。

1.4 exit() vs _exit()

特性 exit() (库函数) _exit() (系统调用)
缓冲区处理 刷新缓冲区(如printf的输出) 不刷新缓冲区
清理操作 执行清理函数(如atexit注册的函数) 直接退出,不执行清理
头文件 #include <stdlib.h> #include <unistd.h>
使用场景 一般用户程序退出 系统底层退出
c 复制代码
// 示例:缓冲区刷新差异
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    printf("Hello");  // 输出到标准输出缓冲区
    
    // fork();  // 如果fork,会有更复杂的缓冲区行为
    
    // exit(0);      // 会刷新缓冲区,输出"Hello"
    // _exit(0);     // 不会刷新缓冲区,可能不输出"Hello"
    
    return 0;        // main返回会刷新缓冲区
}

1.5 缓冲区的位置

  • 一定不在操作系统内部:操作系统的I/O缓冲区是另一回事。

  • 在C标准库中printf等函数的缓冲区是由C标准库维护的。

  • 三种缓冲类型

    1. 全缓冲:文件操作通常使用全缓冲。

    2. 行缓冲 :终端输出通常使用行缓冲(遇到换行符\n时刷新)。

    3. 无缓冲 :标准错误stderr通常无缓冲,立即输出。

2. 进程等待的必要性

2.1 为什么需要进程等待?

  1. 防止僵尸进程:子进程退出后,父进程如果不回收,子进程会变成僵尸进程,导致内存泄漏。

  2. 获取子进程退出信息:父进程需要知道子进程的执行结果(成功、失败、异常)。

  3. 同步父子进程:确保子进程先于父进程完成某些任务。

2.2 僵尸进程的危害

  • 资源泄漏 :僵尸进程的PCB(task_struct)不会被释放。

  • 无法杀死kill -9对僵尸进程无效,因为僵尸进程已经"死亡"。

  • 占用进程号:大量僵尸进程会占用系统进程号资源。

2.3 进程等待函数

2.3.1 wait() 函数

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
  • 功能 :等待任意一个子进程退出。

  • 参数status - 输出参数,存储子进程退出状态。

  • 返回值:成功返回被等待子进程的PID,失败返回-1。

  • 特点 :如果没有任何子进程退出,父进程会阻塞wait()调用处。

2.3.2 waitpid() 函数

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
  • 功能:更灵活的进程等待函数。

  • 参数

    • pid:指定要等待的子进程

      • >0:等待指定PID的子进程

      • -1:等待任意子进程(类似wait

      • 0:等待与调用进程同进程组的任意子进程

      • <-1:等待进程组ID等于|pid|的任意子进程

    • status:输出参数,存储子进程退出状态。

    • options:控制函数行为

      • 0:阻塞等待

      • WNOHANG:非阻塞,立即返回

3. 获取子进程status

  • waitwaitpid,都有一个status参数,该参数是⼀个输出型参数,由操作系统填充。
  • 如果传递NULL,表⽰不关心⼦进程的退出状态信息。
  • 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位):
c 复制代码
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(void)
{
    pid_t pid;
    
    if ((pid = fork()) == -1) {
        perror("fork");
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程
        sleep(20);
        exit(10);
    } else {
        // 父进程
        int st;
        int ret = wait(&st);
        
        if (ret > 0 && (st & 0x7F) == 0) {  // 正常退出
            printf("child exit code: %d\n", (st >> 8) & 0xFF);
        } else if (ret > 0) {               // 异常退出
            printf("sig code: %d\n", st & 0x7F);
        }
    }
    
    return 0;
}

4. 非阻塞等待

4.1 阻塞 vs 非阻塞等待

  • 阻塞等待 :父进程调用waitpid(pid, &status, 0)后,如果子进程未退出,父进程会一直等待(阻塞)。

  • 非阻塞等待 :父进程调用waitpid(pid, &status, WNOHANG)后,立即返回,不会阻塞。

4.2 非阻塞等待的返回值

c 复制代码
pid_t waitpid(pid_t pid, int *status, int options);
// options = WNOHANG 时:
// 返回值 > 0:子进程已退出,返回子进程PID
// 返回值 = 0:子进程未退出,但调用成功返回
// 返回值 = -1:调用失败

4.3 非阻塞等待的应用

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t id = fork();
    
    if (id == 0) {
        // 子进程运行5秒后退出
        printf("子进程开始运行,PID: %d\n", getpid());
        sleep(5);
        exit(0);
    } else if (id > 0) {
        // 父进程非阻塞等待
        int status = 0;
        
        while (1) {
            pid_t ret = waitpid(id, &status, WNOHANG);
            
            if (ret > 0) {
                printf("子进程已退出\n");
                break;
            } else if (ret == 0) {
                // 子进程未退出,父进程可以做其他工作
                printf("父进程等待中,做其他工作...\n");
                sleep(1);
            } else {
                // 错误处理
                perror("waitpid");
                break;
            }
        }
    }
    
    return 0;
}

5. 进程程序替换

5.1 程序替换的概念

  • 定义:将当前进程的代码和数据替换为新的程序的代码和数据。

  • 特点

    1. 不创建新进程:进程PID不变,只是执行新的程序。

    2. 替换成功后:执行新程序的代码,原程序代码不再执行。

    3. exec系列函数:只有失败时才返回,成功则不返回(因为原程序已被替换)。

5.2 exec系列函数

c 复制代码
#include <unistd.h>

// 最基本的形式
int execve(const char *pathname, char *const argv[], char *const envp[]);

// 常用变体(都是基于execve的封装)
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

5.3 函数命名规律

  • l (list):参数以列表 形式传递,可变参数,以NULL结尾。

  • v (vector):参数以数组形式传递。

  • p (path):使用PATH环境变量查找可执行文件。

  • e (environment):可以指定新的环境变量。

5.4 使用示例

5.4.1 execl() - 列表参数,完整路径

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("程序开始\n");
    
    // 执行 ls -l -a 命令
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    
    // 如果execl成功,下面的代码不会执行
    printf("这行不会执行\n");
    
    return 0;
}

5.4.2 execlp() - 列表参数,自动PATH搜索

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("使用execlp执行系统命令\n");
    
    // 示例1: 执行ls命令(自动在PATH中查找)
    execlp("ls", "ls", "-l", "-a", "--color=auto", NULL);
    
    // 示例2: 执行grep命令
    // execlp("grep", "grep", "error", "/var/log/syslog", NULL);
    
    // 示例3: 执行ps命令
    // execlp("ps", "ps", "aux", NULL);
    
    perror("execlp failed");
    return 1;
}

5.4.3 execv() 示例(使用参数数组)

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("使用execv执行命令\n");
    
    // 参数数组方式
    char *argv[] = {
        "ls",           // 程序名(argv[0])
        "-l",
        "-a",
        "--time-style=long-iso",
        NULL           // 必须以NULL结尾
    };
    
    execv("/bin/ls", argv);
    
    perror("execv failed");
    return 1;
}
5.4.4 execle() - 列表参数,指定环境变量
c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("使用execle执行程序,指定环境变量\n");
    
    // 自定义环境变量数组
    char *envp[] = {
        "PATH=/usr/bin:/bin",
        "HOME=/tmp/test_home",
        "USER=custom_user",
        "MYVAR=hello_world",
        NULL  // 必须以NULL结尾
    };
    
    // 执行env命令显示环境变量
    execle("/usr/bin/env", "env", NULL, envp);
    
    perror("execle failed");
    return 1;
}

5.4.5 execvp() - 数组参数,自动PATH搜索

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    printf("使用execvp执行命令\n");
    
    // 示例1: 基本用法
    char *argv1[] = {"ls", "-l", "-a", NULL};
    execvp("ls", argv1);
    
    // 示例2: 执行带复杂参数的命令
    // char *argv2[] = {"find", ".", "-name", "*.c", "-type", "f", NULL};
    // execvp("find", argv2);
    
    // 示例3: 执行shell命令(通过bash解释)
    // char *argv3[] = {"bash", "-c", "echo $PATH && ls -l", NULL};
    // execvp("bash", argv3);
    
    perror("execvp failed");
    return 1;
}

5.4.6 execvpe() - 数组参数,PATH搜索+指定环境

C 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    printf("使用execvpe执行命令\n");
    
    // 参数数组
    char *argv[] = {
        "env",      // 程序名
        NULL        // 参数列表结束
    };
    
    // 自定义环境变量
    char *envp[] = {
        "PATH=/usr/local/bin:/usr/bin:/bin",
        "LANG=en_US.UTF-8",
        "CUSTOM_SETTING=1",
        "DEBUG_MODE=true",
        "HOME=/tmp/my_home",
        NULL        // 环境变量列表结束
    };
    
    // 注意:execvpe不是标准POSIX函数,但Linux glibc支持
    // 如果编译失败,可以使用execve替代
    execvpe("env", argv, envp);
    
    perror("execvpe failed");
    return 1;
}

5.4.7 execve() - 系统调用版本

C 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    printf("使用execve系统调用\n");
    
    // 参数数组
    char *argv[] = {
        "/bin/ls",
        "-l",
        "-a",
        "-i",   // 显示inode号
        NULL
    };
    
    // 环境变量数组
    char *envp[] = {
        "PATH=/bin:/usr/bin",
        "TERM=xterm-256color",
        NULL
    };
    
    // execve是系统调用,其他exec函数都是它的封装
    execve("/bin/ls", argv, envp);
    
    perror("execve failed");
    return 1;
}

5.5 程序替换与fork()结合

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t id = fork();
    
    if (id == 0) {
        // 子进程:执行程序替换
        printf("子进程开始执行新程序\n");
        execl("/bin/ls", "ls", "-l", "-a", NULL);
        
        // 如果execl失败,才会执行下面的代码
        perror("execl failed");
        exit(1);
    } else if (id > 0) {
        // 父进程:等待子进程
        wait(NULL);
        printf("父进程继续执行\n");
    }
    
    return 0;
}

5.6 程序替换的底层原理

  1. 进程结构不变:PID、PPID、文件描述符表等保持不变。

  2. 代码和数据被覆盖:新的程序代码和数据加载到进程地址空间。

  3. 页表重新建立:建立新的虚拟地址到物理地址的映射。

  4. 从新程序的main开始执行:程序计数器指向新程序的入口点。

6. 总结

  1. 进程退出 :通过退出码表示执行结果,exit()刷新缓冲区,_exit()不刷新。

  2. 进程等待:必须等待子进程,防止僵尸进程,获取退出信息。

  3. 状态解析 :使用WIFEXITEDWEXITSTATUS等宏解析子进程状态。

  4. 非阻塞等待 :使用WNOHANG选项,父进程可以继续做其他工作。

  5. 程序替换exec系列函数替换进程代码和数据,不创建新进程。

  6. Shell实现 :通过fork()+exec()执行命令,需要解析命令行参数。

  7. 进程独立性:程序替换不会影响父进程,因为写时拷贝机制。

相关推荐
Linux-lucky1 小时前
33-Linux学习之旅之MySQL用户管理
java·linux·运维·学习·mysql·ubuntu
像风一样的男人@1 小时前
linux --安装openGL,EGL/OSMesa
linux·运维·服务器
ZzzZZzzzZZZzzzz…2 小时前
K8s---组件
运维·云原生·容器·kubernetes·k8s·k8s组件
Maynor9962 小时前
「原子弹爆炸」级别:Astra 复刻游戏合集(含实机截图)
java·linux·运维·数据库·gpt·游戏
陈皮糖..2 小时前
基于 Kubernetes 与 GitLab CI/CD 的云原生自动化交付平台
运维·ci/cd·云原生·架构·kubernetes·自动化·gitlab
CV艺术家2 小时前
openssL生成免费的证书
java·linux·服务器
脚踏实地,坚持不懈!2 小时前
Android 系统工程师(性能/功耗/稳定性)岗位问题深度解析:从内核源码到实战排查(完善版)
android·linux·运维·服务器
0+1112 小时前
Linux --进程信号
linux·运维·服务器
Lethehong2 小时前
服务器越来越多怎么统一监控?Beszel 接入 Agent、SMTP 告警与公网访问
运维·服务器