• 【Linux】进程控制


    一、再识fork()

    对于fork我们是比较熟悉的了,现在我们可以在来看看fork👇

    • fork()函数两个返回值问题

    fork()函数的实现在操作系统内部,函数准备return的时候核心代码已经执行完,子进程早已经被创建,并且可能在OS的运行队列中,准备被调度。

    fork之后,有两个执行流,父子进程代码是共享的,所以return会被调度两次,被父子进程各自执行return的。

    • 理解父进程返回pid,给子进程返回0

    父亲只有一个,孩子可以有多个,这是现实的问题,孩子找父亲具有唯一性

    所以给父进程返回子进程pid便于标识子进程这很好解释了父进程返回pid的问题

    • 同一个id值,怎么会有两个不同的值,让if和else if执行

    返回的本质是写入,所以,谁先返回谁先写入id,因为进程具有独立性,会发生写时拷贝,地址一样,但是内容不一样。

    **这是我们在进程地址空间时候所说的。**这也很好理解

    fork常用法:1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。2.一个进程要执行一个不同的程序。

    当然,fork也会调用失败:1.系统中有太多的进程。2.实际用户的进程数超过了限制.

    自己可以去试一试:

    image-20221119135446248

    对于fork我们就说到这里,接着往下走。


    二、退出码

    main函数的return 0在系统上叫做进程退出时对应的退出码,标记进程执行的结果是否正确

    我们如何找到写的代码完成的任务的结果如何?进程退出码,可用echo $?查询:

    image-20221119211807893

    到了这,有一个问题:那该如何设定main函数的返回值?如果不关心进程退出码,return 0即可,如果关心进程退出码的时候要返回特定的数据表明特定的错误

    退出码的意义:0表示成功,成功只有一个。非0表示失败,失败有多种情况,非0是几,表示不同的错误,不同的数字,表示不同的错误。同时,退出码一般都有对应的退出码的文字描述,可以自定义也可以使用系统的映射关系。>比如之前学过的strerror,我们直接来看一看就知道了:

    image-20221119212941300

    image-20221119213904563


    三、进程终止

    进程退出情况:1.代码运行完结果正确,2.代码运行完结果不正确,3.代码运行完程序异常,退出码无意义这也很好理解

    进程常见退出方法:

    从main函数return返回。任意地方调用exit(code)。这两种我们都太熟悉了,这里就不展开说了

    此外,还有另外一种_exit(),这里我们提一下:

    image-20221119222652618

    image-20221119222912297

    image-20221119222951736

    问题:exit()和_exit()的区别

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

    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
        printf("hello world");
        sleep(1);
        exit(1);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
        printf("hello world");
        sleep(1);
        _exit(1);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    同样的一段代码,exit()和_exit()的结果却是不同。对于exit():结果会打印出来,对于_exit():结果没有显示,这是因为缓冲区的缘故。

    所以exit()终止进程,会主动刷新缓冲区,_exit()终止进程,不会刷新缓冲区。用户级的缓冲区(doge)对于缓冲区在哪的问题后面涉及到在细谈

    image-20221124102436591


    四、进程等待

    我们知道,进程有一种Z(僵尸)状态,Z状态是一个问题:子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏 ,另外,进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程 ,最后,父进程派给子进程的任务完成的如何,我们需要知道, 如何去解决❓通过进程等待的方式进行解决僵尸进程问题

    进程为什么要等待

    1.父进程通过进程等待的方式,回收子进程资源

    2.获取子进程退出信息

    进程等待的方法

    • wait

    返回值:成功返回被等待进程pid,失败返回-1。
    参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

    话不多说,让我们来见一见wait

    image-20221119231914550

    image-20221119234759625

    image-20221119234956040

    • waitpid

    image-20221120000041776

    返回值:正常返回的时候waitpid返回收集到的子进程的进程ID ,如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

    参数:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程

    status:wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充 ,如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待

    image-20221124103203405

    次低8位:退出状态((>>8)&0xFF)。低7位:终止信号(&0x7F)。若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

    image-20221120092350765

    image-20221120092423780

    image-20221120092854912

    对应的错误:

    image-20221120093014284

    僵尸进程退出的时候对应信息放在哪:

    子进程和父进程有对应的pcb信息,父进程调用waitpid,子进程退出的时候把对应的代码和信号保存起来,保存到PCB。而waitpid是系统调用,以操作系统身份执行代码,找到子进程,把传入的status传入子进程里面,把代码和退出信号设置进status,设置完毕之后把值输入status。也就是说,等待的本质是检测子进程退出信息,将子进程退出信息通过status拿回来。所以最终看到了status的结果

    我们可以来看一看tash_struct,找到退出码和退出信息:

    image-20221120125142953

    总结来说,子进程退出变成僵尸,会把自己的退出的结果写入自己的task_struct,wait/waitpid是一个系统调用,OS有能力去读取子进程的task_struct

    对此,我们对于wait和waitpid有了初步的了解。

    但是,对于获得子进程的退出结果,我们可以不采用位操作进行,Linux提供了对应操作的宏

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

    int main()
     {
          pid_t id = fork();
          assert(id!=-1);
          if(id==0)
          {
              int cnt = 50;
              while(cnt)
              {
                  printf("这是子进程pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
                  sleep(1);
              }
              exit(10);
          }
          int status = 0;
          int ret = waitpid(id,&status,0);
          if(ret>0)
          {
              //判断是否正常退出
              if(WIFEXITED(status))
             {
                  //判断结果
                 printf("exit code:%d\n",WEXITSTATUS(status));
             }
              else{
                  printf("child exit:not normal!\n");
             }
          }
             // printf("wait success,exit code:%d,sig:%d\n",(status>>8)&0xFF,status & 0x7F);
          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

    image-20221120231910859


    五、进程的阻塞与非阻塞

    阻塞等待(0):父进程调用wait/waitpid等子进程时,直到子进程退出,这是阻塞时等待

    非阻塞等待(WNOHANG):检测状态,如果没有就绪父进程检测之后立即返回。每一次非阻塞等待都是一次,多次非阻塞等待称为轮询的过程。

    我们看一看非阻塞等待的代码实现:

    image-20221123075156744

    image-20221123075257576

    非阻塞不会占用父进程的精力,可以在轮询期间,让父进程干别的事情,这里可以简单举个例子就能明白:

     #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define NUM 10
    typedef void (*func_t)();
    func_t handlerTask[NUM];
    void task1()
    {
        printf("handler task1\n");
    }
    void task2()
    {
        printf("handler task2\n");
    }
    void task3()
    {
        printf("handler task3\n");
    }
    void loadTask()
    {
        memset(handlerTask, 0, sizeof(handlerTask));
        handlerTask[0] = task1;
        handlerTask[1] = task2;
        handlerTask[2] = task3;
    }
    int main()
    {
        pid_t id = fork();
        assert(id != -1);
        if (id == 0)
        {
            int cnt = 10;
            while (cnt)
            {
                printf("这是子进程pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt--);
                sleep(3);
            }
            exit(10);
        }
        loadTask();
        int status = 0;
        while (1)
        {
            pid_t ret = waitpid(id, &status, WNOHANG);
            //WNOHANG:非阻塞:子进程没有退出,父进程检测之后立即退出
            if (ret == 0)
            {
                //waitpid调用成功&&子进程没退出
                //子进程没有退出,我的waitpid没有等待失败,仅仅检测到而来子进程没有退出
                printf("wait done,but child is running...parent running other things\n");
                for (int i = 0; handlerTask[i] != NULL; i++)
                {
                    handlerTask[i]();//回调
                }
            }
            else if (ret > 0)
            {
                //waitpid调用成功&&子进程退出
                printf("wait success,exit code:%d,sig:%d\n", (status >> 8) & 0xFF, status & 0x7F);
                break;
            }
            else
            {
                //waitpid调用失败
                printf("waitpid call failed\n");
                break;
            }
            sleep(1);
        }
        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
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    image-20221123081251490


    五、进程程序替换

    我们知道:创建子进程可以1.让子进程执行父进程代码的一部分2.让子进程执行一个全新的程序

    我们先来看一看替换函数

    image-20221123092609879

    #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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    废话不多说,先来单独见一见用法,我们以execl为例子:

    image-20221123100802417

    image-20221123100820165

    对于替换函数,我们需要注意到:execl系列的函数结尾以NULL结尾

    同时,这里为什么第二个printf输出语句没有执行?

    替换原理

    用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数
    以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动
    例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

    简单来说程序替换的本质就是将指定程序的代码和数据加载到指定的位置,覆盖自己的代码和数据。进程替换的时候并没有创建新的进程。printf也是代码,在exec之后,exec执行完毕之后代码已经全部被覆盖,开始执行新的代码,所以第二个printf就无法执行了。

    对于返回值问题:

    这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,和接下去的代码无关了。如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

    到了这,我们可以通过创建进程的方式结合替换函数来看看:

    image-20221123205117926

    image-20221123205137888

    因为进程具有独立性,所以这里的替换并不会影响父进程。通过虚拟地址空间以及页表保证进程独立性,一旦执行流想替换代码或者数据就会发生写时拷贝。

    image-20221124105657529

    同时,对于其他替换函数,如何记住用法:

    l(list) : 表示参数采用列表
    v(vector) : 参数用数组,将所有的执行参数,传入数组中,统一传递不用使用可变参数
    p(path) : 有p自动搜索环境变量PATH
    e(env) : 表示自己维护环境变量

    • execlp

    image-20221123213851443

    • execv

    image-20221123214547683

    • execvp

    image-20221123214712312

    • execle

    image-20221123234128898

    image-20221123235250225

    这里的系统的环境变量是null,这其实很好理解,被调起来的程序获得了环境变量,这也说明了环境变量具有全局性。如何同时获得系统的环境变量?putenv

    image-20221124001021375

    前面这些都是执行系统命令,如何执行自己写的程序:

    image-20221123222409676

    注意:进程执行的时候,execl先执行,main后执行。execl系列函数将程序加载到内存中,所以Linux的execl接口是加载器,所以是先加载后执行,main也是函数也要被调用,通过execl/系统传参给main

    程序替换中execve是系统调用,其他都是封装,目的是为了有更多的选择性:

    image-20221124080454530

    image-20221124110627435

  • 相关阅读:
    如何优雅的消除系统重复代码
    offsetof宏计算某变量相对于首地址的偏移量
    基于Python实现的一个发送程序和接收程序
    SpringBoot整合Mybatis-plus
    机器学习分类
    Java中的图数据库应用:Neo4j入门
    一文读懂开源大数据OLAP
    【工程实践】Docker使用记录
    ZooKeeper 的应用场景?
    Apollo第二讲—apollo自动驾驶调试及仿真实践
  • 原文地址:https://blog.csdn.net/weixin_60478154/article/details/128014445