• Linux进程控制


    Linux进程控制

    1.Linux进程创建

    1.1 fork()函数的基本了解

    在linux中fork()函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程!

    1. fork()函数的头文件#include
    2. fork()函数原型pid_t fork(void)
    3. fork()函数返回值父程序返回子程序PID,子程序返回0,出错返回-1

    1.2 fork()调用期间,内核的操作

    进程调用fork,当控制转移到内核中的fork代码后,内核做了一下事情:

    • 分配新的内存块和内核数据结构给子进程
    • 将父进程部分数据结构内容拷贝至子进程
    • 添加子进程到系统进程列表当中
    • fork返回,开始调度器调度

    请添加图片描述

    当一个进程调用fork之后,父子进程共享代码,子进程写时拷贝父进程数据,每个进程都将可以开始它们自己的执行,看如下程序:

    //给了if分流父子进程的fork现象
    #include
    #include
    #include
    int main()
    {
        printf("Now PID:%d\n",getpid());
        pid_t id=fork();
        if(id<0)
        {
            perror("fork error!\n");
            return 1;
        }
        else if(id==0)
        {
            //child
            printf("child pid:%d  return:%d\n",getpid(),id);
            sleep(2);
        }
        else
        {
            //father
            printf("father pid:%d  return:%d\n",getpid(),id);
            sleep(2);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    请添加图片描述

    再来看一个代码现象:

    //未给if分流父子进程的fork现象
    #include
    #include
    #include
    int main()
    {
      pid_t pid;
      printf("Before: pid is %d\n", getpid());
      if ( (pid=fork()) == -1 )
      {
        perror("fork()");
        exit(1);
      }
      printf("After:pid is %d,fork return %d\n", getpid(), pid);
      sleep(1);
      return 0;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请添加图片描述

    以第二个代码例子为例,这里看到了三行输出,一行before,两行after。进程9045先打印before消息,然后它又打印after。另一个after 消息有9046打印的。注意到进程9046没有打印before,为什么呢?如下图所示:

    请添加图片描述

    上图解析:

    • fork之前,我们是先执行before,所以第一行我们打印除了befor
    • fork执行时,子进程写时拷贝一份父进程数据,fork返回两个返回值分别给父子进程
    • fork之后,父子进程代码共享,但fork返回值有两个,于是执行后续的代码2次,打印两个after

    值得注意的是:

    1. fork之后,父子进程谁先执行,完全由调度器决定
    2. fork之后,并不是重新创建一个新的子进程,而是使用老的父进程,也就是我们说的父子进程代码共享,所以进程还是一个,我们说的父子进程只是方便描述
    3. 发生写时拷贝,父子进程的虚拟地址相同,物理地址的映射不同,所以最后父子进程获得的getpid()和pid值是不一样的

    1.3 fork()函数的返回值

    思考一些问题:

    1. 为何要给子进程返回0,给父进程返回子进程的pid?
    2. 如何理解fork有两个返回值的问题?
    1. 第一个问题:

      • 首先父子进程的 1 : n 的关系,所以在父子进程的立场中,父进程不需要标识,子进程需要标识。其次子进程是要执行任务的,父进程需要区分子进程,所以给父进程返回子进程的pid,因为父进程可以通过这个pid来区分是哪个子进程。给子进程返回0,本质上是因为子进程不需要访问父进程pid,因为子进程也不需要知道父进程pid,子进程不需要管理父进程,任务是给子进程的,它只需要知道自己调用成功了就可以
    2. 第二个问题:

    请添加图片描述


    1.4 写时拷贝技术

    通常父子代码共享,父子在不修改时,数据也是共享的,当任意一方试图修改时,便以写时拷贝的方式各自一份副本。具体见下图:

    请添加图片描述

    对于写时拷贝的理解:

    1. 对这里的"共享"怎么理解?
      • 答:父子进程对应的页表指向的是同一块物理内存。当任何一方写入的时候,以便使用写时拷贝的方式生成一份副本
    2. 为何要写时拷贝?
      • 答:进程具有独立性!
    3. 为何不在创建的时候就分开?
      • 答:子进程不一定会使用父进程的所有数据,写入,本质是需要的时候!也就是按需分配,这种方式还做到了一点:延时分配,因为当被创建的时候,不一定被立马调度,如果不立马被调度,那就不需要先给它分配空间。因为要是先给它分配空间了,那也就是在它被调度之前的时间段中,系统可用的内存是变少的,所以延时分配永远可以保证系统可用资源是最大化的!所以延时分配的本质是:可以高效使用任何内存空间!
    4. 为何代码不会写时拷贝?
      • 答:90%的情况不会(但是不代表不能),因为我们学语言到现在,我们要改的永远是数据,我们没有在让程序运行的时候,改程序运行的逻辑

    1.5 fork()函数的常规用法

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

    1.6 fork()函数调用失败的原因

    1. 系统中有太多的进程:内存空间上的不支持
    2. 实际用户的进程数超过了限制:操作系统上的不支持

    2.Linux进程终止

    2.1 进程退出的三种情况

    1. 代码正常运行完毕,结果正确
    2. 代码正常运行完毕,结果不正确
    3. 代码运行异常

    请添加图片描述


    2.2 进程退出的常见方式

    1. 正常终止(可以通过 echo $? 查看最近一次进程退出码):
      1. 从main函数中return返回
      2. 调用exit()函数返回
      3. 调用_exit()函数返回
    2. 异常退出:ctrl+c,信号中止

    Linux下错误码打印方法:

    #include
    #include
    int main()
    {
        int i=0;//这里这样写是因为不支持C99,需要在编译的时候加个-std=c99
        for(i=0;i<150;i++)
        {
            printf("错误码序号:%d,错误信息:%s\n",i,streror(i));
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请添加图片描述

    注:Linux下只有133个错误码,每个错误码都有对应的错误信息!


    2.3 exit()和_exit()的区别

    两者的区别在于,exit会在进程结束做一些收尾工作(比如刷新缓冲区),而_exit不会,看下面的对比:

    请添加图片描述

    #include
    #include
    #include
    void show()
    {
        printf("我是show()函数!");
        exit(10);
    }
    int main()
    {
        show();
        printf("我是main()函数!");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    请添加图片描述

    我们把exit()函数换成_exit()函数来看看效果:

    #include
    #include
    #include
    void show()
    {
        printf("我是show()函数!");
        _exit(10);
    }
    int main()
    {
        show();
        printf("我是main()函数!");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    请添加图片描述

    从上面来个代码效果和剖析函数原型我们可以得出结论:

    1. exit最后也调用了_exit,但exit还做了其他工作:
      • 执行用户通过 atexit或on_exit定义的清理函数
      • 关闭所有打开的流,所有的缓存数据均被写入
      • 调用_exit
    2. exit和return的区别:
      • exit是终止整个进程,任何地方调用都会终止整个进程
      • return是终止函数,等同于exit(n)

    2.4 关于退出码的一些思考

    由退出码想到了一系列问题:

    1. 进程异常退出,退出码还有意义吗?
      • 没有意义!说简单点,到异常的地方就已经被终止了,根本没有执行 return
    2. 进程终止了,操作系统在做什么?
      • 释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据中移除
    3. 为何要有进程等待?
      • 回收子进程资源
      • 获取子进程退出信息

    3.Linux进程等待

    3.1 进程为什么要等待

    1. 回收僵尸进程,解决内存泄漏(僵尸进程是杀不死了,kill -9不能干掉僵尸进程)
    2. 获取子进程的运行结束状态(交给子进程的工作做的怎么样了)
    3. 父进程晚于子进程退出,规范化的进行资源回收
    • 等待的本质也是管理的一种方式,OS的核心就在于管理二字
    • 简而言之,进程等待的原因:获取子进程的信息和防止内存泄漏

    3.2 等待函数wait与waitpid的理解

    我们用man 2 wait来看一下wait和waitpid函数的官方文档:
    请添加图片描述

    补充:在普通状态下用man -2 wait查看,在vim下用! man 2 wait可以快速查看

    wait与waitpid理解:

    1. 头文件:#include和#include
    2. wait返回值:等待成功返回子进程pid,等待失败返回-1
    3. waitpid返回值:等待成功返回子进程的pid,如果指定了WNOHANG选项,且子进程正在运行(没有已退出的子进程可收集)则返回0,等待失败返回-1

    请添加图片描述


    3.3 wait()的使用

    #include
    #include
    pid_t wait(int*status);
    返回值:
     成功返回被等待进程pid,失败返回-1。
    参数:
     输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    wait的使用案例:

    #include
    #include
    #include
    #include
    #include
     
    int main()          
    {                    
       pid_t id = fork();
       if(id == 0)
       {
         int count = 0;
         while(count < 5)
         {         
           printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
           sleep(1);     
           count++;
         }                                                              
         exit(0);   
       }           
       else
       {
           printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
           pid_t ret = wait(NULL);
           if(ret >= 0)                                                                                     
           {                                                             
             printf("wait child success!, %d\n",ret);
           }           
           printf("Father running...");
           sleep(5);
       }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    请添加图片描述

    1. 结论:由此可见,在父进程等待的时候,子进程在运行,重要的是,在子进程运行结束后没有看到Z状态进程,这是因为父进程在等待子进程结束,然后回收子进程
    2. 一点小问题:
      • 在子进程运行期间,父进程wait的时候,父进程在做什么?
        • 就是在 “等” 什么也没干,就是在等子进程退出,这种等子进程退出的过程叫做阻塞等待
        • 因为父子谁先运行不确定,但是wait之后,大部分情况都是子进程先退出,父进程读取子进程退出信息,父进程才退出。建议大家以后一定要让父进程等待子进程退出,如果不等的话,一定会导致僵尸进程的问题

    补充:上面ps命令下面的########的显示方法

    ps ajx | grep test(代码文件名称) | grep -v grep; sleep 1; echo "###########"; done
    //这个代码里的while、do、done是shell编程里使用的,自行了解就行了
    
    • 1
    • 2

    3.4 waitpid()的使用

    pid_ t waitpid(pid_t pid, int *status, int options);
    返回值:
     	当正常返回的时候waitpid返回收集到的子进程的进程ID
     	如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
     	如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
    
    参数:
     pid:
    	Pid=-1,等待任一个子进程。与wait等效
     	Pid>0.等待其进程ID与pid相等的子进程
        
     status:
     	WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
     	WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
            
     options:
     	WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
    • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
    • 如果不存在该子进程,则立即出错返回
    #include
    #include
    #include
     #include
    #include
    
    int main()
    {                    
       pid_t id = fork();
       if(id == 0)
       {
          int count = 0;
          while(count < 5)
          {         
           	 printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
           	 sleep(1);     
             count++;
          }                                                              
          exit(0);   
       }           
       else
       {
           printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
          //pid_t ret = wait(NULL);
           int status = 0;                                                                          
           pid_t ret = waitpid(id, &status, 0);
           if(ret >= 0)
           {              
             printf("wait child success!, %d\n",ret);
           } 
           printf("Father running...");
           sleep(5);
        }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    请添加图片描述

    结论:

    1. 由此可见,waitpid和wait没有什么区别
    2. 进程等待成功是否意味着子进程运行成功?
      • 绝对不是,进程等待成功只意味着子进程退出了

    3.5 进程子状态的获取:int* status

    我们接下来看看status表示的是什么:

    请添加图片描述

    由此可见,status表示的不是退出码,这个数字很奇怪,那它到底表示的是什么呢?

    参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL

    关于status参数的理解:

    1. wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充

    2. 如果传递NULL,表示不关心子进程的退出状态信息

    3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

    4. status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

    请添加图片描述

    关于0xff与0x7f的使用理解:

    1. 0xff:通常用来取得低八位
      • f二进制是:1111,即0xff为:0000 0000 1111 1111
      • 这里的status>>8,取status的后8位,后八位全是0,而&操作,二进制位同真为真
      • 如果(status>>8)&0xff结果为0,则表示后8位全是0,代码运行结果正确
      • 如果(status>>8)&0xff结果不为0,则表示后8位不全是0,代码运行结果不正确
    2. 0x7f:通常用来取得低七位
      • 7二进制是:0111,即0x7f为:0000 0000 0111 1111
      • 异常终止高八位没用,所以这里不需要将status>>8
      • status&0x7f,就是将status的第七位与111 1111进行&操作,从而得到终止信号

    请添加图片描述


    3.6 status的验证

    #include
    #include
    #include
    #include
    #include
    
    int main()
    {
       pid_t id = fork();
       if(id == 0)
       {
          int count = 0;
          while(count < 5)
          {
            printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
            sleep(1);
            count++;
          }
          exit(77);
       }
       else
       {
          printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
          //pid_t ret = wait(NULL);
          int status = 0;                                                                          
          pid_t ret = waitpid(id, &status, 0);
          if(ret >= 0)
          {
             printf("wait child success!, %d\n",ret);
             printf("status: %d\n",status);
             printf("child exit code: %d\n",(status>>8)&0xFF);
          }
          printf("Father running...\n");
          sleep(2);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    请添加图片描述

    1. 我们通过第9行代码获取到了子进程退出的退出码exit(77)
    2. 进程异常的时候,本质是进程运行的时候出现了某种错误,导致进程收到信号!

    那么我们怎么知道我们收到信号了呢?我们接着看:

    请添加图片描述

    由此可见,我们没有收到任何信号,因为信号里没有0号信号!

    请添加图片描述

    由上图,我在进程运行的时候用2号信号把子进程给kill了,子进程立马终止然后传递过去了2号信号。且退出码为0(我刚才说过,如果进程出现异常,退出码没有任何意义!)

    上面都是提到的单进程,接下来我就写一个多进程执行的,前面没有说waitpid的第一个参数,其实第一个参数在单进程的时候就标识的那个参数,多参数的时候就可以指定一个参数,也就是说一个waitpid只能等一个子进程:

    #include
    #include
    #include
    #include
    #include
     
    int main()
    {
       pid_t ids[3];
       for(int i = 0; i < 3; i++)
       {  
          pid_t id = fork();
          if(id == 0)       
          {  
            int count = 3;                                               
            while(count > 0)
            {         
               printf("child do something!: %d, %d\n",getpid(),getppid());
               sleep(1);
               count--;
            }     
            exit(i);  
          }                                                                                    
          //father
          ids[i] = id;
      }               
    
      int count = 0;     
      while(count < 3)
      {
         int status = 0;
         pid_t ret = waitpid(ids[count], &status, 0);
         if(ret >= 0)
         {
            printf("wait child success!, %d\n",ret);
            printf("status: %d\n",status);
            printf("child exit code: %d\n",(status>>8)&0xFF);
            printf("child get signal: %d\n",status&0x7F);
         }
         count++;
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    请添加图片描述

    • 其实我们很少用到创建多进程的场景
    • 在最开始我们说waitpid的时候给了两个宏,也就是说,我们可以不适用位操作,直接使用宏即可,如下代码:
    #include
    #include
    #include
    #include
    #include
     
    int main()
    {
        pid_t ids[3];
        for(int i = 0; i < 3; i++)
        {
          pid_t id = fork();
          if(id == 0)
          {
             int count = 3;
             while(count > 0)
            {
              printf("child do something!: %d, %d\n",getpid(),getppid());
              sleep(1);
              count--;
            }
            exit(i);
          }                                                                                        
          //father
          ids[i] = id;
        }
     
       int count = 0;
       while(count < 3)
       {                                                                                          
          int status = 0;
          pid_t ret = waitpid(ids[count], &status, 0);
          if(ret >= 0)
          {
             printf("wait child success!, %d\n",ret);
             if(WIFEXITED(status))//正常退出
             {
               printf("child exit code: %d\n",WEXITSTATUS(status));
             }
             else
             {
               //不正常退出
               printf("child not exit normal!\n");
             }
            // printf("status: %d\n",status);
            // printf("child exit code: %d\n",(status>>8)&0xFF);
            // printf("child get signal: %d\n",status&0x7F);
          }
          count++;
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    请添加图片描述


    3.7 进程阻塞等待与非阻塞等待的区别

    • 阻塞等待:父进程一直等子进程,什么也不干
    • 非阻塞等待:就是父进程在等待的同时也在做自己的事,在子进程退出后再去读取子进程的退出信息

    请添加图片描述

    请添加图片描述

    我们首先来剖析一下waitpid返回值的文档:

    请添加图片描述

    这里我解释一下waitpid()的返回值:如果成功的话,返回等待子进程的退出码,如果WNOHANG被指定的、并且指定的子进程是存在的、并且这个子进程的状态没有改变,就返回0,否则的话就返回-1

    这里先说一个细节:waitpid的返回值要么大于0要么小于0这两个状态,要是设置成非阻塞就很有可能出现第三个状态,就是调用waitpid调用成功了,但是子进程并没有退出,没有退出的话调用waitpid检车的时候,就相当于,我等它的时候,他没有退出,但是我waitpid调用成功了,因为状态没有变,所以我直接返回了,就相当于我就进行了一次检测。所以如果waitpid的返回值是0的话就证明waitpid调用是成功的,只不过被等的那个子进程没有退出罢了

    #include
    #include
    #include
    #include
    #include
    
    int main()
    {
       pid_t id = fork();
       if(id == 0)
       {
          int count = 0;
          while(count < 5)
          {
             printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
             sleep(1);
             count++;
          }
        exit(1);                                                                                 
       }
    
      int status = 0;
      pid_t ret = waitpid(id, &status, WNOHANG);
      if(ret > 0)
      {
         printf("wait success!\n");
         printf("exit code: %d\n",WEXITSTATUS(status));
      }
       printf("ret: %d\n", ret);
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    请添加图片描述

    1. 由此可见,此时waitpid的返回值立马就是0,但是子进程还在,而父进程立马就退出了,此时子进程的ppid立马改为了1,那么子进程也就变成了孤儿进程
    2. 所以我们可以得到一个结论,如果我们以非阻塞的方式进行等待的时候,此时我们就不应该只等待一次,而是让父进程不断的轮询式的等待
    #include
    #include
    #include
    #include
    #include
     
    int main()
    {
       pid_t id = fork();
       if(id == 0)
       {
          int count = 0;
          while(count < 5)
          {
            printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
            sleep(1);
            count++;
          }
          exit(1);
       }
       while(1)
       {
          int status = 0;                                                                          
          pid_t ret = waitpid(id, &status, WNOHANG);
          if(ret > 0)
          {
            printf("wait success!\n");
            printf("exit code: %d\n",WEXITSTATUS(status));
            break;
          }
         else if(ret == 0)
         {
            printf("father do other things!\n");
            sleep(1);
         }
         else
         {
           printf("waitpid error!\n");
           break;
         }
      }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    请添加图片描述

    由此可见,在子进程在做自己的事情的时候,父进程并不是刻意的去等待,而是父进程也在做自己的事情,它们两个之间并不会相互影响,只不过每隔1秒进行一次检测,当子进程运行结束之后,父进程获取子进程相关的退出信息

    这种检测方案叫做非阻塞接口轮询检测方案!


    4.Linux进程替换

    4.1 进程替换原理

    • 磁盘中保存一程序的代码和数据,程序替换就是,将磁盘中保存的新程序的代码和数据替换进程中的程序和代码。从新程序代码开始执行
    • 注意:程序替换没有创建新进程,所以该进程的pid和数据结构并没有改变

    请添加图片描述


    4.2 替换函数

    在Linux中其实有六种以exec开头的函数,统称exec函数:

    #include 
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg, ...,char *const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execve(const char *path, char *const argv[], char *const envp[]);
    
    //使用说明
    这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
    如果调用出错则返回-1
    所以exec函数只有出错的返回值而没有成功的返回值
        
    //命名理解
    //这些函数原型看起来很容易混,但只要掌握了规律就很好记
    l(list) : 表示参数采用列表
    v(vector) : 参数用数组
    p(path) : 有p自动搜索环境变量PATH
    e(env) : 表示自己维护环境变量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    请添加图片描述

    请添加图片描述

    补充:execl系列的函数,根本就不需要判断返回值,因为只要是返回了就是失败!所以我们一般在程序替换后加上exit(1),也就是说只要你成功了,那你就被替换掉了,只要失败了就不往后走了,就终止进程

    请添加图片描述

    接下来我先来用几个,其实剩下的都是非常相似的:

    #include
    #include
    #include                                                                           
    int main()
    {
      printf("I am a process!\n");
      sleep(2);
      execl("/usr/bin/ls","ls", "-a", "-i", "-l", NULL);
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    请添加图片描述

    • 我们可以看到,我们利用execl成功的调用起了ls命令
    • 我们得到一个结论,我的进程可以把别人的程序调用起来

    我们学语言的时候肯定都听说过一句话,任何程序要被运行之前,必须要先从磁盘中加载到内存当中(因为冯诺依曼体系是这么决定的,因为磁盘属于外设)那么我们就有了以下一些疑问了?

    1. 那么程序如何被加载的呢?
      • 我们刚才用的execl就可以称之为叫做Linux下的加载器所用的底层技术
    2. 当前进程在进行程序替换的时候,有没有创建新的进程?
      • 没有!也就是说,我们在进行程序替换的时候,没有进行任何的程序创建。有的人可能认为不对啊!这里执行的代码和数据都已经被替换掉了,那怎么能是没创建新进程呢?
      • 其实衡量一个进程是进程,并不是根据这个进程执行什么代码、访问什么数据决定的,衡量一个进程是进程是由它在内核中的相关数据结构决定的,而其中我们在进行程序替换时PCB、虚拟地址空间、页表这三种结构是没有发生质的变化的。我们只是把老的代码用新的磁盘上的文件的代码和数据进行了替换,仅此而已
      • 所以这也就印证:进程不等价于程序,进程要比程序大的多
    3. 进程替换之后如果还有代码会执行么?
      • 不会!因为已经被替换了,进程程序替换,一经替换,绝不返回,后续代码不会执行
    4. 如果程序替换失败呢?
      • 程序替换失败后,程序后续并不会受到影响!也就是说,一旦替换失败,后面的代码正常运行

    5.shell的实现原理

    5.1 对shell的理解

    • shell叫做命令行解释器,它的作用是将你输入的命令交给bash去执行,而bash本身不会亲自给你去实现命令,而是会创建一个子进程去帮你执行,因为进程和进程之间是由独立性的,你运行的命令如果没有BUG还好,有BUG的话,这个BUG命令如果bash亲自执行,可能会将bash搞挂了,bash挂了,就没有办法给用户提供新的命令行解释服务了,所以这种事一般由子进程去做,子进程挂掉也不影响,首先不影响父进程bash,而且运行后的结果不管对还是不对,父进程都可以拿到结果

    shell的简单实现方法:

    1. 简单的shell,它的根本原理其中一定要有fork()这样的调用,这是其一
    2. 其二就是我们创建出来的子进程,我们不是为了让子进程帮我们去执行解释器部分的代码,它的任务只是执行命令,所以也就是创建子进程,让子进程去执行一个全新的程序(程序替换)

    请添加图片描述


    5.2 shell的实现

    用下图的时间轴来表示事件的发生次序,其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束

    请添加图片描述

    实现流程:

    1. 获取命令行
    2. 解析命令行
    3. 建立一个子进程(fork)
    4. 替换子进程(execvp)
    5. 父进程等待子进程退出(wait)

    实现代码:

    #include
    #include
    #include
    #include
    #include
    #include
    #define LEN 1024
    #define NUM 32
     
    int main()
    {
       char cmd[LEN];
       char* myarg[NUM];
       while(1)
       {
          printf("[牟建波@my-centos_mc dir]$ ");
          fgets(cmd, LEN, stdin);
         // 我们创建出来的子进程要执行命令(命令再cmd中)
         // 要执行命令就要将一个个命令拆开才可以调用
         // 所以要解析字符串
         //
         // 将最后一个命令的\n去掉(换成\0就行了) 
         cmd[strlen(cmd) - 1] = '\0';
         myarg[0] = strtok(cmd, " ");
         int i = 1;
         while(myarg[i] = strtok(NULL, " "))
         {
            i++;
         }           
    
         pid_t id = fork();
         if(id == 0)//child
         {
            execvp(myarg[0], myarg);
            exit(-1);//随便写的
         }
         int status = 0;
         pid_t ret = waitpid(id, &status, 0);
         if(ret > 0)
         {
            printf("exit code: %d\n", WEXITSTATUS(status));
         }
       }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    请添加图片描述

    对于实现shell后的一些结论:

    1. 我们说shell就是一个进程,现在就可以理解了,当我们 ./test 的时候 test 变成进程了,一直在运行,所以shell是在系统启动的时候就由某些任务将其进行启动
    2. 系统启动以及用户登录的时候,某些登陆软件会自动调用bash程序,将其运行起来变成进程
  • 相关阅读:
    WPF分享一个登录界面设计
    上机实验四 图的最小生成树算法设计 西安石油大学数据结构
    BIM分享 | 建筑BIM应用中,那些有趣的事
    Atcoder abc131
    mysql特殊sql总结
    Centos7 搭建 GitLab服务 下载-安装-配置-卸载 完整版
    超详细讲解H5移动端适配
    Springboot01入门
    代码+视频,R语言对数据进行多重插补后回归分析
    Oracle day9
  • 原文地址:https://blog.csdn.net/qq_29678157/article/details/128065599