目录
进程排队
进程状态
运行状态
阻塞状态
挂起状态
Linux内核具体进程状态
浅度睡眠状态
运行状态
深度睡眠状态
暂停状态
可被追踪的暂停状态
终止状态
僵尸状态
进程排队
进程不是一直在运行的,进程放在了CPU上,也不是一直运行的。
进程可能在等待某种软硬件资源给它输入输出数据。
当进程被放在被CPU执行时,不是等CPU执行完成之后再将其拿下来,而是在规定的时间内执行,只要时间一到,不管这个进程执行完成还是未完成,都要从CPU上拿下来,让CPU执行其他进程。这个规定的被CPU调度执行的时间被称为时间片。
进程排队,一定是在等待着某种资源。比如等待被CPU执行、等待被内存读取、等待被磁盘读取写入。
为什么要进行进程排队,因为在计算机中,硬件是少数,进程是多数,每个进程要想都被执行,就只好排队了。
我们知道,进程=task_struct(PCB)+可执行程序。进程排队实际上是进程中的task_struct在排队。一个task_struct可以被连接在多种数据结构中。
下面来看一下一个task_struct如何被连接在多种数据结构中?
实际上,在内存中,操作系统对进程的管理是这样的。
在task_struct(PCB)内部对象中,定义一个连接节点,由连接节点之间再互相连接,将进程PCB连接起来。此后,操作系统对进程的管理,就转化为了对维护的这个链表进行管理了。
那么通过这个节点的地址,如何获取该PCB节点的首地址呢?
初始地址是连接节点的地址,利用上述两种方法就可以计算出偏移量,从而拿到PCB首元素地址,访问PCB内部属性。
实际上,每一个硬件单元,操作系统都会为其维护一个队列。队列的底层就是由链表/顺序表维护的。比如,一个CPU就维护一个运行队列(由于CPU主要是运行这些进程,所以就是运行队列)、一个磁盘就维护一个管理队列(磁盘主要是进行写入、读取等对数据做管理,所以就是管理队列)等。
所以,一个PCB可以被连接在多种数据结构当中。对进程的管理就需要复用对链表的增删查改方法。
进程状态
所谓的进程状态,本质就是由整数来代表状态。在task_struct中就是一个整型变量。
#define Run 1 #1代表运行状态
#define Sleep 2 #2代表休眠状态
#define stopped 3 #3代表停止状态
#define dead 4 #4代表死亡状态
状态是用来帮助进程确定后续动作的,Linux中可能会存在多个进程都需要根据自己的状态来执行后续动作,所以进程就需要排队了。
每个硬件都有一个运行队列。
我们了解一下运行、阻塞、挂起状态。
运行状态
只要在运行队列里的进程就是运行状态。由此可知,在运行状态的进程,也不一定是真正运行的,可能还在等待着CPU执行它。
一个进程放在CPU上被执行,有两种情况被从CPU上拿下来去运行队列重新排队,一种是这个进程执行完了,另一种是这个进程的时间片到了。
时间片是操作系统为了效率而设置的。意为进程被放在CPU上被执行的时间。
浅看一下上述过程
阻塞状态
进程可能在等待着某种资源,这种资源可能是软件,也可能是硬件。
比如,CPU现在执行进程,执行到scanf函数了,这个时候计算机就等着我输入了,但是如果我迟迟不输入,那么计算机就一直等着我输入,不管其他进程了?显然不是的。所以,类似于执行到scanf函数时,需要键盘这个硬件来输入数据了,就需要键盘这个资源了,所以这个进程由运行状态转化为阻塞状态,并且该进程的PCB从运行队列中剥离,放到硬件的管理队列中,等这个硬件执行到这个进程时并输入了数据(这个数据由操作系统进行保存),就又将这个进程的状态改为运行状态,连接在运行状态的队列中,等待被CPU执行。等CPU执行到这个进程时,操作系统再拿出来刚刚键盘以及输入的保存好的数据,再由CPU执行。
在这整个过程中,这个进程的切换、从运行队列中剥离、连接在键盘管理的队列中、再获取到键盘执行到这个进程了且输入了数据、再状态切换、连接在运行队列中、CPU执行到这个进程了再把数据拿出来这些都是由操作系统来保证的。
下面来演示一下上述操作。
当进程在进行等待某种软硬件资源的时候,资源如果没有就绪,进程的PCB就只能将自己设置为阻塞状态,再将自己的PCB连接再等待资源所提供的队列中去。
挂起状态
这里我们只了解阻塞挂起状态
当计算机内存资源以及开始很紧张的时候,这个时候,可能我们再申请空间就很大可能会申请失败,甚至更严重时导致操作系统崩溃,在这么危急的时刻,操作系统为了保证自己不崩溃,为了给用户提供良好的服务,操作系统就会将现在不执行的进程/阻塞的进程的代码和数据拷贝(交换)到磁盘上的swap分区中,将内存中的代码和数据的空间释放掉,这样就会腾出来一部分空间,供操作系统度过这危急的时刻,等到需要执行这个进程了,再将代码和数据由磁盘swap分区拷贝至内存,使PCB与其连接,CPU执行即可。
这个过程称为操作系统将代码和数据通过IO交互唤入唤出到磁盘swap分区上。
换出:把数据从内存拷贝到磁盘swap分区。
换入:将数据由磁盘swap分区拷贝到内存中。
注意:PCB不会被唤入唤出。PCB唤出了,操作系统就不知道有这个进程了。另外,创建进程时,首先是PCB被创建出来,然后再是可执行程序(代码和数据)加载到内存。因为操作系统管理进程只管理进程的PCB,先让操作系统创建出PCB是为了让操作系统知道有这个进程了,什么时候调度等一系列操作再由操作系统掌握。
对于磁盘的swap分区,一般容量是内存的0.5-1倍。因为swap分区操作系统都要用完,所以,swap分区的容量如果太大,就会使内存和磁盘之间的交互过于频繁,由于木桶原理,就导致操作系统效率低下。
所以,进程状态的变迁,实际上是将进程的PCB被操作系统变迁到不同的软硬件管理队列中去。
Linux内核具体进程状态
以上有关进程知识都是操作系统理论部分,接下来了解一下Linux实际的进程状态。
下面来看一段Linux内核源代码
/*
* 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 僵尸状态*/
};
查询当前进程信息,STAT字段为进程状态字段。
浅度睡眠状态
按道理来说,只要该程序属于运行期间,就是运行状态。
运行状态就是程序运行时的状态,可为什么这个在运行的进程的状态是S+呢?
这是因为云服务器在远端,进程在云服务器上运行,根据冯诺依曼体系结构,结果会通过网卡设备接收->预加载到内存->由CPU处理->内存定期刷新到显示器文件。由于显示器效率比内存效率慢,并且要预加载到内存中来,所以这个进程的调度期间,CPU大部分时间都是在等待代码数据预加载到内存,所以就是睡眠状态了。由于大部分时间在等待资源加载和刷新,所以睡眠状态是阻塞状态的一种。
这种睡眠状态称为浅度睡眠状态,又叫可中断睡眠状态。
前/后台进程
S后面的+表示这是一个前台进程,后面没有+表示这是一个后台进程。
前台进程可被ctrl+c终止掉进程,并且进程执行期间不可执行其他命令。
后台进程不可被ctrl+c终止进程,只能使用kill -9 进程pid终止掉,并且后台进程执行期间可执行其他指令。
一般来说,默认运行都为前台进程。
#指定以前/后台进程来运行可执行程序
./可执行程序 #以前台进程运行该可执行程序
./可执行程序 & #以前台进程运行该可执行程序
运行状态
那如何才是运行状态呢?
此时,不需要打印操作,while循环会一直运行,该进程就成为了运行状态了。
深度睡眠状态
D状态为深度睡眠状态,又叫不可中断睡眠状态。
操作系统在资源紧张的状态下会释放掉进程(可执行程序和PCB都会释放),为了防止操作系统在这种情况下将还没有执行完成的进程释放从而导致用户丢失数据信息。所以,设置D状态,处于D状态的进程不可被操作系统释放掉。D状态进程是等待资源调度,所以D状态也是阻塞状态。
暂停状态
T状态是暂停状态。
kill -l #在Linux中查看信号
kill -19 进程pid #使进程暂停
kill -18 进程pid #使进程继续执行
进程一旦被暂停过,再次启动运行都会转为后台进程。因为前台需要进程占着。
暂停状态等待着再一次被执行,所以暂停状态也是阻塞状态的一种。
可被追踪的暂停状态
t状态为可被追踪的暂停状态。在调试的时候,该进程属于t状态。
处于t状态的进程也是阻塞状态。
终止状态
X状态是终止状态(程序死亡状态),一般是查询不到的,这是一个瞬时状态。
僵尸状态
Z状态是僵尸状态。
进程在执行完成之后,代码和数据可以直接被释放,但是自己PCB不会被立即释放。
对于一个进程来说,进程是为了给用户提供服务的,进程在执行完成之后,需要将自己的执行信息以及执行成功还是失败等信息返回给它的父进程、操作系统需要获取这个进程执行完成之后返回的数据,之后操作系统会反馈给用户。
进程退出但是操作系统并未获取到该进程退出的信息时,这个进程处于僵尸状态。
让操作系统获取到这个进程退出的数据时,才会释放PCB,这个进程才是终止状态。
所以说,一个进程执行结束,先到僵尸状态,然后变为终止状态被系统释放。
孤儿进程
子进程退出之后父进程必须要读取。如果不读取,则PCB会被一直保存,长期会造成内存泄漏。
父进程存在时,子进程僵尸状态会被父进程回收获取返回信息。
子进程存在时,父进程僵尸状态会被bash自动回收获取返回信息。并将子进程交给操作系统(1号进程)所领养。此时子进程ppid变为1。
bash会自动回收获取僵尸状态的进程的返回值。
被操作系统所领养的进程为孤儿进程,孤儿进程都是后台进程。