文章目录
- 进程状态概念
- Linux中的进程状态
- R(running)状态
- S(sleeping)状态
- D(disk sleep)状态
- T(stopped)状态
- t(tracing stop)状态
- X(dead)状态
- Z(zombie)状态
- 特殊的孤儿进程
- 进程优先级
- 进程性质补充
进程状态概念
《现代操作系统》中给出的进程状态的定义如下:
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
这里不讲什么三态五态模型,
只是从一个相对通俗的角度去看待一下进程状态。
首先理解一个简单的问题,
一个进程开始运行是一直占用CPU资源到进程结束吗?
就从我们的电脑能同时运行时几个进程就可以看出答案肯定是否定的。
所以多个进程已经具备被CPU调度的条件之后,
也就是进程就绪之后,
可以简单认为进程会进入一个操作系统维护的运行队列中,
在这个运行队列中排队等待被调度,
当进程运行够一段时间之后再放到队尾排队等待下一次调度,
此时下一个位于队头的进程继续被CPU调度,
所以进程在运行的过程中会经历不断地进程切换,
而在这个运行队列中的进程,就是上述所说的就绪态和运行态。
但是与此同时还有一个问题就是,
如果运行中的进程要访问外设,
需要向外设输入输出呢?
因为向外设的io行为相比CPU的运行效率,
实在是太低了!
CPU如果等的话效率会大大降低!
就比如在银行排队办理业务时,
排到你的时候需要你填一张表,
工作人员是让你去旁边填完表再过来,继续为后面的客户办理业务,
还是就让你当场填,等填完再继续呢?
大部分情况下都会是选择前者。
操作系统也是这么想的!
当一个进程要访问外设时,
操作系统会将这个进程移出运行队列,
此时就可以说这个进程处于阻塞状态。
当然,进程进入到阻塞状态并不一定都是因为访问外设。
但是资源是有限的啊,
如果内存一旦不足,
操作系统作为管理者定不会坐视不管,
而处在运行队列中的进程肯定不好被清理,
而处于阻塞状态的进程呢?
进程对应的代码和数据暂时并用不到,
是否可以考虑暂时将进程对应的代码和数据移出内存呢?
确实可以,操作系统也确实是这么做的。
我们就可以称进程被挂起了。
综上,我们主要介绍了操作系统宏观层面的进程状态,这里做一个小小的总结。
我们主要介绍了运行、阻塞和挂起。
一个进程正在运行,不一定是正在占用CPU资源,
也有可能是在等待被CPU调度。
而正在被调度的进程,可能因为有IO操作或其他系统服务请求,
而因为某些原因请求无法立即完成,
为了让CPU资源得到最大的释放,
操作系统选择将等待请求的进程调出运行队列,
此时就可以说进程处于一个阻塞状态。
而当内存资源不够用的时候,
操作系统又会将“不那么着急的进程”的数据和代码调出内存从而缓解资源不足,
此时又可以说进程被挂起了。
操作系统宏观层面的概念介绍得差不多了,
下面介绍一下Linux下的几种进程状态。
Linux中的进程状态
上面主要介绍了操作系统层面宏观的进程状态,
下面就具体到Linux操作系统中看看进程状态是怎么体现出来的。
首先进程状态是一种数据,
数据就要存储,
而他又是进程的一种属性,
进程的属性数据存放在哪呢?
当然是PCB中啦,也就是task_struct中。
下面的状态在kernel源代码里定义:
/*
* 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 */
};/*
说白了,进程状态就是用一个数字标识。
对于上面提到的几种进程状态下面会进行简单的展示。
R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
当我们使用ps axj命令查看进程时,标红的那一列就是进程状态;
像S就是运行状态,+ 表示的是在前台运行,
前台运行后台运行的区别后面会进行演示。
下面就介绍几种容易被捕捉到的进程状态。
R(running)状态
R状态是运行状态。
前面提到了,
处于R状态的进程并不代表正在被CPU调度,
也有可能是在运行队列中排队。
所以进程状态所提到的就绪态和运行态就是这里的R状态。
一般是只有计算需求的进程才会长时间处于R状态,
只要是有IO行为的进程很大概率都不会被观察到在R状态。
为什么?下面给出答案。
S(sleeping)状态
S状态是睡眠状态,
准确地说应该是浅度睡眠状态,
是可以被中断的一种状态,
同时也是阻塞的一种情况。
怎么让一个进程进入浅度睡眠呢?
换句话说,怎么让一个进程处于阻塞状态呢?
最简单粗暴的方法就是,
直接调用sleep接口,
但是这样太无趣了,
不妨试试先面的代码,
观察一下生成的可执行程序运行时的进程状态:
int main()
{
while (1)
{
printf("我是一个进程, pid : %d\n", getpid());
}
return 0;
}
下面是演示:
左侧这个进程明明一直在运行,
但是右侧却一直显示它处于睡眠状态?!
在介绍阻塞状态的时候说过,
当进程有IO行为时,
因为IO效率相比CPU的运行效率差太多了,
所以当进程要访问外设时CPU为了不耽误时间,
就将其移出运行队列进行转移,
此时进程就处于阻塞状态。
这里同理,
我们调用了printf函数,
要向显示器打印,
所以进程大部分时间都处于一个阻塞状态,
也就是这里的S状态,
只有极短的时间内进程才被CPU调度。
在这种情况下我们在某一时刻去观察进程状态,
观察到S状态的概率肯定就很大。
某一时刻肯定能观察到进程处于R状态,
只是概率很低罢了。
D(disk sleep)状态
D状态是深度睡眠状态。
与前面的浅度睡眠状态相比,
处于深度睡眠的进程不可被打断,
只能等IO结束或条件到位才能恢复运行,
即使是操作系统,也不能打断它!
下面用一个小例子去理解为什么会有D状态:
现在有操作系统、进程和磁盘。
进程现在要向磁盘写入大量数据,
在写入的时候进程就什么也不干,
等着磁盘那边发来结束的信号。
而此时内存正好满了,
操作系统需要清理一下门户来释放一些空间。
此时操作系统就找到了这个什么也没干的进程,
就把他干掉了。
而磁盘此时写入失败了,返回失败信息给进程,
但是进程此时已经不在了,
能怪谁呢?
所以对于有高IO需求的进程会长时间处于阻塞状态,
而这种进程又是不能被随便杀掉的,
所以就有了D状态,
处于D状态的进程不能被用户或操作系统挂掉。
而一旦当进程中存在大量处于D状态的进程时,
此时可能就处于一个非常危险的情况了。
因为D状态高IO的需求代价可能比较大,
所以这里就不做演示了。
T(stopped)状态
T状态是暂停状态,也是阻塞的一种。
怎样让一个运行中的进程变成暂停状态呢?
我们可以用kill给进程发暂停信号:
具体就是kill -19 PID
:
此时进程被暂停了,
而进程状态后面的那个小加号也没了,
此时进程就从前台调到后台了,
当进程重新被唤醒的时候也不会自动调到前台了。
下面用kill指令将进程唤醒看看效果:
kill -18 PID
后台进程相比前台进程,
不能直接用Ctrl C挂掉,
当然也不影响在前台使用各种命令;
但是不怕,我们还有kill:
以上就是对暂停状态的介绍,
并稍微认识了一下前台进程和后台进程。
t(tracing stop)状态
t状态是追踪暂停状态。
处于这种状态下的进程会被追踪,
最直观的一个例子就是我们调试代码的时候,
进程会在断点处停止运行,
但是此时进程仍在被追踪,等待下一步指示:
X(dead)状态
X是死亡状态。
死亡状态没什么好说的,
处于死亡状态的进程会被回收,
包括进程控制块和对应的代码和数据,
而回收过程几乎是瞬间完成的,
所以这里很难做出演示。
Z(zombie)状态
Z状态是僵尸状态,
处于Z状态的进程就称其为僵尸进程。
进程运行完毕,无论失败与否,
都要向父进程或操作系统返回结束信息,
当父进程或操作系统收到结束信息后会对进程进行清理工作。
而如果父进程或操作系统长时间没有接收结束信息,
或接收信息后没有对进程进行清理,
会发生什么呢?
此时进程已经运行完了,
但是进程没有被清理,
至少PCB还留在内存中占用资源,
而这也就造成了资源的浪费。
下面用一个简单的例子来演示一下僵尸进程:
int main()
{
int id = fork();
if (id > 0) //父进程
{
while (1);
}
else if (id < 0) //进程创建失败
{
perror("fork");
exit(-1);
}
else //子进程
{
sleep(5);
exit(-2);
}
return 0;
}
这个代码创建了一个子进程,
而子进程刚运行几秒就结束了,
父进程一直运行也没有回收动作,
下面是运行结果:
特殊的孤儿进程
存不存在这样一种情况;
父进程因为某些愿意突然退出了,
而子进程好好的一直在运行呢?
对上面的代码简单改变一下很容易实现:
int main()
{
int id = fork();
if (id > 0) //父进程
{
sleep(5);
exit(-2);
}
else if (id < 0) //进程创建失败
{
perror("fork");
exit(-1);
}
else //子进程
{
while (1);
}
return 0;
}
进程刚运行5秒父进程就噶了,
而子进程好好地一直在运行,
此时会发生什么呢:
父进程的父进程是命令行bash,
bash会在父进程结束的时候读取结束信息并回收它。
此时子进程就变成了孤儿进程,仍然在运行,
但子进程的PPID变成了1,
1进程是谁呢?
其实就是操作系统。
此时孤儿进程就被操作系统领养了。
加点打印信息,可以观察到子进程结束时会被操作系统回收:
进程优先级
Linux一个多用户、多任务的操作系统,
系统中通常运行着非常多的进程。
哪些进程先运行,哪些进程后运行,
就由进程优先级来控制。
查看进程优先级可以用下面一条指令;
ps -al | grep name
图中标注的就是进程优先级相关的两个参数,
分别是PRI(priority)和NI(nice)。
当然这是Linux下的进程优先级表示方法,
并不具有普适性。
PRI还是比较好理解的,即进程的优先级,
或者通俗点说就是程序被CPU执行的先后顺序,
此值越小进程的优先级别越高。
那NI呢?
这就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。
什么意思呢?
可以用一个简单的公式来解释:
PRI(new) = PRI(old) + nice
这样,当nice值为负值的时候,
该程序将会优先级值将变小,即其优先级会变高,
则其越快被执行。
nice其取值范围是-20至19,一共40个级别。
需要强调一点的是,
进程的nice值不是进程的优先级,
它们不是一个概念,
但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正数据。
我们可以用top
命令更改进程的nice值:
进入top后按"r" –> 输入进程PID –> 输入nice值 -> 按"q"退出:
可以看到将子进程的nice值修改为10之后它的PRI值就变成了90。
此时再变一下:
我们又将子进程的nice值修改为了-10,
但是子进程的PRI并没有变成90-10=80,
而是变成了80-10=70,
不难看出上面那个简单公式中的PRI(old)很多时候固定就是80。
另外,我们可以试试将nice值修改至超出其范围看看它是不是真有取值范围:
进程性质补充
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。很简单的一个例子,我的微信长时间未响应,但不影响QQ的运行。
并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。我们的笔记本都是单CPU,所以我们打开进程管理器这么多运行的进程基本都是并发。