如何理解:进程控制

文章目录

前言:

​ 对于前面的地址空间的学习,我们现在了解到原来所谓变量的地址其实是虚拟地址,该虚拟地址是通过页表的映射关系从而找到内存中真实的物理地址!下面我们进入到关于进程的控制

进程创建:

​ 现在我们对进程就有了新的定义:++进程 = 内核的相关管理数据数据结构(task_struct + mm_struct + 页表) + 代码和数据++。 其中代码是父子进程共享的,数据是判断是否发生写实拷贝的。

  • fork函数的返回值

    我们之前也有过介绍,fork函数是用来创建子进程的,创建子进程成功则返回0,对于父进程的返回值则是子进程的pid ,这一点虽然我上次没有细讲,但是在截图时就会发现。可以看看我之前博客------>进程理解

    • 子进程返回0
    • 父进程返回子进程的pid
  • 为什么父进程返回的是子进程的pid,给子进程返回的却是0呢?

    要记住,我们在讲解进程状态的时候,对于僵尸进程我们有过介绍,父进程是会管理 子进程的,谈到管理永远是6个字------>"先描述,再组织"。

    所以当然是为了让父进程方便对子进程进行标识,进而进行管理!

  • fork函数的常规用法

    1. 一个父进程希望复制自己,使得父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
    2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
  • fork调用失败的原因

    1. 系统中有太多的进程。
    2. 实际用户创建数超过限制。

进程终止:

  • 进程终止是在做什么呢?

    首先我们应该清楚一件事情------>当加载进程是,++应当是先创建PCB、页表和地址空间等++,再是加载代码和数据。

    所以进程终止是在:

    1. 释放曾经的代码和数据所占据的空间。
    2. 释放内核数据结构(若task_struct受僵尸状态,则演示释放)
  • 进程终止的3种情况

    • 提出问题:为什么main函数最后要返回0呢?为什么不是1或者100呢?
    c 复制代码
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int main()
    {
    	printf("I am a process! pid: %d, ppid: %d\n", getpid(), getppid());
    	sleep(1);
    	return 100;
    }

    !TIP

    我们可以使用指令:echo $?

    该指令代表父进程bash获取到子进程的退出码 ,0表示成功,非0表示失败。退出码的意义就是告诉关心方(父进程),我把任务处理的怎么了。

    ​

    但我要是再次执行echo $?指令,表示的就是刚刚子进程的退出码,毕竟该指令是获取最近进程的退出码。

    ​

    那这个退出码究竟有什么用,你想返回100或者0不是都可以吗。诶,这个时候操作系统会给我们提供个新的系统调用函数:strerror函数用来获取系统错误信息或打印用户程序错误信息,下面我们来用用这个系统调用

    !TIP

    c 复制代码
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <string.h>
    
    int main()
    {
     int errorcode = 0;
     for(errorcode = 0; errorcode <= 255; ++errorcode)
     {
         printf("%d -> %s\n", errorcode, strerror(errorcode));
     }
     return 100;
    }

    ​

    ​

    我们会打印特别多的错误码,每个错误码都对应了一个描述。所以为什么我们最后都会返回0,就是因为如果全部代码都执行完毕后,那肯定是不会存在问题的,那么就返回0。如果其中有一处有错误,系统就会直接进行返回相应的错误码。

    ​

    这里是我随便ls一个文件夹,因为在当前目录未找到该文件夹,所以执行该指令的进程就会向bash返回相对应的错误码2。

    所以我们在输入指令的时候,本质也是OS创建子进程然后子进程在执行。这点我们通过指令echo $?可以很好的证明。
    这也能很好地说明bash为什么要得到子进程的退出码,为了知道子进程退出的情况(是否成功,失败又是什么原因),当然bash只是提供信息,不会自动解决,这只是一种为用户负责的体现

    • 我们也可以实现自定义退出码

      c 复制代码
      #include <stdio.h>
      #include <unistd.h>
      #include <sys/types.h>
      #include <string.h>
      
      enum
      {
          ERROR_DIV = -1,
          NOMARL_DIV
      };
      
      int exit_code = NOMARL_DIV;
      
      int Div(int a, int b)
      {
          if(b == 0)
          {
              exit_code = ERROR_DIV;
              //printf("This is error!\n");
              return exit_code;
          }
          exit_code = NOMARL_DIV;
          return a/b;
      }
      
      const char* ErrorMode(int mode)
      {
          switch(mode)
          {
              case ERROR_DIV:
                  return "Div zero";
              case NOMARL_DIV:
                  return "Div nomarl";
              default:
                  return "Unknow error!";
          }
      }
      
      int main()
      {
      	int ans = Div(4, 0);
      	printf("ans is-> %d[%s]\n", ans, ErrorMode(exit_code));
      	ans = Div(4, 2);
      	printf("ans is-> %d[%s]\n", ans, ErrorMode(exit_code));
          return 0;
      }

      通过判断分母是否为0,从而实现判断结果是否是正确的!

      • 退出信号

        c 复制代码
        int main()
        {
            int* p = NULL;
            *p = 3;
            printf("%d\n", *p);
            return 0;
        }

        对于上述代码,我们执行起来百分之百会出现报错,如果我们在VS上也写过这样的bug,那么该错误就是一个很典型的------段错误

        这个时候代码不会跑完,因为在执行的过程中出现了异常,就提前退出了。就像在VS写代码时,代码崩溃了,此时OS发现你的进程做了不该做的事情,OS就会杀掉进程,一旦出现了异常,退出码就没意义了。

        比如此时我使用指令echo $? 打印退出码,是会发现退出码为139,但是退出码在133后就是未知了:

        • 为什么会出现异常呢?

          本质上是因为进程收到了OS发给进程的信号。

          我们可以使用指令:kill -l

          段错误的出现就是对应的11号信号

          所以衡量一个进程的退出,我们(父进程bash)只需要两个数字:退出码 + 退出信号

          第一步是先确认是否异常

          若不是异常就一定是代码跑完了,看退出码就好了。

!IMPORTANT

所以进程终止的3种情况:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码执行时,出现了异常,提前退出了

如何终止进程?

  1. main函数的return,表示进程终止(非main函数的return,代表函数结束)
  2. 代码调用exit函数(在代码任意位置调用exit函数都表示进程退出)
  3. 通过系统调用函数_exit( )
  • exit( ) VS _exit( )

    1. exit( )是库函数,_exit( )是系统调用

    2. exit函数会在进程退出的时候,重置缓冲区,而 _exit( )不会,因此我们可以猜测缓冲区是在exit那一层,也可以说明使用exit( )库函数的本质是在调用系统调用函数 _exit( ).

      c 复制代码
      #include <stdio.h>
      #include <stdlib.h>
      
      int main()
      { 
          printf("Hello, this is a test code!");
          exit(0);
      }

      ​

      c 复制代码
      #include <stdio.h>
      #include <stdlib.h>
      
      int main()
      { 
          printf("Hello, this is a test code!");
          _exit(0);
      }

进程等待

​ 任何进程,在退出的情况系,一般必须要被父进程进行等待!

  • 为什么父进程要等待?

    1. 进程在退出的时候,如果父进程不管不顾,退出进程会出现僵尸状态从而到时内存泄漏。所以父进程通过等待,解决子进程出现的僵尸问题,为了回收系统资源。(这是一定要考虑的)
    2. 获取子进程的退出信息,知道子进程是因为什么退出的。(可选的功能)
  • 该怎么进行等待?

    我们可以使用系统调用函数:wait( ) 和 waitpid( ) 函数

    #include <sys/types.h>

    #include <sys/wait.h>

    pid_t wait(int* status);

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

    • wait( )

      • 父进程阻塞等待任意一个子进程,子进程不退则父进程不退。
      • 该函数能够回收子进程资源,以及获取子进程的 pid。

      返回值:成功返回被等待进程的pid,失败返回-1.

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

      c 复制代码
      #include <stdio.h>
      #include <unistd.h>
      #include <sys/types.h>
      #include <stdlib.h>
      #include <sys/wait.h>
      
      void ChildRun()
      {
          int cnt = 1;
          while(cnt <= 5)
          {
              printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid());
              sleep(1);
              cnt++;
          }
      }
      
      int main()
      {
          printf("I am process!\n");
      
          pid_t id = fork();
          if(id == 0)
          {
              // child
              ChildRun();
              printf("Child quit...\n");
              exit(123);
          }
          // father
          pid_t rid = wait(NULL);
          if(rid > 0)
          {
              printf("wait success, rid: %d\n", rid);
          }
          else
          {
              printf("wait failed!\n");
          }
          sleep(2);
          return 0;
      }

      ​

      最后是等待成功并且返回了子进程的pid

    • waitpid( )

      回收子进程资源,解决僵尸问题的同时,还能够获取子进程退出信息。

      返回值:

      • pid_t > 0:等待成功,子进程退出,并且父进程回收成功
      • pid_t < 0:等待失败
      • pid_t == 0:检测是成功的,只不过子进程还没退出,需要你下一次重复等待。

      参数:

      1. pid_t pid

        • pid == -1,等待任意一个子进程,与wait一样。
        • pid > 0,等待其进程的ID与pid相等的子进程。
      2. int* status

        • 输出型参数,用来保存退出信息,保存退出码 + 退出信号,让我们知道"退出"的情况如何
      3. int options

        • 指定父进程的等待方式,为 0 则让父进程进行 阻塞 等待,非 0 则进行 非阻塞 等待。

          c 复制代码
          #include <stdio.h>
          #include <unistd.h>
          #include <sys/types.h>
          #include <stdlib.h>
          #include <sys/wait.h>
          void ChildRun()
          {
              int cnt = 1;
              while(cnt <= 5)
              {
                  printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid());
                  sleep(1);
                  cnt++;
              }
          }
          
          int main()
          {
              printf("I am process!\n");
          
              pid_t id = fork();
              if(id == 0)
              {
                  // child
                  ChildRun();
                  printf("Child quit...\n");
                  exit(123);
              }
              // father
              int status = 0;
              pid_t rid = waitpid(-1, &status, 0);
              if(rid > 0)
              {
                  printf("wait success, rid: %d\n", rid);
              }
              else
              {
                  printf("wait failed!\n");
              }
              sleep(2);
              printf("father process quit, status: %d\n", status);
              return 0;
          }

          ​

          最后的运行结果status是31488,这么奇怪的数字。

          • 分析status

            我们介绍过,status是输出型参数,记录了该进程的退出码 + 退出信号,但是我们应该如何利用一个值记录退出码和退出信号呢?

            !NOTE

            我们都知道,一个int* 变量的大小是4字节,那么也就是32个比特位,但status不能简单的当做int 来看待,可以当做位图来看,我们只研究低16比特位*

            分为两种情况:

            1. 正常终止时:高8位为退出码信息,低8位默认全部为0
            2. 被信号杀掉时(出现异常):高8位不再使用,低7位存储终止信号,中间还有一位存core dump标志位

            红色各段就代表着 退出码 + 退出信号,退出码拥有8个比特位,而终止信号拥有7个比特位

            所以我们上述的31488其实是两个数据的整合,但是我们也可以打印出来看看。

            我们需要将退出码先挪动到低8位,再转换为10进制,所以我们可以用位运算操作符:

            打印退出码:(status >> 8) & 0xFF

            打印退出信号:status & 0x7F

            c 复制代码
            printf("exit_code: %d, exit_signal: %d\n", (status >> 8)&0xFF, (status & 0x7F));

            最后的输出结果:

            因为我的子进程最后exit了123,又因为这是正常终止,所以返回退出码接收到了123.

            但如果我加一个段错误,再运行就会如下图所示:

            ​

            此时是异常退出,status就会收到退出信号 而非退出码。

            我们也可以利用两个宏,来查出退出码:

            1. WIFEXITED(status):若位正常终止子进程返回的状态,则为真。(主要是查看子进程是否正常退出,本质上是查看signal位是否满足。
            2. WEXITSTATUS(status):若WIFEXITED非空,提取子进程的退出码。
            c 复制代码
            if(WIFEXITED(status))
            {
            	printf("child process quit success, chid exit code: %d\n", WEXITSTATUS(status));
            }
            else
            {
            	printf("child process quit unnormal!\n");                                     
            }

非阻塞等待:

​ 我们上面讲的等待过程是属于阻塞等待,还记得我们在进程状态部分讲解过阻塞态吗,还记得阻塞态是会进入等待队列的吗?如果你有疑问的话可以去看看我之前写的博客:进程的祖册、挂起和运行状态。

​ 那我们刚刚介绍waitpid( )系统调用时,也介绍了参数option是控制关于非阻塞等待的,如果参数option为0,那就是阻塞等待,父进程会等待子进程结束,再进行父进程的操作,这是阻塞等待。

​ 非阻塞等待是父进程在等待子进程的过程中,父进程同时也在做某些操作,这就是非阻塞等待。

!NOTE

我们可以使用宏:WNOHANG 来表示父进程进入非阻塞等待。

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

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

void DoOtherThing()
{
 printf("   I am doing my father things while child process is running!\n");
}

int main()
{
 printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
 pid_t id = fork();
 if(id == 0)
 {
     //child
     ChildRun();
     printf("child process quit...\n");
     sleep(1);
     exit(0);
 }
 //father
 int status = 0;
 while(1)
 {
     pid_t rid = waitpid(0, &status, WNOHANG);
     if(rid == 0)
     {
         printf("   Just checking child process...\n");
         DoOtherThing();
         sleep(1);
     }
     else if(rid > 0)
     {
         printf("wait suceess!\n");
         break;
     }
     else
     {
         printf("wait failed!\n");
         break;
     }
 }
 if(WIFEXITED(status))
 {
     sleep(1);
     printf("father process quit success!\n");
     printf("exit_code: %d, exit_signal: %d\n", WEXITSTATUS(status), status&0x7F);
 }
 else
 {
     printf("quit unnormal!\n");
 }
 return 0;
}

通过以上代码,我们就能实现子进程运行的同时,父进程也在运行自己的任务:

总结:

​ 现在我们已经学会了有关进程创建的话题,接下来我们将要讨论进程替换的话题。

相关推荐
雯宝6 小时前
mnt_init 函数
linux
律宏阔12 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔12 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴12 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千12 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3712 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3712 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3712 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW12 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW12 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux