• Linux——进程控制


    fork()

    我们之前是接触过这个函数的,这个函数我们之前是要来创建子进程的函数,我们今天来深入学习一下这个函数:

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

    #include 
    pid_t fork(void);
    //返回值:自进程中返回0,父进程返回子进程id,出错返回-1
    
    • 1
    • 2
    • 3

    返回值:子进程中返回0,父进程返回子进程id,出错返回-1。

    那么fork做了哪些事情呢?

    1. 创建子进程: 调用 fork() 函数时,操作系统会创建一个几乎与父进程完全相同的子进程。这意味着子进程继承了父进程的内存、文件描述符、环境变量等。
    2. 返回值: fork() 函数在父进程中返回子进程的进程ID(PID),而在子进程中返回0。这是为了让父进程和子进程能够区分彼此。通常,父进程可以通过检查返回值来确定它是父进程还是子进程。
    3. 复制父进程: 子进程是通过复制父进程的地址空间来创建的,这包括父进程的代码、数据和堆栈。子进程独立于父进程运行,但它们之间的数据不会共享,除非使用进程间通信(Inter-Process Communication,IPC)机制。
    4. 并行执行: 一旦子进程创建成功,父进程和子进程可以并行执行,各自独立运行。

    我们可以写段代码来验证一下:
    在这里插入图片描述在这里插入图片描述
    我们可以看到第一个29374是父进程的pid,返回值29375是子进程的id,而下面的29375是子进程,返回值是0。

    这个过程是怎样的呢?
    在这里插入图片描述
    在调用 fork() 函数后,父进程会继续执行其后的代码。fork() 函数创建了一个新的子进程,该子进程是在调用 fork() 时父进程的几乎完全复制。父进程和子进程将几乎同时开始执行下面的代码。

    缺页中断

    我们之前讲过进程地址空间,我们说父进程和子进程都是一个相同的虚拟地址,然后映射到不同的物理内存中。
    但在子进程修改自己的值之前,父进程和子进程都是映射到相同的物理内存上,那就有一个问题:操作系统怎样知道在哪个时间完成对子进程的修改呢?

    其实父子进程任意一方想修改的时候,这个时候会发生写时拷贝,同时父子进程的权限都会改为只读

    这个时候我们修改,就会报错,但是操作系统过来看看,发现原本是读写的权限,被改为了只读,就会判定这个不是错误,会在物理内存中重新找一块空间重新建立映射关系到父或子进程。但这个过程我们是不知道的。
    在这里插入图片描述
    我们称这样的机制为缺页中断

    缺页中断(Page Fault)是计算机操作系统中的一个重要概念,它发生在程序试图访问虚拟内存中的一个页面(或页)时,但该页面当前不在物理内存中。当发生缺页中断时,操作系统会介入处理,以便将需要的页面加载到物理内存中,从而使程序能够继续执行。

    下面是关于缺页中断的一些重要信息:

    1. 虚拟内存和分页系统: 大多数现代操作系统使用虚拟内存和分页系统来管理内存。这意味着每个进程看到的内存都是虚拟内存,而不是物理内存。虚拟内存被划分成固定大小的页面(通常为4KB),这些页面被映射到物理内存。
    2. 页面不在物理内存中: 当程序尝试访问一个在虚拟内存中但不在物理内存中的页面时,会触发缺页中断。这可能是因为该页面从物理内存中换出(置换)以腾出空间给其他页面,或者是因为该页面是程序首次访问的部分。
    3. 操作系统响应: 当发生缺页中断时,操作系统会介入,根据程序访问的虚拟地址,找到相应的页面,并将其加载到物理内存中的合适位置。这通常涉及从磁盘上的页面文件或其他存储设备中读取数据。一旦页面被加载到物理内存中,程序可以继续执行,就好像该页面一直在内存中一样。
    4. 页表: 缺页中断的处理依赖于页表,页表是操作系统维护的数据结构,它将虚拟地址映射到物理地址。操作系统使用页表来确定缺页中断的地址映射关系,以便正确地加载页面。
      性能影响: 缺页中断是计算机性能的重要因素之一。频繁的缺页中断会导致程序执行速度减慢,因为加载页面需要时间。因此,操作系统会尽量减少缺页中断的发生,通过使用高效的页面置换算法和合理的内存管理策略来优化性能。

    那么什么时候会发生缺页中断呢?

    1. 首次访问页面: 当程序首次访问虚拟内存中的一个页面时,该页面通常不在物理内存中,因此会触发缺页中断。这是因为操作系统采用了一种"延迟加载"策略,只有在页面被首次访问时才将其加载到物理内存,从而节省内存空间。
    2. 页面被置换出: 如果操作系统需要腾出物理内存空间以容纳其他页面,它会选择一些不常用的页面进行置换(将其移出物理内存)。当程序再次访问这些页面时,会触发缺页中断,因为它们不再在物理内存中。
    3. 页面被交换到磁盘或其他存储设备: 某些操作系统具有交换(Swap)机制,这允许将页面从物理内存交换到磁盘或其他存储设备以释放内存。当程序尝试访问已经交换到磁盘上的页面时,会触发缺页中断,因为它们不在物理内存中。
    4. 多任务操作系统切换进程: 在多任务操作系统中,当操作系统决定切换执行另一个进程时,当前进程的页面可能会被置换出物理内存。当再次切换回该进程时,会触发缺页中断,因为所需的页面不再在内存中。
    5. 共享内存: 如果多个进程共享某些内存区域,而其中一个进程对共享内存进行了修改,而其他进程需要访问这个修改后的数据,可能会触发缺页中断。这是因为修改后的数据需要被加载到其他进程的物理内存中。

    我们的父子进程属于第5种情况。

    进程终止

    我们之前写程序的时候,总会在最后一行写:
    在这里插入图片描述
    其实我们都大概可以猜得到,这个0可能代表了状态,我们的父进程bash会接收这个返回值,我们可以用**$?**来查看这个返回值:
    在这里插入图片描述
    其实我们返回值,通常是想知道这个程序到底运行的怎么样。我们一般有三种状态:

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

    我们先来看前两种情况,我们一般来说,返回值为0表示运行完毕结果正确,非0值表示运行完毕,结果不正确。

    其中,不同的值对应不同的状态,我们可以用strerror来查看不同数字所对应的不同的退出状态。
    在这里插入图片描述
    在这里插入图片描述
    果然和我们猜的一样,0代表成功。其他数字都代表了不同的错误状态。
    我们把这些代表不同状态的数字叫做退出码

    其中 ** ?**这个字符会保存上一个操作的退出码:
    在这里插入图片描述
    但是这个可能不准,所以不能太依赖这个。

    有时候,程序运行并不一定会成功,这个时候我们可以把程序在执行过程中返回的数字储存起来,到最后我们可以打印出来看:

    这个用来储存的东西叫做errno
    在这里插入图片描述
    在这里插入图片描述

    进程异常

    我们之前讨论过,程序的三种状态跑完 正确,跑完 程序不正确,程序异常,这里我们来看看程序异常:
    程序异常,跟前面两种情况都不是很一样,前面两种情况,起码还有一个返回值(成功为0,不成功为非0),但是如果程序异常,直接结束,下面的代码直接不执行。
    我们来模拟一下最常见的异常:
    在这里插入图片描述
    在这里插入图片描述我们发现我们之前写的printf那一句话,没有机会执行,因为a /= 0的异常,程序直接终止掉了。
    这里程序终止的原因是因为,这个进程在出现异常时,会向操作系统发出异常的信号,让操作系统直接杀掉这个进程。
    我们看一下,会有哪些信号会让操作系统杀掉进程:
    在这里插入图片描述这些信号发给操作系统,操作系统就会杀掉这个进程。前面的数字代表了相应的异常,我们可以向操作系统发出相应的异常信号,帮我们杀掉某些进程:
    我们模拟一个正确的场景:
    在这里插入图片描述在这里插入图片描述
    我们向操作系统发送了25127有11号异常,操作系统会以这个11号异常的名义杀掉25127这个进程。
    最后退出码和信号就是两个数字,通过这两个数字我们可以推断这个程序的状态。

    exit

    我们之前讨论过退出码这个概念,那么如果在不同的部分退出,会有什么不同的现象呢?
    在这里插入图片描述现在我们函数中有一个退出码,主函数中有一个退出码,我们看最后程序是哪个退出码?
    在这里插入图片描述
    我们看到最后程序是以主函数的退出码退出的,那表明了一件事就是:函数中的退出码只是表示函数调用的结束,并不能代表最后程序的状态。

    我们之前也接触过** exit() **这个函数,我们来看看这个函数是不是和return n的效果是一样的:在这里插入图片描述
    在这里插入图片描述
    我们发现退出码变成了0,其实我们可以查一下exit的手册:
    在这里插入图片描述
    手册上面提到,exit可以引起一个正常进程的结束,我们exit中的这个参数就是我们程序返回的退出码,程序遇到exit直接结束而且以exit中的这个参数作为退出码的。

    _exit

    其实我们除了exit,还有一个函数跟它长得很像,_exit(),我们可以查一下它的手册:
    在这里插入图片描述
    这个_exit也是可以立即结束一个进程,我们来试试:
    在这里插入图片描述那么这个函数和exit有什么区别呢?我们来看看:
    在这里插入图片描述注意一下我们这里的printf是带有行刷新的,这个对于观察之后的现象有比较关键的作用。
    在这里插入图片描述
    现象很正常,我们现在把换行符去掉:
    在这里插入图片描述这时候一开始是什么也没有的:
    在这里插入图片描述
    三秒钟过后:
    在这里插入图片描述

    我们发现I am a process被刷出来了,说明exit是会强制刷新缓冲区的。
    那我们来看看_exit:
    在这里插入图片描述在这里插入图片描述也符合我们的预期,我们现在来试试将换行符去掉:
    在这里插入图片描述一开始也是什么也没有:
    在这里插入图片描述三秒钟过后:
    在这里插入图片描述我们发现了,_exit不会强制刷新缓冲区。

    其实exit是由_exit封装的,两者的区别一个是库函数,一个是系统函数之所以exit会刷新缓冲区,是因为缓冲区就在这个库里面,exit可以直接对这个库进行操作。

    进程等待

    进程等待呢,在我的理解下,就是进程等待相应资源的过程。
    那么为什么要进行进程等待呢?以我们现有的知识水平来看,有那么以下几点:

    1. 解决相应的僵尸进程的内存泄漏问题。
    2. 可以收集对应的子进程完成任务的状态。

    了解到上面两点之后,我们再来看看进程等待的函数wait和waitpid

    wait

    我们先来看看wait的手册:
    在这里插入图片描述返回值:
    在这里插入图片描述

    我们发现wait要传一个int*的指针参数,我们暂时先不管,先传NULL值使用一下:
    我们来写一段测试代码:
    在这里插入图片描述
    这里子进程结束了之后,父进程会休眠5秒,这个时候子进程就会变成僵尸进程,我们写一段监控脚本来监视一下:
    在这里插入图片描述我们看到在某个时刻子进程变成了僵尸进程。因为这时候父进程在睡眠,没有回收子进程。
    在这里插入图片描述在五秒结束之后,父进程回收了子进程,这个时候子进程的僵尸状态会被父进程回收。僵尸状态消失。
    这里顺便说一下,如果这里我们的父进程不睡眠的话,这时候我们的父进程会阻塞等待。直到回收子进程。

    waitpid

    了解了wait之后,我们来了解一下waitpid:
    在这里插入图片描述

    waitpid的一个参数是pit_t pid 这个可以表示我们要指定回收哪个进程,比如这个数字是12345,我就要回收12345号进程。
    后面两个参数我们暂时先不用管,用NULL和0先顶替着。
    我们测试只需改变这一行:
    在这里插入图片描述
    我们来测试一下:
    我们看到是和wait一模一样的:
    在这里插入图片描述

    status

    我们之前在wait和waitpid中有一个int* status 的指针,这个参数是干嘛的呢?其实这个参数是输出型参数,意思是我们先把这个参数输进去,之后它又会返回给我们:
    在这里插入图片描述我们可以来试试,这里为了测试,我们将子进程的退出码设为10:
    在这里插入图片描述
    测试一下:
    在这里插入图片描述
    我们看到status的返回值是2560,这跟我们的10差的有点远啊,这是为什么呢?
    其实我们的status被输入进去之后,被划分了区域:
    在这里插入图片描述
    我们的次低8位描述的是退出状态,第七位是core dump标志,剩下的位数表示终止信号。
    我们的0这个数字有32个比特位,就被分成了这样几个区域:

    如果我们想拿到我们的退出状态和终止信号的话我们得对我们的status进行一些位运算来帮助我们拿到真实的数字。

    在这里插入图片描述测试一下:
    在这里插入图片描述我们看到end state退出状态是10,退出信号是0,跟我们的逻辑是符合的。
    我们来试试程序异常会怎么样:
    在这里插入图片描述
    在这里插入图片描述

    WIFEXITED

    如果上面的方法有点过于复杂了,我们定义了一个WIFEXITED的宏帮我们判断退出状态:
    在这里插入图片描述在这里插入图片描述如果有异常,返回的值就不为1:
    在这里插入图片描述在这里插入图片描述

    多进程等待

    我们进程等待,我们可以多进程等待:
    在这里插入图片描述
    在这里插入图片描述

    阻塞等待和非阻塞等待

    我们waitpid还有最后一个参数没有讲解:options

    在这里插入图片描述
    这里我们的options我们有两种选择:

    0:阻塞等待
    WNOHANG:以非阻塞方式等待。

    在这里插入图片描述
    啥叫阻塞等待呢,阻塞等待就是操作系统很老实,在等待资源的过程中,它不会去做其他的事情,就乖乖的等待资源。非阻塞就不一样,在等待资源的过程中,抽空去做其他的事情。

    我们可以写一段代码来验证:
    在这里插入图片描述
    在这里插入图片描述
    我们也可以把父进程具体在做什么写的清楚一点:
    在这里插入图片描述

    进程替换

    我们之前创建的子进程都是父进程的一部分,现在我们想要让子进程独立于父进程,是一个全新的进程,那该怎么办呢?

    单进程的进程替换

    在进行进程替换前,我们的了解一下,进程替换的接口:
    在这里插入图片描述这些都是进程替换的接口,我们一个一个来试:

    execl

    我们看到execl是要指定文件路径,加上可变参数列表:
    在这里插入图片描述

    在这里插入图片描述
    我们发现我们写的程序可以和ls指令有相同的效果,并且我们发现我们最后一行的printf并没有打印出来,这是为什么呢?这个我们暂时先放在一边,我们先来看看多进程的进程替换:
    在这里插入图片描述在这里插入图片描述
    这里程序替换之后,我们的子程序就和原来的程序没有任何的关系了,变成了一个不依赖父进程的独立程序,所以在被替换之后,之后的代码子进程也不会管,所以最后一个printf没有打印,但是,子进程虽然不依赖父进程了。但是依然存在父子关系,所以父进程可以等待成功。

    我们可以获取一下execl的退出值:
    在这里插入图片描述在这里插入图片描述这个时候我们发现,没有返回值,因为进程都被替换了,不执行后面的代码了。
    如果我们输入指令的地址根本就是错的,或者这个指令本来就是错的:
    在这里插入图片描述这个时候就有返回值了,并且是-1:
    在这里插入图片描述

    execlp

    除了我们的execl这种方法之外,我们还有其他的方法来进行进程替换,比如,我们来看execlp:
    在这里插入图片描述这个的名字中带了个p,表示它是在环境变量中去找的
    在这里插入图片描述在这里插入图片描述

    execv

    在这里插入图片描述
    这里的argv是一个指针数组,用于存放我们要执行的命令:
    在这里插入图片描述
    在这里插入图片描述
    现在我们都替换的是系统的程序,我们可不可以调用我们自己写的程序呢?
    我们创建一份C++代码:
    在这里插入图片描述
    修改一下我们的Makefile
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

    execle

    我们已经了解了带 l 的,还有带v的,现在我们来看看带e的:
    在这里插入图片描述看这个样子的话,应该是把父进程的环境变量导入到子进程中,这个过程中我们会用到putenv这个函数:
    在这里插入图片描述
    这个函数是方便我们在程序之中添加我们的环境变量:
    在这里插入图片描述
    这个时候我们不调用execle这个函数,我们看看子进程mytest的变化:
    在这里插入图片描述我们发现在环境变量表的最后发现了我们添加的环境变量,诶?但是我们还没有向子进程导入我们的环境变量呀?
    这里要注意一下子进程本身就会继承父进程的环境变量,不受进程替换的影响,其实不需要我们调用什么接口去继承。但是既然提供了,我们还是可以用一下:
    在这里插入图片描述顺便可以把命令行参数打印出来:
    在这里插入图片描述执行一下:
    在这里插入图片描述我么看到子进程继承了父进程的环境变量。
    注意我们也可以传我们自己的表,但这会覆盖我们原有的环境变量表,所以如果我们想新增的话,我们的先提前加好才可以。

  • 相关阅读:
    2.4 矩阵的运算法则
    VScode中js关闭烦人的ts检查
    130家小程序,“火”的一塌糊涂
    【计算机网络】深入理解TCP协议的三次握手和四次挥手 一、前言
    LVS+DR+apache+keepalived负载均衡
    拼多多手势验证
    TCP和UDP的区别以及它们各自的优缺点
    TCO-PEG-FITC 荧光素-聚乙二醇-反式环辛烯 TCO-PEG-荧光素
    卷积神经网络实现咖啡豆分类 - P7
    09-DOM元素操作
  • 原文地址:https://blog.csdn.net/qq_67693066/article/details/134099856