在之前的Linux《进程概念(上)》当中我们已经了解了进程的基本概念以及如何去创建对应的子进程,那么接下来在本篇当中我们就继续来进程的学习,在本篇当中我们要学习到进程的状态、进程的优先级、进程切换、Linux真实的调度算法------O(1)调度算法。一起加油吧!!!

1.进程状态
我们知道在不同的情况下人是处于不同的状态下的,就例如在上课、在打球、出去玩等。和人的状态类似进程其实也有不同的状态,并且描述进程的状态就是使用一个整数 来实现的,这个整数是存储在task_struct内的。
在此在详细了解Linux当中进程的具体的状态之前我们先要来了解操作系统抽象的进程状态有哪些
以下就是进程各个状态的转换图

以上大致的描述出了各个进程状态之间的转换图,不过以上图示当中的进程转换其实不是我们要重点学习的,接下来我们要了解以上所示的进程运行状态、阻塞状态、挂起状态具体是什么样的。
1.1 了解各个状态
由于接下来我们会详细的了解Linux当中进程的调度,因此关于进程如何运行接下来就来大致的了解。
由于CPU的资源是有限的,而进程在一些情况下又是多个的,那么此时就会有一个调度队列来管理进程。要让一个CPU来选择一个进程来运行其实选择的不是进程的代码和数据去运行;而是选择特定进程的PCB去运行,这是因为在通过PCB内的指针就可以找到特定进程的代码和数据。
当多个进程同时存在时就会如下所示

其实在一个CPU当中会有对应的一个调度队列,而调度队列其实就是将各个进程连接成一个队列。

在操作系统学科当中就存在一个FIFO算法就是按照先进先出的顺序来将调度队列内的进程依次进行执行

在此就将只要在调度队列当中的进程都称当前的进程状态为运行状态,而以进程状态转换图当中的运行状态和就绪状态其实是对运行状态的再一细化,在此我们现在不需要了解的那么深,可以将这两种状态统称为运行状态。

那么在了解了什么是进程运行状态,那么接下来就来了解进程的另外一个状态------阻塞状态。
在了解进行阻塞状态之前我们先来回想一下之前的C/C++的学习当中有什么情况进程会出现阻塞
其实很容易想到的就是之前在C使用scanf和C++当中使用cin的时候,进程就会停在需要用户输入的位置,在此只有用户输入之后才会接着运行下去。但其实在使用scanf的时候其实进程不是等待用户输入而是等待键盘硬件就绪。
因此阻塞的意义就是等待某种设备或者资源就绪
在此在了解进程的阻塞状态是如何产生的那么在此之前就要先来了解在计算机当中是如何对硬件资源进行管理的。在计算机当中硬件是有硬盘、显示器、键盘、网卡、摄像头、话筒等,在此操作系统要对这些硬件进行管理其实和进程一样也是通过先描述再组织完成的。
因此和进程一样硬件也会有对应的PCB内核的结构体来存储对应硬件的状态信息,在结构体内部就如下所示:
在此将这些PCB结构体进行连接就形成了设备队列

此时你可能就会有疑问了,不同的设备内进行访问的操作方式或者是进行数据的读取都是不一样的,那么是如何使用一个相同类型的结构体来进行管理的。
其实解决这种情况是很简单的,这需要使结构体内的指针指向不同的数据就可以实现个性化的需求。
以上我们就了解了操作系统是如何对硬件进行管理的,其实简单来时对硬件的管理就与对进程的管理类似,本质都是先描述再组织。
那么了解了以上的知识之后接下来就要思考进程阻塞具体的表现形式究竟是什么样的呢?进行阻塞和之前提到的进程运行之间又是怎么进行转换的的呢?
其实当多个进程PCB同时运行时会先后插入到调度队列当中,当轮到对应的进程要被CPU处理时,如果当前的进程还未获取到对应硬件的相应的资源,此时进程PCB就会从调度队列当中转换到阻塞队列当中。

当前运行的进程此时还无法运行就会按照以下的形式将进程从调度队列当中连接到对应硬件的阻塞队列当中。

当在阻塞队列当中的进程等待到了相应的等待的设备或者资源就绪,由于操作系统是硬件的管理者因此一旦进程对应硬件就绪了,那么此时操作系统就会将此时该进程的PCB从设备的等待队列当中重新链路到到调度队列当中,并且将该进程的状态从阻塞变为运行。
通过了解进程的运行状态和进程的阻塞状态就可以了解到进程状态的变化表现形式之一就是在不同的队列当中流动,其实本质就是数据结构的增删查改。
其实以上进程在调度队列和等待队列当中进行转换的情况就和我们在投递简历的时候一样,如果当HR一开始觉得你的虽然不是很符合他们的要求但是又觉得你的能力还可以,此时可能就会将你的简历放到待定区内,之后再看了其他的人时候突然觉得你还是挺符合公司的要求的,那么此时就会将你的简历从待定区重新捞出来。在此其实进程的PCB被链路到等待队列的过程就可以类别HR将你的简历放到待定区,进程的PCB从阻塞队列重新链路到调度队列就可以类比HR将你的简历从待定区当中捞出来。
以上我们就了解了进程阻塞状态的概念,那么接下来继续来了解进程的另外一种状态------挂起状态
通过以上的学习我们知道当进程被链路到等待队列的时候对应的硬件没有就绪时对应的进程是一定不会被调度的,那么在这种情况下如果当前的进程内存资源严重不足的时候,那么此时操作系统会怎么做呢?
对应出现以上的情况时,由于有一些数据不会立即被访问,但是这些代码和数据对应的进程如果是在等待队列当中时,此时操作系统就会使用磁盘当中的swap分区。此时就会将阻塞状态的进程的代码和数据直接置换到磁盘的swap分区上,在操作系统当中只保存这些进程的PCB,当这些进程重新被调度的时候再从swap分区当中将代码和数据重新连接到对应进程的PCB上。以上通过这样的方式就可以在内存资源严重不足的时候,释放当前内存的压力。
注:swap分区是磁盘的一个存储块,大小一般和内存大小一直
在此就将等待队列当中数据或者代码被置换到swap只剩下PCB的进程状态称为阻塞挂起状态。
除了以上的情况之外如果已经将等待队列当中的进程当中的代码和数据置换到磁盘的swap分区之后内存的资源还是严重的不足,那么此时操作系统就会将调度队列当中末端的进程的代码和数据置换到磁盘的swap分区当中。将这种在调度队列当中只省下PCB的进程状态称为运行挂起状态。
以上无论是阻塞挂起还是运行挂起其实挂起本质都是将对应的进程的代码和数据唤入到磁盘的swap分区当中,之后从挂起状态恢复回原来的状态就是将进程的代码和数据从swap分区当中唤出。
1.2 理解内核链表
其实进程的PCB之间是通过双链表连接起来的,但是此时就有问题了,以上提到的进程的PCB是存放到调度队列当中的,这时不会会出现同一个节点同时存在在两个数据结构当中了吗,这不就和之前我们学习一个节点只能存在一个数据结构相违背了吗?
要解答这个问题就需要我们来查看Linux的内核源代码,查看之后对比我们实现的数据结构看有什么区别。
在Linux当中就是将存储当前节点前后节点的指针单独存放在list_head的结构体当中的,再将该结构体存放到节点数据的节点当中。


那么Linux内核当中将各个节点设计未为这样有什么好处呢?
我们之前实现数据结构的节点时无论是链表还是队列等,都是将各个节点的前后节点指针同时和节点的数据存放到同一个结构体当中,这样就会使每个节点只能属于一种数据结构。

而在Linux内核当中使用节点前后指针与节点数据分离的方式就可以让各个节点通过专门的结构体连接起来,此时就能实现一个节点属于多个数据结构。

那么此时也就可以解释为什么一个进程的PCB能属于多个数据结构,其实就是按照以上的形式实现的。这就使得Linux当中的许多的数据结构是网状的。
因此操作系统当中的一个进程的PCB在操作系统当中也是只有一份的,当从调用队列从调整到阻塞队列其实是将其内部的相应的结构体的指针指向改变。

接下来我们还要思考一个问题就是使用以上这种结构时如果在得到一个节点的指针时,要怎么样得到对应的节点内的数据呢?
和我们之前实现的链表不同,我们实现的链表对应的结构体的指针就是其节点的起始地址,接下来就可以直接对节点的地址进行加减得到对应的数据,而在以上的结构当中得到的是节点内存储其前后指针的结构体地址;这就会使得得到的地址不是节点的起始地址,因此要解决该问题就要将得到的地址计算出对应节点的起始地址。
我们知道在结构体当中成员变量的地址是依次增长的,当前我们得到的是结构体内一个成员的地址,那么此时就可以先来计算该成员与结构体起始地址的偏移量。
假设在地址为0出就存在对应的结构体,强转为结构体指针之后再得到其内部对应指针结构体的地址,计算形式如下
&((struct tast_struct*)0->list_head)
以上就得到了对应节点起始地址到list_head成员的偏移量,接下来将之前得到的指针减去偏移量就可以得到结构体的起始地址,之后就可以进行该结构体内成员的访问了。
(struct task_struct)(list_head-&((struct tast_struct*)0->list_head))
1.3 Linux进程状态
以上我们了解的都是操作系统理论的进程状态,那么接下来在了解了基本的概念之后接下来就来学习Linux当中具体的进程状态有哪些
• 为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状
态(在Linux内核⾥,进程有时候也叫做任务)。
下⾯的状态在kernel源代码⾥定义:
cpp
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
static const char *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */
"D (disk sleep)", /*2 */
"T (stopped)", /*4 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};
从以上的代码可以看出在Linux当中进程的状态具体是有7种的,那么这些状态对应的进程具体的表示形式又是怎么样的呢?这就需要接下来我们来了解看看
S状态和R状态
在此我们创建一个test.c的文件之后在其内部写入以下的代码
cpp
#include<stdio.h>
int main()
{
while(1)
{
printf("hello\n");
}
return 0;
}
接下来再使用gcc编译生成对应的可执行文件mytest,接下来运行该可执行程序,再开一个Xshell的窗口使用ps命令监视我们运行的mytest程序
之后运行mytest

在以上就会看出在运行mytest程序之后当我们使用ps每隔一秒进行监视时就可以看到对应的进程的状态一直是S,那么是不是就是说明进程在运行的时候在Linux当中状态表示就是S呢?
其实不是这样的在,在Linux当中进程运行的时状态应该是R,以上运行程序的时候一直输出的是S,这是因为在程序当中使用的是printf,在此只有当printf在向显示器输入的时候进程会真正的运行,其他时间进程都是在休眠的,而printf在执行的时间是非常的短的,而我们监视的程序是每隔一秒才查看一次的,这就使得我们大概念看到的是S状态。
因此我们要对我们的test.c内的代码进行修改,不再使用printf,而是直接在死循环内进行变量的++。代码如下所示:
cpp
#include<stdio.h>
int main()
{
int cnt=0;
while(1)
{ cnt++;
//printf("hello\n");
}
return 0;
}
之后重新编译生成mytest之后执行程序并进行监视

此时同ps监视输出的结果就可以看出进程在运行的时候进程的状态确实是R。
那么S状态具体的表示形式又是什么样的呢?
其实在使用scanf的时候就会直到键盘输入之前会使得进程的状态一直保持在S在此我们再将以上的代码修改为以下的形式:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("pid:%d",getpid());
scanf("%d",&cnt);
int cnt=0;
while(1)
{
cnt++;
//printf("hello\n");
}
return 0;
}
之后重新编译生成mytest之后执行程序并进行监视

此时通过以上的使用p后的显示就可以就看出在使用了scanf之后在没有输入之前进程的状一直是S,当我们使用键盘输入之后进程的状态就变为了R
t状态和T状态
以上我们就了解了进程R和S的状态具体是什么样的,接下来继续来了解两个进程状态t和T
在此t状态和T状态其实都是进程被暂停了,不过这两个是不同的暂停导致的,接下来就来了解看看。
当我们使用gcc编译源代码的时候使用-g就可以使用gdb进行调试,在此当我们打断点之后再运行对应的程序就会看到对应的进程状态变为t
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int cnt=0;
while(1)
{
cnt++;
}
return 0;
}
cpp
gcc test.c -o mytest -g

通过以上的使用ps时的输出就可以看到当我们使用调试的时候打了断点之后再运行程序就会出现t状态。
在此其实小t状态就是追踪状态,当进程在debug时,打断点的之后再运行程序就会使得进程被暂停
接下来来了解另外的一个进程状态T
当test.c内的代码是如下所示时:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("hello\n");
}
return 0;
}
当我们运行的时候用户在键盘当中输入CTRL+z之后就会看到对应进程的状态变为了T

以上我们就看到了T和t状态的进程,那么此时我们就要思考了暂停状态的进程和我们之前了解的阻塞状态有什么区别呢?暂停状态的作用是什么呢?
进程阻塞是在等待某种资源,而暂停不同是条件不具备或者当中进程进行了非法操作,此时操作系统就会将对应的进程暂停,该进程是Linux当中特有的,其他的进程可能也会有这种状态当时在操作系统学科当中一般不对该状态进行说明。当操作系统检查到当中的进程有问题时但是又无法决定是否要将该进程杀掉就会将该进程的状态设置为暂停状态,这样就可以让用户来决定时候继续运行。
以上我们了解的是使用键盘的输入来让对应的进程的状态变为T,其实除了使用键盘输入还可以使用kill 指令来使得对应的进程暂停下来。
在此使用kill -l指令就可以查看kill指令详细的选项。

在此以上当中的选项-19就可以将对应的进程暂停

在此我们将test.c编译之后形成mytest可执行程序之后再运行,此时使用kill -9 对应的进程号就可以将该进程暂停下来

接下来要将暂停的进程再重新运行就需要使用到kill当中的-18 选项


在此如果要杀掉对应的进程可以使用kill -9 来实现

注:在此我们只是先了解kill的使用,详细的讲解到之后Linux信号部分会进行
D状态
其实在Linux当中以上我们提到的S状态其实本质是可中断休眠 ,而接下来要提到的D状态就是不****可中断休眠也就是深度睡眠。
那么该如何去理解D状态呢,在此就通过一个IO的示例来解释D状态的具体表现形式是什么样的,例如当前在操作系统当中有一个进程要在磁盘当中读取大小为100MB的数据,那么此时进程就先找到磁盘当中对应的数据之后接下来就等待对应的数据写入到对应的内存当中。假设当中的磁盘进行IO的效率较低,那么要将写入完成就需要一定的时间,这时操作系统看到了进程占据的系统的资源但是却没有进行任何的操作,如果这时系统的资源严重的不足,那么此时操作系统可能就会将闲置的进程给杀掉,此时操作系统看之前要完成在磁盘当中进行数据读取的进程是闲置的,那么此时就会把对应的进程给杀掉。那么接下来问题就来了,那就是将进行磁盘数据读取的进程杀掉之后,就会使得无法得到磁盘读取是否成功,此时假设磁盘进行完了对应的工作之后,它就无法向操作系统告知操作的结果,这时磁盘就会认为对应的数据是无用的就会直接将对应的磁盘资源直接清除。这时就会造成数据的丢失。

假设整个计算机就是一个银行,那么操作系统、进程、磁盘就是银行当中三个不同的员工,如果以上丢失的数据是银行一天的流水记录,此时银行就来找出现问题的地方,就将操作系统、进程以及磁盘来拷打,要找出现以上的问题是谁的责任。
此时实现就是进程来解释了,它说它一直在等待磁盘进行读取的操作,磁盘没有完成它的操作我肯定要等待磁盘啊,谁知道之后操作系统就把我给杀了,这我有什么办法。接下来磁盘就要解释了,它说我进行的读取的操作之后进程人就没了,那我肯定不知道要把这部分的数据要存储还是丢失啊,我看进程都不管我了那么就觉得数据是没有的,就把它给丢弃了啊,出现问题不能怪我。最后操作系统就来解释了,说你知道的我是你最信任的,你给了我这么大的权力,那么我看内存资源已经完全不足了,那么这时候我肯定要把闲置的内存进程给杀掉啊,要不让之后内存完全满了,那就不只是一个进程被杀掉这个小问题了,可能之后整个操作系统都会奔溃,那不是我呢提就更严重了吗?所以这个问题不能怪我。
这样看了一圈之后就就发现操作系统、进程、磁盘好像说的都没问题啊,但是问题就是出现了,那么就要思考接下来如何避免再出现以上的问题呢?
在此就引入了D状态,当进程的状态为D状态的时候操作系统就无权将D状态的进程杀掉,操作系统只能将S进程杀掉来解决内存资源不足的问题。
因此在操作系统内凡是涉及对磁盘这样的关键存储设备进行IO的时候,都将进程的状态设置为D,这样就可以避免数据的丢失。
注:在Linux当中S状态的进程和D状态的进程其实都属于阻塞状态的进程,只不过这两个状态的进程是Linux当中特有的。
操作系统要杀掉阻塞状态的进程时 ,是只能将S状态的进程杀掉的,是无法将D状态的进程杀掉的,D状态的进程要结束只有当对应的进程自己恢复或者将计算机关闭才会停止。
X状态和Z状态
在Linux当中X状态是死亡状态,Z状态是僵尸状态,那么接下来我们就来了解这两个状态
首先在了解Z状态之前先通过一个场景来带出为什么在Linux当中要存在僵尸状态
假设现在在一个人在道路上突然死亡了,那么在法医法医到达之前其实所处的状态就是僵尸状态,知道法医真正的判断出他是由于什么原因死亡的才能真正的宣告其的死亡。在这里将这种处于未被法医检查的状态就称为僵尸状态,
以上提到的示例类似在操作系统当中我们知道每个进程都有其对应的父进程;其父进程可能是bash进程也可能是我们创建的进程,但其实无论是什么样的父进程创建子进程的目的都是一样的;都是为了让子进程完成某种事情 ,那么在子进程完成了对应的事情之后,当父进程还未获取到子进程的退出信息之前,就将该进程的状态称为****僵尸状态。在此对应的子进程处于僵尸状态的目的其实就是让父进程得到其的退出信息,毕竟子进程是为了完成父进程给的任务的,那么最后肯定是要将是否完成了对应的人任务的状态告知给父进程。
那么接下来就要接着来了解一些关于进程退出时退出信息有哪些,以及退出信息是存储在什么位置的呢?
在一开始学习C语言的时候我们就被告知C语言的程序在main函数的最后要写上return 0,之后学习到函数的时候我们知道了return是用于返回当前函数的返回值的,但是直到现在我们还是没有解释为什么在main函数当中为什么月嫂有return 0,难道是有什么会接收main函数的退出信息吗?
确实是这样的,其实main函数的退出码是会被返回给其进程的父进程的,之前写的程序return 0就表示对应进程的退出码未0即表示当前进程正常执行完毕。其实在main函数当中还可以使用其他的退出码;表示的就是对应进程由于什么原因而异常的退出,具体的讲解在之后的进程控制当中会进行。
其实在进程的退出信息当中除了以上提到的退出码之外还有信号码等,这些到之后对应的章节都会进行讲解,现在我们只需要知道在退出信息当中有这些即可。
当进程退出的时候对应的代码和数据都被释放掉了,那么对应的退出信息其实就只能被存放到PCB当中,之后其父进程会通过相应的系统调用找到对应的子进程的PCB再得到对应的退出信息。
以上我们了解了Z状态具体是什么样的之后,接下来就试着在Linux当中模拟出Z状态的进程
首先在test.c的源文件当中实现以下的代码:
以下的代码当中通过fork系统调用创建了子进程,之后在子进程当中在运行五秒之后就结束,而父进程则一直执行不退出,这时我们就可以查看到子进程的状态变化。
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid=fork();
if(pid<0)
{
perror("fork");
}
if(pid==0)
{
//子进程
int cnt=5;
while(cnt--)
{
printf("我是子进程,%d\n",cnt);
sleep(1);
}
}
else{
//父进程
while(1)
{
printf("我是父进程\n");
sleep(1);
}
}
return 0;
}
以上源文件编译生成名为mytest的可执行程序之后,接下来使用ps来监视进程的状态
这时就可以看出进程子进程在5秒之后就从S状态变为了Z状态

那么接下来我们就要接着思考一个问题了,那就是如果父进程一直不去获取子进程的退出信息,那么是不是Z状态会一直存在呢?
确实是这样的,如果父进程一直不去获取子进程就一直会处于僵尸状态,又因为子进程在处于僵尸状态时对应的PCB会一直存在,因为PCB是要占用资源的。那么如果子进程的PCB一直不被处理就会造成内存泄漏问题。在之前学习C/C++时我们就已经了解了在使用malloc/new时如果在不使用之后不用free/delete就会出现内存泄漏问题,现在我们就了解到了除了申请之前的原因外不对僵尸进程进行处理也会造成内存泄漏。
那么对应内存泄漏接下来就来引入以下的一个知识点,那就是在内存泄漏当中的如果对应的进程退出了,那么内存泄漏问题还会存在吗?
在此考察的进程不是以上的僵尸进程,也就是当进程退出是是不会留下PCB类似的数据结构对象的。其实只要进程退出了内存泄漏的问题也就不在了,这也就再引出了一个常识,那么就是在计算机当中其实那运行时间不长的进程引发的内存泄漏是不太可怕的,可怕的是那种从操作系统启动就一直运行的进程,那么这种进程即使内存泄漏的问题不太严重,只要经过的时间很长,那么也可能会导致系统崩溃。
僵尸进程的危害:
• 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
• 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
• 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置进⾏开辟空间!
以上就了解了Z状态的进程,而X状态的进程我们是无法模拟出来观察的,这是因为进程被杀掉是在一瞬间的。 其实除了以上的僵尸进程之外还有一种进程也是腰围特殊的,这就是接下来要学习的孤儿进程。
那么孤儿进程又是什么呢?
接下来先看以下的代码:
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid=fork();
if(pid<0)
{
perror("fork");
}
if(pid==0)
{
//子进程
while(1)
{
printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else{
//父进程
int cnt=5;
while(cnt--)
{
printf("我是父进程,pid:%d,ppid:%d,%d\n",cnt,getpid(),getppid());
sleep(1);
}
}
return 0;
}
将以上的代码编辑成可执行程序之后接下来使用ps来监视对应的进程信息

在进程运行的过程当中就可以看出子进程的父进程一开始是原来我们创建的进程,但当父进程结束之后子进程的父进程就变为了pid为1的进程,那么这个pid为1的进程是什么呢?

其实在Linux当中1号进程就是在操作系统一开始启动时就创建出来的进程,之后用户的bash进程就是该进程的子进程,在此就可以简单的认为该1号进程就是操作系统。当一个进程的父进程结束之后如何子进程还没有结束的话,此时子进程就变为了孤儿进程。按照Linux系统的规定,孤儿进程都会被1号进程领养,成为1号进程的子进程。
那么此时你可能就会有疑惑了,为什么要让1号进程领养孤儿进程呢?
我们知道当子进程运行结束之后如果不对子进程的退出信息做处理,那么对应的子进程就会变为僵尸进程,最终会导致内存泄漏。因此为了避免该问题就将失去父进程的子进程的父进程设置为1号进程,这样就能让1号进程来结束子进程的退出信息。
对于处于孤儿状态的进程其实是会变为后台进程的,而只有前台进程才能获取键盘的信号,这也就使得无法使用CTRL+结束当前正在运行的后台进程,这时只能使用kill -9指令。

2.进程优先级
首先来了解进程优先级的概念:
• cpu资源分配的先后顺序,就是指进程的优先权(priority) 。
• 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
• 还可以把进程运行到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
那么为什么在操作系统当中进程要有优先级呢?
其实这是因为在CPU内部目标资源是有限的,因此就要通过优先级来确定谁先谁后的问题。
在操作系统当中进程的优先级其实就是由数字 来描述的,该数字是存储在task_struct 内部的。当该进程的优先级更高的时候,其的数值就越低 。但由于Linux是基于时间片的分时操作系统,为了考虑公平性,各个进程之间的优先级变化幅度不能过于大;接下来就讲解优先级值的规则。
其实体现优先级的数字就是PRI,当我们运行mytest进程的时候,mytest内进程还创建了一个子进程,那么这是使用ps带上-al选项就可以看到以下的信息

在此以上的UID其实就是用户的id,在Linux当中操作系统区分用户其实不是使用用户名的,而是根据对应的UID来区分的。
在以上当中就可以看到对于进程的PRI值,那么这个值旁边的NI又是什么呢?
在此PRI代表这个进程可被执行的优先级,而NI则表示该进程的nice值,其作用是用于修正PRI。
因此真正的优先级值=PRI(默认值80)+(NI)nice值修正
此时如果压修改进程的PRI值就可以使用道以下的指令

除了以上的指令之外还可以使用top进行nice值的修改:
• top
• 进⼊top后按"r"‒>输⼊进程PID‒>输入nice值
注:在此每次修改PRI的值都是从80的基准值上进行修改的,而不是从上一次修改的结果下修改的。并且一帮不建议盲目的修改进程的PRI值,这样不能会影响操作系统的调度。当进程的优先级设置不合理就会导致优先级低的一直得不到CPU的资源,从而导致出进程饥饿
在对进程的优先级值PRI进行修改时其实也是有范围限制的,范围是60~99,这就使得对nice值的修改只能在-20~19区间内。
补充概念-竞争、独立、并行、并发
• 竞争性 : 系统进程数目众多,而CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
• 独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰
• 并行 : 多个进程在多个CPU下分别,同时进行运行,这称之为并行
• 并发: 多个进程在⼀个CPU下采⽤进程切换的方式,在⼀段时间之内,让多个进程都得以推进,称之为并发
以下图示就可以简单的解释并发和并行:

3. 进程切换
在了解Linux当中进程是如何切换之前我们先来了解两个小知识点:1.死循环进程是如何运行的 2.CPU内的寄存器
首先了解的就是当运行起来一个死循环的程序,是不是CPU就会被这一个程序一直占用了呢?
其实通过之前的经验就会发现不是这样的,我们在运行死循环的程序时要是一直占用CPU,那么操作系统的内存资源应该会被一直占用啊?最后系统就会奔溃。但其实操作系统是根据时间片来调度程序的,即使是一个死循环的程序,当它的时间片到了也会被停止调度。
接下来还要来了解CPU当中是存在寄存器的
其实在CPU当中是存在非常多的寄存器的,其作用就是用于保存正在运行的程序的临时数据,就例如当我们在计算1+1的时候就会有对应的寄存器保存两个操作数,之后还会有对应的寄存器保存运算的结果,最终就可以通过该寄存器得到结果。
在此关于寄存器有两个结论:
1.寄存器就是CPU内部的临时空间
2.寄存器!=寄存器内的数据注意:寄存器是空间,而寄存器内的数据是内容是可以变化的,这两个概念是不一样的。
以上就是在学习进程的切换之前要补充的知识点,那么接下来就通过一个故事来引入进程的切换
假设你时一个上到大二的学生,最近你看到了征兵的宣传就想着最近的就业形势不怎么样要不去当兵入伍两年,还能得到退伍费,而且自己的体格也还行,说不定就通过选拔了呢?不就之后你就参加了入伍的xba果不其然你成功通过了,那么这时你就需要找你学校当中你的辅导员将你的学籍保留,这样你就可以在退伍之后再重新回学校上课。之后你就将学籍保留了。过了两年之后你重新回到了学校,那么此时你就要找学校了的辅导员将你的学籍信息恢复回来,这样你才能重新在学校立上课。
在以上的故事当中其实你就是程序 ;学校就是CPU ;辅导员就是调度器 ,当你在学校上课的时候就是进程在CPU内运行 ,之后当你要去当兵了就是将进程从CPU上剥离下来 ,也就时对应的进程被替换 了,那么在此你的学籍就就是正在运行的进程在CPU内寄存器内的数据 也就是CPU内部当前进程的硬件上下文数据 ,当要保留学籍就是将进程的上下数据保存下来 ,之后要恢复学籍的时候就是将进程的上下文数据恢复。
以上我们通过当兵的故事来了解进程的替换其实是感性的了解,那么接下来我们就来理性的理解看看,并且再来补充以上没有提到的一个问题,就是保存的进程德上下文数据是保存到哪里呢?
例如以下正在运行着A程序,假设当前进程运行到100行,产生了一些临时的数据在CPU的寄存器内,那么此时要将正在运行的A进程替换为B进程就其实就需要将A进程德上下文数据保存到A进程的task_struct当中

其实再进程替换的时候的上下文数据就是保存在进程的PCB也就死task_struct内的tss对象内,在此将TSS成为任务状态段。 以下我们可以通过Linux的源代码来验证

其实在当代的计算机当中由于task_struct内的数据已经很多了,如果在将进程的上下文数据直接存储到PCB内就会导致PCB十分的臃肿,所以现代的计算机都是将进程替换时的上下文数据数据保存到另外的数据结构当中,当进程要恢复的时候只需要找到对应的数据结构即可。
此时关于进程调度还有一个问题就是在操作系统当中是如何区分全新的进程和已经被调度的进程呢?
其实很简单只需要在task_struct使用一个整型变量来标识即可,当变量是全新的进程时对应的变量值为0,反之被调度之后变量的值就修改为1。
4.Linux内核进程O(1)调度队列
以上我们了解了进程的替换以及在之前学习了OS当中是存在调度队列和阻塞队列的,那么接下来就来了解Linux当中具体的进程调度算法------O(1)调度算法
其实在之前我们了解的进程的调度,也就是进程是如何在调度队列和阻塞队列当中切换的方式是操作系统学科当中描述的,而不是Linux当中具体的调度方式,在Linux当中调度算法是会体现出进程优先级不同的而导致调度过程不同。这就是接下来我们要了解的进程的O(1)调度算法。
接下来我们描述的都是单CPU的情况
在一个CPU当中都会有以下的一个运行队列,在Linux当中叫做runqueue

首先来看以上当中的queue[140],那么这个queue[140]是什么呢?
其实只需要将其的写完整我们就可以轻松知道,queue[140]的全名其实是struct task_struct* queue[140],也就是一个指针数组;该数组内元素有140项。
在该数组当中0~99下标其实是实时优先级,那么这时候问题就来了,那就是之前我们了解到进程的优先级的范围不是60~99吗,那这时候为什么又有140个下标;这里的实时优先级又是什么?

要解答以上的问题就需要来了解一下实时操作系统和分时操作系统
其实可以将操作系统是否按照时间片划分分为实时操作系统和分时操作系统,简单来说这两个的区别就是分时操作系统是会按照进程的时间片以及进程的优先级来对进程合理的调度,而实时操作系统则是完全按照进程的优先级来决定进程的运行顺序,优先级最低的进程会立刻被调用到运行。
**实时操作系统更适合于工业领域,而分时操作系统更适合于计算机、手机、服务器等领域。**我们使用的Windows以及Linux操作系统都是分时操作系统。
那么接下来又有问题了,那就是我们知道了Linux是分时操作系统,那么为什么在还要提供实时优先级呢?
这其实是Linux为了兼顾更多的使用场景,有了实时优先级Linux就也可以使用到工业领域当中了,更广阔的用户市场是操作系统更想看到的。但是正常情况下我们是不会使用到0~99的优先级的。而在queue数组当中剩下的下标101~140就正好对应了之前的PRI值60到99区间,此时只要通过算法就可以将PRI的值转化为相应的数组下标。
并且在queue数组当中每个数组下标内存放的都是队列的指针,这就可以让同一优先级的队列链路到同一队列当中
有了以上的结构就可以在queue数组当中进行调度,数组的每一个元素就指向一个队列,因此queue数组其实就是一个哈希表。那么此时在以上的queue当中查询要进行调度的进程如果只有进程优先级为99那不就要遍历一次queue数组才能查询到,这时虽然说只需要进行40个的查询,但是理论上时间复杂度还是O(n);那么这时要进行查询效率优化就在在runqueue当中添加了一个bitmap[5],在此bitmap内元素的类型为无符号整型,这5个元素总的比特位总数为32x5=160 ,这时就时就使用0到140的比特位来标识对应queue数组元素是否为空,是的话对应的比特位位置的值就为0;反之就为1,在此就可以使用位图来实现O(1)的查询效率。
并且在runqueue当中还使用一个变量nr_active来统计当中所有队列的进程总数,当值为0的时候就说明当中无进程链路到队列当中。
以上的调度过程看似已经可可以满足要求了,但是其实还存在一个问题就是如果将已经调用过的进程从新链路到以上的queue当中,此时就会出现只有将高优先级的执行完才会执行低优先级的进程,这不就和我们的预期相违背了吗?
因此为了解决以上的问题在runqueue当中就再创建了一个过期队列,当进程的时间片结束之后但是进程还没有执行完就将对应的进程链路到过期队列当中。要实现这样的效果只需要在runqueue当中创建requeue_elem的结构体,在该结构体内部存在active、bitmap[5]、queue[140]。在queue当中创建两个requeue_elem的对象的数组,之前的调度队列就是下标为0的元素;而过期队列就是下标为1的元素。


那么当运行队列上的进程都运行完了之后如何转而去运行过期队列上的进程呢,在此在runqueue当中就创建两个指针变量active和expired,active初始化的时候就指向运行队列;expired就指向过期队列,当要指向过期队列时只需要将这两个变量进行交换即可。

cpp
swap(&active,&expired);
而新创建的进程如果直接插入到运行队列当中就实现了内核优先级抢占 ,如果是插入到过期队列当中就实现进程的就绪状态。
通过以上的各个模式的配合就实现了进程的O(1)调度队列。
以上就是本篇的全部内容了,接下来我们将继续学习进程的概念,接下来我们将重点学习环境变量和进程地址空间。