• Linux下的进程控制


    前面我说了进程的概念。既然学了概念,我们还要学会如何使用它
    在这里插入图片描述

    1. 进程创建

    1.1 fork函数初识

    linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
    在这里插入图片描述
    进程调用fork,当控制转移到内核中的fork代码后,内核做了什么呢
    1.分配新的内存块和内核数据结构给子进程
    2.将父进程部分数据结构内容拷贝至子进程
    3.添加子进程到系统进程列表当中
    4.fork返回,开始调度器调度

    在这里插入图片描述
    所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。谁先执行完全由调度器决定

    在进程地址空间我们说过:进程具有独立性,代码和数据必须是独立的。数据独立是可以发生写时拷贝的。而代码是只读的。
    下面,有一个问题:fork之后,是否只有fork后面的代码是被父子进程共享的呢
    一般情况下,fork之后父子共享所有的代码后面会说一下特殊情况。在这里,我们要区分:子进程执行的后续代码!=共享的所有代码,只不过子进程只能从这里开始执行。
    为什么呢?在CPU里有一个epi:程序计数器(PC指针),它的工作是:保存当前正在执行指令的下一条指令
    在这里插入图片描述
    像CPU是如何知道程序运行到哪里了,遇到循环函数时是如何执行的,都是通过修改epi来执行的。
    当创建子进程时,epi是保存在父进程的上下文中。然后把epi拷贝给子进程,子进程便从该epi所指向的代码处开始执行了。
    fork之后,操作系统做了什么
    我们知道:进程=内核的进程数据结构+进程的代码和数据
    所以,操作系统创建子进程的内核数据结构(struct task_struct+struct mm_struct+页表)+代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立

    1.2 写时拷贝

    通常,父子代码共享,父子不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
    在这里插入图片描述
    页表不仅仅有映射功能,还具有读写属性。当任意一方试图写入,页表就会改变读写属性,将只读改成正常。写时拷贝本身就是OS的内存管理模块完成的。

    为什么要写时拷贝?创建子进程的时候,就把数据分开,不行吗?
    1.父进程的数据,子进程不一定全用,即使使用,也不一定全部写入。这样就会有浪费空间的嫌疑。
    2.最理想的情况:只有会被父子修改的数据,进行拷贝分离。不需要修改的共享即可。但是从技术角度难以实现。
    3.如果fork的时候,就无脑拷贝数据给子进程,会增加fork的成本(内存和时间)。
    最后,写时拷贝只会拷贝父子修改的,变相的就是拷贝数据的最小成本。同时只有真正在使用的时候,才给你,变相的提高内存的使用率。
    所以,根据这些原因,最终采用了写时拷贝。

    2. 进程终止

    2.1 进程退出场景

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

    下面,有些问题:在C/C++时,main函数是入口函数,我们为什么要写return 0?return 0给谁return?为什么是0,其它值呢?
    当我们一个进程代码,我们需要知道结果是否正确。其实return 0里的0代表的意思就是成功,非0代表的就是失败。而失败了,我们就需要知道失败的原因。所以,非0标识不同的失败原因这些数字也叫做进程退出码,表示进程的退出的信息,而这些退出信息是让父进程读取的
    举个例子:
    在这里插入图片描述
    然后我们来编译运行一下:可以通过 echo $? 查看最近一次执行完毕的进程退出码
    在这里插入图片描述
    我们可以看到打印的是123。
    在这里插入图片描述
    从这我们也可以看到,成功了退出码就为0,其它数字就代表失败。
    一般来说,失败的非零值我们该如何设置呢
    是由我们自己来自定义的。

    2.2 进程常见退出方法

    1. 在main函数中return
    2. 在自己的代码任意地点中,调用exit函数
    3. _exit函数

    第一点我们就不多说了,我们来说一下第二点:
    我们先看一下exit函数:
    在这里插入图片描述
    它的作用就是进程退出。
    在这里插入图片描述
    我们运行一下:
    在这里插入图片描述
    可以看出进程的退出码是111,它在fun函数里直接退出了。这里exit(n)等同于执行return n
    然后,我们再说一下第三点,我们来看一下_exit函数:
    在这里插入图片描述
    下面来说一下exit和_exit的区别:
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    我们再看一下_exit的情况:
    在这里插入图片描述
    运行结果如下:
    在这里插入图片描述
    什么都没有。这是因为:
    exit终止程序,刷新缓冲区。
    _exit直接终止进程,不会有任何刷新操作。

    如下图所示:
    在这里插入图片描述
    那么关于终止,内核做了什么呢
    我们知道:进程=内核结构+进程代码和数据
    首先,进程会进入Z状态,然后父进程等待子进程的结果信息。最后,把进程设为X状态,释放进程的内核结构和进程的代码和数据。
    但是像进程的内核结构(task_struct&&mm_struct),操作系统可能并不会释放该进程的内存结构。它会把这个结构保留下来,在某一个地方进行管理。当下次再创建这个进程时,就不需要重新开辟空间了。这样效率就会提高。这个被叫做内核的数据结构缓冲池,slab分派器

    3. 进程等待

    3.1 进程等待必要性

    第一个原因:之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题。进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程,进而造成内存泄漏。

    第二个原因:父进程派给子进程的任务完成的如何,我们需要知道。

    3.2 进程等待的方法

    wait方法和waitpid方法都是系统调用,下面就来简单介绍一下:

    3.2.1 wait方法

    在这里插入图片描述
    下面教大家如何使用:
    在这里插入图片描述
    这里的意思是:父子进程一起运行,父进程先休息10秒,在这10秒内把子进程杀死,此时可以看到子进程的状态是Z。当10秒后,子进程被父进程回收,监控就看不见子进程了。

    然后再创建一个脚本监控:
    在这里插入图片描述
    每隔1秒监控一下数据,用分割符分开。
    看下面的运行结果:
    在这里插入图片描述
    我们杀死之后,用ret接受成功了。
    在这里插入图片描述
    从这里也可以看出子进程从S状态变成Z状态。
    在这里插入图片描述
    然后等待成功后,子进程终止(X状态看不到)。

    3.2.2 waitpid方法

    在这里插入图片描述
    参数:返回值
    >0,等待子进程成功,返回值是子进程的进程pid
    =-1,等待失败

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

    3.2.2.1 status参数讲解

    参数:status
    是一个输出型参数。输出型参数通过调用该函数,从函数内部拿出特定的数据
    那么到底是拿什么呢?怎么拿的呢?
    在这里插入图片描述
    当子进程的代码执行到return或者exit时,子进程就退出了。然后子进程会将自己的退出信息写入自己的task_struct。子进程进入Z状态。
    在这里插入图片描述
    父进程会通过wait/waitpid函数从子进程的拿退出信息码,给输出型参数*status。如果子进程没有退出,那么父进程就是阻塞等待。
    在进程终止有一个异常退出的情况没有细说,这个status也能获取信号
    status不能简单的当作整形来看待,可以当作位图来看待(只研究该整数的低16个比特位),具体细节如下图:
    在这里插入图片描述
    在正常终止的情况下,这个整数的次8位存储的是进程退出码。我们来验证一下:
    在这里插入图片描述
    这个程序的意思是:子进程运行5次后,就退出。然后父进程用status去接收子进程的进程退出码。先把得到的整数右移8位,然后按位与0xFF,得到的就是次8位。
    在这里插入图片描述
    从运行结果来看验证成功。在这里,我们用的是按位的方法来计算,在Linux中,其实已经有帮我们写好的宏。
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    在这里插入图片描述
    运行结果如下也是可以的。
    在这里插入图片描述

    然后status的最后7位代表的是终止信号。
    终止信号的意思是:如果进程异常退出,是因为这个进程收到了特定的信号
    命令:kill -l的意思是查看全部信号
    在这里插入图片描述
    我们再举个例子来验证一下:
    在这里插入图片描述
    子进程是死循环,但里面使用了野指针。我们来看一下运行结果:
    在这里插入图片描述
    11是:SIGSEGV也就是段错误。为什么这里的退出码为0呢?因为一旦进程出现异常,只关心退出信号,退出码没有任何意义

    3.2.2.2 options参数讲解

    参数:options
    这个参数分为阻塞等待和非阻塞等待。
    options=0,阻塞等待。在等待的时候,让它阻塞(软件阻塞)
    当我们调用某些函数时,因为条件不就绪(可能是任意的软硬件条件),需要我们阻塞等待(如果子进程没有结果,一直等待下去)。本质:就是当前进程自己变成阻塞状态,等条件就绪的时候,才被唤醒。
    在这里插入图片描述
    那么,父进程阻塞的一个具体过程就是:
    父进程的task_struct状态由R变成S,从运行队列投入到等待队列,等待子进程退出。然后子进程退出,本质是条件就绪,OS就会把父进程从等待队列放到运行队列,然后父进程的状态由S变成R

    那么非阻塞状态又是什么呢
    非阻塞状态就是父进程等待子进程时,它可以先不着急一直等待下去,可以先做一些自己其它的事情,过一会来看子进程有没有结果。如果没有再去做自己的事情,然后再过来看看子进程情况。这样一次等待的过程就叫做非阻塞等待,那么多次调用非阻塞接口就叫做轮询检测。
    options=WNOHANG,非阻塞等待。

    那么waitpid真正的返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID。
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

    下面,我就写一个基于非阻塞等待的轮询检测方案:
    在这里插入图片描述
    在这里插入图片描述
    然后我们来看一下运行结果:
    在这里插入图片描述
    这个过程就是非阻塞等待的轮询检测。如果我们想让父进程真正的做一些事情呢?
    在这里插入图片描述
    typedef有两种用法:
    一、定义已有类型的别名
      typedef 类型 定义名;
    二、创建一个新的类型
    typedef 返回值类型 新类型名(参数列表);

    然后这个写的是什么意思呢?就是一个vector里面存了一些函数,然后父进程做其它事情时就去调用这些函数。因为我们用的是auto,所以编译的时候要加上-std=c++11。
    在这里插入图片描述
    我们现在运行一下:
    在这里插入图片描述
    这样父进程在非阻塞等待时,就去做它自己的事情了。

  • 相关阅读:
    verilog实现DDS波形发生器模块,可实现频率、相位可调,三种波形
    FlinkCDC 菜鸟教程-文章目录
    字符串函数(二)
    Kafka 架构
    部署 k8s 集群
    支持向量机(SVM)案例分析
    Vue——ref/reactive、toRefs、计算属性、监视、Vue3响应式原理、Vue3生命周期
    你的项目需要自动化测试吗?
    ES6新增循环对象的四种方法(通俗易懂)
    Java update scheduler
  • 原文地址:https://blog.csdn.net/qq_52154068/article/details/126577860