• 进程控制(跑路人笔记)


    前言

    这部分我们讲子进程和父进程的各种联系, 学习进程等待,学习进程替换,学习进程终止和?环境变量

    进程创建

    fork简单介绍

    我们可以使用fork();函数进行创建我们的子进程。

    还是重新简单介绍一下fork();

    对父进程返回子进程pid

    对子进程返回0

    错误时返回-1

    我们在进程地址空间(跑路人笔记)中知道了我们fork();创建进程的过程。

    • 分配新的内存块和内核数据结构给子进程。

    • 将父进程的数据结构拷贝给子进程

    • 添加子进程到系统进程中

    • fork返回开始调度

    写时拷贝

    我们在进程地址空间(跑路人笔记)中详细的解释了发生过程。

    其实就是我们的父进程和子进程在数据和代码是共享的,当一个进程需要对一部分进行修改的时候我们就会发生写时拷贝,及将被修改的部分重新开辟一段空间将它的值进行保存由此来保证进程的独立性。

    我用图片来解释一下

    image-20221025201232335

    eip寄存器

    为什么我们的子进程是从父进程fork位置开始进行呢?

    fork生成的子进程其实是共享所有的代码,但是子进程只能从fork位置的出发。
    CPU里有程序计数器eip,他会保存正在实行指令的下一条指令。
    eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行。

    抱歉这个我不知道如何演示。大家看字面意思理解吧。

    进程终止

    每个进程都是要终止的。

    正常终止:

    • 从main函数结束(return x;
    • 调用exit(x)函数
    • 调用_exit(不常见

    从main函数中return 的值和exit(x)这里的值都被我们的操作系统接收(如果父进程是bash的话)

    为什么我们都喜欢在main函数的代码最底的时候reurn 0;?

    这只是一个默契 我们规定0为正确的代码运行结束而非0为错误的运行结果。

    exit_exit 有什么区别呢?很简单_exit不会刷新缓冲区。

    我们来演示一下:

    注:我们printf不使用\n是不会刷新缓冲区的.这些在缓冲区的数据先保存在我们的缓冲区当我们程序退出时才会被刷新出来.而_exit不会刷新缓冲区.看演示:

    int main()
    {
        printf("12345");
        exit(0);//代码唯一改变就是这里是_exit还是exit.
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们先使用exit();

    image-20221025213747064

    我们需要的12345被打印了出来

    我们再使用一下_exit;

    image-20221025213821146

    并没有被打印.


    在Linux中我们有一个环境变量用于保存最近一次进程的返回值?

    我们可以使用echo $? 来查看它的值(当然其他可以查看环境变量的方法都可以)

    int main()
    {
        return 3;
    }
    
    • 1
    • 2
    • 3
    • 4

    image-20221025213422350

    异常退出

    • ctrl+c,信号终止

    终止时内核做了什么

    首先我们要知道我们

    进程 = 内核数据结构(task_struct+mm_struct……) + 进程代码+ 数据

    我们创建进程的时候创建的内核数据结构和代码等数据我们在终止的时候会将他们释放掉。

    但是我们Linux会在终止的时候进行优化可能将内核数据结构不删除不释放,而保存下来用于下一个进程创建.(及省去下一个进程创建内核数据结构的时间).

    优化结构我们用一个链表来将我们废弃的内核数据结构保存下来.这些数据结构一般使用分派器(slab分派器)进行保存(现在不讲比较难)

    进程等待

    我们的父进程是通过进程等待的方式来得到我们的子进程的退出信息的。

    这个退出信息非常重要。我们可以从退出信息得到子进程是否是正常退出。以及退出码等。

    而且

    • 子进程退出如果父进程不管,就可以变成僵尸进程,进而造成十分麻烦的内存泄漏(毕竟僵尸进程已经死亡我们无法杀死)
    • 父进程通过进程等待的方式,回收子进程资源,获得子进程退出信息。

    wait

    我们使用wait函数来解决子进程Z状态(僵尸状态)。让子进程进入X状态(死亡状态)-----这里的状态我们之前讲过

    pid_t wait(int *status);

    image-20221025222606235

    wait会让我们的父进程等待我们子进程完毕.

    当然我们不显示调用wait父进程也会等待,我们调用了之后就可以控制进程等待的位置了.

    如果我们的父进程wait我们的子进程,子进程在结束后就会进入僵尸状态(Z状态),当等待完成后,子进程就会结束死亡.

    我们用下面代码进行测试

    #include
    #include
    #include
    #include
    int main()
    {
        int id = fork();//创建子进程
        if(id == 0)
        {
            while(1)
            {
                printf("我是子进程我正在运行 PID:%d\n",getpid());
                sleep(1);
            }
        }
        else if(id>0)
        {
            printf("我是父进程我将在40秒后结束子进程我的pid: %d\n",getpid());
            sleep(40);//保证我们将子进程杀死后父进程还没有等待他.
            int status;
            int pid = wait(&status);//status值在后面waitpid部分讲.
            sleep(20);//等待完成后继续睡眠20s方便观看
            printf("等待完成子进程的pid为%d status值为%d\n",pid,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

    第一阶段,未杀死子进程.

    image-20221025230259963

    image-20221025230323582

    使用kill杀死子进程.

    第二阶段:子进程因为没有被等待是Z状态.

    image-20221025230427715

    第三阶段:子进程被等待成功,父进程睡眠之后死亡.

    image-20221025230508539

    所以我们说wait是用来解决子进程的僵尸问题的也不为过.

    waitpid

    首先说明啊:wait和waitpid都是系统调用接口.

    image-20221025230814434

    头文件和wait相同但是,waitpid相比于wait更加自由.

    下面我们来介绍一下他的每个接口.

    • pid: >0是多少就等待那个子进程(一个父进程有多个子进程.)
      • -1:等待任意进程
    • status: 这个参数是一个输出行参数.通过调用函数从函数内部拿出特定的数据(之前做题经常有遇到,尤其是C语言的题目.)
      • 从哪里拿出来呢? 答:从子进程的task_struct中拿出子进程的退出码.
    • options: 0:表示阻塞等待.宏(WNOHANG)表述非阻塞等待------后面会讲👍

    返回值:等待成功返回值就是子进程的pid, 等待失败返回-1;

    pid

    传-1值表示waitpid等待任意一个子进程,至于这个任意是哪一个,我们后面讲.

    image-20221030215838308

    status

    我们来讲status.

    这个参数既然是输出性参数,那他的数据是什么呢?

    我们使用下面代码来向大家演示.

    #include
    #include
    #include
    #include
    int main()
    {
        int id = fork();
        if(id == 0)
        {
            printf("我是子进程我在5秒后结束我的pid:%d\n",getpid());
            exit(33);
        }
        else if(id>0)
        {
            int status;
            int pid = wait(&status);
            printf("等待完成子进程的pid为%d status值为%d\n",pid,status);
        }
        return 0;
    }
    //输出结果为:等待完成子进程的pid为7946 status值为8448.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    就很奇怪.我们的status值却是好像对我们无意义的值.

    其实我们的status值是通过位图的方式给我们信息的.

    这里的位图我们只需要关心低16比特位即可(int有32个比特位).

    分为两种情况

    • 正常终止:及return终止或exit _exit终止.
      • image-20221026112030066
      • 上图就是正常终止低16位我们需要的信息其实我们只要15~8的部分即可.
      • 通过(status>>8)&0xff就可得到.我们在代码中将其打印出来.
      • image-20221026112318645
      • 我们就得到了正常终止时子进程的返回值.
    • 被命令杀死如kill命令
      • image-20221026112501233
      • core dump标志会在后期的文章讲解.
      • 我们这个终止信号位置是在低7比特位的位置我们通过status&0x7f的方式来的得到他的值.
      • 让我们将子进程进行死循环然后使用kill命令来看看是否属实
      • image-20221026121931418
      • 如图啊,我们使用kill -9来完成的终止信号就是9来试试其他的数字.
      • image-20221026122108744
      • 这个数字当然是有意义的我们使用kill -l来查看每个数字的意思.
      • image-20221026122155161
      • 我们之前的代码出现错误了如野指针等问题,然后进程的退出其实就是我们的操作系统将我们的进程通过发送命令的方式结束了.

    当然我们直接使用位操作有些麻烦,我们C语言有维护的宏来帮助我们使用status

    • WIFEXITED(status) : 若为正常终止返回真.查看进程是否为正常退出
    • WEXITSTATUS(status): 若WIFEXITED为非0,提前子进程退出码.

    options

    0选项

    有选项让我们,分为阻塞等待和非阻塞等待.0表示阻塞等待

    阻塞等待很简单,在我们的子进程结束之前,也就是说在我们的父进程不成功等待到我们的子进程之前父进程是不会进行任务的.

    证明如下:

    image-20221030180653476

    可以看出直到我们kill了子进程我们的父进程的后续命令(printf)才继续进行.

    //代码如下
    #include
    #include
    #include
    #include
    int main()
    {
        int id = fork();
        if(id == 0)
        {
            while(1)
            {
                printf("我是子进程我死循环执行,我的pid:%d\n",getpid());
                sleep(1);
            }
            //exit(33);
        }
        else if(id>0)
        {
            //printf("我是父进程我将在40秒后结束子进程我的pid: %d\n",getpid());
            //sleep(40);
            
            int status;
            int pid = waitpid(id,&status,0);
            //sleep(20);
            printf("等待完成子进程的pid为%d status值为%d 子进程退出码为:%d 子进程终止信号为:%d\n",pid,status,(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
    WNOHANG

    非阻塞等待,这种等待我们需要通过循环进行反复等待,就是父进程循环的干一些活,干完之后再循环回去看看是否可以直接等待完成(及子进程完毕),不可以就继续干他的活.可以就结束.我们来看看.

    看看我们的代码

    #include
    #include
    #include
    #include
    int main()
    {
        int id = fork();
        if(id == 0)
        {
            while(1)
            {
                printf("我是子进程我死循环执行,我的pid:%d\n",getpid());
                sleep(1);
            }
            //exit(33);
        }
        else if(id>0)
        {
            int status = 0;
            int pid = 0;
            while(!pid)//子进程还未死亡的时候waitpid返回值为0,成功了就是返回子进程pid
            {
                pid = waitpid(id,&status,WNOHANG);
                printf("干一些活哈\n");
                sleep(1);
            }
             printf("等待完成子进程的pid为%d status值为%d 子进程退出码为:%d 子进程终止信号为:%d\n",pid,status,(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

    image-20221030214514281

    至于让父进程干什么活就很自由了.

    比如可以用数组里面装几个函数指针让后依次调用函数来让父进程资源也不闲着.这很资本家👍

    如何理解我们的父进程阻塞.

    类似于其他进程阻塞,我们父进程的进程阻塞等待其实就是将我们的父进程投入到我们的等待队列中,当我们的子进程完成了之后就是条件就绪就可以继续将我们的父进程放到运行队列运行了.

  • 相关阅读:
    【力扣】35. 搜索插入位置
    idea灰屏问题
    Java 基础_框架阶段核心面试题
    LINUX 下如何检查服务器是虚拟机,还是物理机
    代码随想录算法训练营总结 | LeetCode
    04 动力云客之登录后获取用户信息+JWT存进Redis+Filter验证Token + token续期
    海藻酸钠-聚丙烯酸|PAA-alginate|海藻酸钠-聚乙二醇-聚丙烯酸
    通用密钥,无需密码,在无密码元年实现Passkeys通用密钥登录(基于Django4.2/Python3.10)
    dhclient.conf配置参数timeout和retry的含义
    AP1272 线性稳压IC 电子地称 无绳电话 水表 电表芯片
  • 原文地址:https://blog.csdn.net/qq_61434711/article/details/127748320