印象系列-理解进程的存在

前言

大多数开发者并不对进程有过多细致了解,至少在不少层面上,普通开发者不必理会这些细节,操做系统存在的意义就是消除开发者在这方面遭遇的恐慌,使得可以快速编写出能够执行的代码。那么理解进程的意义正是但愿在操做系统层面有新的认识,在处理多进程和并发时能从根本上判断问题。php

本文将从最简单的操做系统模型去分析进程的存在,在操做系统中,任何代码的执行都必须依附于某个进程,能够说,进程就是代码在操做系统执行的基本单元。所以,下面将操做系统各个核心模块作个描绘,而后了解进程在整个系统中存在的形式。java

代码的最终形式

不管是何种语言,高级的如php、java、python仍是低级的c最终都会转化成统一规范的二进制流,在操做系统的控制下二进制流流通于各个硬件,经由CPU实现计算处理。python

这里,咱们以c语言为例,讲述代码转化的过程。当你建立以下代码的c文件code.c:linux

int sum=0;
int sum(int x,inty){
    int t=x+y;
    sum+=t;
    return t;
}复制代码

操做系统接下来经过编译器转化成以下形式的汇编代码code.s:程序员

pushl %ebp   
movl %esp,%ebp
movl 12(%ebp),%eax
addl 8(%ebp),%eax
addl %eax,sum
popl %ebp
ret复制代码

汇编代码无限接近代码文件在操做系统最终存在的形式,若是不理解上面每一行的意义并没关系,咱们如今只须要知道每一行表明着一个指令,而指令是处理器(CPU)执行的基本单位。尽管汇编代码已经无限接近机器硬件了,但计算机硬件只能识别二进制,因此汇编代码还会通过一次转换,经过汇编器将汇编代码转化成二进制形式(下文左边):golang

55                               pushl %ebp
89 e5                            movl %esp,%eb    
8b 45 0c                         movl 12(%ebp),%eax
03 45 08                         addl 8(%ebp),%eax
01 05 00 00 00 00                addl %eax,sum
5d                               popl %ebp
c3                               ret复制代码

为了便于文本编写,上文左边二进制代码咱们经过16进制来表示,每行的右边是对应的原汇编代码说明,经过上面的转化,咱们编写的c代码最终变成了二进制流!编程

接下来CPU将加载上面每一行二进制流,按顺序执行每一个指令,完成整个过程。bash

CPU如何运算代码

计算机从上个世纪发展至今,性能实现了巨大的飞跃,但计算机的处理模型一直没有改变,就是咱们熟知的"冯诺依曼结构计算机"。这个结构的计算机要求:计算机的数制采用二进制,计算机应该按照程序顺序执行服务器

时至至今,咱们所用的计算机依然是冯诺依曼计算机(额...你或许据说过量子计算机,但你应该没体验过)。CPU保持了高速的运算能力,这里的运算能力体如今对指令的执行频次上,一个完整逻辑的代码最终都将转化成按顺序排放的指令序列,CPU时时刻刻地对待处理指令按序执行。架构

一个单核的CPU在每一个时钟周期内完成一次运算,咱们常说的主频就是指 CPU 内核 工做的 时钟频率,即每秒钟能产生多少次时钟中断。咱们须要了解的是,时钟中断是计算机硬件运做的一种方式,每次时钟的中断至关于一次对硬件的触发(你或许能够想象为对函数的一次调用了)。一个主频为1GHz的CPU意味着每一秒钟CPU可实现1000000000次触发,若是每条指令均可以在一次触发中完成执行,那么CPU的运算能力咱们能够简单理解为每秒可执行1000000000条指令。

那么,CPU在运算过程当中和指令的关系是怎样的?

经过上文可知,代码编译后的最终形式是二进制流,而且会保存在存储器中供加载。在指令运行过程当中,CPU须要知道每次执行的下一条指令地址,而且执行后的相关状态须要实时保存,在CPU执行频率如此快的状况下,要求必须有足够快的速度实现数据存储,而寄存器就是这么一个离CPU最近的存储硬件,它速度足够快,知足CPU的高速存储需求,接下来咱们结合上文示例的汇编代码来说述寄存器和CPU之间的关系。

寄存器表明的是CPU可直接访问的高速存储器,在Y86处理器中,有8个寄存器(在汇编程序中以%符号开头来表示,如%eax,%ecx....表明不一样寄存器),每一个寄存器可存储四个字节

上一节在讲述代码编译的过程当中,咱们以一个sum函数作了示例,该函数实现了两个参数x、y的相加,其中咱们看到以下一条指令:

movl 12(%ebp),%eax复制代码

该指令在原c代码中至关于获取参数x的值,具体功能为:将寄存器%ebp保存的值加上12,而后将获得的值做为内存地址去内存中获取对应值,并保存到%eax寄存器中。能够理解为,%ebp保存着内存某个地址,而参数x的值被放置在该内存地址偏移12(这里的单位是字节)的地方,找到该值后放置到%eax寄存器中,存储状态以下图所示:


随后开始执行指令:

addl 8(%ebp),%eax复制代码

其中addl指令会命令CPU执行加的运算操做,具体功能为:将%ebp保存的值加上8,而后获得的结果做为内存地址从内存空间取出对应值,将该值和%eax保存的数据进行相加操做,最后将结果保存到%eax寄存器中。能够理解为,参数y一开始放置在%ebp所保存内存地址偏移量为8的地方,addl命令首先从该地址获取y值,接下来从%eax获取x值,最后执行相加操做,由此实现了sum函数中对参数x和y的计算。存储状态以下图所示:


经过上面两条指令,咱们大体了解了sum函数的核心计算过程,CPU在这个过程当中扮演了主要角色,寄存器则配合CPU完成了一些存储状态的相关操做(放置参数变量的地址,保存相关结果值...)。

值得注意的是,这个过程还有一个专门寄存器用于存放待执行指令的地址,每执行一条指令时都会更新PC寄存器,用于指向下一条指令的地址,操做系统经过维护好PC寄存器保存的内容很好地控制了CPU的运算过程。

操做系统中的调度

从CPU的执行过程来看,在单核的条件下,CPU在每一个时钟周期最多只能处理一条指令,若是CPU一直在不间断处理某个代码逻辑,那么其它的应用程序将没法获得执行的机会。所以,必须引入一种机制合理分配计算资源,让不一样的代码逻辑可以及时获得处理,调度的概念在这里出现了,操做系统经过调度的机制,在每秒钟处理频次如此高的状况下,每一秒钟的单位时间CPU能够处理不少不一样代码逻辑的指令,实现了“并发”的操做。

那么,调度的对象是什么,这个过程如何去理解呢?

上文中的示例代码,完成了两个数值的相加操做,假设如今还有另外一个代码逻辑也在运行过程当中,完成的是两个数值的相减操做,咱们但愿这两个过程互不相关互不干扰。

另外一个代码逻辑以下:

int sub(int x,inty){
    int t=x-y;
    return t;
}复制代码

编译后的形式为:

55                               pushl %ebp
89 e5                            movl %esp,%ebp   
8b 45 0c                         movl 20(%ebp),%eax
61 45 08                         subl 16(%ebp),%eax
01 05 00 00 00 00                addl %eax,sum
5d                               popl %ebp
c3                               ret复制代码

从汇编代码能够知道,该函数的x、y参数在执行过程当中放置在%ebp寄存器所保存地址对应偏移字节的位置处,最终的执行结果也保存在%eax寄存器,只不过%ebp在两个执行过程当中会存放不一样的值用于区分不一样代码空间的内存地址。寄存器是被全部执行过程共享的,按照咱们的设想,若是CPU在运算过程当中直接来回调用两个逻辑的指令,那么期间寄存器保存的值将会受到另外一个程序的干扰,那么将没法获得正确的结果!

咱们来模拟CPU运算过程可能遭遇的一种状态,当PC寄存器定位到sum函数以下指令时:

movl 12(%ebp),%eax复制代码

完成了x值在%eax寄存器的保存操做,此时操做系统经过调度将PC指定到sub函数的以下指令:

movl 20(%ebp),%eax复制代码

若是在此过程不对%ebp进行更新操做,那么sub函数的x值将会按照sum函数执行的存储状态来继续处理,这显然是不对的,会形成取值的错误。

一样的状况,当CPU执行完sum函数的下一条指令时:

addl 8(%ebp),%eax复制代码

此时,%eax保存着x、y相加的结果,接下来操做系统经过调度将PC指定到sub函数的以下指令:

movl 20(%ebp),%eax复制代码

能够预见的是,%eax值被覆盖了,那么当CPU从新调度到sum函数的后续指令时,将发生各类可能的错误。

因此,在调度过程当中,必须设计一套模型,使得各个代码逻辑在执行过程当中,关于存储空间的利用不会互相干扰,而且CPU能完整地执行完各个逻辑。

上下文的概念和进程的存在

在前面示例的案例中,若是要让两个函数同时运行,经过CPU高频的调度能够“无感知”地分配计算资源进行处理,但在两个不一样代码逻辑中来回计算,须要考虑一些存储空间的冲突问题,避免数据受到彼此干扰,这里须要说起一个重要概念:上下文环境

我的刚开始接触编程时,偶尔会在书本或相关文档中见到“上下文”的字眼,当时并无太在乎这个概念,也没真正理解过。何谓上下文?百科是这样归纳的:

上下文,即语境、语意,是语言学科(语言学、社会语言学、篇章分析、语用学、符号学等)的概念。

但在刚刚分析的问题中,我我的能够用蹩脚的话语来讲明上下文环境:某个执行中代码逻辑的先后关系和存储状态,先后关系说明了在这个环境下需保证逻辑的正确性,存储状态说明了相关数据内容在先后执行过程当中需保持一致的状态,不可异常变更。

假设CPU在执行sum函数过程当中,还未执行指令movl 12(%ebp),%eax的状况下直接执行addl 8(%ebp),%eax那么这个先后关系就被破坏了。若是在执行sum函数的指令addl 8(%ebp),%eax后开始调度sub函数的movl 20(%ebp),%eax指令,那么%eax数值被干扰,当CPU从新调度执行sum函数后续指令时,存储状态的一致性被破坏了。

所以,维护好上下文环境就是将每一个独立的代码逻辑当作一个完整而封闭的执行单元来区别处理,进程的概念就是在这样的需求下被设计了出来,能够说,进程做为不一样程序执行的基本单元,维护了相应的上下文环境,在CPU高速调度过程当中保证了不一样程序的正确运行!

关于进程的概念,linux操做系统中是这样理解的:

程序是一个可执行文件,而进程是一个执行中的程序实例。利用分时技术,在Linux操做系统上能够同时运行多个进程。分时技术的基本原理是把CPU的运行时间划分红一个个规定长度的时间片,让每一个进程在一个时间内运行。当进程的时间片用完时系统就利用调度程序切换到另外一个进程去运行。所以实际上对于具备单个CPU的机器来讲某一时刻只能运行一个进程。但因为每一个进程运行的时间片很短,因此表面看起来好像全部进程都在同时运行着。

当前包括Linux等操做系统通过数十年发展,进程在操做系统内部的表示已经变得很是复杂,包括线程、协程等概念也被创造出来,但全部的程序最终都依附于进程。本文意在对进程完成初步印象,所以咱们将用最简单的方式来结合前面案例构建一个进程结构。

在上面的调度案例中,咱们遭遇了上下文环境不一致的问题,一个是代码逻辑方面、一个是存储状态方面,咱们分别从这两个方面进行分析。

代码逻辑

CPU在调度过程当中,从sum函数切换到sub函数执行时,首先须要知道接下来该执行sub函数哪一个位置的指令,从而切换到sub函数执行前能够更新PC寄存器的值,让CPU沿着上一次执行的位置按序处理后续指令。这里,须要有一个存储空间用于保存各个代码逻辑待需执行的指令位置,每当CPU调度到该程序时,从该空间提取出指令位置信息,恢复到PC寄存器,整个过程能够实现完整的处理。

存储状态

当执行完sum函数的 movl 12(%ebp),%eax 指令后,%eax保存着sum函数的x变量值,用于后续指令的调用,但当CPU接下来调度到sub函数的 movl 20(%ebp),%eax 指令执行后,%eax的值将受到干扰,所以每次调度都必须对当前上下文环境的寄存器值进行保存,即在准备调度到sub函数前将%eax的值保存到sum函数专有的内存空间,在后续从新执行sum函数时,再从该内存空间恢复%eax值,这样保证了sum函数后续的指令 addl 8(%ebp),%eax 正常处理!这些过程由操做系统的进程机制自动处理,对程序员而言都是透明的。

因此,每一个进程必需要求有一个独立的内存空间,这个内存空间的第一做用就是维护当前代码逻辑的上下文环境信息!

linux操做系统是怎么实现进程管理的?咱们来窥探下linux本人在30年前开发linux内核第一版时的思路:linux内核在内存空间为每一个进程开辟一个独立而固定的内存空间来存放进程结构,进程结构保存了不一样进程当前的上下文环境信息。结合前文的分析,咱们了解到至少在该空间保存了进程待执行指令的地址(用于恢复PC寄存器),当调度发生准备切换到另外一进程时,操做系统会将各个寄存器值保存到进程空间对应位置中,当调度从新切换到该进程时再从进程空间恢复到各个寄存器,进程切换就是在这么一个过程当中反反复复。

若是你理解了这个基本过程,那么应该明白进程的调度实际上是有成本的,操做系统每次对进程的调度,都须要对不少存储状态进行处理,目前操做系统不少进程状态都存储在内存中,这就要求每次调度都会对内存进行了必定的IO操做,这对于每秒钟亿万次运算的CPU来讲不得不等待IO的过程,可能会形成必定的延迟。

相关问题

有个技术编写一个从远端服务器拉取数据的脚本,在四核CPU的服务器上操做。当时一会儿开启了几十个进程同时往远端拉取,发现进程开的越多,整个服务器从远端拉取数据的速度反而越慢,最终尚未开4个进程的块,这是为何呢?

为何说线程比较轻量?这个轻量的轻如何从底层去理解?

golang语言做为21世纪的C语言,以高性能、高并发著称,其中golang有一个叫“协程”的概念,经过协程一个应用程序能够在消耗极少内存和其它计算资源的条件下实现大量并发处理,这种技术究竟是怎么实现的?

后语

在本人的理解中,进程存在的意义就是为了管理不一样的程序,维护着不一样程序的上下文环境,这些都是在调度的场景中被设计出来的。不管操做系统如何庞大复杂,进程调度的核心概念都离不开此。本文在不少关键细节上并无作很是细致而严谨的说明,突出的是进程在调度中扮演的角色,这个角色若是深究还有很是多的特性和细节,能够自行查找相关资料了解。


参考:《深刻理解计算机系统》、《linux内核彻底注释》、《linux内核架构》

相关文章
相关标签/搜索