我所理解的线程和进程

我所理解的线程和进程(一)

0x00 没什么卵用的前言

最近刚巧和朋友讨论起进程和线程的概念,于是突发奇想写了这篇博客,希望不依托于具体的编程语言和平台简述进程和线程的概念,所以本篇博客不讨论实现细节,仅从宏观上探讨,预计本文分为两篇,分别对进程和线程进行介绍。笔者才疏学浅,难免有疏漏之处,望大神指正。不论是发现错误、给出修改意见还是讨论相关问题,可以给我发邮件联系,我的邮箱:[email protected]

0x01 追根溯源——为什么会有进程和线程的概念

在遥远的上古时代,计算机大神们在草稿纸上写程序,拿大脑模拟运行来Debug,用打孔纸带把程序输入计算机,这个时候尚未出现操作系统,整台计算机由一个用户独占,输入什么指令就执行什么指令,一切资源由一个人独占,自然也就不存在进程线程的概念。
我所理解的线程和进程
随着时间的推移,程序员越来越多,自然创造出的程序就越来越多,随着要运行的任务的增加,计算机运算速度的增加,原始的手工代码输入方式却仍旧大的改变,于是暴露出一个巨大的问题:许多程序仅仅只运行几分钟,将程序输入计算机却动辄需要十几分钟乃至半个小时,极大的降低了计算机的使用效率,这怎么能成?大神脑海中闪过无数个点子,却得等着操作员慢慢地把写好的程序输入计算机,伟大的设计无法实现,这简直是叫人痛不欲生的折磨,于是大神们想出来一个初级的解决方案:批处理系统。

大神们把写好的程序依次输入到计算机,保存在磁带上(这个年代还没有磁盘),当一个程序运行完计算机就自动的把磁带中的下一个程序加载入内存,这样就省去了输入程序的时间,提高了计算机的工作效率,当一盘磁带的程序运行完只需要换一卷磁带即可。这样许许多多的程序就一批接一批地不断运行,于是起名为“批处理系统” ,windows中的cmd脚本翻译为批处理也有这方面的原因。

现在来看把这个简陋的东西称作“系统”是不合实际的,因为所谓的系统并没有现代操作系统的特征,仅仅是负责把磁盘中的程序依次装入内存,但已经称得上是极大的进步,所以我们才说现代操作系统的雏形在此时出现了。

到了60年代,计算机技术进一步发展,运行速度和操作方式都有了不小的进步,CPU可以命令他的辅助处理器来进行输入输出这种低端的操作,集中精力于程序的运行,但程序通常总得等待速度相对较慢的辅助处理器(通道)把数据从磁带读入,磁带已经很慢了,更要命的是等待用户的输入,这就同样浪费了宝贵的CPU资源,让CPU长期闲置,处在一种无事可做的状态。

CPU资源的浪费是一种不可饶恕的罪过,特别是那个计算机极其昂贵的年代,为了剥削可怜的计算机,于是大神们升级了批处理系统,开发出一种能够更充分利用计算机资源的操作系统:多道批处理系统。

多道批处理系统同时在内存中放置多个程序,当一个程序等待较缓慢的输入输出操作执行完毕时,系统自动让另一个程序运行,这样就使得CPU可以几乎不用等待地全速运转(同时让电表全速运转),又一次极大的提升了效率。而到了多道批处理的时代,操作系统才算是得到了真正的完善,有了点现代操作系统的样子。也正是这个时代开始有了早期进程的概念。

这时的进程基本和程序相对应,一个程序即是一个进程,拥有自己的代码和数据。

在此之后进一步发展出了分时操作系统、实时操作系统等等诸多种类,而现代操作系统多数是历史上种种操作系统类型加以融合的产物,而现代操作系统为了进一步提高运行效率,方便程序开发又提出了线程的概念,进一步缩小了操作系统和程序员控制程序执行的粒度。

所以总的来说,进程线程的发展是伴随操作系统发展而发展的,操作系统的设计又决定于当时时代计算机的性能,所以说操作系统发展史就是程序员们剥削计算机,榨干计算机性能的计算机的苦难史和斗争史。

0x02 进程初探——“进程”是个啥

在1978年的全国操作系统会议上,经讨论,老一辈的计算机专家给出了这样的中文定义:

“进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动”

看懂了吧,好我们继续——个屁嘞!你看懂了理解了就已经是大神了,请立即点击右上角的小叉来节约宝贵的时间,没看懂才是正常的,这是从计算机科学角度给出的一个极其抽象的严谨定义,要理解这个概念最好还是先实例化了,把玩一番再返回头品味。 我所理解的线程和进程

之前我们说到进程的概念是伴随着多道批处理系统的产生而产生的,在当时来看是与程序相对应的概念,一个程序即是一个进程,一个程序包括两部分——代码(指令)和数据,那么相应的,一个进程也就应该包含两部分,一是代码(指令),二是数据。所以基本上我们可以认为进程就是磁盘中程序载入内存的一个副本。

我们之前说,之所以发明进程,是为了在一个进程等待一个占用大量时间而不需要CPU参与的工作时,可以让CPU执行其他工作来提高效率,所以相应的进程就有了两种状态——干活和等待,不需要CPU时进程无聊的在一边等数据,数据来了就继续干活,这就是进程的“两种状态模型”——即运行态和未运行态,类比一下就是你每搬了一天砖就得睡一觉休息休息。

当你打开任务管理器时,总会感觉内存不够用,而你肯定不希望已经运行完了的程序继续待在你宝贵的内存里,占用着你的DDR4黄金等价的内存。当年设计多道批处理系统的设计师们同样想到了这个问题,当年的内存比今天可是贵了不止一星半点。

于是:当一个程序被载入内存时创建一个相对应的进程,而当程序运行完毕停止,就应该释放掉它所占用的空间,把这个进程从内存中清理掉,留下空间给其他要运行的程序使用,这就有了进程的创建和退出状态,就好比你来一个公司应聘被录取,办理入职手续熟悉工作环境的状态和被开除了搬东西滚蛋的状态。

而另一方面,并不是进程等待的数据来了进程就可以马上继续运行,内存中同时存在着多个进程,可能当一个进程可以继续运行时另一个进程正在干的热火朝天停不下来,怎样调度CPU完全取决于操作系统心情(设计),所以你的进程很有可能还得等当前进程干完了手头的活才能继续,这个能运行了的等待我们就称它为:“就绪态”,而把不能运行等数据的等待称为“阻塞态”,当等了许久好不容易被系统批准运行,进程继续干活的状态就称为“运行态”。

再打个比方:你好不容易完成了入职,老板给你分派了工作你开始干活,这就是“运行态”,干了几天你说得找项目经理问问客户需求的细节,老板说项目经理不在你等着吧,于是你工作没法进行得等项目经理回来,得到了一段偷懒的时间,这时候你就处于“阻塞态”,项目经理回来了你问完了问题,经理说哎呀不巧上午你没啥事,你工位电脑就借给隔壁小王用了,你再等等吧,这时候你能干活了但经理让别人先用着你的电脑,这段闲着的时间就是“就绪态”。 我所理解的线程和进程

以上这创建、退出、就绪、运行、阻塞五种状态就构成了进程的“五状态模型”,至于为什么就绪排在运行前面呢,因为一个进程创建好后往往正有别的进程正在运行,所以新建的进程总得在就绪态等一会儿。

如果你既喜欢chrome又没有常关网页的习惯,你会发现chrome总有办法用光你的内存(chrome每打开一个网页就新建一个进程),但事实上你不会同时看所有网页,很多网页你甚至不会再看仅仅是忘了关而已,但这些网页的进程缺占用着你宝贵的内存,俗称占着茅坑不拉屎。

现代操作系统为了解决这个问题,大多实现了虚拟内存机制,windows下有内存分页文件,Linux下有swap交换分区,这都是虚拟内存占用的磁盘空间,原理就是当一个进程(或进程的一个部分)一直用不到又没被关闭的时候,就把这个进程从内存调到磁盘中,腾出内存空间供一直在忙碌的进程使用,而一旦被调到磁盘中,进程想继续执行还得重新载入内存,进程位于磁盘中的状态就称为“挂起态”。

假设公司有几台比较快的新电脑和一些卡的要命的老古董,经理发现你天天占着快的电脑却啥都不干,就把你安排到了老古董机器前,把好电脑让给一直在忙着干活的员工,不过值得注意的一点是:磁盘中被挂起的进程往往还能被调回内存,但总是无所事事而被“挂起”的员工往往不会有好下场。

但通常你之所以成为一个被挂起的员工往往是有原因的,要么你是在等BOSS对你新想法批准的会议进行,你想工作却不得不等你的老板举行会议,这样你就处在“阻塞/挂起态”,要么就是你原来工位上的哥们占着地盘不走,你又不好意思轰走他,就不得不等他忙完,那这你就处在“就绪/挂起态”。

对于计算机中的进程而言,当进程因为等待一个事件(比如IO读写)而被挂起就进入了“阻塞/挂起态”,而当进程等待的事件已经发生,载入内存就能运行,却因为种种原因无法被立刻载入内存时,进程处在的状态就是“就绪/挂起态”,这样我们就完成了挂起态的介绍,进一步完善了之前的“五状态进程模型”。
我所理解的线程和进程
添加了挂起态的五状态模型也正是现代操作系统广泛采用的进程状态模型,但各个操作系统有不同的实现,对五状态模型也有不同的增改,初步学习进程了解添加了挂起态的五状态模型就足以了。

0x03 进程控制——操作系统的工作

为了控制各个进程,对各个进程进行调度,操作系统需要一个特殊的数据结构——进程控制块(可以理解为一个只有成员变量没有方法的类),这个数据结构中包含了诸多变量来描述一个进程的相关信息,比如存放在内存中的位置、进程标识信息、处理器状态信息和进程控制信息等。

进程控制块是操作系统中最重要的数据结构,没有之一,不接受反驳。操作系统对进程的一切控制都是基于进程控制块进行的。

玩游戏的时候总会遇到开挂的神仙,外挂本身也是一个程序,有自己的进程,按道理外挂进程不应该干涉到游戏进程,但外挂通过利用系统漏洞等方式,更改了游戏进程中的数据和代码,于是就实现了诸如无敌一类的操作,外挂玩家令人不齿,更叫人头疼的是种种黑客软件,往往被用于非法途径以牟利。

为了避免一个进程访问其他进程的内存空间或是操作系统的内存空间,提高系统安全性,CPU厂商给CPU的种种指令进行了分级,部分会危害到操作系统和其他进程安全的指令被划为特权指令,仅有操作系统可以执行,而另外的指令才是用户进程可以直接执行的指令。要判断究竟是谁在执行指令就要看执行指令的进程的状态,以此分为用户态和内核态(有时也成为系统态),特权指令只有处在内核态才可以执行,从而提高了计算机的安全性。

当一个用户进程要用的内核态才能执行的特权指令时,必须通过系统调用,把控制权交给操作系统,由操作系统先对指令安全性进行检查,再再内核态下执行,执行完毕后把控制权再交还给用户进程,这个过程无疑增大了开销但大大提高了安全性,总体来看还是利大于弊的。

操作系统控制进程所做的最多的工作是进程的创建和切换。进程创建时,操作系统将为进程对应程序的代码和数据分配内存空间,并载入内存,分配进程标识符(进程ID),创建相应的进程控制块,这个时候进程就处于创建态。运行中的进程常常遇到进程切换和模式切换,这两种切换同样是由系统进行调度的。

进程切换往往因为如下原因发生:一是时钟中断,当一个进程用完了系统分派给他的CPU时间片(时间片,即进程在中断前可以执行的最大CPU时间段),那么此时该进程就会暂时中断,CPU控制权由操作系统交给下一个进程;二是I/O中断,如果发生一个I/O活动,操作系统会检查是否有进程正在等待这个事件并决定是否中断正在运行的进程,把控制权交给等待这个事件的进程,这一点通常由进程优先级决定;三是内存失效,如果一个进程的一部分内存块被置换到了虚拟内存(即磁盘)中,那当进程访问这块不存在于内存中的数据时就会引起一个内存中断,由操作系统先把这部分内存块从磁盘加载到内存中,再继续执行被中断的进程。

进程切换时需要保存当前处理器的上下文状态(即寄存器数据),更新当前进程的进程控制块,选择下一个执行的进程并更新其进程控制块,恢复该进程的处理器上下文,这个过程涉及了许多额外工作,开销较大。而模式切换只在由用户态切换至内核态时发生,仅仅涉及进程运行状态的更改,只需更改内存控制块和保存恢复处理器上下文,因而开销较小。

还有值得注意的一点是,操作系统同样是由处理器执行的一个程序,与一般的用户程序并没有多大的不同,如果抽象的来看,操作系统不过是一个拥有极大特权的用于管理其他进程的进程。