Linux——进程控制

Linux——进程控制

  • fork()
    • 缺页中断
  • 进程终止
  • 进程异常
  • exit
  • _exit
  • 进程等待
    • wait
    • waitpid
      • status
      • WIFEXITED
  • 多进程等待
  • 阻塞等待和非阻塞等待
  • 进程替换
    • 单进程的进程替换
      • execl
      • execlp
      • execv
      • execle

fork()

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

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

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

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

那么fork做了哪些事情呢?

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

我们可以写段代码来验证一下:
Linux——进程控制_第1张图片Linux——进程控制_第2张图片
我们可以看到第一个29374是父进程的pid,返回值29375是子进程的id,而下面的29375是子进程,返回值是0。

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

缺页中断

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

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

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

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

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

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

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

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

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

进程终止

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

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

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

其中,不同的值对应不同的状态,我们可以用strerror来查看不同数字所对应的不同的退出状态。
Linux——进程控制_第7张图片
Linux——进程控制_第8张图片
果然和我们猜的一样,0代表成功。其他数字都代表了不同的错误状态。
我们把这些代表不同状态的数字叫做退出码

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

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

这个用来储存的东西叫做errno
Linux——进程控制_第9张图片
Linux——进程控制_第10张图片

进程异常

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

exit

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

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

_exit

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

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

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

进程等待

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

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

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

wait

我们先来看看wait的手册:
Linux——进程控制_第32张图片返回值:
在这里插入图片描述

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

waitpid

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

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

status

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

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

Linux——进程控制_第42张图片测试一下:
Linux——进程控制_第43张图片我们看到end state退出状态是10,退出信号是0,跟我们的逻辑是符合的。
我们来试试程序异常会怎么样:
Linux——进程控制_第44张图片
Linux——进程控制_第45张图片

WIFEXITED

如果上面的方法有点过于复杂了,我们定义了一个WIFEXITED的宏帮我们判断退出状态:
Linux——进程控制_第46张图片Linux——进程控制_第47张图片如果有异常,返回的值就不为1:
Linux——进程控制_第48张图片Linux——进程控制_第49张图片

多进程等待

我们进程等待,我们可以多进程等待:
在这里插入图片描述
Linux——进程控制_第50张图片

阻塞等待和非阻塞等待

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

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

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

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

我们可以写一段代码来验证:
Linux——进程控制_第51张图片
Linux——进程控制_第52张图片
我们也可以把父进程具体在做什么写的清楚一点:
Linux——进程控制_第53张图片

进程替换

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

单进程的进程替换

在进行进程替换前,我们的了解一下,进程替换的接口:
Linux——进程控制_第54张图片这些都是进程替换的接口,我们一个一个来试:

execl

我们看到execl是要指定文件路径,加上可变参数列表:
Linux——进程控制_第55张图片

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

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

execlp

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

execv

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

execle

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

你可能感兴趣的:(linux,服务器,性能优化)