Linux-进程控制

发布于:2024-04-23 ⋅ 阅读:(18) ⋅ 点赞:(0)

Linux进程控制【中】


文章目录:

进程控制【中】

    进程退出函数

      exit函数
      _exit函数

    进程等待

      wait接口
      waitpid接口
      导出错误码
      阻塞与非阻塞

    总结


前言:

  进程控制不仅仅是管理程序的执行顺序,还涉及到资源的分配等问题,那么话不多说,开始我们今天的话题!

在这里插入图片描述


🚀进程退出函数

✈️exit函数

  上次我们说到,进程退出时,都会返回一个退出码,用来表示进程退出的状态,而在更前面,我们曾经说过exit函数用来退出进程:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     while(1)
  8     {
  9         printf("I'm a process, pid=%d\n", getpid());
 10         sleep(1);
 11 
 12         exit(3);
 13     }
 14     return 0;
 15 }

在这里插入图片描述
  进程退出函数exit,函数参数可作为进程退出状态:

在这里插入图片描述
  eixt退出进程 status进程退出状态退出码


✈️_exit函数

在这里插入图片描述

  这是man手册里的_exit(),我们通过下面这段代码测试一下他们功能有何异同:

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>

void Print()
{
    printf("hello Print\n");
    _exit(4);
}

int main()
{
    while(1)
    {
        printf("I am a process: %d\n", getpid());
        sleep(1);
        Print(1);
        //exit(3);放出另外一个_exit,反之则放出exit关闭_exit
    }
    return 0;
}

在这里插入图片描述

  这是注释掉了exit(),调用Print函数的_exit()的结果。

在这里插入图片描述

  这是注释掉Print函数内的_exit,使用exit()函数返回的结果。

  从此看来我们并没有发现什么不同之处,返回的错误码也没问题。他们的区别可以通过一下代码分析:

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
    printf("hello guy\n");
    sleep(1);

    exit(1);
    return 0;
}

在这里插入图片描述

  exit函数返回的结果符合我们的预期,如果把打印的\n删除:

在这里插入图片描述
  也没什么问题,但是如果换成_exit函数:

在这里插入图片描述

  从这里可以看到,当_exit函数遇到像printf打印却没有换行符的时候,就不能正确打印出自己想要的数据。

区别
  exit是用来终止进程的,exit(退出码),在我们进程的任何地方调用exit() 都表示进程退出,而_exit与exit的区别就是,exit() 会刷新缓冲区(以后会详谈)。

  实际上,_exit()函数是Linux下的一种系统调用,为什么要存在exit() 和 _exit() 两个不同的接口呢?exit() 函数是语言层面的封装,使用这种封装的好处就是语言具有 跨平台、可移植性

  不同系统、平台可能给你提供的系统调用不相同,但是在语言层的封装却都是相同的,所以 C语言具有可移植性、跨平台性。


🚀进程等待

  我们之前说过,如果一个进程变僵尸那么使用kill -9也无能为力,造成僵尸的原因就是子进程退出了,父进程并没有将资源回收所导致的,所以系统提供了一些系统调用,通过父进程等待的方式获取子进程退出信息。

✈️wait接口

  在Linux中,为了防止进程变僵尸,系统系统了这样一个接口 wait()

在这里插入图片描述
  wait接口是用来回收子进程资源的一个接口,我们看到wait接口的参数是一个指针,这其实是一个 输出型参数 ,我们现将其设置为 NULL

接口功能

  默认会进行阻塞等待,等待任意一个子进程。
返回值:
   >0时,等待成功,等待的子进程pid。
  <0时, 等待失败

  我们通过代码来认识一下它的功能:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        printf("child prepare for exit, will be ZM!\n");
        exit(0);
    }
    printf("father is sleep...\n");
    sleep(10);
    printf("father start recycle\n");
    //father
    pid_t rid = wait(NULL);
    if(rid > 0)
    {
        printf("wait sucess, rid: %d\n", rid);
    }
    printf("father get source sucess!\n");
    sleep(3);

    return 0;
}

在这里插入图片描述

  图中画的红色框框表示僵尸了一段时间,而当父进程开始回收之后,子进程的僵尸状态就没有了。

  由此我们可以间接得出,fork之后,父进程一定要最后退出


✈️waitpid接口

  Linux也提供了wait方式来获取子进程退出信息的接口 waitpid()

在这里插入图片描述
  其中waitpid返回值与wait的返回值含义相同,第一个参数的pid有很多种表示方法,但是常见的表示有这两种:

  pid参数意义

pid = -1,等待任意子进程,与wait等效。
pid>0,等待其进程id与pid相等的子进程。

  status同样是一个输出型参数。options有两种传值,一种是0,一种是非阻塞,我们目前将其设置为0表示阻塞状态。

  通过下面代码感受getpid():

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    //father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);//阻塞等待
    if(rid > 0)
    {
        printf("wait sucess, rid: %d  status: %d\n", rid, status);//查看rid和status
    }

    return 0;
}

在这里插入图片描述
  我们给status的初始值为0,但是这里却变成了256。这里的status并不是指单纯的整数,与进程的 退出码退出信号 有关,而其存储形式类似于位图的存储:

在这里插入图片描述

  我们只看比特位的后十六位:
在这里插入图片描述
  中间有一个名为core dump的比特位我们在信号部分在详谈。

  所以我们status为什么是256其实就很简单了:

0000 0000 0000 0000 0000 0001 0000 0000 转换为10进制就是2的8次方也就是256。


✈️导出错误码

  上面说了status的最后16位比特位有效,并且这十六位由退出码和信号编号所组成,所以我们可以使用位运算的方式将退出码和退出信号提取出来:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 50;
        while(cnt)
        {
            printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    //father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
      //status & 0x7F: 0000 0000 0000 0000 0000 0000 0111 1111
        printf("wait sucess, rid: %d  status: %d  exit signo%d  exit code: %d\n", rid, status, status&0x7F, (status>>8)&0xFF);
    }

    return 0;
}

  将status按位与上 0x7F就可提取出退出信号,因为只有后七位有效。将status右移8位再按位与上0xFF即可提取退出码。

在这里插入图片描述
  使用了kill -9得出的退出信号就是9,进程被杀死退出码为0。如果在代码中出现了逻辑错误,比如除零错误:

在这里插入图片描述
  还有类似空指针等情况,有兴趣可以自己尝试,这里就不在试了。

  实际上,Linux给我们提供了两个常见的宏定义:

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

  有了这些宏,我们就不用那么麻烦直接使用位操作了,我们可以直接使用对应的宏:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
            cnt--;
            sleep(1);
        }
        exit(123);
    }
    //father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
      //status & 0x7F: 0000 0000 0000 0000 0000 0000 0111 1111
       // printf("wait sucess, rid: %d  status: %d  exit signo%d  exit code: %d\n", rid, status, status&0x7F, (status>>8)&0xFF);
        if(WIFEXITED(status))//使用宏定义
        { 
            printf("wait sucess, rid: %d  status: %d  exit code: %d\n", rid, status, WEXITSTATUS(status));
        }
        else 
        {
            printf("child process error!\n");
        }
    }

    return 0;
}

在这里插入图片描述

  源码中的status:

在这里插入图片描述

✈️阻塞与非阻塞

  我们前面代码所采用的等待方式均为阻塞等待,即在回收子进程的资源之前,父进程什么事情可不干,可以用grep 查询:

ps ajx | grep -i mybin#查询当前进程状态

在这里插入图片描述
  既然有阻塞,也必然有非阻塞:

表1 阻塞与非阻塞区别

阻塞 非阻塞
方式 当waitpid函数以阻塞模式调用时,父进程等待子进程退出,这期间父进程不做任何事情 当waitpid函数以非阻塞调用时,父进程以轮询的方式每段时间检测子进程是否退出,没退出就返回做父进程的事情
参数 0 WNOHANG

  我们以下面这段代码来理解非阻塞:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
            cnt--;
            sleep(1);
        }
        exit(123);
    }
    //father
    int status = 0;
    while(1)
    {
        pid_t rid = waitpid(id, &status, WNOHANG);//非阻塞调用
        if(rid > 0)
        {
            printf("wait sucess, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));
            break; 
        }
        else if(rid == 0)
        {
            printf("father say: child is running, do other things\n");
        }
        else 
        {
            perror("waitpid");
            break;
        }
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

  父进程在等待子进程资源回收的时候自己也在不断地执行,最后也可以成功回收子进程。

  为了更直观看到现象,我们不妨模拟进程等待的环境:

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

#define NUM 5

typedef void(*fun_t)();
fun_t tasks[NUM];

void printLog()
{
    printf("this is a log print task\n");
}

void printNet()
{
    printf("this is  a net\n");
}

void printNPC()
{
    printf("this is a flush NPC\n");
}

void initTask()
{
    tasks[0] = printLog;
    tasks[1] = printNet;
    tasks[2] = printNPC;
    tasks[3] = NULL;
}

void executeTask()
{
    for(int i = 0; tasks[i]; ++i) tasks[i]();

}

int main()
{
    initTask();
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("child is running, pid: %d, ppid: %d\n", getpid(), getppid());
            cnt--;
            sleep(1);
        }
        exit(123);
    }
    //father
    int status = 0;
    while(1)
    {
        pid_t rid = waitpid(id, &status, WNOHANG);
        if(rid > 0)
        {
            printf("wait sucess, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));
            break; 
        }
        else if(rid == 0)
        {
            printf("father say: child is running, do other things\n");
            printf("##############task begin############\n");
            executeTask();
            printf("##############task end##############\n");
        }
        else 
        {
            perror("waitpid");
            break;
        }
        sleep(1);
    }
    return 0;
}

在这里插入图片描述


📒✏️总结

  •  在Linux下,进程退出提供了两个接口,exit()_exit(),他们的区别就是 _exit()函数没有刷新缓冲区这一功能。
  •  一个创建子进程的父进程有必要进行进程等待,用来防止进程变为僵尸进程从而危害系统。其中进程等待有两个接口 wait()waitpid()
  • wait和waitpid:
区别 wait waitpid
参数 *status id, *status, options
状态 等待 任意 子进程退出,并返回终止子进程id 等待 指定 子进程退出,并返回终止子进程id
阻塞 无(默认阻塞调用) 0 和 WNOHANG(用来支持阻塞非阻塞的宏)
  •  根据你所期望进程执行的行为从而选择对应的接口。

在这里插入图片描述
创作不易,如果对您有帮助的话,还望留下一个小小的赞~~