Linux进程与运行


进程

什么是进程?

在Windows里,进程见到最多的地方就是——任务管理器。
任务管理器有一项叫进程,进程里有一堆东西,这些东西可能不占cpu,可能不占显卡,但是他一定占内存。
电脑死机了,我打开任务管理器,一看进程里,我的妈内存满了。于是我直接杀掉了不需要的东西,进程没了内存使用就小了,电脑就恢复正常了。所以,被杀掉的东西,就叫进程;杀掉进程,实际上是清掉进程的内存;在任务管理器进程栏出现的,都叫进程。 

  • steam新出了一个游戏,你等不到他史低了,直接一个扫码买下了这个游戏。这个时候,这个游戏叫进程吗?不叫,他还不能被玩,任务管理器里没有他
  • 这个游戏入库了,你跪在地上求校园网快点,最后终于下完了。这个时候,这个游戏叫进程吗?不叫,他还没有打开,任务管理器里没有他
  • 游戏下好了,你双击这个游戏,进入游戏开始了账号登录。虽然连新手教程都没有开始,但是这个时候,这个游戏叫进程吗?现在叫进程了,因为风扇开始转了,电脑开始卡了,一看任务管理器有他了

所以,进程是什么?占着内存在运行的东西,就叫进程

我们都会接触到一个词,叫程序设计。比如最短路程序设计,就是要设计出一段代码,计算机能通过这段代码,可以在所有情况下找到最短的路径。所以,程序,就是计算机运行的流程顺序;程序设计,就是设计出计算机能解决一个问题实现一个功能的流程顺序。
但是,我们写出的程序,一张纸,一个txt文件,一个屎山代码,机器别说执行,看都看不懂这一坨。所以,这个时候我们把写好的程序用一些工具翻译成可执行exe程序,然后需要的时候双击这个文件,你猜怎么着?

所以,再完善一下进程的概念:

一个被加载到内存中的程序,就叫进程


进程的描述 

先描述,再组织 

我们在操作系统里讲过,任何一个计算机部分的设计,都要遵循一个原则——先描述,再组织。 

但是在进程中,我们要描述什么?

描述

在Windows任务管理器下,给一个进程赋予的特征有:
名称,状态,CPU占用,内存占用,磁盘占用,网络占用等等

但是实际上这些都是给用户看的,这些信息对用户有用,所以才列在了任务管理器里。因为再多想想,还有一些必要的信息,没有被包含在里面: 

  • 操作系统想找到一个进程,难道是根据他的名字来找吗?如果是的,有重名的怎么办?同一个进程开了两次怎么办?所以当然不是,与Lambda表达式一样,对于每一个进程,操作系统都为他安排了一个PID序号,要找的时候,直接去找这个PID值就行了
  • 我们怎么判断一个进程跑了多少时间,这个进程结束没有?所以就要有一个status,和time
  • 一个进程在动态开辟内存的时候,临时变量和全局变量的内存地址是不连续的。所以多个进程之间,操作系统必须要知道每个进程用了哪些内存,避免同一块内存被多个进程同时占用导致牛头人了,此时就要有一个memory_block,记录每个进程使用的内存
  • ...

而这些怎么组织起来?对,结构体。对每个进程,都用一个结构体来把它包含起来,比如:

struct
{
	long long PID;//进程ID
	time_t time;//时间
	st status;//进程状态
	...
}

在操作系统的概念,把这个结构体称作为——PCB

PCB——process control block

而在Linux中,这个结构体的具体实现名称叫:

struct task_struct

组织

一个计算机,自然不可能只有一个进程。

  1. 我们深夜emo,想打开wyy音乐找几首歌听听,打开电脑,首先看到的就是显示器进程。
  2. 然后想连个Wifi,有个网卡进程。
  3. 打开wyy音乐,有个进程叫Cloud Music。
  4. 最后点开一首歌,还有一个播放的进程 

所以,我们只是进行了一个简单的动作,就有无数的进程被创建开始运行。而且,网易云在运行的时候,难道网卡进程就不运行了吗?难道显示器进程就不运行了吗?当然不是,所有进程的运行都是不停的。

有这么多进程,那我们怎么快速找到所有进程?我们怎么在找到进程的同时,去处理好他们运行的先后顺序?

先描述,再组织

easy,一个链表。我们用一个链表去管理进程,而链表中的结点:

struct task_node
{
	task_struct* PCB;//指向当前进程
	task_node* next;//指向下一个进程
}

也就是对每一个进程的结点,不仅包含了当前的进程,还包含了下一个进程的地址,

我们查找进程,就是查找链表结点;我们删除进程,就是用O(1)删除链表结点;我们按顺序运行进程,就是遍历链表。用这种方法,就把所有进程,用一张链表管理了起来。 

所以,为什么我们打开Wiodows任务管理器的时候,进程不会一瞬间全部出现,而是会一边加载一边出现?因为链表不能像数组一样随机访问,遍历链表是很耗时间的。


进程的运行

在启动一个进程的之后,操作系统会把三个东西放进内存里:

  1. PCB
  2. 数据段
  3. 代码段 

PCB很好理解。任何进程都有PCB,没有PCB操作系统就找不到他
数据段是什么?数据段就是进程里包含的变量,a=10,b=20,c=a+b=30
代码段是什么?代码段就是我们写的屎山,机器通过看代码段来决定怎么运行。 

在一个进程被运行之后,大部分情况,操作系统都不会去管数据段和代码段,那是进程自己内部的事情。操作系统只会去看PCB,PCB就像一个身份证

我们想让一个程序暂停,不可能去跑代码段现改代码,我们直接修改PCB的status,便可以了;
我们想让两个程序相互联系,我们也不可能跑数据段把另一个进程加上,我们只需要把PCB的地址传给他,两个进程便联系上了。
但是,为什么两个进程要相互联系? 


什么是同时运行? 

我们去思考一个问题:所有进程,是怎么满足同时运行的?

  • 首先,不可能是真的同时运行。因为一个计算机,就算配置再豪华,也只是八核十六核。每个核只能同时负责一个进程的运算,就像我们不能同时去思考19乘以15,和526除以9这两个式子的结果等于多少,我们只能解决完一个再解决另一个,不然数据混一起,我们也分不清楚哪个数据是哪个式子的计算结果。所以,计算机在“真同时”运行的进程数,最多只能为核的数量,而此时同时运行的进程叫做并行
  • 其次,也不可能是一个进程运行完之后再去执行下一个。因为我们知道,显示器是一刻不停在运行的,如果等运行完显示器再运行下一个,那排在显示器后面的进程就永远不会被运行。 

 所以,此时只剩下了一种情况——一个cpu在运行很多进程。那如何做到呢?

在很多游戏,或者制作视频的时候,我们肯定听过一个概念——帧率
一个游戏,假设帧率为60帧,意思就是,画面每秒刷新60幅画。但是,在一秒内,这60幅画可能在0.002秒就被计算完了,剩下的时间,我难道就抱着这60幅画发呆吗?我直接把这60幅画给显示器,还有这其他好多进程要等着我运算,我就扭头去计算其他进程了。
而同样,对每个进程,CPU都可以在极短的时间内计算完这一秒内需要用到的数据,然后把他搁在一边去计算下一个进程。

 但是,有好多进程,可能并没有秒的概念,比如说:

int main()
{
	int count = 0; 
	while(true)
	{
		count++;
	}
	return 0;
}

这里我就是想让他一直循环,我也不管一秒内需要多少次,你能算就一直算下去

所以,我们就采用一种公平的方式——时间片
说人话,就是每个进程只能运行固定的时间,不管你运行到了哪里,你都搁在一边,转过去运行下一个进程。
这个固定的时间,可能只有1毫秒,反正一切都是为了保证,在一秒内,所有进程都能被运行到,而且运行相同的时间,来使时间的分配合理且公平。
而假设我们想从网易云听歌,在这一秒内,网卡进程用几毫秒收到了网络信号,显示器用几毫秒刷新了屏幕,网易云进程用几毫秒找到了歌曲,所有进程都进行了运行且完成了这一秒内需要完成的任务,最终呈现出来的效果就如同伪同时运行。

但是,就延伸出了另一些问题:
一个进程在每秒运行完之后,我要将他搁在哪里?进程的运行是间断的,为什么播放器放出的声音却是连续的? 


运行队列

在上面讲到的伪同时运行中,会产生两个新问题:

  1. 怎么安排进程运行的顺序
  2. 怎么保证进程运行的速度 

这两个问题有点过于抽象了,我们还是分开来具体叙述:

运行顺序

首先,大量进程在cpu中等待着运行,绝对不是靠cpu挑选幸运儿一般随机挑选运行的。打个比方:

现在有ABCD四个进程。今天我cpu看你C进程顺眼,我就把你C进程放在cpu上跑一跑。
时间到了,我再看你D进程顺眼,我就把你D进程放在cpu上跑一跑。
再下一步,我又看到你C进程,不行,我还要把C进程跑一跑。

如果像这样,在一秒内,只有C进程和D进程在不断交替运行,A进程和B进程在一旁看着,这显然不符合资源分配的合理和公平性,所以进程的运行选择权不应该交给cpu,而是用一种公平的方式解决——排队。

 在组织中说到,所有的进程都可以用一个链表list串起来。同理,我们同样可以用一个链表,表示一个cpu对应的 需要运行的进程链表task_list

我们需要运行的时候,就将头部的进程拿出来交给cpu运行,运行完之后,就放入尾部重新排队 

往复交替,在所有进程都运行了一遍过后,最终又回到了task1为头,而进程的顺序没有发生改变。如此,每个进程的运行就是公平的,并且能保证进程不会有遗漏 

运行速度

但是这样的运行队列可以很直观感受到,运行队列如此往复,最终产生的结果为——同一个进程的运行是间断的。

换句话说,我们听一首歌看一个视频,播放器进程只会在一秒内的部分时间内工作,而非它工作的时间,我们是听不到播放器声音的。最终呈现出的效果,便是声音一卡一卡的,视频一卡一卡的,声音在一秒内播放一会就停下来了,但是实际上并非如此。
或者,假如进程一多,一个进程来不及在一秒内运行到,导致一些重要的进程运行被遗漏了。比如显示器进程如果没有在一秒内运行到,最后电脑在这一秒黑屏了,这种情况怎么解决? 

而说明这些问题,需要先解释两个概念——


OI是拖累速度的最大黑手 

 不妨用一段代码来测试一下:

//输出类似的内容:

//不断调用IO所用的时间
int clock1 = clock();
for (int i = 0; i < 10000; i++)
{
	printf("hello Linux\n");
}
int clock2 = clock();

//只调用一次IO使用的时间
int clock3 = clock();
string s;
for (int i = 0; i < 20000; i++)
{
	s += "hello Linux\n";
}
cout << s;
int clock4 = clock();

//输出结果
printf("调用IO执行10000次:  %d\n", clock2 - clock1);
printf("不调用IO执行20000次:%d\n", clock4 - clock3);
IO的输出结果

就算第二个程序输出的数量远超第一个,但是还是第二个用时更少,为什么?
(有人可能会说,因为第二个用了cout,而事实是——cout比printf更慢) 

我们在冯诺依曼中讲到,访问外设是计算机最慢的部分。每次访问外设,都要先通过操作系统调用系统接口,再调来调去最终才可以执行类似于printf等代码。第一个程序调用了10000次,而第二个程序虽然输出了20000个"hello Linux",但是调用只调用了一次,所以第二个快。

说了这么多,其实只想证明一件事情——
cpu的运行速度远超外设,在结果被呈现到显示屏上时,cpu早早就运行完成了。 

时间片与上下文 

还是这个程序

int main()
{
	int count = 0; 
	while(true)
	{
		count++;
	}
	return 0;
}

这个程序的运行是没有一个计算结果的。而正是有这样的进程,计算机无法判断进程的中断条件,所以只靠之前所说的理论,“一个进程计算完一秒内所需要计算的任务就切换到下一个进程”,是不可靠的。

在cpu的计算里,每个进程都配备有固定的运算时间。假设我们只让一个进程运行10毫秒,那么这个进程从运行开始计时,无论其运行到了哪个位置,运行了多少,一旦到了十毫秒,那么这个进程就会被强行中断,切换到下一个进程。 

比如我们这里从count=0开始运行,运行了10ms后,假设此时count=1000,没有代表任何具体的意义。但是时间到了,进程直接中断掉,切换到下一个进程。而这个10ms,一个进程被分配到的一次运行时间,就叫做进程的时间片。而运行完了10ms,就叫做进程的时间片到了。
而所有进程是一个队列。这个进程时间片到了,就被搁置到队列的末尾;所有进程都被运行了一遍,最终又会回到这个进程。此时,这个进程就会按照时间片,再进行一次运行,时间片到了,继续搁置末尾,像个旋转木马一样不断循环运行所有进程。

那有人就要问了,那进程被中断之后,再到他的下一次运行,难道是再从count=0重新开始跑一遍吗? 

用屁股想都不是。进程在中断的时候,会把运行时产生的临时数据,比如临时变量,运行到的行数等等全部放回进程的PCB中。而等到下一次运行的时候,就把PCB中的这些临时数据拿出来。cpu看到临时数据中的运行行数,就知道从哪个地方开始运行,而运行的途中发现一些变量,就从临时数据中找临时变量的值,这样就能保证虽然进程是中断的,但是数据并不是中断的。 

还是刚刚的进程,我们运行到count=1000的时候,时间片到了,进程中断。此时,cpu把产生的临时数据,比如行数(运行到了第6行)和临时变量(count=1000)放到PCB里,然后cpu就开始执行下一个进程。等到运行了一圈重新回到了该进程,就去PCB里找运行行数,发现是第6行,对应的是count++。但是count是什么?值是多少?就去PCB里找临时变量,发现count是一个int,值是1000,于是执行该语句将count++变为1001。 

而这些临时的数据(行数和变量),就被称作为进程的上下文。进程的上下文,就是为了定位进程运行到了哪一步,与运行到此时产生的数据。 


解决了这些概念,我们再回到那个问题:

  1. 首先,进程的运行是很快的。我们举例时的时间片是10ms,但是实际上可能比10ms更短。而且计算机会根据进程的多少去动态分配时间片,所以不可能出现某些进程没有运行到。
  2. 而更夸张的说,不仅是不可能出现某些进程没有运行到,甚至每个进程在一秒内都运行了几百遍。
    打个比方:一个进程的时间片是1ms,1秒内就能运行1000个进程。而假设一个cpu需要运行10个进程,那么每个进程就会在一秒内被运行100次。1秒100次,啥概念,世界拉裤链记录最快也才30秒203次,你都已经看不清了,那1秒100次,你一个人能观察出来他的间断吗? 

  3. 其次,IO比cpu的运算慢的多得多。所以,我们在看到显示器的画面,听到播放器的声音时,实际上cpu早已经计算好了,只剩下IO还在龟速地跑。所以,如果我们哪天看到显示器卡了,听到播放器卡了,首先不要怀疑cpu烧了,而应该想想

    进程的相互联系

最后,我们回到最初的问题:

为什么两个进程要相互联系? 

对每个cpu,都有一个属于自己的task_list,用于管理自己需要运行的进程。每一个cpu都会处理大量进程,而在同一个cpu上进行伪同时运行进程,我们便称之为进程的并发。但是,一个计算机不可能只有一个cpu,在不同的cpu上同时运行的进程,便叫做进程的并行。

为什么两个进程要相互联系?两个进程无非就两种关系:并发和并行。当我们要创建一个新进程的时候,首先要考虑的便是,把这个进程挂载到哪个cpu上,也就是加入到哪个cpu运行队列之中。而加入队列,便是把自己的PCB装进一个结点,与队列中的某个结点链接在一起,虽然进程是相互独立的,但是他们的结点是连在一起的,这便是两个进程的相互联系。

而真需要运行整个运行队列的时候,不可能把所有进程的数据都放在cpu里。我们只需要把每一个进程的PCB地址都放在队列里,需要运行的时候,通过地址去查找对应的PCB数据,而这个数据可能是这样的:

struct task_struct
{
	void* code;//代码段数据
	void* data;//上下文数据
	...
}

PCB里存储的也是指针,指向了需要执行的代码,和具体执行到了哪一行,对应的数据是什么。通过这样不停的地址查找,数据是放在一个固定的地方不变的,cpu不断执行来执行去,队列循环来循环去,其实只是一堆地址变量在移动来移动去,所以,


 Linux中的运行队列

所以,根据这些理论,Linux中便有了运行队列的雏形:

进程优先级 

按照之前所说,一个进程在完毕之后,就搁置在队列尾,然后重新开始排队。但是设想一个场景:

你正在玩一个网络游戏,计算机要做的首要任务是放大招吗?是祖安钢琴家吗?都不是,首要任务是联网。所以网卡进程,一定会在游戏进程之前。但是,我们只能保证把进程放在队列尾,却不能保证把进程的先后顺序,这个时候就需要一个东西——优先级,来确保进程运行的先后顺序。

PRI和NI

PRI,就是进程的优先级数。PRI值越低,优先级越高,放在越前面运行。
NI,是进程优先级的修正值,在比较优先级的时候,比较的是PRI+NI,也就是NI值越高,最终的优先级越低。
NI值的取值范围是-20~19,一共40个级别,至于为什么有40个,马上就会说道:

队列寻址 

为什么要分为40个优先级?或者说,为什么优先级会有一个范围?
我们去实现含有优先级的进程队列,当一个进程被放在队列中时,能想到最快的方法自然是二分搜索,找到一个合适的位置插入进去。但是,O(log N)对于系统来说,还是慢了,有没有O(1)的方法?
O(1)?怎么可能,除非狗运随便一选就能选到合适的位置。但是,如果优先级的范围是固定的,那有没有可能?
我们用哈希表的方法:

把相同优先级的进程放在一起,存在一个数组中,一共可以形成40个这样的数组。然后把所有数组的地址形成另一个数组,便是运行队列。当一个进程要被插入到运行队列中时,只需要根据优先级的值找到对应的数组,然后放在数组最后一个元素后,便用O(1)完成了插入。

所以,为什么NI值有一个范围?因为运行队列的数组地址个数一定要是固定的。 

活动队列和过期队列 

Linux中有两个运行队列:活动队列和过期队列。

再设想一个场景:
如果一个进程的优先级非常高,而另一个进程的优先级非常低,那优先级高的进程运行完,插入到队列中,还是来到了低优先级进程的前面。最后低优先级的进程永远不会被运行到,而高优先级的进程会被一直运行。

为了解决这个问题,Linux中采用了两个队列,活动队列和过期队列。活动队列便是现在正在运行的队列,而过期队列是运行完毕的队列,啥意思?

活动队列和过期队列的结构是完全相同的。用最简单的话来概括,

  • 活动队列是负责运行的队列
  • 过期队列是负责插入的队列 

当一个进程被运行完,并不会插入到活动队列中,而是插入到过期队列中,之后继续运行活动队列的下一个进程。而当所有的活动队列进程都运行完,都被插入到过期队列中,此时活动队列为空,过期队列则会按优先级的顺序排列完全。

这个时候我们直接将活动队列和过期队列交换,所有需要被运行的进程又回到了活动队列中,过期队列又恢复为了空。活动队列继续运行,一直到运行完所有进程,又变为了活动队列为空,过期队列排列完全,然后交换。如此往复,所有进程就能在保证优先级顺序的情况下,公平运行到所有的进程。 


总结

今天讲的稍稍有点多而且有点抽象,所以把这篇文章小小总结一下:

什么是进程?

进程是被加载到内存里的程序。 

怎么管理进程? 

先用一个process control block,PCB的结构体把进程中所有的特征描述出来,今天讲述了PCB的部分有:

struct task_strcut
{
	//基本三大数据
	int pid;//进程id,用于找到进程
	int status;//进程状态
	int time;//进程运行时间

	void* code;//代码段地址
	void* data;//上下文地址
}

不同进程的PCB地址串起来,就形成了一个链表,对进程的操作,就是对链表的增删查改,属于先描述再组织的思想。

进程怎么运行?

进程不是同时运行的。在一个cpu内,所有要被运行的进程PCB会被串成一个运行队列,从队列头(即链表头)开始,依次往后运行。当队列头的进程运行时间片到了,就搁置到队列尾,然后第二个进程便成为了队列头,依次循环,平均往复运行所有进程。
当一个进程被再次运行的时候,cpu会在PCB里寻找运行的上下文,通过上下文中提供的运行行数和临时变量,继续从特定位置向后运行。
而每个进程的时间片都极短,并且进程的计算速度极快,所以每个进程都会以人类无法观察到的速度大量运行,形成了伪同时伪连续的运行。


相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-16 17:12:03       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-16 17:12:03       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-16 17:12:03       58 阅读
  4. Python语言-面向对象

    2024-07-16 17:12:03       69 阅读

热门阅读

  1. Node.js 事件循环

    2024-07-16 17:12:03       21 阅读
  2. 常用几种远程控制协议总结(telnet,rlogin,ssh,rfb,rdp)

    2024-07-16 17:12:03       20 阅读
  3. 爬虫技术探索:Node.js 的优势与实践

    2024-07-16 17:12:03       19 阅读
  4. Cordova是一个开源的开发框架

    2024-07-16 17:12:03       23 阅读
  5. Vue和React中常用的组件间通信方式

    2024-07-16 17:12:03       16 阅读
  6. mybatis-plus映射mysql的json类型的字段

    2024-07-16 17:12:03       20 阅读
  7. 并查集,LeetCode 721. 账户合并

    2024-07-16 17:12:03       22 阅读
  8. 人像视频淡入淡出效果的灵敏检验方法

    2024-07-16 17:12:03       19 阅读
  9. Go并发编程和调度器

    2024-07-16 17:12:03       22 阅读