概述
先看一个小故事:
为了使我们写的代码能够自己跑起来,我们将它送进了城(进程)里。
既然进了城里,就要遵守城里人的规矩,城中有个专门管辖你们的 城管(操作系统),人家让你休息就休息,让你工作就工作,毕竟 摊位(CPU)不多,而要工作的人多着去了,每个人都想要占这个摊位来工作。
所以 城管(操作系统)为了公平起见,使用了一种 策略(调度),给每个人一个固定的工作时间 (时间片),时间到了就会通知你去休息,换另外一个人上场工作。另外,你在休息时候也不能偷懒,要记住工作到哪了,不然下次到你工作了,你忘记工作到哪了,那还怎么继续?
当然,一个城中肯定还有不少 县城(线程)。有的人就进入了 县城(线程)工作,这里相对轻松一些,在休息的时候,要记住的东西相对较少,而且还能共享城里的资源。
通过这个故事,我们是不是对进程线程有了个大概的印象。什么!没有?也没关系,接下来的正文我们就详细探讨下进程,线程的运作。
一、进程的概念
我们编写的代码只是一个存储在硬盘的静态文件,通过编译后会生成一个二进制的可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」。
问个问题:你去煮开水时,你会傻傻的等水壶烧开吗?
很明显,小孩都不会傻等,我们会在水烧开之前去做其他事情。当水烧开了,我们自然就会听到“嘀嘀嘀”的声音,于是再把烧开的水倒入到水杯里。
同样,假如有一个读取硬盘文件数据的程序被执行了,那么当从硬盘读取数据的时候,硬盘的读写速度是非常慢的,如果这个时候CPU还傻傻的等硬盘返回数据的话,那么CPU 的利用率是非常低的。
所以,当进程要从硬盘读取数据时,CPU 不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU 会收到一个中断的信号,于是 CPU 再继续运行这个进程。
这种 多个程序交替执行的思想,就是 CPU 管理多个进程的初步想法。对于一个支持多进程的系统,CPU 会从一个进程快速切换至另一个进程,其间每个进程各运行几十或几百个毫秒。
虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生了并行的错觉,实际上这是「并发」。
那么并发和并行有什么区别呢? 来,请看下图:
进程与程序的关系的类比
到了晚饭时间,一对小情侣肚子都咕咕叫了,于是男生见机行事,就想给女生做晚饭,所以他就在网上找了辣子鸡的菜谱,接着买了一些鸡肉、辣椒、香料等材料,然后边看边学边做这道菜。
突然,女生说她想喝可乐,那么男生只好把做菜的事情暂停一下,并在手机菜谱标记做到哪一个步骤,把状态信息记录了下来。
然后男生听从女生的指令,跑去下楼买了一瓶冰可乐后,又回到厨房继续做菜。
这体现了,CPU 可以从一个进程(做菜)切换到另外一个进程(买可乐),在切换前必须要记录当前进程中运行的状态信息,以备下次切换回来的时候可以恢复执行。
所以,可以发现进程有着「运行 - 暂停 - 运行」的活动规律。
二、进程的状态
在上面,我们知道了进程有着「运行 - 暂停 - 运行」的活动规律。一般说来,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。
它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。
所以,在一个进程的活动期间,至少具备 运行状态、就绪状态、阻塞状态。
上图中各个状态的意义:
运行状态(Runing):该时刻进程占用 CPU;
就绪状态(Ready):可运行,但因为其他进程正在运行而暂停停止;
阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;
当然,进程另外两个基本状态:
创建状态(new):进程正在被创建时的状态;
结束状态(Exit):进程正在从系统中消失时的状态;
于是,一个完整的进程状态的变迁如下图:
再来详细说明一下进程的状态变迁:
☞ NULL -> 创建状态:一个新进程被创建时的第一个状态;
☞ 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
☞ 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
☞ 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
☞ 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
☞ 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
☞ 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占用着物理内存就一种浪费物理内存的行为。
所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。
那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。
另外,挂起状态可以分为两种:
☞ 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
☞ 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;
这两种挂起状态加上前面的五种状态,就变成了七种状态变迁(留给我的颜色不多了),见如下图:
导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:
☞通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程。
☞用户希望挂起一个程序的执行,比如在 Linux 中用 Ctrl+Z 挂起进程;
三、进程的控制结构
在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的。
那 PCB 是什么呢?打开知乎搜索你就会发现这个东西并不是那么简单。
哈哈,开个玩笑~~
我们可是正经人,怎么会去看那些问题呢?是吧,回来回来。
PCB 是进程存在的唯一标识
这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
PCB 具体包含什么信息呢?
进程描述信息
☞ 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
☞ 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
进程控制和管理信息
☞ 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
☞ 进程优先级:进程抢占 CPU 时的优先级;
资源分配清单
☞ 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
CPU 相关信息
☞ CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
可见,PCB 包含信息还是比较多的。
那么每个 PCB 是如何组织的呢?
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
☞ 将所有处于就绪状态的进程链在一起,称为就绪队列;
☞ 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
☞ 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。
就绪队列和阻塞队列链表的组织形式如下图:
除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。
一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。
四、进程的控制
我们熟知了进程的状态变迁和进程的数据结构 PCB 后,再来看看进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制。
1. 创建进程
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程。
注意:Linux 操作系统对于终止有子进程的父进程,会把子进程交给 1
号进程接管。本文所指出的进程终止概念是宏观操作系统的一种观点,最后怎么实现当然是看具体的操作系统。
创建进程的过程如下:
☞ 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB,PCB 是有限的,若申请失败则创建失败;
☞ 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源;
☞ 初始化 PCB;
☞ 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行;
2. 终止进程
进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)。
终止进程的过程如下:
☞ 查找需要终止的进程的 PCB;
☞ 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
☞ 如果其还有子进程,则应将其所有子进程终止;
☞ 将该进程所拥有的全部资源都归还给父进程或操作系统;
☞ 将其从 PCB 所在队列中删除;
3. 阻塞进程
当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。
阻塞进程的过程如下:
☞ 找到将要被阻塞进程标识号对应的 PCB;
☞ 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
将该 PCB 插入的阻塞队列中去;
4. 唤醒进程
进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。
如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。
唤醒进程的过程如下:
☞ 在该事件的阻塞队列中找到相应进程的 PCB;
☞ 将其从阻塞队列中移出,并置其状态为就绪状态;
☞ 把该 PCB 插入到就绪队列中,等待调度程序调度;
进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。
五、进程的上下文切换
各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。
在详细说进程上下文切换前,我们先来看看 CPU 上下文切换
大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运行,于是就造成同时运行的错觉。
任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器。
CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。
再来,程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。
既然知道了什么是 CPU 上下文,那理解 CPU 上下文切换就不难了
CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。
那么进程的上下文切换到底是切换什么呢?
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。
发生进程上下文切换有哪些场景?
☞ 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
☞ 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
☞ 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
☞ 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
☞ 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
以上,就是发生进程上下文切换的常见场景了。
最后
以上就是精明老鼠为你收集整理的(1)我们的代码被 “送进城里 ” 后发生了什么 ^o^ —— 「进程」篇一、进程的概念二、进程的状态三、进程的控制结构四、进程的控制五、进程的上下文切换的全部内容,希望文章能够帮你解决(1)我们的代码被 “送进城里 ” 后发生了什么 ^o^ —— 「进程」篇一、进程的概念二、进程的状态三、进程的控制结构四、进程的控制五、进程的上下文切换所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复