在我们一般的操作系统学科中,它的进程状态是:运行、阻塞、挂起
上图中的新建状态就是,一个进程刚刚创建出来的时候,即PCB刚刚创建出来
终止状态就是已经不用了,代码被运行完了
运行状态就是正在被调度的状态
如下图所示,我们知道,我们的程序在运行的时候,想运行的进程是非常之多的,而CPU在极端场景下只有一个。所以这么多进程就要存在竞争。而因为调度器的存在,就让他们较为均衡的使用
所以每一个CPU都要维护一个它自己的运行队列(struct runqueue),它可以对这些进程做出调度
struct runqueue
{
//运行队列
struct task_struct* head;
struct task_struct* tail;
};
如下图所示,CPU就可以直接从运行队列中拿到一个进程进行执行。
这就叫做进程在CPU上进行排队,该队列叫做运行队列。
而调度器就是一种函数,可以将这个运行队列作为参数传进来,从而就可以找到所有在排队的进程。
而在运行队列中的所有进程,都是R状态,即运行态。
运行态就是我已经准备好了,随时可以被调度!!!只要处于运行队列,都是运行态
当我们创建好一个新进程的时候,只要它入队,就是运行态,只要等调度它就行了
问题: 一个进程只要把自己放到CPU上开始运行了,是不是要一直执行完毕,才把自己放下来?
不是!就比如我们前面所写的while死循环代码。
为了防止某个进程上去以后就下不来了,陷入了死循环,所以每一个进程就有一个时间片的概念,它也是PCB里面的一个数值。比如说是10ms,一旦超过这个10ms,计算机就要强制的把这个进程从CPU上扒下来,放到队列尾部。
所以在一个时间段内,所有的进程代码都会被执行!
对于上面着这种我们也叫做并发执行。
所以大量的把进程从CPU上放上去,拿下来的动作----也叫做进程切换
我们知道操作系统的管理的核心就是先描述、在组织。
如下是我们的操作系统的结构
操作系统要管理下面的设备、外设。就需要先描述后组织的方式
对于进程管理是隶属于软件的,而对于硬件的管理也可以用类似的方式。
struct dev
{
int type;
int status;
struct task_struct* waitqueue;
//............
};
假设现在有一个进程,它的任务是从键盘中读取数据
我们现在不给他输入,那么它就会等待,而且它也无法放入运行状态中,因为它当前的软硬件资源没有就绪。
所以它就会等待某种资源,而这在操作系统中只需要将他放入到键盘所对应的等待队列中
当未来还有数据需要进行输入的时候,这时候就需要往后面排队了
同理每一个设备都可能这样的情况。
当未来我们这个键盘给这个进程数据的话,那么它就就绪了,可以进入运行队列了,从而CPU就可以去调度了
所以我们将这个正在等待排队的进程称作阻塞状态,每个这样的设备都有一个等待队列。当进程从读取到数据的时候,就会唤醒:从阻塞状态改为R状态
我们现在有一个设备叫做磁盘
如果我们当前有多个进程正在等待键盘这个资源,可是这个键盘的资源一直在就绪,因为没有人摁它。
所以这些进程,只能以阻塞状态在等待队列中等待
可是如果,在等待的时候,操作系统的内存资源严重不足了。
所以操作系统就需要在保证正常的情况下,省出来内存资源
以阻塞状态为例,只要它没有运行的那一刻,我们当前进程的代码和数据其实是在内存里处于空闲的,没有被使用的。
所以此时操作系统会将这些进程的PCB给保留,将这个代码和数据放到磁盘外设中。
所以就相当于这个进程只有一个PCB在排队,而它自己的代码和数据在外设里面。
当这个进程就绪了,要放到运行队列了,在考虑将这个代码和数据重新放回来。
这个过程就是换出和换入的过程
如果一个进程只有它的自己的PCB在,而代码和数据都换出了,并没有在内存当中,那么此时就是挂起状态
如果所有的进程都这样做,那么瞬间操作系统就能腾出一大块空间
其实我们现在场景下所说的挂起应该称作阻塞挂起状态
当然还有运行挂起,就绪挂起…等等,我们都不去考虑
如下所示,是linux系统中的状态。我们可以看到与前面传统的操作系统教材中所说的状态还是有很大的差别的。
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中R状态就是运行状态
我们先看下面的代码
当我们运行的时候,我们可以去查看它的运行状态
我们也可以在重新多运行一下
我们注意到的现象是,大部分情况下都是S状态,极少部分情况是R状态
我们我们的代码明明就是在运行,却显示是S状态呢?
我们先将代码稍作修改
然后在运行
我们就可以注意到,现在都是R状态了
那么这是为什么呢?
这是因为我们用我们自己的感受去揣测了CPU的速度
因为我们刚刚一直在printf,需要访问外设(显示器)。我们的设备并不一定能处于一个直接写入的状态,所以我们的这个进程有非常大的概率在等待
而我我们将printf去掉以后,一直在快速的循环,就一直处于运行态了。
我们也可以用top命令,就相当于任务管理,可以随时查看进程
同时我们也可以注意到,我们前面使用ps命令查看的时候,状态后面有个+号。这个加号代表着这个进程在前台运行。(前台运行意味着我们继续输入其他指令是没有任何反应的)
如果我们要在后台运行一个进程,我们在后面加上一个&即可
像这种后台进程,我们只能通过kill命令来杀掉了
我们将代码改为如下
这个时候我们查到的进程大概率都是S状态
这个就是因为CPU太快了,而这个进程要访问外设,这就显得太慢了
不过我们还可以用下面这个例子会更加直观
而这个状态就和我们操作系统学科中的是一样的。
也就是说linux中的S状态就是操作系统中的阻塞状态
而像我们操作系统中的一般都是阻塞状态,即在等待某种资源就绪。
所以所谓的阻塞状态,就是在等待某种资源就绪
像我们前面的scanf就是在等键盘输入,像printf就是在等显示器就绪
在linux中,除了S状态是阻塞状态以外,还有一种状态也是阻塞状态,D状态,不过它也叫做深度睡眠;而S状态我们也称为浅度睡眠。
两者的区别就是,S状态,即浅度睡眠是可以被唤醒的
也就是说,虽然这个进程还在继续,但是我们可以用kill去杀了他,即随时可以相应外部的变化
像下面的这种就是处于浅度睡眠的
而深度睡眠就是不可以被唤醒的
D状态(disk sleep磁盘休眠)
比如下面这个例子
但是这个过程是需要画出一定的时间的。而在这段时间内,这个进程就必须得在这里等磁盘把数据全部写完,等他反映的结果。
如果此时OS的内存压力很大,它现在已经把能置换的资源全部置换了。那么操作系统如果看这个进程不爽,直接就会把他杀掉
也就说,当OS内存压力很大的时候,就会杀掉一些不重要的进程。
即如果OS可以的话,那就直接去用内存,如果扛不住了,那就尝试置换,如果还是扛不住,那就只能杀掉进程了。
这时候,如果一旦磁盘写入失败,还发现了进程已经被杀了,此时就无法回应给进程了,磁盘也无法做出决策。这时候就看具体硬件的做法了,有的硬件会直接丢掉这个数据,有的会尝试在写一次。
如果被丢失的这1GB数据很重要,那么就糟糕了
为了解决上面这个问题,所以我们需要保证让进程在等待磁盘写入完毕期间,这个进程不能被任何人杀掉,就可以了
所以说当一个进程正在等待磁盘写入数据的时候,不能设置为S状态,必须得设置为D状态,D状态就不能被任何人所杀掉
当这个数据全部写完以后,这个进程再将D状态恢复为R状态
同时也意味着,如果操作系统里面只要出现一个D状态,那么操作系统已经即将崩溃了
深度睡眠不可以被杀死的原因就是:不相应任何请求
如果我们想要模拟一下的话,可以使用dd
命令,这个可以模拟高IO的情况
T状态我们称为暂停状态,也叫做stop状态
我们使用如下代码
当我们运行的时候,是处于S状态的
在linux中有一个信号的东西,如下我们之前杀掉进程用的是9号信号
然后在这里我们可以给他发送18和19号信号
19号进程SIGSTOP它的作用将一个进程给暂停,如果我们将他暂停后想重新运行起来那就发送18号信号SIGCONT
如上就是变成了T状态了,当然也可以用18号信号将他给继续跑起来
我们可以看到,这个先暂停后再恢复,它就没有了+号了,就说明它已经变成了后台运行了
那么这个T状态和S状态有什么区别呢?
其实是有的,否则也不会分成两种状态了。
S状态一定是为了等待某种资源就绪的,而T状态暂停后当然也可以等待某种资源就绪,当然也有可能是为了等待其他事件发生后才继续执行,它是不接收除了某些信号之外的其他请求的。也就是说,T状态有可能我们只是单纯的想让他暂停一会。
不过我们暂时可以将他理解为也是某种阻塞状态
如下所示,当我们在使用gdb调试的时候,这个程序就处于t状态(T、t我们暂时不做区分)
所谓的X状态就是我们前面操作系统中的终止态,就相当于一个进程结束了。
然后就可以将这个进程放到一个垃圾回收的队列中,最终操作系统就会将这些回收掉
当一个进程死亡的时候,在死亡之时不会立即进入X状态,而是会先进入Z状态(僵尸状态)
即一个进程在退出时,操作系统会先将这段信息维持一段时间。这段时间就是僵尸状态
而一个进程在退出时候,只有父进程最关心这个信息
我们可以用如下代码来进行验证。在如下代码中,子进程循环结束以后直接退出。而父进程中并没有针对子进程做出任何事情,也就意味着,一旦子进程退出了,这个父进程还在,但是啥也不干,也就意味着子进程将一直维持一种僵尸状态,它要一直等父进程来获取它的退出信息
同时运行这段代码和下面的指令
while :; do ps ajx | head -1 && ps ajx | grep mytest;sleep 1 ; done
运行结果如下所示
我们可以看到,当子进程退出了以后,它这个进程处于Z状态了,并且后面出现了defunct这个单词,它的意思是无效的,死者,死人
这就是僵尸状态的进程
进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会让自己一直处于Z状态,进程的相关资源尤其是task_struct结构体不能被释放
我们把处于Z状态的进程也称之为僵尸进程
内存一直会被占用着,就会发生内存泄漏了
不过我们还要注意
当我们将程序将父进程也结束的时候,子进程也被退出了,它也没有处于僵尸状态。
这是因为,父进程退出的时候,它也有自己的父进程bash,bash瞬间将父进程给回收了,所以没有看到父进程处于僵尸状态。
前面是子进程先退出的案例,我们在看一个父进程先退出的案例
运行结果如下
我们发现当父进程挂掉以后,它的子进程的父进程变为了1
那么这是为什么呢?我们先看一下1号进程是什么
可以看到就是系统的进程
即1号进程就是操作系统本身
所以我们得到以下结论:
如果父子进程中,父进程先退出,子进程的父进程会被改为1号进程(操作系统)
父进程是一号进程,我们将这个进程称为孤儿进程
该进程被系统所领养!
那么为什么要被领养呢?
因为孤儿进程未来也会退出,也要被释放。
那么为什么不让bash领养呢?
bash做不到,bash只是创建的它的子进程,无法管理它的孙子进程。而操作系统可以直接从内核去释放掉
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎
么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话
说,Z状态一直不退出,PCB一直都要维护?是的!- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空
间!- 内存泄漏?是的!
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收