Linux 进程等待与替换

news2024/11/16 6:34:22

✏️ 代码引入:

#include <stdio.h>
#include <unistd.h> // _exit()要此头文件,使用方法与 exit()类似
#include <stdlib.h> // exit(),要此头文件

// int fun()
//{
//   printf("call fun function done!\n");
//   return 11;
//   // 任意地点调用exit,表示进程退出,不进行后续执行。
//   // exit(21);
// }
//
int main()
{
    printf("you can see me !");
    sleep(3);
    _exit(1);
    //  fun();
    //  printf("i am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    //  // 1. exit(参数), 参数是进程的退出码,类似与 main 函数的return n
    // _exit(12);

    // 1. 在 main 函数中直接进行 return
    // 2. 在其他函数中调用return 表示的是函数调用结束
    // return 21;

    return 0;
}

✏️ _exit函数

#include <unistd.h>  
void _exit(int status);
  • status定义了进程的终止状态,父进程通过wait来获取该值。

✏️ exit函数

exit在调用前会:

  1. 执行用户通过atexiton_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit

示例

#include <unistd.h>
#include <stdio.h>
int main()
{
    printf("hello");
    exit(0);
}

✏️ 关于_exitexit的区别:

  • _exit是系统调用。
  • exit是库函数。
    两者在终止进程时对缓冲区的处理差异:
  • exit被调用终止进程时,会自动刷新缓冲区。
  • _exit被调用终止进程时,不会自动刷新缓冲区。

🏷️ 进程等待

📌 什么是进程等待

通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程

📌 为什么要进行进程等待

a. 解决子进程僵尸带来的内存泄漏问题---- 目前必须要解决
b. 父进程为什么要创建一个子进程呢? ---- 要让子进程来完成任务。
子进程任务完成的怎麽样,父进程要不要知道?要知道,所以我们要通过进程等待的方式来获取子进程退出的信息。----即:获取子进程退出时的那两个数字(1. 信号编号 2. 进程退出码)

✏️ 进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。

  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

📌 如何进行进程等待(重点)

✏️ 进程等待的方法

  • wait方法
  • waitpid方法

📎 wait方法, 先验证 2 个问题

  1. wait是真的能帮我回收子进程,也就是僵尸状态,最后由有一直到无我们要通过wait来回收到让大家看到
  2. 再验证一下wait调用时父进程在调的时候,如果子进程没退出,那么在父进程在干什么?
wait的头文件,返回值

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int*status);

返回值:

成功返回被等待进程pid,失败返回-1。

参数:

输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

程序验证 代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(0);
    }
    else
    {
        // 父进程
        sleep(10);
        pid_t rid = wait(NULL);
        if (rid == id)
        {
            printf("wait success, pid: %d\n", getpid());
        }
    }
    sleep(10);
    return 0;
}

[!NOTE] Liunx
在命令行中,我们使用 ./myprocess,来运行程序,在这里我的可执行的程序名叫:myprocess
然后我们新建一个远程链接,在同样的目录下输入我们的监控指令,监控指令如下:
while :; do ps ajx |head -1 && ps ajx | grep myprocess |grep -v grep; echo '-------------------------------------------------------------------------'; sleep 1; done

我们可以看到如下结果:

在这里插入图片描述

下面我来解释一下:

在这里插入图片描述

在这里插入图片描述

现在我们来回答之前的两个问题:

  1. wait是真的能帮我回收子进程,也就是僵尸状态,最后由有一直到无我们要通过wait来回收到让大家看到

进程等待能够回收子进程僵尸状态,就是系统把子进程的状态由 Z-变成 X, x 状态就可以彻底宣判此进程的资源可以回收了。此时操作系统就会瞬间把资源给回收了,所以我们看不到这个 x 状态,

  1. 再验证一下wait调用时父进程在调的时候,如果子进程没退出,那么在父进程在干什么?

我们来观察一下之前的代码:

#include <stdio.h>
#include <unistd.h> // _exit()要此头文件,使用方法与 exit()类似
#include <stdlib.h> // exit(),要此头文件
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(0);
    }
    else
    {
        // 父进程
        sleep(10); /************************ 看这里 ****************************/
        pid_t rid = wait(NULL);
        if (rid == id)
        {
            printf("wait success, pid: %d\n", getpid());
        }
    }
    sleep(10);
    return 0;
}

之前的时候,我们让我们的父进程sleep10秒, 在父进程 sleep 的时候是我们的子进程在跑,跑了 5 秒。如果我们不要父进程 sleep,那么父进程在运行的时候,就会立马执行到代码里的pid rid = wait(NULL), 立马执行到 了wait , 这个时候问题来了,在子进程执行期间(也就是还没有退出的时候),父进程有没有调用 wait 呢?如果调用了的话,那么在干些什么呢?
为了解决我们上面👆🏻的问题,我们对代码做出如下的修改:

#include <stdio.h>
#include <unistd.h> // _exit()要此头文件,使用方法与 exit()类似
#include <stdlib.h> // exit(),要此头文件
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(0);
    }
    else
    {
        // 父进程
        // sleep(10);  /********************  看这里    ***************************/
        printf("wait before\n"); // 我们在wait 前面加上这个
        pid_t rid = wait(NULL);
        printf("wait after\n");  // 在 wait 后面加上这句 ,我们在观察打印出来的结果
        if (rid == id)
        {
            printf("wait success, pid: %d\n", getpid());
        }
    }
    sleep(10);
    return 0;
}

在这里插入图片描述

上面👆🏻这个例子说明,当我们在执行wait的时候,如果人家子进程没有退出,那么你的等待就要让父进程进行阻塞时等待,即:如果子进程根本没有退出,父进程必须在 wait 上进行阻塞等待,直到子进程僵尸🧟‍♀️, wait 自动回收返回!

同时通过上面的例子🌰,我们也可以知道,一般而言,父子进程谁先运行我们不知道,但是一般都是父进程最后退出。因为父进程要等待子进程死掉了,然后父进程再把这些僵尸进程给回收♻️了,才退出。 所以在多进程代码中,往往是多进程的 多执行流由父进程发起 ,最后由父进程统一回收。

到这里,我们已经成功的解决了[[#📌 为什么要进行进程等待]]这个问题里面的a问题(a. 解决子进程僵尸带来的内存泄漏问题)。
现在我们来解决b 问题:(b. 父进程为什么要创建一个子进程呢?
---- 要让子进程来完成任务。子进程任务完成的怎麽样,父进程要不要知道?要知道,所以我们要通过进程等待的方式来获取子进程退出的信息。----即:获取子进程退出时的那两个数字(1. 信号编号 2. 进程退出码)
为了获取进程退出时的那两个数字(1.信息编号 2. 进程退出码)我们可以使用 waitpid 这个函数来获取

📎 waitpid — 获取退出的信息

函数原型

pid_t waitpid(pid_t pid, int* status, int options);

在这里插入图片描述

返回值:

waitpid 和 wait 的返回值的意思一模一样

  • 如果 >0 正常返回时,返回收集到的子进程的进程ID。
  • 如果调用中出错,则返回-1,并且errno会被设置成相应的值。
参数部分:
  • pid: 指定等待的子进程的进程ID。

    • Pid=-1: 等待任一个子进程,与wait函数等效。
    • Pid>0: 等待其进程ID与pid相等的子进程。
  • status: 用于存储子进程状态信息的指针。

    • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真,用于检查进程是否是正常退出。
    • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。
  • options:

    • WNOHANG: 若指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
  • 当子进程已经退出时,调用waitwaitpid会立即返回,同时释放相关资源,并提供子进程的退出信息。

  • 如果在任意时刻调用waitwaitpid,并且子进程正在正常运行,则调用进程可能会阻塞。

  • 如果尝试调用waitwaitpid的子进程不存在,则调用会立即出错并返回。

我们使用 waitpid 来做两个实验

  • 实验一: 用 waitpid 来替代掉我们之前代码中的 wait 来达到同样的效果
  • 实验二: 用 waitpid 来获取我们想要获得的参数
我们先来看实验 1:

我们之前的代码是这样的:

#include <stdio.h>
#include <unistd.h> // _exit()要此头文件,使用方法与 exit()类似
#include <stdlib.h> // exit(),要此头文件
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(0);
    }
    else
    {
        // 父进程
        // sleep(10); 
        printf("wait before\n");
        pid_t rid = wait(NULL);
        printf("wait after\n");
        if (rid == id)
        {
            printf("wait success, pid: %d\n", getpid());
        }
    }
    sleep(10);
    return 0;
}

我们将其中的wait 全部替换为 waitpid 同时参数部分也应该要做相应的修改:

pid_t rid = wait(NULL);---------> pid_t rid = waitpid(id, NULL, 0);

我们修改之后的代码:

#include <stdio.h>
#include <unistd.h> 
#include <stdlib.h> 
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(0);
    }
    else
    {
        // 父进程
        sleep(10);                // 为了看到更好的效果,我们这里取消注释
        printf("wait before\n");
        pid_t rid = waitpid( id, NULL,0); // 修改之后
        printf("wait after\n");
        if (rid == id)
        {
            printf("wait success, pid: %d\n", getpid());
        }
    }
    sleep(10);
    return 0;
}

观察代码的运行结果,你会发现结果也 同样的符合我们的预期

接下了,我们进行实验二

下面👇🏻我们细细说一下waitpidstatus 参数:

在编程中,参数可以被分为几种类型,其中之一就是“输出型参数”(Output Parameter)。输出型参数是一种特殊的参数,它在函数调用时被传递给函数,并且在函数执行过程中被修改,其修改后的值在函数返回后可以被调用者访问。
status 参数是一个指向整数的指针(int *status),这意味着它是一个输出型参数。当 waitpid 函数被调用时,如果 status 不是 NULL,函数会将子进程的退出状态写入到 status 指针指向的内存位置。这样,调用者就可以在 waitpid 函数返回后检查这个状态,了解子进程是如何终止的。

在这里插入图片描述

好了,在了解 status 之后,我们来修改一下之前的代码:

#include <stdio.h>
#include <unistd.h> 
#include <stdlib.h> 
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(10);     /***************       我们将退出结果修改为 10     **************/
    }
    else
    {
        // 父进程
        //sleep(10);                
        printf("wait before\n");
        int status = 0;     /*************  我们加入这一行,默认设为 0 **************/
        pid_t rid = waitpid( id, &status,0);  // 这里也可以体会一下 status 输出型参数的用法,
										      // 把子进程退出时的退出信息通过 status 给我们返回。
        printf("wait after\n");              
        if (rid == id)
        { 
            printf("wait success, pid: %d, status: %d\n", getpid(), status);
        }
    }
    sleep(10);
    return 0;
}

我们在 Linux 命令行中使用命令来运行上面的代码之后,会看到这样的结果:

在这里插入图片描述

这个时候,我们就有一个疑问❓,我们不是设置了exit(10)吗?那为什么最后打印的status 不是 10 而是 2560?

为了理解这个问题,我们要谈一谈下一个话题:status 的一个构成问题。

在 Linux 系统中我们的waitpid 的第二个参数,即:status 是一个 int 类型的数 ,但是这里的整数并不是整存整取的,我们知道一个整数有 32 个 bit, 这里是按照我们将一个整数进行区域划分 ,从不同的 比特位区域来表示不同的含义的,而我们在后续使用这个status的时候,只考虑 status整数的低 16 位,(从左向右一共 32 个 bit,我们只需要最右侧的那 16 个 bit 就可以了)

在这里插入图片描述

现在我们来解释为什么打印的的时候status = 2560 。(记住:我的代码中子进程的退出码是设置的 10,即:exit(10) )

在这里插入图片描述

下面👇🏻我们要做的就是根据刚才的分析内容,来得到我们要的退出信息,所以我们要修改一下我们的代码:


#include <stdio.h>
#include <unistd.h> 
#include <stdlib.h> 
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(10);   
    }
    else
    {
        // 父进程
        //sleep(10);                
        printf("wait before\n");
        int status = 0;     
        pid_t rid = waitpid( id, &status,0);  
        printf("wait after\n");              
        if (rid == id)
        { 
            printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);
        }
    }
    sleep(10);
    return 0;
}

修改的部分:

printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);
rpid: %d ,rid     // 我们可以理解:waitpid 对应的返回值。
exit sig: %d, status&0x7F  
// 表示的是退出时收到的信号
//它是位于低 7 位的,所以当我们按位与上(&):0x7F使用按位与运算来确保只获取 `status` 变量的最低7位,通过与 `0x7F`(二进制 `0111 1111`)进行按位与运算实现。这样,任何高于第7位的位都会被置为0,而低于或等于第7位的位则保持不变。而后 7 位就是我们想要的退出信号了

exit code: %d, (status >> 8)&0xFF  
// 表示的是退出时收到的退出码
//我们这里是先给它右移动 8 位,然后在来按位与的操作,这样就可以得到我们的退出码了。
🧲不会进制转化的铁汁:

将十六进制数 0x7F 转换为二进制的过程相对简单。每个十六进制位可以直接转换为一个四位的二进制序列,因为十六进制是基于16的,而二进制是基于2的,十六进制的每一位可以表示二进制的四位。

下面是转换过程:

  • 十六进制的 7 转换为二进制是 0111
  • 十六进制的 F 对应于十进制的15,转换为二进制是 1111

因此,十六进制的 0x7F 转换为二进制是:

0x7F = 0111 1111

运行上面的代码之后,我们就可以在打印的结果中看到退出信息:exit sig,退出码:exit code

我们为了看到打印的退出码和退出信息,我们可以故意将我们的代码写错,特意的制造一些错误:比如:
我们可以对空指针解引用,当我们特意这么做时,我们可以看到打印的结果如下:

在这里插入图片描述

根据上面的现象,我有几个问题:

当一个进程异常了(即:收到了信号),退出码(exit code ), 还有意义吗?

答:当一个进程出异常了它的退出码是没有意义的,来看我们下面这张图:

在这里插入图片描述

在这里插入图片描述

所以当一个进程异常的时候,它对应的退出码没有被使用,可能就是一些随机值之类的。

那我们怎么判定有没有收到信号?

我们可以通过 kill -l 来查看我们的信号列表:

在这里插入图片描述

我们可以知道上面👆🏻的信号列表中是没有 0 号信号的,所以我们只用知道 exit sig 是不是 0 来判断有没有收到信号。

这样也恰如我们之前所说:

exit sig : 0, exit code : 0; // 表示的是,代码跑完,结果正确
exit sig : 0, exit code : (非 0); // 表示的是,代码跑完,结果不正确
exit sig : (非 0), exit code : (随机数); // 表示的是,出异常了。

🏷️知识拓展

📌 父进程是如何得知子进程的退出信息的?

调用 wait waitpid 这样的系统调用,在操作系统层面是怎么做的。

在这里插入图片描述

🧲 忘了进程的状态的老铁,看这里简单了解一下:

在Linux系统中,进程的状态被详细划分为多种,以便操作系统能够有效地管理和调度进程。这些状态反映了进程在其生命周期内的不同执行阶段和等待条件。以下是Linux系统中常见的进程状态及其解释:

1. 运行状态(Running,R)
  • 进程正在CPU上执行,或者已经准备好执行并等待CPU时间片。在Linux中,这个状态通常被称为TASK_RUNNING
  • 当多个进程处于这个状态时,它们会被放入CPU的可执行队列中,等待调度器分配时间片。
2. 可中断的睡眠状态(Sleeping,S)
  • 进程因为等待某些事件(如I/O操作完成、信号等)而被阻塞。这种状态下,进程可以被信号或中断唤醒。
  • 在Linux中,这个状态通常与TASK_INTERRUPTIBLE相对应。当等待的事件发生时,进程会被唤醒并重新进入就绪状态。
3. 不可中断的睡眠状态(Disk Sleep,D)
  • 进程正在等待某些IO操作完成,且不能被任何信号中断。这种状态通常出现在进程对硬件设备进行IO操作时,需要保证操作的原子性。
  • 在Linux中,这个状态称为TASK_UNINTERRUPTIBLE。由于这种状态下进程无法被中断,因此通常只在非常关键的IO操作中使用。
4. 暂停状态(Stopped,T)
  • 进程被暂停执行,通常是因为收到了SIGSTOP、SIGTSTP等信号。在这种状态下,进程不会占用CPU资源,也不会响应任何信号,直到收到SIGCONT信号才能恢复执行。
  • 在Linux中,这个状态可能对应TASK_STOPPEDTASK_TRACED,后者表示进程正在被调试器跟踪。
5. 僵尸状态(Zombie,Z)
  • 进程已经执行完毕,但其父进程尚未通过wait()或waitpid()等系统调用回收其资源。在这种状态下,进程占用的PCB(进程控制块)仍然存在,但已经不再执行任何代码。
  • 僵尸状态是进程退出过程中的一个过渡阶段,用于保存进程的退出状态等信息,以便父进程查询。
6. 追踪状态(Tracing Stop,t)
  • 进程正在被调试器跟踪,并处于暂停状态。这种状态与暂停状态类似,但通常是在调试过程中由调试器主动设置的。
7. 死亡状态(Dead,X)
  • 严格来说,死亡状态并不是Linux进程的一个独立状态,因为一旦进程达到这个状态,其资源就已经被回收,进程本身也就不复存在。但是,在描述进程生命周期时,有时会提到这个状态作为进程结束的标志。

我们上面图中的系统调用的工作:

  • 将我们的进程状态由 Z -> X
  • 将退出码和退出信息组合起来传递给我们传入的变量:status。
    • 具体过程就是:*statusp = ( exit_code << 8 ) | exit_siganl

上面获取 exit_code 和 exit_siganl 我们都使用了 位操作 ,但是 Linux 也做了一个封装,让我们可以不用位操作来获取进程的退出码和退出信息。如下👇🏻:

status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WIFEXITED,这个宏对 stauts 中的信号值做检测,如果进程是正常终止的这个条件就为真

WEXITSTATUS(status) : 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

如果进程是正常退出的。 WEXITSTATUS,这个宏可以提取进程的退出码

好,我们现在将上面的内容加入我们的代码之中,我们可以这样写:

if(WIFEXITED(status)) //注意哈,这里的拼写和下面 👇🏻printf里的那个拼写是不一样的哈。
{
	printf("child process normal quit, exit code: %d\n", WEXITSTATUS(status));
} else {
	printf("child process quit except!\n");
}

完整代码是这样的:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void Worker()
{
    int cnt = 5;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d\n", getpid(), getppid(), cnt--);
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        Worker();
        exit(1);
    }
    else
    {
        printf("wait before\n");
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        printf("wait after\n");
        if (rid == id)
        {
            if (WIFEXITED(status)) // 注意哈,这里的拼写和下面 👇🏻printf里的那个拼写是不一样的哈。
            {
                printf("child process normal quit, exit code: %d\n", WEXITSTATUS(status));
            }
            else
            {
                printf("child process quit except!\n");
            }
        }
    }
    return 0;
}

📌 等待多个进程

我们之前的例子🌰都只是 fork()了一个子进程,现在我们来创建多个子进程看父进程是如何等待的。

我们首先要修改一下我们的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void Worker(int number)
{
    int cnt = 10;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d, number: %d\n", getpid(), getppid(), cnt--, number);
    }
    sleep(1);
}

const int n = 10;

int main()
{
    for (int i = 0; i < n; i++)
    {
        pid_t id = fork();
        if (id == 0) // 如果 id=0,说明它是一个子进程
        {
            Worker(i);
            exit(i);
        }
    }

    // 如何等待多个子进程?
    for (int i = 0; i < n; i++)
    {
        int status = 0;
        pid_t rid = waitpid(-1, &status, 0);
        // 如果``pid > 0``,代表的是指定等待的那个id的进程。
        // 如果是 ``-1`` 代表的是任意一个退出的子进程
        if (rid > 0)
        {
            printf("wait child process : %d success, exit_code: %d\n", rid, WEXITSTATUS(status));
        }
    }
    return 0;
}

📌 waitpid 的第 3 个参数。

问题:
  • 我们为什么不用全局变量来获取子进程的退出信息?而用系统调用??

在我们的代码里,你看看:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int status = 0; // 我们为什么不在这里定义一个全局变量

void Worker(int number)
{
    int cnt = 10;
    while (cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt= %d, number: %d\n", getpid(), getppid(), cnt--, number);
    }
    sleep(1);
}

const int n = 10;

int main()
{
    for (int i = 0; i < n; i++)
    {
        pid_t id = fork();
        if (id == 0) 
        {
            Worker(i);
			status = i; // 我们在加入这样一行 
            exit(0);
        }
    }

    for (int i = 0; i < n; i++)
    {
        int status = 0;
        pid_t rid = waitpid(-1, &status, 0);
        if (rid > 0)
        {
            printf("wait child process : %d success, exit_code: %d\n", rid, WEXITSTATUS(status));
        }
    }
    return 0;
}

你所定义的全局变量,是父进程开始有的。然后当你在创建子进程的时候,你如果要子进程退出时将子进程的状态写到这个全局变量里,此时你写入的这个值父进程是读不到的。父进程为什么读不到?因为从操作系统的层面上在多进程当中,当对同一个变量进行写入操作时,会发生写时拷贝。因为进程具有独立性,所以不要觉得父子进程写在一份代码里,我们就能获取子进程的数据了 。因为进程具有独立性所以父进程没办法拿到子进程的数据,所以我们是要通过系统调用让操作系统帮我去拿。

在这里插入图片描述

❓什么是非阻塞?

如何来理解呢?

🍖阻塞:
想象一下,你在一家餐厅里,服务员问你需要什么。如果你选择“阻塞等待”,就像是你坐在餐桌旁,什么都不做,只等着服务员给你上菜。在这期间,你不能做其他任何事情,比如玩手机或者和朋友们聊天,因为你的注意力完全集中在等待上菜上。

🍖非阻塞:
而“非阻塞等待”则像是你可以告诉服务员,你不需要他们立即上菜。这样,你就可以继续做其他事情,比如和朋友聊天或者玩手机。当你的菜准备好了,服务员会过来告诉你,这时候你再开始吃饭。在等待的过程中,你没有被“卡住”,而是可以做其他事情。

在编程中,非阻塞操作就是让计算机程序在等待某个任务完成(比如从网络下载文件)时,不必停下来不做其他事情。程序可以继续执行其他代码,直到那个任务完成并通知程序。这样,程序就可以更有效地利用时间,不会在等待的时候“闲置”。

所以当我们把最后一个参数(options)设置为 WNOHANG

我们的waitpid 就会有三种返回值:

  1. rid > 0
  2. rid == 0
  3. rid < 0

(1). rid > 0 : 代表等待成功
rid < 0 : 代表等待失败

(2). rid == 0 : 代表的是等待是成功的,但是对方还没有退出。

非阻塞轮询等待:
#include <stdio.h>
#include <unistd.h>    // fork()  要用此头文件
#include <stdlib.h>    // exit() 要用此头文件
#include <sys/types.h> // waitpid 要用此头文件
#include <sys/wait.h>  // waitpid 要用此头文件

void Worker(int cnt)
{
    printf("i am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
}

int main()
{
    pid_t id = fork(); // 创建子进程
    if (id == 0)
    {
        // 这里是子进程 child
        int cnt = 5;
        while (cnt)
        {
            Worker(cnt);
            sleep(2);
            cnt--;
        }

        exit(0);
    }
    else
    {
        while (1) // 由于我们使用的参数是:WNOHONG,非阻塞等待,所以我们的父进程要轮询
        {
            // 这里是父进程 father
            int status = 0;
            pid_t rid = waitpid(id, &status, WNOHANG);
            if (rid > 0)
            {
                // 等待成功,子进程退出
                printf("child quit scuess, exit code : %d, exit signal: %d\n", (status >> 8) & 0xFF, status & 0x7F);
                break;
            }
            else if (rid == 0)
            {
                // 等待成功但是子进程还没有退出,父进程可以去完成其他的事
                printf("child is alive, wait again, father do other thing....\n");
            }
            else if (rid < 0)
            {
                // 等待失败,不知道子进程啥状态
                printf("wait failed!\n");
                break;
            }
            sleep(1);
        }
    }
    return 0;
}

🏷️ 本章图集

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2105697.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3 html5之css新选择器和属性

要说css的变化那是发展比较快的&#xff0c;新增的选择器也很多&#xff0c;而且还有很多都是比较实用的。这里举出一些案例&#xff0c;看看你平时都是否用过。 1 新增的一些写法&#xff1a; 1.1 导入css 这个是非常好的一个变化。这样可以让我们将css拆分成公共部分或者多…

BrushNet重绘电商商品背景效果测试

&#x1f3a8;背景 之前写过一篇文章&#xff0c;简单的介绍了brushnet这个局部重绘节点&#xff0c;如何安装和使用可以参考我之前写的这篇文章&#xff0c;本篇重点测试下在背景生成这部分&#xff0c;brushnet是不是跟默认的inpaint有比较大的效果提升。 上一篇节点介绍内…

R语言数据统计分析与ggplot2高级绘图

R语言在数据统计分析领域具有广泛的应用&#xff0c;它提供了丰富的函数和扩展包&#xff0c;使得数据处理、分析和可视化变得高效而直观。 R语言特别适合进行描述性统计分析&#xff0c;这得益于其内置的多种函数和方法。例如&#xff0c;使用summary()函数可以轻松获取数据的…

【2024最新】Adobe Lightroom Classic安装教程(直接使用)

给大家分享一个Adobe Lightroom Classic的安装教程&#xff0c;下载链接在文章末尾&#xff0c;直接可用 介绍 Adobe Lightroom Classic 是一款专业的照片编辑和管理软件&#xff0c;专为摄影师和影像爱好者设计。它提供了一套全面的工具集&#xff0c;用于组织、编辑和分享照…

全网最适合入门的面向对象编程教程:45 Python实现常见数据结构-链表、树、哈希表、图和堆

全网最适合入门的面向对象编程教程&#xff1a;45 Python 实现常见数据结构-链表、树、哈希表、图和堆 摘要&#xff1a; 数据结构是计算机科学中的一种组织和存储数据的方式&#xff0c;它决定了数据的访问方式和操作效率&#xff0c;数据结构的选择和实现对程序的性能和设计…

Microk8s ingress启动失败, 10254端口被占用问题定位

问题描述 RHEL9 VM里安装了Microk8s&#xff0c;且使用了Nginx ingress Controller插件&#xff0c;443端口正常。 VM重启一次后&#xff0c;发现443端口没有LISTEN&#xff0c;不能对外提供服务。 定位过程 查看ingress pod状态&#xff0c;为CrashLoopBackOff # kubectl …

【Python篇】PyQt5 超详细入门级教程(中篇一)

文章目录 PyQt5入门级超详细教程中篇&#xff1a;信号槽机制与表格数据展示第4部分&#xff1a;事件处理与信号槽机制4.1 什么是信号与槽&#xff1f;4.2 信号与槽的基本用法4.3 信号与槽的基础示例代码详解&#xff1a; 4.4 处理不同的信号代码详解&#xff1a; 4.5 自定义信号…

【软考】设计模式之代理模式

目录 1. 说明2. 应用场景3. 结构图4. 构成5. 适用性6. 优点7. 缺点8. java示例 1. 说明 1.代理模式&#xff08;Proxy Pattern&#xff09;。2.意图&#xff1a;为其他对象提供一种代理以控制对这个对象的访问。3.通过提供与对象相同的接口来控制对这个对象的访问。4.是设计模…

kali——wpscan的使用

目录 前言 查看帮助&#xff08;-h&#xff09; ​编辑 常规扫描&#xff08;--url&#xff09; 破解用户名和密码 插件枚举 扫描插件漏洞 扫描主题漏洞 前言 wpscan 是 Kali Linux 自带工具&#xff0c;主要用于扫描WordPress网站的各种安全漏洞&#xff0c;包括Word…

深度学习TensorFlow框架

深度学习介绍 深度学习和机器学习区别 机器有人工参与&#xff0c;而深度学习是靠网络&#xff1b; 深度学习需要大量的数据集&#xff0c;训练神经网络需要大量的算力 机器学习有&#xff1a;朴素贝叶斯&#xff0c;决策树等 深度学习主要是神经网络 深度学习应用场景 CV&…

[数据集][目标检测]轮胎缺陷检测数据集VOC+YOLO格式2154张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2154 标注数量(xml文件个数)&#xff1a;2154 标注数量(txt文件个数)&#xff1a;2154 标注…

【爬虫软件】小红薯评论区采集工具

一、采集目标与应用场景 您好&#xff01;我利用Python技术自主研发了一款高效的爬虫软件&#xff0c;批量收集小红薯平台上的评论&#xff0c;包括主评论及其下的二级评论。 为了拓宽用户群体&#xff0c;让不具备编程基础的小白用户也能轻松上手&#xff0c;我开发成了界面…

Burp Suite Professional 2024.8 for macOS x64 ARM64 - 领先的 Web 渗透测试软件

Burp Suite Professional 2024.8 for macOS x64 & ARM64 - 领先的 Web 渗透测试软件 世界排名第一的 Web 渗透测试工具包 请访问原文链接&#xff1a;https://sysin.org/blog/burp-suite-pro-mac/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页…

设计模式-装饰器代理观察者

3.7 装饰器模式&#xff08;代码见vs&#xff09; 装饰器又叫做包装模式&#xff0c;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其结构。这种模式创建了一个装饰类&#xff0c;用来包装原有的类&#xff0c;并在保持类方法完整性的前提下&#xff0c;提供了额…

基于Android Studio的行程记录APK开发指南(二):熟悉一个项目结构

前言 最近博主在unity开发独立游戏&#xff0c;UE5系列的相关长期教程先暂时不更新了,请大家多多谅解本系列教程我们来看看如何使用Android Studio去开发一个APK用于用户的实时行程记录 第一期&#xff1a;基于Android Studio的用户行程记录APK开发指南(一)&#xff1a;项目基…

CTF---密码学知识点总结

✨Ascall编码&#xff1a;在 ctf 比赛中&#xff0c;flag 的标志一般是以 Ascall 码的形式存在&#xff0c;其对应的码值为102&#xff0c;108&#xff0c;97&#xff0c;103&#xff08;其中{的码值是123&#xff09;&#xff01; ✨Unicode编码&#xff1a;又名万国码&#…

OpenHarmony持久化存储UI状态:PersistentStorage

前两个小节介绍的LocalStorage和AppStorage都是运行时的内存&#xff0c;但是在应用退出再次启动后&#xff0c;依然能保存选定的结果&#xff0c;是应用开发中十分常见的现象&#xff0c;这就需要用到PersistentStorage。 PersistentStorage是应用程序中的可选单例对象。此对…

海外云服务器安装 MariaDB10.6.X (Ubuntu 18.04 记录篇二)

本文首发于 秋码记录 MariaDB 的由来&#xff08;历史&#xff09; 谈起新秀MariaDB&#xff0c;或许很多人都会感到陌生吧&#xff0c;但若聊起享誉开源界、业界知名的关系型数据库——Mysql&#xff0c;想必混迹于互联网的人们&#xff08;coder&#xff09;无不知晓。 其…

C++中protobuffer的具体使用方法以及重要原理的实现

一、protobuffer的具体使用 对于基本的知识可以看我之前的文章。 那一片文章主要是知识点&#xff0c;这一片是实战。 1、头部 我们通过syntax 这个来指定版本号&#xff0c;如果不写的话就会默认为proto2&#xff0c;2这个版本是一个比较旧的版本。旧的版本写起来就比较繁琐。…

地平线Sparse4D论文解析(含论文原文)

0. 摘要 在自动驾驶感知系统中&#xff0c;3D 检测和跟踪是两个基本任务。本文深入研究了这一领域&#xff0c;并在 Sparse4D 框架的基础上进行了扩展。我们引入了两个辅助训练任务&#xff08;时间实例去噪和质量估计&#xff09;&#xff0c;并提出了解耦注意力机制&#xf…