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;
}

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

相关推荐
再睡一夏就好19 小时前
LInux线程池实战:单例模式设计与多线程安全解析
linux·运维·服务器·开发语言·javascript·c++·ecmascript
zfj32119 小时前
Linux第一个用户空间进程init进程的演进过程
linux·运维·网络
柏木乃一19 小时前
进程(8)虚拟地址空间/虚拟内存概述.part1
linux·服务器·c++·进程·虚拟内存·fork
oMcLin20 小时前
CentOS 7.6 磁盘空间不足导致服务崩溃:如何有效清理日志文件和临时文件
linux·运维·centos
秋风不问归客20 小时前
linux 网络相关命令 及常用场景
linux·服务器·网络
牛奶咖啡1320 小时前
Linux文件快照备份工具rsnapshot的实践教程
linux·服务器·文件备份·文件快照备份·rsnapshot·定时备份本地或远程文件·查看指定命令的完整路径
大模型铲屎官20 小时前
【操作系统-Day 47】揭秘Linux文件系统基石:图解索引分配(inode)与多级索引
linux·运维·服务器·人工智能·python·操作系统·计算机组成原理
拾光Ծ20 小时前
Linux 进程控制:进程终止与等待・waitpid 选项参数与状态解析(告别僵尸进程)
linux·运维·服务器·进程控制
linux修理工20 小时前
ubuntu 2204 tsinghua
linux·运维·ubuntu
琥珀.20 小时前
查看linux下java服务进程是否正常
java·linux·运维