网站开发 加密保护,深圳网站维护制作,wordpress 插件表单 写入数据库,查询备案网站谈谈进程的性质
进程的竞争性
由于CPU资源是稀缺的,进程数量是众多的。不可避免需要造成进程排队等待CPU资源的动作#xff0c;内核的设计者为了让操作系统合理的去调度这这些进程#xff0c;就产生了进程优先级的概念。设置合理的进程优先级能让不同进程公平的去竞争CPU资…谈谈进程的性质
进程的竞争性
由于CPU资源是稀缺的,进程数量是众多的。不可避免需要造成进程排队等待CPU资源的动作内核的设计者为了让操作系统合理的去调度这这些进程就产生了进程优先级的概念。设置合理的进程优先级能让不同进程公平的去竞争CPU资源从而使操作系统运行更加高效。
进程的独立性
操作系统启动后会运行许许多多个进程。由于硬件资源都是一个个独立的进程也独享各种系统的资源所以在进程被设计之初就要求其在运行期间不干扰别的正在运行的进程。在生活中我们写代码时控制台程序崩溃了并不影响我们电脑上正在运行的微信。在前文fork()创建子进程的部分当子进程修改代码时父进程运行的结果也不受子进程的干扰。这就是进程独立性的概念。具体为什么需要独立怎么做到独立那就等下面谈进程地址空间时再展开说。
进程的并行和并发的概念
进程的并行是指在同一时刻有多个任务线程或进程在多个处理器核心上同时执行。这要求系统具备多核心CPU或支持多线程处理的硬件架构。现在大家用的手机、电脑、平板电脑等设备都是多核心架构的CPU所以我们可以一边打游戏一边听歌甚至再屏幕录制啥的。这就是进程的并行。
进程的并发指的是在同一时间段内有多个任务线程或进程被处理这些任务在宏观上看似同时执行但在微观上可能是交替执行的。并发强调的是任务之间的交替执行而不一定要求同时执行。在单个处理器核心上并发通常是通过时间片轮转即分时复用的方式实现的多个任务轮流使用CPU资源。
进程的并发具体在LInux系统中通过基于进程切换和基于时间片轮转的调度算法。上文中介绍了Linux内核2.6版本的大O(1)调度算法。它从原理的层面介绍了内核进行进程切换的方式吗通过维护两个开散列哈希桶以及两个指针变量就能让进程的PCB根据优先级被CPU公平的调度。
进程切换
进程上下文的概念
为什么C/C语言的函数返回局部变量时调用方能在函数调用结束后拿到结果呢因为函数返回值被存到了CPU的寄存器中。如return a; - mov eax 10。寄存器资源时最稀缺的所以当返回值占的空间过大时我们需要返回指向定义在堆区上的对象的指针。
操作系统如何得知进程当前执行到哪一行代码了通过程序计数器pc或者是eip寄存器。eip寄存器用于记录当前进程正在执行指令的下一条指令的地址。寄存器分为可见寄存器和不可见寄存器。通用寄存器有eax、ebx、ecx、edx。栈帧寄存器有ebp、esp、eip等。状态寄存器有eflags、status等。那寄存器主要扮演什么角色寄存器主要存储进程被高频次访问的数据以提高进程的效率。所以CPU寄存器里保存的通常是进程相关的数据。这些数据称之为进程的上下文。
进程切换的概念
进程切换就是将正在被CPU调度的进程PCB从CPU上扒下来把运行队列中排队的的进程PCB换上去调度执行。当进程并发时操作系统需要高频次的进行进程切换以达到让多个进程交替执行的效果。当进程的时间片用完了代码还没执行完要从CPU上被“拔”下来了。此时CPU的寄存器内存放着这个进程的上下文。那寄存器内的上下文就要被进程打包带走。下次调度这个进程的时候再将上下文恢复到寄存器中然后继续执行代码。所以进程切换的核心动作就是保存上下文和恢复上下文。
环境变量
在学习JAVA的时候配置开发环境需要用到环境变量。那环境变量具体是什么下面就带大家看看Win11系统下的环境变量如何查看。 下面再通过几个小实验看看LInux下的环境变量。先介绍PATH环境变量。编写一份简答的C代码。然后编译后执行它。 在前面的学习中可以知道我们写的C/C程序编程可执行程序后也是一个指令。但是与Linux原生的指令如ls、pwd等相比。我们支持自己写的指令时需要带 ./ 进程名才能执行它。而操作系统携带的指令则不需要。这是为什么呢 这是因为BASH在匹配这些指令时会去系统的PATH这个环境变量下所有的路径中查看是否有该指令。这也就意味着当我们把我们的写的指令的当前路径加入的PATH环境变量中就不要 ./ 也能执行我们写的代码了。
当我将原本的PATH环境变量全部删掉就会导致系统指令如ls、clear等无法使用。不过此时不用担心PATH是内存级别的环境变量。进需要重启终端即可恢复因为每次BATH启动后回去配置文件中获取PATH环境变量的值。 下面再介绍一些常见的环境变量通过env指令就能查看当前的环境变量。
下面简单介绍一下getenv()接口。它用于获取一个环境变量的值。char *getenv(const char *name)。若name不存在则返回空存在则返回它对应的值。
有了上面的认知后再谈一谈环境变量。环境变量是操作系统提供的一组 变量名Value 形式的变量。不同的用户有不同的环境变量且环境变量通常具有全局属性。
命令行参数
什么是命令行参数在平时我用终端连接云服务器时用一些指令如ls时难免需要携带命令行参数如 -l、-a等等参数,这些指令通常是C/C代码实现的。具体它是如何过命令行参数来实行不同的功能呢下面通过一段demo代码来看一看。首先需要直到main函数是可以带参的。int main(int argc, char* argv[])这种形式不知道大家见没见过。这里的argv就是用于存放命令行参数的。而argc表示它的长度参数个数)。虽然我们没有显示的传递argc这个参数但是系统是可以通过argv这个指针数组的有效元素个数来进行对argc进行初始化的 通过上图样例可以发现argv默认是可执行程序指令的名称它的默认长度是1注意数组下标从0开始访问。而当我们输入参数时以空格作为分隔符。bash命令行输入的参数会被传到main函数的argv中。所以当我们使用命令行参数时程序可以通过argv进行接收然后对我们进行对应逻辑的相应。
其实在Windows中也可以使用命令 命令行参数的形式来启动程序如使用shutdown命令时可以带-t、-s等等选项
main函数的环境变量向量表
main函数不仅有一个命令行参数向量表接收传参还有一个环境变量向量表。它的本质和argv也是一样的是一个char* 的指针数组。下面通过一个简单的实验带大家看看它的存在。 不仅如此还可以使用一个全局的指针变量environ来获取当前进程从bash父进程那里继承的变量。environ指针本质是指向了父进程的地址空间中的环境变量向量表的起始地址位置。 下面先看看env命令。 下面在运行一下上面的C代码。 可以发现好像模拟实现的env命令了。这是因为我们在使用终端SSH远端云服务器时需要和bash进程进行交互所以在bash命令行启动的进程都是bash的子进程。bash进程在启动时会从操作系统的配置文件中获取对应的环境变量而我们./启动进程会从bash的环境变量向量中继承系统的环境变量。进程获取环境变量的方式可以分为三种系统调用接口获取 和 main函数的env向量获取以及environ指针获取。
下面通过一个简单的实验验证一下上面我们说的./运行的程序会继承bash的环境变量。首先输入export namevalue来导入一个环境变量。然后验证一下结论。取消导入的环境变量用 unset name。
本地变量
本地变量是一个只在bash内部有效的变量值本地变量不会被子进程给继承。可以用set指令查看当前的环境变量本地变量。而定义一个本地变量的方式是 namevalue的方式就是定义一个本地变量。
不是说本地变量不能被子进程继承吗不是说bash会创建一个子进程去执行对应的指令吗为什么echo能在显示器上输出我们的本地变量值呢这就需要引入一个概念内建命令。前面我们说的bash会创建一个子进程去执行对应的指令这类指令通常是常规指令。向echo、cd等需要bash亲自去执行的指令称之为内建命令。
下面通过一个实验来看一看内建命令cd是如何改变当前路径的。通过代码修改当前路径需要使用一个接口chdir(char* path)。然后我们由于编写的程序./启动后是bash的子进程。所以通过休眠来对比下具体进程的动作观察一下内建命令和常规命令的区别。 由此可以观察到当我们在bash中输入命令时当argv[0] cd时bash会亲自去调用chdir修改当前路径。而对普通命令他会fork()一个子进程去让子进程执行对应的函数。
进程地址空间
重新认识地址空间
在学习C/C的时候从语言的角度上了解了地址空间的概念。但是这部分概念是有缺失。我们从语言层面上学习的地址空间自上往下有栈区、堆区、静态区以及常量区。而今天就从操作系统的层面上再认识一下地址空间。
从操作系统的角度出发可以将地址空间自上往下分为栈区、堆区、全局变量区未初始化全局变量、已初始化全局变量、字符常量区以及代码区。下面通过代码进行验证。 从上图的验证结果可以看到Linux系统中的地址空间分布以及栈区空间从高地址往低地址处增长堆区空间从低地址处往高地址处增长。static修饰的局部变量的地址会处于全局变量区。 命令行参数和环境变量存储在进程地址空间的栈区。 下面通过代码验证下。
在前面学习C/C语言的时候将程序地址空间理解成C/C语言地址空间其实是有问题的。无论是什么计算机语言编写的进程操作系统都要为它创建PCB、以及维护地址空间。
虚拟地址
下来看看下面的程序的运行结果。 通过上面的程序我们可以发现当cnt减到0的时候子进程将全局变量g_val修改成了200。此时父进程和子进程都在访问这个变量这个变量的地址都是0x5558d69ec010。但是父进程访问时它是100子进程访问时它是200。同一个变量同一个地址。通过这个现象可以得出一个结论这个0x5558d69ec010一定不是物理地址。它是虚拟地址线性地址。由此也可以引申出我们编写的C/C代码的指针变量并不是操作物理地址而是操作虚拟地址。下面通过引入新的概念增加我们对这一现象理解。
地址空间的概念
实际上在创建一个进程时系统都会为它维护一块地址空间称之为进程地址空间。上面的地址都是虚拟地址由于数据终究是要存在物理空间上的。如何将虚拟地址转化成物理地址呢答案是用一个kv映射关系结构的数组称之为页表。 在父进程创建时操作系统会为它维护一块进程地址空间和一份页表。当fork()创建子进程时父子进程共享一块进程地址空间和页表。当子进程将它进程地址空间上的g_val值修改成两百此时操作系统会进行写实拷贝在物理内存上开辟新的空间并将页表中key虚拟地址映射的value物理地址值修改成新开辟的物理地址。然后将物理内存地址上的值修改200。而从进程地址空间的角度来看页表如何修改物理空间的映射都与它没有关系进程依旧可以通过虚拟地址映射关系访问物理地址。这就是宏观层面上同一个变量同一块地址却有不同的值的原因。
什么是地址空间呢地址总线是用来连接计算机的各个硬件的使它们能够完成数据通信和计算的。32位平台下地址总线的根数是32根。每一根总线有0和1两种情况0对应低电频1对应高电频。而CPU中有一个地址寄存器32位平台下它的大小是32位。它可以表示2^32种地址排列组合的方式。所以地址空间的本质是地址排列组合的方式形成的范围[0, 2^32]。
如何理解地址空间的区域划分呢想必各位在学生时期都会经历和同桌画38线来划分桌子的区域把。假如一个桌子长150cm你和你的同桌决定平分区域那么你占有0cm到74cm的空间你的同桌占有76cm到150cm的空间。这也就意味着你和同桌各自占用一段连续的空间这就是一段线性地址虚拟地址这个就是对于桌子的区域划分。用计算机语言表示如下。
struct destop_area
{int me_start;int me_end;int deskmate_start;int deskmate_end;
};所以在属于我的桌子区域中每一厘米的空间都属于一块独立的区域有独立的地址标识可以直接被我使用。比如说在20cm到40厘米处放着我的铅笔盒。所谓地址空间就是描述进程可视范围的大小地址空间一定存在着区域划分通过修改end和start就能够对区域进行增大或者缩小。而地址空间需要被操作系统所管理而管理的本质就是先描述在组织。 要描述进程地址空间就需要定义不同区域的start、end字段来划分不同区域。
//32位平台
struct mm_struct
{long code_start, code_end; //代码区long str_start, str_end; //字符串常量区long init_start, init_end; //已初始化全局数据long uninit_start, uninit_end; //未初始化全局数据long heap_start, heap_end; //堆区long stack_start, stack_end; //栈区
};而在经典的2.6内核中关于地址空间区域划分的成员变量如下图。
再谈进程以及地址空间
现在我们对于进程的理解可以更进一步了。进程就是内核数据结构(task_struct mm_struct 页表) 加上 代码和数据构成的。站在上帝视角操作系统是一个富裕但是私生活丰富的老爸每一个进程都是它的私生子。这意味着进程之间不了解彼此也不关心彼此。而操作系统给每个进程都画了一张大饼即告诉每个进程你的进程地址空间有2^32大小的空间4GB32位平台的上限。每一个进程的都认为这4GB空间是我的只是我的“老爹”帮我暂时保管我不够就管他要。只要不太过分他都会给我足够的空间。
虚拟地址线性地址和页表的出现使得进程在访问物理内存空间时需要一个系统层面上的映射转化。在这个过程中操作系统会对转化的安全性和合法性进行查验。一旦进程访问异常的内存空间时会在系统层面进行拦截访问不让数据到达物理内存以到达保护物理内存的安全。
页表
页表它是被操作系统管理的操作系统管理它是通过CPU的一个寄存器即cr3寄存器X86架构。cr3寄存器保存了页表的地址物理地址。当进程进行上下文切换时cr3内部保存的数据也是进程上下文的一部分。需要在进程调度结束后带走以及调度进程时加载。
页表是一个key value模型的容器里面存放的键值对是虚拟地址和物理地址的映射关系。不仅如此它还记录了当前不同虚拟地址空间的权限。咱们学习语言是熟知的代码区和字符串常量区是不能被修改的。原因就是当我们修改字符串常量区的内容时页表在映射物理内存时发现该区域的权限为只读。然后就会将进程的状态设置成异常。然后我们的进程就会被操作系统杀掉。所以在应用层看来我们的进程会异常退出并且错误码被设置。 页表在设计层面上完成了进程地址空间的虚拟地址和物理内存上的物理地址进行了解耦操作。哪怕两个进程同时指向一个虚拟地址实际上在物理内存的层面上操作系统会维护两块不同的物理地址。当一个进程退出后依旧不影响另一个进程去使用这个虚拟地址以及访问虚拟地址映射的物理地址。
页表还有一个关键的作用让所有进程以统一的视角去看待物理内存。如果让操作系统直接通过物理内存地址去管理进程的代码和数据时由于物理存储是无序的所以管理的成本极高。当由页表映射虚拟地址和物理内存后管理进程的代码和数据可以通过页表操作的虚拟地址来访问对应的物理地址。在逻辑层面上可以认为虚拟地址是连续且有序的。这样大幅度减小了管理物理内存空间上代码和数据的成本。
站在操作系统的视角上它本身不信任用户。因为它内部维护很多的重要数据不允许用户轻易地修改。所以对用户提供了进程地址空间让用户以统一的视角看待操作系统的内存资源。能否合理的使用操作系统的内存取决于页表上面的权限字段所决定的。因为操作系统需要维护自身的数据安全。
挂起状态的介绍
Linux操作系统中有没有挂起状态呢答案是有这个状态。挂起状态是指进程被暂时停止执行并且被移出内存保存在外设通常是磁盘中等待特定的事件发生后再恢复执行。Linux中没有一个具体的状态描述符来表示挂起状态。当进程收到SIGSTOP信号而暂停时此时进程就处于暂停挂起状态。当进程因为等待资源而长时间处于这种状态时也可以认为是一种广义的挂起状态。所以在Linux系统中需要根据进程的实际状态和挂起的原因来判断。
关于进程程序加载的理解
首先需要有一个共识现代操作系统几乎不会做浪费空间和浪费时间的事。在我们玩大型游戏的时候不可能一股脑将所有的代码都加载到内存中。因为当前主流的内存大小是16GB、32GB。而一个大型游戏动则几十上百GB的大小。这也就意味着我们在玩大型游戏时内存中始终只有有一部分的代码在被执行。这种运行方式称之为惰性加载模式。因为游戏的代码和数据没有办法全部加载到内存里。但是操作系统如何判断当前的需要的代码是否在物理内存里呢答案是通过页表的一个标记位字段来进行确认。因为在进程运行时操作系统会将页表的虚拟地址都进行填写。当进程访问到一个虚拟地址对应的物理地址为空时就会产生缺页中断。 注这个概念先暂时提出来不详细谈。然后操作系统会去物理内存上开辟空间然后将磁盘的代码和数据拷贝到物理内存中。最后将物理地址和虚拟地址的映射关系建立。这样才能正常的玩游戏。
现在我们不能仅仅是将进程加载理解为将程序的代码和数据加载到内存中。程序的加载应该是先由操作系统创建对应的内核数据结构对象如task_struct、mm_struct、页表等。然后在操作系统的统筹内存管理下将对应的映射关系建立好后将程序的代码和数据加载到内存中。
再谈谈进程切换的概念
有了上述知识的铺垫我们对进程切换的理解也应该有所改观。进程切换不仅仅是PCB要进行切换。对应的进程地址空间、页表以及代码和数据都要切换。而这一切的动作都是操作系统自动为我们做的。虽然在设备高度精密化、芯片性能越来越棒的今天这进程切换对于用户的感知是不明显的。但是进程切换依旧是一个不小的工程。
如何做到进程独立
首先创建一个进程需要维护对应的PCB数据结构对象、进程地址空间数据结构对象以及页表数据结构对象。这些数据结构对象都是一个个独立的对象。创建的每一个进程都需要维护这三个数据结构对象以达到进程独立性。
其次进程的代码数据加载到物理内存中。通过页表的映射下虚拟地址可以一样而物理地址完全不一样这样每个进程的代码数据和虚拟地址就解耦了。对于父子进程而言它们指向同一块代码但是数据区各自私有一份。哪怕其中一个进程结束了依旧值释放与自己相关的PCB、页表以及对应的物理内存空间。并不会影响到其他人。