• 【Linux进程】进程等待 与 进程替换 原理与函数使用


    一、进程等待

    1.1 意义 / 必要性

    为什么要有进程等待? 我们知道,当子进程退出时,如果父进程不对其进行操作,子进程会变为僵尸进程。

    而且当子进程结束时,我们如何知道子进程的最后执行情况呢?

    父进程通过进程等待,可以:

    1. 回收子进程资源
    2. 获取子进程退出信息
    3. 同步操作

    1.2 进程等待的函数(wait / waitpid)

    wait函数

    pid_t wait(int *status);
    
    • 1
    • 该函数会暂停当前进程的执行,直到一个子进程结束。
    • 如果子进程已经结束,该函数会立即返回。
    • 函数返回值:执行成功 返回被等待进程pid,失败返回-1。
    • status参数:获取子进程的退出状态,不需要则设为null。

    waitpid函数

    pid_t waitpid(pid_t pid, int *status, int options);
    
    • 1
    • 参数:

      • pid
        pid = -1,表示等待任意子进程
        pid = 0,等待进程id=pid 的子进程
      • status
        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
      • option
        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
    • 该函数会暂停当前进程的执行,直到指定的子进程结束。

    • 返回值是结束子进程的进程ID,或者出错时的返回值。

    主要的区别和用法总结如下:

    • wait函数只能等待任意子进程结束,而waitpid函数可以指定具体的子进程进行等待
    • waitpid函数提供了更多的选项,可以在等待子进程时指定不同的行为。
    • waitpid函数可以通过设置options参数来控制等待的方式,例如非阻塞、只等待指定状态等。
    • 在多进程的场景中,可以使用waitpid函数更灵活地管理子进程的状态。

    1.3 status参数

    • waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
    • 如果 传递NULL,表示不关心子进程的退出状态信息
    • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

    status是int型的整数,用于存储子进程的退出状态信息。其低7位(0-6位)第7位(第7位),其他位(8位及以上),分别存放不同信息。

    进程正常终止情况下

    • 低八位(0-7位) 存储的是进程的退出状态。这个状态是由exit系统调用或者C语言中的exit函数传递给操作系统的。
    • 第八位:存储是否生成了core dump文件,如果生成了core dump文件,则该位会被置为1。
    • 高八位(16-31位) 通常被设置为0,用于标识进程的终止状态。

    被信号终止情况下

    • 低八位 :存储的是导致进程终止的信号编号。当进程被信号终止时,状态的低八位会存储信号的编号,例如SIGSEGV表示段错误,SIGKILL表示进程被强制终止等。
    • 第8位 :存储是否生成了core dump文件,如果生成了core dump文件,则该位会被置为1。
    • 高八位 :通常被设置为0,用于标识进程的终止状态。

    我们用位图来解释status

    在这里插入图片描述

    1.4 获取子进程status

    我们通过下面一段代码 获取子进程status

    // 程序获取进程status
    int main() {
        pid_t id = fork();
    
        if (id == -1) {
            // 创建子进程失败
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (id == 0) {
            // 子进程
            printf("子进程正在运行\n");
            exit(10);  // 子进程退出状态设为 10
        } else {
            // 父进程
            int status;
    
            pid_t iid = waitpid(id, &status, 0);
            printf("After the wait function\n");
            if(iid == id)
            {
                printf("wait success, pid: %d, status: %d\n", iid, status);
            }
            sleep(10);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    代码执行结果如下

    在这里插入图片描述

    为什么我们给出退出码exit(10)而最后status值为2560?

    根据上面status的存储分析:如果子进程正常退出,status 的值将会是子进程退出码的左移 8 位。

    即前八位存储的是退出状态, 0000 1010 0000 0000,当我们返回其十进制后,即为2560。

    1.5 进程的阻塞等待与非阻塞等待

    意义

    进程的阻塞等待和非阻塞等待是指在等待某个事件完成时,进程所采取的不同等待方式。

    1. 阻塞等待(Blocking Wait)

    • 当一个进程进行阻塞等待时,它会暂时停止执行,并将控制权交给操作系统。进程将进入睡眠状态,直到所等待的事件发生或满足某个条件。
    • 在阻塞等待期间,操作系统会将进程从可运行状态转换为阻塞状态,并在事件发生后将进程重新唤醒。
    • 阻塞等待是一种同步等待方式,即进程会一直等待直到事件完成或超时。

    2. 非阻塞等待(Non-blocking Wait)

    • 当一个进程进行非阻塞等待时,它会继续执行其他任务,而不会暂停等待。进程会周期性地检查所等待的事件是否已经发生或满足某个条件。
    • 如果事件已经发生,则进程可以立即处理该事件并继续执行。如果事件还未发生,则进程可能会继续轮询等待,或者进行其他操作。
    • 非阻塞等待是一种异步等待方式,即进程可以同时进行其他任务,而不必一直等待。

    代码实现

    1.5.1 进程阻塞等待

    下面是代码实现:

    int main()
    {
        pid_t id = fork();
    
        if(id < 0) {
            // fork error
            perror("fork error");
            exit(EXIT_FAILURE);
        }
        else if(id == 0) {
            // 子进程
            std::cout << "子进程正在运行, pid: " << getpid() << std::endl;
            exit(10);
        }
        else{
            // 父进程
            int status;
            pid_t rid = waitpid(id, &status, 0); // 阻塞等待,options设为0
            std::cout << "after wait" <<std::endl;
            if(rid == id && WIFEXITED(status))
            {
                std::cout << "wait success, pid: " << rid << ", status: " << status << std::endl;
            }
            else
            {
                std::cout << "子进程等待失败, return." << std::endl;
                return 1;
            }
            sleep(10);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    代码执行结果如下

    在这里插入图片描述

    1.5.2 进程非阻塞等待

    代码实现如下:

    int main()
    {
        pid_t id = fork();
        if(id < 0){
            // error
            perror("fork erorr");
            exit(EXIT_FAILURE);
        }else if(id == 0){
            printf("子进程正在运行,pid:%d \n", getpid());
            exit(10);
        }else{
            int status;
    
            pid_t rid;
            do{ // 有子进程退出时 rid返回其pid
                pid_t rid = waitpid(id, &status, WNOHANG);
                if(rid == 0)
                {
                    printf("child process is running\n");
                }
                sleep(5);
            }while(rid == 0);
            
            if(rid == id && WIFEXITED(status))
            {
                printf("wait success, child return code is :%d.\n",WEXITSTATUS(status));
            }else{
                printf("wait child failed, return.\n");
                return 1;
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    代码执行结果

    在这里插入图片描述


    二、进程替换

    2.1 引言

    首先,我们看下面一段代码:

    #include 
    #include 
    
    int main() {
        std::cout << "This is the original program." << std::endl;
    
        // 执行程序替换,将当前进程替换为/bin/ls(列出当前目录内容)
        char *args[] = {"/bin/ls", "-l", NULL};
        execv("/bin/ls", args);
    
        // 如果execv执行成功,下面的代码不会被执行
        std::cerr << "Failed to execute /bin/ls!" << std::endl;
        return 1;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    代码的执行结果如下:

    我们可以看到,程序运行后,执行了 ls -l 操作,而 Failed to execute /bin/ls! 并没有被打印到下面。

    而ls本身也是一个程序,可以理解为,进程 被替换为了 ls 程序

    2.2 进程替换原理

    根据上面的图,我们分析进程替换原理,进程替换可以理解为下面的步骤:

    1. 加载新程序映像:操作系统会从磁盘上读取新程序的可执行文件,并将其加载到内存中。
    2. 创建新的页表:在加载新程序映像时,操作系统会创建一个新的页表来管理新程序所需的内存空间。
    3. 替换进程内容:一旦新程序映像加载到内存中,操作系统 会将当前进程的内容替换为新程序的内容(代码、数据和堆栈等)
    4. 开始执行新程序:替换完成后,控制权转移到新程序的入口点,新程序从那里开始执行

    2.3 替换函数

    像上面代码中所使用的execv函数,叫做程序替换函数:

    下面列举这些程序替换函数,定义如下:

    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execve(const char *filename, char *const argv[], char *const envp[]);
    
    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[]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这些函数都是程序替换函数,它们的区别在于传递参数的方式和环境变量的处理:

    1. execv:
    • 参数:接受一个参数数组 argv,以及程序路径 path。
    • 示例:execv("/bin/ls", argv);
    1. execvp:
    • 参数:与execv 类似,但是可以通过环境变量 PATH 搜索可执行文件。
    • 示例:execvp("ls", argv);
    1. execve:
    • 参数:接受一个参数数组 argv,一个环境变量数组 envp,以及程序路径 filename。
    • 示例:execve("/bin/ls", argv, envp);
    1. execl:
    • 参数:接受多个参数,最后一个参数必须是 NULL,不接受参数数组,需要将每个参数单独列出。
    • 示例:execl("/bin/ls", "ls", "-l", NULL);
    1. execlp:
    • 参数:与execl 类似,但是可以通过环境变量 PATH 搜索可执行文件。
    • 示例:execlp("ls", "ls", "-l", NULL);
    1. execle:
    • 参数:与execl 类似,但是可以传递自定义的环境变量数组 envp。
    • 示例:execle("/bin/ls", "ls", "-l", NULL, envp);
  • 相关阅读:
    项目经理年终夜话:我的“第二年状态”
    137.如何进行离线计算-3
    Runable和Callable的区别?首先要搞清楚Thread以及FutureTask!
    第一章Hadoop概述
    【算法】复习搜索与图论
    Spring整合mqtt原理分析
    我也惊呆了!原来软件开发根本不需要会编码
    【Sharding-JDBC】分库分表实际应用
    java计算机毕业设计智能办公管理系统源程序+mysql+系统+lw文档+远程调试
    中兴面试-Java开发
  • 原文地址:https://blog.csdn.net/Dreaming_TI/article/details/134435624