文章目录
- 1、进程各状态的概念
- 1.1 运行状态
- 1.2 阻塞状态
- 1.3 挂起状态
- 2、Linux进程状态
- 2.1 运行状态 R
- 2.2 睡眠状态 S
- 2.3 深度睡眠 D
- 2.4 停止状态 T
- 2.5 僵尸状态 Z 与 死亡状态 X
- 孤儿进程
Linux内核中,进程状态,就是PCB中的一个字段,是PCB中的一个变量,一般是宏定义出的一批数字。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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 */
};
再配上下面的图,我们看看各进程状态之间的转换:
状态的变化,本质就是修改整形变量。
看到这大家可能还是比较懵的,下面我们就来细看每一种状态。
首先我们先看教材上的进程状态,再具体到Linux下的进程状态。
1、进程各状态的概念
1.1 运行状态
每个CPU在系统层面都会维护一个运行队列。(n个CPU就会维护n个运行队列)
什么叫做运行状态?
只要在运行队列中的进程,状态都是运行状态。
只要是在运行队列中的进程,它的PCB中状态字段就是R,数据是准备就绪的,只等CPU运行了。
1.2 阻塞状态
进程 = 代码 + PCB(内核数据结构)。而我们的代码中,一定或多或少回访问系统中的某些资源(比如:磁盘、显示器、键盘、网卡等)。
我们下面举例来理解一下阻塞状态,下面是一整个链路,串起来理解:
1、当我们是C/C++代码,代码中有 scanf/cin,需要从键盘中输入时,我们用户就是不输入,键盘上的数据就是没有准备就绪的,这就是进程所要访问的数据没有就绪,即不具备访问条件,导致进程的代码无法向后执行。
2、访问的数据没有就绪,操作系统一定是最先知道的,因为操作系统是一款搞管理的软件,它管理计算机的所有软硬件。操作系统管理硬件,本质也是管理数据,“先描述,再组织”。
3、当进程在CPU中被运行的时候,用户一直不输入,这时访问的数据是没有就绪的,于是PCB就被操作系统从运行队列中放到硬件的等待队列中,PCB中状态字段就被改为 阻塞状态,然后去排队等待。一旦用户输入,数据状态立马就被改为了就绪状态,操作系统再将PCB放入到运行队列(将进程唤醒),并将PCB状态改为运行状态,CPU继续开始运行进程。
总结:
1、当PCB不在CPU所维护的运行队列,而在硬件的等待队列中,此时状态就是阻塞状态。
2、进程状态变化的本质:更改PCB中status整数变量; 将PCB链入到不同的列队中。
3、这里的所有过程,只和进程的PCB有关,与进程的数据代码都无关。
4、操作系统中,会有非常多的队列,运行队列、等待硬件的设备等待队列等。
这里我们也就不难想到,平时使用计算机时,启动了非常多的进程后,为什么会那么卡呢?
其实就是操作系统以我们能感知的时间里,将进程的状态不断在改变。
1.3 挂起状态
如果一个进程当前被阻塞了,这个进程等待的资源是没有就绪的,该进程就没有办法被调度。
如果此时,恰好 操作系统内的内存资源严重不足(前提) 了,怎么办?
此时我们阻塞进程的代码和数据就可以先写入磁盘中,等数据就绪后,再拷贝到内存中,这时就叫做 阻塞挂起状态(结果) 。这里将内存数据置换到磁盘,针对所有的阻塞进程。这个过程虽然慢了点,但是与资源严重不足将要宕机相比,慢点是可以接收的。
这里数据会被置换到swap分区。一般swap分区大小与内存大小差不多大,如果很大,swap分区很难被写满,内存稍微不足,操作系统就会将数据换出到swap分区,频繁的换出就会导致效率变慢。因此设置小点就会倒逼操作系统自己来处理,而不是频繁使用置换算法,提高效率。
当进程被os调度,曾经被置换出去的进程代码和数据,又要被重新加载到内存。
具体还有就绪挂起,在运行队列中,但是还没有被调度的进程,代码和数据被换出,等调度的时候再换入,这就是就绪挂起,这样会导致效率变低。
2、Linux进程状态
2.1 运行状态 R
- R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
我们写一段C语言代码来跑一下,看看进程是什么状态。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("Hello Linux, pid:%d\n", getpid());
sleep(1);
}
return 0;
}
我们编译完代码后运行,并用 ps ajx 命令查进程,看看进程状态:
ps ajx | head -1 && ps ajx | grep mytest | grep -v "grep"
这里明明是运行着呢,为什么状态是S呢?
这是因为CPU跑代码很快,但是外设显示器的速度很慢,大多时间都是在等待显示器。这就是访问了外设,外设数据没有就绪,进程是阻塞状态。
我们去掉 printf 语句,不让代码去访问外设,直接运行,看到的就是R 运行状态了。
大家一定还有疑问,我们说的是R与S状态,但是查出来的却是R+与S+,这是怎么回事呢?
+表示的是前台进程的意思,所谓前台进程就是推在前台的,一旦启动我们的bash(命令行解释器)就无法在使用了,前台进程可以使用 ctrl+c 终止掉的。
后台进程就是跑在后台的,不影响我们bash的工作(输入命令可以执行),只能使用 kill -9 pid 来终止。后台进程的状态就没有+。
后台进程的启动:./exe &
2.2 睡眠状态 S
- S睡眠状态(sleeping): 意味着进程在等待事件完成浅度睡眠,可以被终止掉,会对外部信号做出响应。(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
我们写一段C语言代码,编译后运行:
此时我们不输入,进程就阻塞了,在Linux中具体的状态就是S。
S状态可以被终止掉,或者可以再换到运行状态。
2.3 深度睡眠 D
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
D状态我们不好演示,我们举例讲一下,什么时候是深度睡眠状态。
如果我们的进程是往磁盘中做写入操作的时候,写入的数据量很大,这时是磁盘在工作了,进程就休眠了,但是写入成功与否,磁盘还要给进程反馈。恰巧此时内存资源严重不足,置换算法也解决不了时,(Linux下)操作系统是可以杀进程的,它肯定是不会杀掉运行状态的进程,所以它会杀掉睡眠中的进程,此时这个进程被杀掉了。这时磁盘没有将数据写入成功,后面还有进程需要向磁盘写数据,此时磁盘想给进程返回没有成功时,进程被杀掉了,磁盘只好将数据丢弃掉做后面的工作,进程也不知道是否写入成功。如果这是具体的银行10万条存款记录,这就是一次重大事故。
因此,防止向磁盘写入重要数据时进程被终止掉,就有一个深度睡眠状态 D,深度睡眠状态下的进程是不可以被杀掉的。一般情况下,我们用户要是可以查到D状态,内存就很忙了,也就意味着几乎要宕机。
2.4 停止状态 T
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
我们先来看看一些信号:
这些数字对应的信号,在Linux内核中其实是宏定义出来的。
我们以下面的例子来看看,试一下暂停:
编译后运行:
我们发送信号让进程暂停了,此时我们发现,bash提示符在闪烁等待我们输入,一暂停进程就被改为后台进程了,不影响我们其他的操作。这时我们想要进程再运行起来,我们做下面的操作:
这很简单,但是我们为什么要将状态设置为停止状态呢?
当进程访问软件资源的时候,可能暂时不让进程进行访问,就将进程设置为STOP。
而 t 状态,是我们使用gdb调试代码时,追踪程序,遇到断点,进程就暂停了。
这里我们能看到,当我们打了断点后,去运行程序,走到断点处状态变成了t,t表示tracing追踪的意思。
所以,不管是 T/t 都是阻塞状态,这里没有等待硬件资源,而是等待用户的指令,这就叫做 等待软件条件就绪。 因此在具体的os中这些都叫做阻塞状态。
2.5 僵尸状态 Z 与 死亡状态 X
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- Z 僵尸状态(zombie):一个比较特殊的状态。当进程退出并且父进程/OS(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程(父进程要知道子进程把任务完成的怎么样)。
- 僵死进程会以终止状态保持在进程表(PCB)中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。 如果不及时回收,就会造成内存泄漏(字段会申请资源)。
我们写一段C语言代码来验证一下 Z 状态,我们父进程不回收子进程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0) return 1;
else if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, run times: %d\n", cnt--);
sleep(1);
}
printf("I am child, dead: %d\n", cnt--);
exit(2);
}
else
{
// father
while(1)
{
printf("I am father, running any time!\n");
sleep(1);
}
// 回收操作
}
return 0;
}
父进程没有回收子进程,子进程从S状态变为Z状态,defunct就是死者,死亡的意思。
僵尸进程的危害:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏 ?是的!
孤儿进程
刚我们讲的是子进程先退出,父进程不回收,导致子进程僵尸状态,那如果子进程不退出,而父进程先退出呢?
我们写一段这样的C语言代码来看看:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0) return 1;
else if(id == 0)
{
// child
while(1)
{
printf("I am child ...\n");
sleep(1);
}
}
else
{
// father
int cnt = 5;
while(cnt)
{
printf("I am father, run times: %d\n", cnt--);
sleep(1);
}
printf("I am father, dead: %d\n", cnt--);
exit(2);
// 回收操作
}
return 0;
}
当父进程退出时,是由bash回收的,但是子进程是要被父进程回收的,但是父进程先退出了,子进程要被领养,变成孤儿进程。
一般孤儿进程是要被1号进程领养,如果不领养就无法回收,导致内存泄漏。
那1号进程是谁呢?我们查一下:
这里一号进程叫做systemd,它其实就是操作系统。