继上回书Linux进程概念(二)--进程状态&进程优先级,我们在了解了Linux进程状态和优先级的概念,初步掌握了进程状态的相关知识,最终,我们以Linux进程的优先级,引出了一些其他的概念:
那么今天,我们就从这些概念开始说起,进一步了解Linux进程切换的原理以及运行队列的进程排队机制,以及简单了解一下有效避避免产生进程饥饿的进程调度和插队算法,顺便我们接着讲一下命令行参数的相关知识,为后面学习环境变量做一下铺垫,话不多说,开始吧~
目录
1.进程的并行,并发&进程切换
进程的并发与进程优先级是否矛盾
CPU内部的寄存器
临时变量的保存
进程执行状态的记录
Linux2.6内核进程调度队列
优先级
活动队列与过期队列
实时进程与普通进程的不同处理方式(一般 调度算法)
bitmap与进程调度算法
2.命令行参数&环境变量初阶
命令行选项
环境变量
基本概念
查看系统的环境变量
添加环境变量
本地变量&环境变量
本地环境变量与导出
常见环境变量举例
PWD
HOME
通过代码获取环境变量
通过getenv函数获取
命令行第三个参数获取
通过第三方变量environ获取
一些关于环境变量的命令
1.进程的并行,并发&进程切换
并行性指的是多个任务同时在不同的CPU或计算单元上执行,以实现更高的执行效率和性能。在并行执行中,每个任务都在独立的处理单元上独立执行,并且可以同时进行。
我们的重点是并发性,与并行性不同,并发性指的是多个任务在同一时间段内交替执行,并共享相同的计算资源。在并发执行中,多个任务按照一定的调度策略交替执行,每个任务都会分配一定的执行时间片,然后切换到下一个任务。并发性通常用于处理I/O密集型的任务,如网络通信、用户界面等,以实现任务之间的响应性和交互性。
需要注意的是,并发性并不要求任务真正地同时执行,而是通过快速切换和时间片分配来模拟任务的同时执行。这种交替执行的方式可以在单个处理器上实现,并通过调度器的调度算法来决定任务的执行顺序和时间片分配。
进程的并发与进程优先级是否矛盾
进程切换的本质是推动多个进程同时推进,而进程优先级是确定了进程的排队顺序,那么就存在到底是按优先级优先执行优先级高的进程还是按找进程切换,排队的同时进行多个进程,二者是否会存在矛盾?
进程切换是操作系统在多任务环境下,将CPU的控制权从一个进程切换到另一个进程的过程。进程切换可以由多种原因触发,例如时间片轮转、中断处理、阻塞等。在进程切换时,操作系统会保存当前进程的上下文,并加载下一个进程的上下文,以便新的进程可以继续执行。
进程优先级是操作系统为了管理和调度进程而分配给每个进程的相对优先级。不同的进程可以有不同的优先级,优先级较高的进程通常具有更高的调度优先权,有更多的机会获得CPU的执行时间。
进程切换和进程优先级之间的关系可以通过调度算法来体现。调度算法决定了在进行进程切换时,系统选择哪个进程来执行。一些调度算法会考虑进程的优先级,倾向于选择优先级较高的进程进行调度,一些情况下,这种调度算法可以给予高优先级的进程更多的占用时间。
然而,并非所有的调度算法都严格遵循进程优先级。一些调度算法可能更加关注公平性和均衡性,以确保所有进程都能获得公平的执行机会,而不会过度偏袒高优先级进程。这种情况下,进程切换的决策可能更多地依赖于其他因素,如时间片轮转或就绪队列中的进程顺序。
CPU内部的寄存器
CPU中的寄存器是一组用于临时存储数据和指令的内部存储器。寄存器是CPU内部最快速、最直接可访问的存储单元,其速度比其他存储器(如缓存、内存)更快。
那么,这些寄存器,又对进程切换产生了什么样的影响呢?我们首先来思考这样一个问题,我们的临时变量是如何被外部接收到的呢?比如我们c中写一个函数,函数返回一个值被main中的一个变量接收,这是如何实现的呢?
临时变量的保存
其实,这是寄存器(eax)在CPU内部起了作用,我们函数中的return语句,在执行时被当做了一条汇编语言的move指令,就会将当前所产生的需要返回的临时变量的值移动到eax中保存起来,之后再进行栈帧的销毁,当需要这个函数返回的临时变量的值的时候,直接去eax寄存器中寻找,这样一来,虽然临时变量已经被销毁,但是它的值被保存到了寄存器中。
进程执行状态的记录
每一个进程不是一次就能执行完程序并退出的,CPU为了保证一定程度的多个进程同时推进,会根据时间片来进行进程切换,但是当前程序的运行进度需要被保存下来,以便下次轮到该进程直接从上一次的进度开始执行,这就叫做进程的切出与切入。
这得益于CPU中的另一个寄存器:程序计数器(PC),程序计数器是一个特殊的寄存器,用于存储当前正在执行的指令的地址或下一条指令的地址。它在指令执行期间不断更新,帮助CPU跟踪程序的执行顺序。
还有,本次进程在被从CPU上剥离之前(进程未退出),要将自己进程的执行情况(包括数据等保存在CPU寄存器上的资源中的内容也一并带走,并将其打包放在自己的PCB中的一个结构体中,这里不展开说明这个结构了,有兴趣的可以自行百度一下),然后再进行进程切换。
下面是简单的进程切换的一个过程:
Linux2.6内核进程调度队列
下面是Linux2.6内核中进程队列的数据结构
我们只是选择其中的绿色部分的数据成员来进行讲解,目的是了解进程的运行对队列和进程切换的原理以及简单的了解调度算法的原理。
优先级
在Linux中一般进程一般分为实时进程和普通进程,我们日常所写的代码所生成的可执行程序什么的,一般都是普通进程,实时进程优先级为0~99,在运行对列中的保存位置也是在下标0~99处,即优先级与下标一一映射,而普通优先级却不同,普通进程的优先级是60~99,其保存位置是在运行队列的下标100~139处,属于映射保存,也就是优先级为60的进程保存在下标100的位置,优先级为61的保存在下标101的位置,以此类推,实时进程之所以保存在运行队列前端,是因为其要优先被响应和执行,这个我们下面还会说到。
这里可能会产生一个疑问,为什么实时进程的优先级是0~99的同时普通进程还能是60~99,这两者不会冲突吗?
事实上,Linux内核上是可以直接将这两种进程结构区分开的,之所以这两者的进程优先级数字有重叠,是因为历史原因造成的,这里我们只需要知道,实时进程的优先级和普通进程的优先级不是单纯靠优先级的数字进行区分的,他们之间更多的是结构上的不同,举个例子,同一个餐厅,老师排队对列中的第5名和学生排队队列的第5名意义肯定是不一样的。
活动队列与过期队列
活动队列和过期队列在结构上是两个一模一样的运行队列,只是名字不同罢了,为了搞清楚这两个队列的区别,我们先来看为什么要引入过期队列,注意:我们下面的讨论均站在进程都是普通进程的前提条件下
在活动队列执行进程的过程中,假设我们正在执行普通优先级为80的进程,此时又新来了一个普通进程,优先级为75,那么,此时如果你是OS,你会怎么处理这个新加入的进程呢?很明显,这个新的普通进程比当前正在执行的进程优先级高,按理来说应该在当前执行的进程的前面执行,但是,如果我们将当前执行的进程从cpu剥下,转而执行新的进程的话,但是,如果长此以往,是不是优先级低的进程就可能一直在等待,就会导致饥饿问题。由于普通进程没那么急,操作系统又想实现进程间的公平和协调性问题,尽可能让每一个进程都有执行的机会,为此,我们引入第二个运行队列结构,和运行队列结构完全一致,称之为过期队列,此时,当再次有新的普通进程加入时,我们不将其插入到活动队列,而是将其直接插入到过期队列中,这样,活动队列中的进程就会慢慢减少,直到最后减少为空(一个进程任务都没有了),那么此时,我们就可以将过期队列和活动队列的数据互换(改变一下指针的执行就行了),这样,就能一定程度的减少饥饿问题,保证公平和均衡性。
实时进程与普通进程的不同处理方式(一般 调度算法)
在上面我们已经简单阐述了普通进程在执行过程中遇到其他优先级的普通进程都要将其直接加入到过期队列进行等待,其实这里还有好几种情况,我们来分类进行讲解:
需要注意的是,进程的调度和执行顺序还受到一些其他因素的影响,例如进程的优先级设置、调度策略、CPU负载等等。我们这里只是一般的处理方式,具体的行为可能会因操作系统的版本、配置和调度算法的不同而有所差异。
1.当前正在执行实时进程:
<1>新加入了一个实时进程
实时进程通常具有固定的时间限制,需要在规定的时间内完成任务,因此,将其放在了运行队列的头部部分,目的是让其在遍历执行的时候能够更早的遍历,像汽车的刹车系统、控制系统和信号系统等等都属于实时进程任务,所以,当正在执行实时进程时,如果新加入的实时进程比当前正在执行的实时进程的优先级高,那么新加入的实时进程就会直接加入到活动对列的指定下标处,并抢占CPU时间,使得当前进程从CPU上剥离,但是被剥离的实时进程在退出时还是会保存退出信息,并在活动队列原来的位置继续等待时间片轮转。
<2> 新加入了一个普通进程
这就没什么悬念了,当实时进程正在执行时,新加入的普通进程会被放入活动队列等待执行。在实时调度策略下,实时进程具有较高的优先级,优先级比普通进程更高。因此,当实时进程处于执行状态时,普通进程以较低的优先级进入活动队列,并等待其轮到执行。
2.当正在执行普通进程时:
<1> 新加入了一个实时进程
实时进程通常具有固定的时间限制,需要在规定的时间内完成任务,而普通进程则采用时间片轮转的方式进行调度。所以当前的普通进程必须要为这个新的实时进程“让路”,在 Linux 中,实时进程具有更高的优先级,当实时进程准备好执行时,它会打断正在执行的普通进程。被打断的普通进程会退出执行,并被放入过期队列。过期队列中的进程等待重新调度,并有可能在下一次的调度中获得执行机会。
<2>新加入一个普通进程
这就是我们之前说的那种情况,这种情况下,不论新加入的普通进程优先级高于还是低于当前正在执行的进程,一律都将其加入到过期队列中等待下一轮调度执行。
bitmap与进程调度算法
我们在遍历队列寻找哪一处下标存在待执行的进程的时候,除了按照下标顺序从小到大执行之外,我们还需要考虑有实时进程插入的情况,那么,如何判断当前正在执行的进程的时候有实时进程插入了呢?一个暴力的做法是在每次执行进程之前都先遍历一遍该进程对应的映射在运行队列上的下标之前的所有下标,看看是否有进程存在,但是这样时间复杂度无疑是非常慢的,于是,我们便可以通过用比特位的0或者1的变化来简单的判断该比特位对应的位置上有无进程存在,bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
2.命令行参数&环境变量初阶
命令行选项
我想一定有人见过有人写main函数带上了两个参数,像下面这样:
是不是很好奇这两个参数有什么用?我们不妨来输出看一下结果:
它直接把我们上一步输入的命令行给打印出来了,说明我们上一步的命令行被main函数给保存到了argv数组中去,那么,我们尝试着在命令行中在写多一些字符串,来证实一下我们的猜想:
果然是这么回事,这样,我们便不难理解我们的命令行选项的概念了。
在Linux中,命令行选项是通过命令行输入的参数,用于修改命令的行为或提供额外的功能。命令行选项通常以短划线(-)或双短划线(--)开头,后面跟着一个字母或单词。这些选项允许用户在执行命令时自定义命令的行为,通过不同的选项组合可以实现各种不同的功能。我们经常使用的指令其实就相当于一个个可执行程序,后面的一些像 -l,-a,-al之类的均为命令行参数,相当于可以执行不同的功能 。
环境变量
我们经常在使用一些集成开发环境时,需要我们自己配置环境变量用来满足代码编译需求,跟着网上的教程配好了是不是还是云里雾里?环境变量到底是个啥?下面我们一起来认识
基本概念
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
在Linux中运行代码时我们就会发现,同样是可执行程序,我们自己写的代码就需要带 ./ 才能运行,但是系统中的命令,比如whami 、ls等却不用带 ./ 照样可以执行呢?其实,这里的 ./表示了我们的当前目录,带着目录运行一个可执行程序当然是可以的,我们知道,要执行一个可执行,必须找到自己的可执行在哪里,但是,为什么系统的指令就可以不带目录而独自运行呢?所以,只能有一种解释:系统指令有自己的默认的查找路径!
查看系统的环境变量
我们可以使用命令:
echo $PATH
来查看我们当前系统中的环境变量:
在查询结果中,每个环境变量之间以 :分隔开,从上面的结果中我们可以看到,一共用6个系统的默认的环境变量,我们在使用系统命令时,系统会自动挨着在这些目录下查找是否有对应的可执行文件,有就执行,没有就报错。
添加环境变量
知道了这个功能,我们想尝试着将我们自己的可执行文件的目录添加到环境变量中去,这样,我们运行我们的程序就不用加目录了,我们直接来试一下:
这里需要注意,默认的环境变量的修改,仅限于当前的这一次登录,如果用户退出再登录,环境变量就会恢复到原来的状态。这个我们在这里先简单的介绍一下原因,具体后面还会提及:
我们知道,命令行启动的进程都是bash(命令行解释器)的子进程,子进程的命令行参数和环境变量,是父进程传递给的 ,我们每次更改子进程的环境变量信息,实际上更改的是bash进程内部的环境变量信息,而每一次的重新登录,我们知道,bash也是一个进程,所以,我们每次登录时,系统就会为我们创建一个新的bash解释器,重新读取环境变量信息保存到自己的数据信息中。
那么,问题又来了,bash又是从哪里读取到环境变量的信息的呢?其实这个环境变量信息是以配置文件的方式存在的,也就是脚本形式存在的,这个配置文件在对应用户的家目录的一个隐藏文件 .bash_profiles 中,每次用户登录,都会读取该文件中的内容,该文件会为bash进程生成环境变量表信息,至于这个配置文件 ,这里只做了解,我们不再展开。
本地变量&环境变量
本地变量的一般形式为: [名称] = [内容] ,其只能在当前bash进程的内部有效,也就是说,由bash创建的子进程就不能再使用这个本地变量,下面是利用fork函数的证明:
我们发现,子进程并没有找到对应的本地变量的内容。
但是,我们发现像一些系统定义的命令,比如echo这种命令,却都能够获取本地变量的内容,众所周知,这些命令本质上也是bash所创建的一个子进程,按理来说,它们也应该获取不到本地变量的值才对,这是为什么呢?
其实,在Linux中,命令分为常规命令(shell fork让子进程创建形成的)和内建命令(shell命令行的一个函数),而这些简单而又安全的命令,比如pwd,echo和export之类的命令可以作为shell的函数而在不创建子进程的同时读取shell本地的变量。
本地环境变量与导出
注意,此处我们的mycode.c代码变更,以下是变更后的代码,变更后可以实现对应的当前用户的系统环境变量,至于具体原理,将在后面提到:
上面我们在添加环境变量的时候,重新登录就会重置我们之前添加的数据,现在,我们尝试在命令行中手动添加一个我们自己的环境变量,看看其会不会添加到我们对应的 env 中,如下常见了一个本地环境变量 MYENV ,
但是,我们通过管道查询发现其并没有保存在对应的 env 数组内,而此时我们将这个变量叫做Linux中的本地变量(形式为: [名称] = [内容] ,本地变量只在当前bash进程内部有效,不能够被子进程继承下去),那么该如何将这个我们自己的环境变量添加到对应的我们的bash进程的环境变量信息中去呢?我们采用的是 export 命令:该命令可以将我们创建的本地变量导出成系统进程的环境变量,
这个时候,我们的本地变量就成功的导出成为了环境变量,这样一来,我们bash的子进程就能够获取到这个环境变量了,上述的过程也可以直接用 export 执行,相当于省去了定义本地变量的环节。
这个时候,我们再来测试我们的用户重新登录之后该环境变量还存不存在,会不会被重置,
我们发现,环境变量又没了,这其实和我们上面添加环境变量处的情况相似,我们的bash解释器是将环境变量拷贝到了内存,而bash进程是在内存中运行的,我们对内存的所有操作,都会因为当前bash进程的退出(用户退出)而被销毁,那就真的没有办法了吗?
办法还是有的,从上面的原理中,我们知道了在用户登录的时候, .bash_profile 这个配置文件会识别用户属性然后为用户配置环境变量表和命令行参数表,那么,我们是否可以将我们的环境变量信息添加到这个配置文件中呢?我们来尝试一下,
注意,这个配置文件是在对应用户的家目录下:
直接在配置文件中添加我们自己的环境变量:
当我们再次重新登录用户,我们发现此时环境变量已经被保存了下来,并且能被我们查询到。
当然,因为我们的配置文件是因用户而异的,也就是说,每一个用户的配置文件都不一定相同,所以,我们当前的配置,在其他用户下不能生效(包括root用户)。但是,对于同一个用户,只要我们修改了其配置文件,bash所有的子进程、孙子进程等进程都可以使用使用该环境变量了。
所以,现在我们应该能大题了解了什么是环境变量,环境变量本质就是找到可执行程序的目录,如果我们想要运行一个可执行,需要告诉系统我们的可执行的目录,好让系统自动找到可执行的目录得以执行程序。
常见环境变量举例
我们可以使用语句 env(environment)来查看当前用户的所有的环境变量,注意当我们切换到root登录时,环境变量也会相应的发生变化,系统中会存在大量的环境变量,
PWD
PWD是保存当前路径的环境变量,当我们使用pwd命令时,系统就会找到环境变量PWD中的内容并将其打印出来,
HOME
我们知道,root用户和普通用户默认所处的目录不一样,但是这是为什么呢?
事实上,当我们用户在刚开始登录的时候就需要通过确定身份来初始化当前用户对应的HOME环境变量,如果是root,那么就初始化为/root,是普通用户就初始化为普通用户的家目录。
通过代码获取环境变量
通过getenv函数获取
getenv函数可以通过名字获取指定的环境变量,
获取了环境变量,我们就可以根据环境变量的内容来针对性的做很多事情,比如阻挡普通用户访问程序,我们只需要查看和辨别USER环境变量的内容即可,下面是简单实现:
命令行第三个参数获取
mian函数在传参时,是不是只能有两个参数呢?其实不然,main函数还有第三个参数,名字叫做 char *env[ ] ,为了知晓其内部保存的是什么变量信息,我们可以将其打印出来。
实际上,运行结果其实和我们当前系统的所有环境变量的结果一致,所以,这个main函数的第三个参数env数组也具有类似于命令行参数的指针数组操作,每一个位置存储的是一个字符串指针,指向一个对应的环境变量。
通过第三方变量environ获取
在Linux中,environ
是一个全局变量,它是一个字符串数组,用于存储当前进程的环境变量。它是一个指向指针的指针,每个指针指向一个以"key=value"
形式表示的环境变量字符串。
environ
变量被定义在C标准库的<unistd.h>
头文件中,并由操作系统在程序启动时自动创建和初始化。它存储了操作系统为当前进程提供的环境变量的副本。通过environ
变量,可以访问和操作当前进程的环境变量。例如,可以使用循环遍历environ
数组,查看和修改环境变量的值。
一些关于环境变量的命令
除了上面的export、env、echo等命令,我们再来介绍两个命令:
unset: 清除环境变量
set: 显示本地定义的shell变量和环境变量
好了,一不小心把环境变量讲完了,但是想要更加深入的了解环境变量,在后面的进程控制中我们还会提到,下一节,我们将要讲解的是进程地址空间的相关知识,我们将会对前面章节中的疑问进行解析,我也会尽快更新的,敬请期待~~