进程状态的概念
顾名思义,进程状态就是进程目前所处的状态。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
一般我们把进程分为六种状态:
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- Z僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。
不过,详细介绍这6种状态之前,我们还需要了解阻塞和挂起这两个概念。
阻塞
所谓组设,就是指进程因为等待某种条件就绪,而导致一种不推进的状态。
我们都知道,进程是先描述再组织的。对于我们调用某一项资源也是这样。
举个例子:
OS是款搞管理的软件,所以它能够管理网卡、屏幕、键盘等硬件资源,但是OS并不是直接管理这些硬件的,OS会先描述出每个硬件基本信息,并且将这些基本信息放入一个结构体中,然后再利用某种数据结构将这些结构体再组织起来,这样OS就可以很好地管理硬件资源了。
管理起了硬件资源后,那么我们如何去使用这些硬件资源呢?
实际上在存储硬件信息的结构体中,还有一个队列,这个队列是专门用来记录需要申请该硬件资源的进程的pcb。当我们的某一个进程需要调用某个硬件资源的时候,就会将该进程的pcb放入该硬件资源调用的这个队列中。而此时进程不会进行下去,所以该进程的pcb也就移出了cpu队列。
当然,整个内存中可能有许多进程需要调用这一个硬盘资源。但这个硬盘资源又不可能被多个进程同时调用。所以,当一个进程在调用该硬盘资源的时候,其他需要调用该硬盘资源的进程就会在申请该硬盘资源的队列中进行等待。此时进程并没有被cpu调度,所有在等待调用资源的进程的行为就是阻塞。而当这个等待的进程等到需要的资源时,该进程的pcb就会加载进cpu队列中,等待cpu的处理。
挂起
挂起状态可以说是一个特殊的阻塞状态。
挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态。
但是OS并不会销毁转移进磁盘的进程的pcb,所以OS还是能管理到该进程的。此时进程处于的这种状态就叫做挂起状态。当该进程分配到对应资源时,OS又会将该进程的代码数据从外存调回内存。
运行状态
对于处于运行状态的程序来说,并不代表着进程一定处在运行中,它表明进程要么是在运行中要么在运行队列中。
不过一个程序处于R状态的时间非常短暂。因为cpu的运行速度是非常快的,往往一个进程一瞬间就不再是R状态了。大部分时候,进程都处于等待资源调用的状态即S状态。
睡眠状态
当进程为了等待某种资源,而处于等待队列当中的状态称为睡眠状态。
而我们上面所讲解的阻塞状态也属于睡眠状态,被称为浅睡眠状态或可中断睡眠。即我们可以kill掉处于该状态的程序。
以上面程序为例,按理说程序是一直printf,是一直在执行。所以这个程序会一直处于R状态么?
不难发现test程序是一处于S状态的,这是为什么呢?
因为cpu在处理printf语句时,会让该进程调用屏幕资源,将“******”打印到屏幕。此时,该进程就会离开cpu队列,去屏幕资源的等待队列中等待屏幕资源的分配,所以我们看到的是S状态。因为cpu运行很快,但资源调用远远没有cpu处理快,所以即使是这么一个死循环进程,进程大部分时间也处于屏幕资源调用的等待队列中,也就是S状态。
磁盘休眠状态
磁盘休眠状态我们可以理解为特殊的S状态。S状态我们是可以杀掉的,但是D状态我们是不能杀掉的。只能等待该进程分配到等待的资源,或者重启计算机。否则该进程就会一直处于D状态。
因此一般情况下我们的计算机都不会出现处于这种状态的进程,一旦有进程处于这个状态,那么计算机就有可能死机。
停止状态
我们平时在写代码的时候一定少不了调试吧。在调试的时候,如果我们不打断点,直接进行编译那么编译器就会跑完整段代码。但是当我们在中间打了断点过后,编译器运行的代码就会在断点处停下来。而此时进程就处于停止状态。
同样的,我们也可以通过
kill
指令发送信号来使使进程进入停止状态。
kill
指令可将指定的信息送至程序。
kill指令的信息如下:
其中可以通过发送 SIGSTOP(19) 信号给进程来停止进程。这个被暂停的进程我们也可以通过发送 SIGCONT(18) 信号让进程继续运行。
我们用之前的代码来测试一下:
然后通过kill指令的19号信息来使进程进入停止状态:
可以看到,在执行了kill -19 16002
之后,我们原来的进程状态由S+
变为了T
。然后也可以观察到我们左边的进程已经停止运行了。
同时我们也可以通过kill -18 16002
让进程恢复运行:
如上图所见,我们已经恢复了进程的运行,但是现在进程处于的是S
状态,而不是S+
状态。那么这两个状态有什么区别呢?
简而言之,如果一个进程的状态后面还跟了个+号,那么表明这个进程是个前台进程,如果一个进程的状态后面没有+号,表明这个进程是后台进程。
前台进程可以通过Ctrl+c
和kill -9 pid
来杀掉该进程。
而后台进程我们只能通过kill -9 pid
来杀掉,Ctrl+c
不行。
同时前台进程执行的时候我们是无法输入Shell命令,而后台进程执行的时候我们可以正常输入Shell命令。
死亡状态
一般而言死亡状态只是一个返回状态,我们不会在任务列表里看到这个状态。所以这里我就不过多赘述了。
僵死状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵尸进程。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会进入Z状态。
我们先来看一段代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
else if (id > 0)
{ //parent
printf("我是父进程,我还有30s死亡\n");
sleep(30);
}
else
{
int n = 5;
while (n--)
{
printf("我是子进程,我还有%d秒死亡\n", n);
sleep(1);
}
}
return 0;
}
执行结果如下:
当我们子进程还在执行的时候,子进程处于S状态。
而当我们子进程死亡但父进程还活着的时候,这个时候子进程就处于Z状态了。因为此时子进程已经死亡,它在等待父进程去处理它的退出码。
在父进程没有接受他的退出码这段时间中,子进程就处于僵尸状态。
最后,父进程也允许结束之后,我们可以发现处于僵尸状态的子进程也消失了。
这是因为到最后父进程结束时也没有退出码,于是当父进程结束的时候子进程就变成了孤儿进程,这个时候OS会让一个init
进程(即OS)来把这个孤儿进程“领养”。
于是这个孤儿进程就有了一个父进程了,同时这个init
进程会去处理这个处于僵尸状态的子进程。
当子进程的返回码被init
进程处理之后,这个僵尸进程也就结束了。
那么,OS为啥么要设计这么一个方法去处理僵尸进程呢?
僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就会一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB就需要一直维护。
- 如果一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间,如果不去回收,就会造成内存泄漏。
孤儿进程
在上面我们已经提到了孤儿进程,那么孤儿进程到底是什么呢?
简单来说如果父进程先退出,子进程就称之为“孤儿进程”。
而这个孤儿进程就会被1号进程即init
进程领养,然后交给init
进程去回收。
为了方便演示,我们执行下面代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
else if (id == 0)
{//child
while (1)
{
printf("我是子进程,我的pid是:%d\n", getpid());
sleep(1);
}
}
else
{//parent
int n = 5;
while (n--)
{
printf("我是父进程, 我的pid是:%d,我还有%d秒死亡\n", getpid(), n);
sleep(1);
}
}
return 0;
}
执行结果如下:
在父进程死亡之前,子进程的PPID都还是父进程的PID。
当父进程死亡之后,子进程的父进程就变成了1号进程。
与此同时,子进程的状态由S+
变成了S
,也就是说子进程由一个前台进程变成了后台进程。
此时我们只能通过kill -9 pid
来杀掉该进程,而Ctrl+c
则不行。