Linux 之 【进程等待】

目录

1.进程等待的概念

2.进程等待必要性

3.进程退出方法

3.1wait

单子进程回收

多子进程回收

3.2waitpid

解读status

进程等待失败原因

解读option


1.进程等待的概念

通过系统调用wait/waitpid,来对子进程进行状态检测与回收的功能

2.进程等待必要性

  • 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏
  • 另外,进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成结果对还是不对,或者是否正常退出。(可选)
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.进程退出方法

3.1wait

|----------|--------------------------------------------|
| 函数原型 | pid_t wait(int *wstatus); |
| 头文件 | <sys/types.h> <sys/wait.h> |
| 功能 | 等待任意子进程终止或停止,并回收其资源 |
| 参数 | wstatus - 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL |
| 返回值 | 成功 :返回终止的子进程PID 失败:返回-1,设置errno |
| 阻塞行为 | 如果没有子进程已终止,则阻塞调用进程 |
| 作用 | 防止产生僵尸进程(Zombie Process) |

单子进程回收

复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 #include<errno.h>
  7 #include<string.h>
  8 
  9 int main()
 10 {
 11     //验证wait函数回收僵尸进程
 12     //创建进程 
 13     pid_t id = fork();
 14     if(id == 0)
 15     {
 16         //子进程运行两秒后先退出
 17         int cnt = 2;
 18         while(cnt--)
 19         {
 20             printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid(), getppid()    , cnt);
 21             sleep(1);
 22         }
 23         exit(0);//子进程不运行后续代码
 24     }
 25     else if(id > 0)
 26     {
 27         //父进程运行5秒后退出                                                             
 28         int cnt = 5;
 29         while(cnt--)
 30         {
 31             printf("I am a parent, my PID: %d, my PPID: %d, cnt:%d\n", getpid(), getppid(    ), cnt);
 32             sleep(1);
 33         }
 34         pid_t ret = wait(NULL);//暂时不关心子进程的退出状态,将输出型参数置空
 35         if(ret < 0)                                                                       
 36         {
 37             printf("等待子进程失败,错误码: %d, 错误描述:%s\n", errno, strerror(errno));
 38             sleep(2);//不立即退出
 39             exit(errno);
 40         }
 41         else 
 42         {
 43             printf("等待子进程成功,子进程PID:%d\n", ret);
 44             sleep(2);//不立即退出
 45             exit(0);
 46         }
 47     }
 48     else 
 49     {
 50         //出错,暂时不管
 51     }
 63     return 0;
 64 }

多子进程回收

复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 #include<errno.h>
  7 #include<string.h>
  8 
  9 #define N 5
 10 
 11 void childRun()
 12 {
 13     int cnt = 3;
 14     while(cnt--)
 15     {
 16             printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid    (), getppid(), cnt);
 17             sleep(1);
 18     }
 19 }
 20 
 21 int main()
 22 {
 23     //验证wait函数回收僵尸进程
 24     //多子进程回收
 25     //瞬间创建 N 个子进程
 26     for(int i = 0; i < N; ++i)
 27     {                                                                        
 28         pid_t id = fork();
 29         if(id == 0)//子进程运行三秒就退出
 30         {
 31             childRun();
 32             exit(i);//退出码为0~N
 33         }
 34     }
 35     //父进程运行5秒再回收子进程
 36     int cnt = 5;
 37     while(cnt--)
 38     {
 39             printf("I am a parent, my PID: %d, my PPID: %d, cnt: %d\n", getpi    d(), getppid(), cnt);
 40             sleep(1);
 41     }
 42     //批量回收子进程
 43     for(int i = 0; i < N; ++i)
 44     {
 45         pid_t ret = wait(NULL);//暂时不关心退出状态
 46         if(ret < 0)
 47         {
 48             printf("等待子进程失败,错误码:%d,错误描述:%s\n", errno, strer    ror(errno));
 49             exit(errno);
 50         }
 51         else 
 52         {
 53             printf("等待子进程成功,子进程PID:%d\n", ret);
 54         }
 55     }
 56     sleep(3);//回收完子进程之后等待3秒再退出
121     return 0;
122 }

父进程瞬间创建N个子进程,3秒后所有子进程都退出,此时父进程还没对他们进行回收。

所有子进程成为僵尸进程,2秒后父进程回收所有子进程,3秒后父进程也退出了

  • 值得注意的是,wait等待的是任意一个终止的子进程
  • 当子进程未退出而父进程调用wait函数等待子进程时,父进程会处于阻塞等待状态,直到子进程退出,wait函数回收子进程资源完毕;
  • 阻塞等待:内核将父进程加入自身的等待队列中,当子进程退出时,内核唤醒等待队列中的父进程,父进程随后回收子进程资源并继续执行

3.2waitpid

|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 函数原型 | pid_t waitpid(pid_t pid, int *status, int options); |
| 头文件 | #include <sys/types.h> #include <sys/wait.h> |
| 功能 | 等待指定子进程的状态变化,并获取其状态信息 |
| 参数 pid | - >0:等待进程ID为pid的特定子进程 - -1:等待任意子进程(等效于wait) - 0:等待与调用进程同一进程组的任意子进程 - <-1:等待进程组ID等于pid绝对值的任意子进程 |
| 参数 status | 指向整数的指针,用于存储子进程退出状态。可用宏分析: - WIFEXITED(status):正常退出返回真 - WEXITSTATUS(status):获取退出码 - WIFSIGNALED(status):被信号终止返回真 - WTERMSIG(status):获取终止信号 - WIFSTOPPED(status):暂停状态返回真 - WSTOPSIG(status):获取暂停信号 |
| 参数 options | 选项组合(可用|连接): - 0:默认行为,阻塞等待 - WNOHANG:非阻塞,立即返回 - WUNTRACED:报告暂停的子进程 - WCONTINUED:报告继续运行的子进程 |
| 返回值 | - 成功:返回状态变化的子进程PID - 使用WNOHANG且无状态变化:返回0 - 失败:返回-1,errno设置错误码 |
| 常见错误 | - ECHILD:指定子进程不存在 - EINTR:被信号中断 - EINVAL:无效选项 |
| 应用场景 | 1. 等待特定子进程结束 2. 非阻塞轮询子进程状态 3. 处理子进程暂停/继续信号 4. 获取子进程退出状态 |
| 备注 | - waitpid提供了比wait更精细的控制 - 结合信号处理时需注意竞态条件 - 多次调用waitpid可能回收同一子进程,但第二次会失败(ECHILD) |

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

解读status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由用户传入,操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的终止状态信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

下面是Linux下的所有信号

因为信号是从1开始的,所以,当status低七位为0时,表示子进程未收到信号且正常退出,否则就是收到了信号导致子进程终止或暂停

  • 之所以要通过输出型参数status拿到子进程终止状态信息是因为,进程是具有独立性的,父进程无法访问子进程的代码和数据,只能通过参数交由操作系统帮忙获取
  • 操作系统通过访问子进程的task_struct拿到子进程的终止状态信息,然后通过位运算将他们整合到变量status中

操作系统通过位运算得到status,那我们就可以通过位运算提取相关信息

但库里面提供了宏,简化了提取操作

复制代码
// 进程状态判断宏(简化注释版)

// 判断是否正常退出:低7位为0表示正常退出
#define WIFEXITED(status)    (((status) & 0x7F) == 0)

// 获取退出码:取8-15位(正常退出时有效)
#define WEXITSTATUS(status)  (((status) >> 8) & 0xFF)

// 判断是否被信号终止:低7位非0且不是127
#define WIFSIGNALED(status)  ((((status) & 0x7F) != 0) && \
                             (((status) & 0x7F) != 0x7F))

// 获取终止信号:取低7位(信号终止时有效)
#define WTERMSIG(status)     ((status) & 0x7F)

// 检查是否生成core文件:第7位为1表示生成
#define WCOREDUMP(status)    ((status) & 0x80)

// 判断是否被暂停:低8位为0x7F表示暂停
#define WIFSTOPPED(status)   (((status) & 0xFF) == 0x7F)

// 获取暂停信号:取8-15位(暂停时有效)
#define WSTOPSIG(status)     (((status) >> 8) & 0xFF)
  • 演示

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

    int main()
    {
    pid_t id = fork();

    复制代码
      if(id == 0)
      {
          // 子进程:运行两秒后退出
          printf("子进程启动 PID=%d\n", getpid());
          for(int cnt = 2; cnt > 0; cnt--)
          {
              printf("子进程: PID=%d, PPID=%d, 剩余时间: %d秒\n", 
                     getpid(), getppid(), cnt);
              sleep(1);
          }
          printf("子进程正常退出\n");
          exit(9);  // 退出码9
      }
      else if(id > 0)
      {
          // 父进程:运行5秒后等待子进程
          printf("父进程启动 PID=%d,子进程PID=%d\n", getpid(), id);
          
          for(int cnt = 5; cnt > 0; cnt--)
          {
              printf("父进程: PID=%d, 剩余时间: %d秒\n", getpid(), cnt);
              
              // 可以在这里检查子进程状态(非阻塞)
              int status;
              pid_t check = waitpid(id, &status, WNOHANG);
              if(check == id) {
                  printf("子进程已提前退出,立即回收\n");
                  break;  // 提前结束循环
              }
              
              sleep(1);
          }
          
          // 等待子进程
          int status = 0;
          printf("\n父进程开始等待子进程...\n");
          
          pid_t ret = waitpid(id, &status, 0);  // 指定等待特定子进程
          
          if(ret < 0)
          {
              printf("等待失败: %s (errno=%d)\n", strerror(errno), errno);
              
              // 检查是否是ECHILD(子进程不存在)
              if(errno == ECHILD) {
                  printf("可能的原因:子进程已被其他wait回收\n");
              }
              
              return errno;
          }
          else 
          {
              printf("等待成功,回收子进程PID=%d\n", ret);
              
              if(WIFEXITED(status))
              {
                  printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
              }
              else if(WIFSIGNALED(status))
              {
                  printf("子进程被信号终止,信号:%d", WTERMSIG(status));
                  if(WCOREDUMP(status)) {
                      printf(" (生成core dump)");
                  }
                  printf("\n");
              }
              else if(WIFSTOPPED(status))
              {
                  printf("子进程被暂停,信号:%d\n", WSTOPSIG(status));
              }
              
              return 0;
          }
      }
      else
      {
          // fork失败的处理
          perror("fork失败");
          return 1;
      }
      
      return 0;

    }

  • 进程结构是多叉树,父进程只对直系的子进程直接负责回收,爷孙关系隔代就不管了

进程等待失败原因

错误码 原因 常见场景 解决方法
ECHILD 没有子进程可等待 最常见错误 检查fork是否成功,子进程是否已被回收
EINTR 被信号中断 信号处理场景 重新调用wait/waitpid,或使用SA_RESTART
EINVAL 无效参数 参数错误 检查pid值、options标志
EFAULT 无效地址 status或rusage指针无效 检查指针是否有效
EPERM 权限不足 跨权限等待 确保有足够权限

解读option

选项常量 值(十六进制) 作用 使用场景
0 0x00 默认阻塞模式 传统wait行为,阻塞等待
WNOHANG 0x01 非阻塞轮询 不阻塞,立即返回
  • 父进程一味的等待子进程其实并不划算,因为等待过程可能还可以做一些轻量化的任务
  • 所以waitpid提供了option参数,当option等于0时,父进程阻塞等待子进程,当option等于WNOHANG时,waitpid立即返回
  • 那么,我们就可以选择在多次调用waitpid的过程中,做一些父进程自己的事
  • 其中多次调用waitpid的过程就叫做非阻塞轮询
  • 非阻塞轮询搭配父进程做自己的事效果更佳

演示非阻塞轮询

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

#define N 5

void childRun()
{
    int cnt = 3;
    while(cnt--)
    {
            printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid(), getppid(), cnt);
            sleep(1);
    }
}

#define TASK_NUM 10

typedef void(*task_t)();
task_t tasks[TASK_NUM];

void task1()
{
    printf("这是一个打印 日志的任务\n");                                                                                                                                                    
}

void task2()
{
    printf("这是一个监测网络状态的任务\n");
}

void task3()
{
    printf("这是一个绘制图形界面的任务\n");
}                                                                                                                                                                                           

int addTask(task_t t);
//管理任务
void initTasks()
{
    for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
    addTask(task1);
    addTask(task2);
    addTask(task3);
}

int addTask(task_t t)
{
    //遍历数组,在空的位置添加任务
    int pos = 0;
    for(; pos < TASK_NUM; ++pos)
    {
        if(tasks[pos] == NULL) break;
    }
    if(pos == TASK_NUM) return 0;//添加失败返沪0

    tasks[pos] = t;
    return 1;//添加成功返回1
}

void delTask() {}
void checkTask() {}
void updateTask() {}

void executeTasks()
{
    //按照数组中存放顺序执行任务                                                                                                                                                            
    for(int i = 0; i < TASK_NUM; ++i)
    {
        if(tasks[i] != NULL) tasks[i]();
        else break;
    }
}

int main()
{
    //单子进程回收
    ////创建进程 
    pid_t id = fork();
    if(id == 0)
    {
        //子进程运行5秒后退出
        int cnt = 5;
        while(cnt--)                                                                                                                                                                        
        {
            printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
        exit(9);//子进程不运行后续代码,任给一个退出码
    }
    else if(id > 0)
    {
        //父进程运行3秒后阻塞等待子进程
        int cnt = 3;
        while(cnt--)
        {
            printf("I am a parent, my PID: %d, my PPID: %d, cnt:%d\n", getpid(), getppid(), cnt);
            sleep(1);
        }

        initTasks();
        while(1)//轮询
        {
            int status = 0;
            pid_t ret = waitpid(id, &status, WNOHANG);//非阻塞
            if(ret < 0)//等待失败
            {
                printf("等待子进程失败,错误码: %d, 错误描述:%s\n", errno, strerror(errno));
                sleep(2);//不立即退出
                exit(errno);
            }
            else if(ret > 0)//等待成功
            {
                printf("等待子进程成功,子进程PID:%d\n", ret);
                if(WIFEXITED(status))
                {                                                                                                                                                                           
                    printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
                }
                else 
                {
                    printf("终止信号:%d\n", WTERMSIG(status));
                }
                sleep(2);//不立即退出
                exit(0);

            }
            else//子进程还没结束 
            {
                printf("子进程还未结束,做点自己的事:\n");
                executeTasks();
                usleep(500000);
            }
        }
    }
    else 
    {
        //出错,暂时不管
    }
    return 0;
}

需要注意的是,非阻塞轮询+做自己的任务的过程中,等待子进程是主要事件,周期性做自己的任务是次要事件,所以自己的任务一定要轻量化,可以晚一点点回收子进程,但一定不能太晚

相关推荐
遇见火星5 小时前
Linux性能调优:理解CPU中的平均负载和使用率
linux·运维·服务器·cpu
Chennnng6 小时前
Ubuntu 安装过程的 6 大常见问题类型
linux·运维·ubuntu
阿干tkl6 小时前
传统网络与NetworkManager对比
linux·网络
Evan芙7 小时前
Linux 进程状态与进程管理命令
linux·运维·服务器
码农12138号7 小时前
Bugku HackINI 2022 Whois 详解
linux·web安全·ctf·命令执行·bugku·换行符
Joren的学习记录8 小时前
【Linux运维进阶知识】Nginx负载均衡
linux·运维·nginx
用户2190326527358 小时前
Java后端必须的Docker 部署 Redis 集群完整指南
linux·后端
胡先生不姓胡8 小时前
如何获取跨系统调用的函数调用栈
linux
里纽斯10 小时前
RK平台Watchdog硬件看门狗验证
android·linux·rk3588·watchdog·看门狗·rk平台·wtd