Linux中的进程状态
以下是linux-2.6.11.5版本中所有的进程状态:
c
static const char *task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)" /* 32 */
};
我们看到每一种进程状态都对应一个整型值,这也说明了进程状态本质上就是进程的其中一个属性,即task_struct对象内部的一个整型变量。
接下来,我们将一个一个认识这些状态。
1、R状态
R (running),就是运行状态。
c
// test1.c
#include <stdio.h>
int main()
{
while (1)
{
// ...
}
return 0;
}

小贴士
上面程序中我们没有往死循环内部加上任何语句,所以程序运行,查看进程的时候,我们发现的确是R状态。
当我们想在死循环中执行printf打印语句和sleep休眠函数,或者直接执行打印语句,我们却发现进程处于S状态:
这是因为,CPU执行打印操作,以及休眠函数sleep,占据了绝大部分时间。
- CPU与内存的交互时间是纳秒~微秒级的,而内存与外设的交互时间是毫秒级的,printf函数输出内容到外部显示屏上所用时间必定是远远大于操作系统的操作时间的。此时操作系统等待printf完成输出任务的过程中,就会将进程设置为S状态。等到输出任务完成,进程又会回到R状态。
- 而我们调用的sleep函数,设置的是休眠1秒,此时操作系统等待休眠时间,也会将进程设置为S状态。等到休眠时间结束,进程又会回到R状态。
所以进程在执行的一段时间中确实是有R状态和S状态的,只是S状态的存在时间比R状态的要大得多 ,所以我们很难观察到R状态。但也不是观察不到。
2、S状态
S (sleeping),就是休眠状态。
比如我们之前写的scanf函数,键盘未获取数据之前,系统就让进程进入阻塞状态。
c
// test2.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d\n", a);
return 0;
}

所以,S状态属于阻塞状态。
我们还可以执行^C,把处于S状态的进程终止掉。
可能有细心的同学观察到了:S后面为什么要跟个"+"?
其实,进程状态后带上+,表示这个进程是在前台运行的。
现象1
如果我们直接执行程序,查看进程时默认是有+的,也就是说进程默认是在前台执行的。此时我们执行不了ls, pwd等命令行指令,但是我们可以^C终止进程,命令行指令又可以执行了。
现象2如果我们执行程序的时候再加一个
&选项,查看进程的时候就没有+了,进程此时就在后台运行。进程运行的过程中我们可以执行命令行指令,但是我们无法^C终止进程,只能执行kill指令kill进程。
如果我们要对上面的现象做出合理的解释,我们就必须搞清楚前台与后台到底是什么。
2.1、什么是前台/后台
操作系统中可能有多个进程,需要通过键盘获取数据资源。
比如,-bash在等待键盘获取指令与数据,scanf函数也在等待键盘输入。但是键盘只有一个,那么键盘输入的资源会到哪里去呢?
答案是前台进程,
- 任何时刻,在Linux命令行操作的过程中,有且只有一个进程是前台进程;
- 前台进程,是能够从键盘读取数据,拥有键盘文件的进程。
2.2、为什么要有前台,与现象理解
Linux要明确键盘输入的数据,到底是给哪一个进程,所以需要"前台"。
现象1的解释
比如,当我们编译test.c成test.exe,然后执行test.exe:
c
// test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while (1)
{
printf("I'm a process. pid: %d\n", getpid());
sleep(1);
}
return 0;
}
此时test.exe是在前台运行的,所以./test.exe启动的进程有读取键盘数据的权限;由于只有一个进程是前台进程,那么-bash理所当然就移到了后台,-bash也就没有读取键盘数据的权限。我们使用键盘发送的ls, pwd就不会输入到-bash,而是到./test.exe启动的进程中,而进程不需要什么键盘输入。所以我们会看到进程运行的过程中,我们指令不了ls, pwd。

当我们^C结束进程时, ^C输入到了进程中,进程终止,-bash就回到了前台,也就重新拥有了读取键盘数据的权限。此时我们就又可以执行ls, pwd。
现象2的解释
当我们编译test1.c成test.exe,然后执行test.exe,但是加上&选项:
c
// test1.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d\n", a);
return 0;
}
此时./test.exe启动的进程就在后台执行,也就没有读取键盘输入的权限,但是scanf函数强制要读取键盘输入。操作系统察觉到了后台进程也想要读取键盘的异常,就将进程设置成了T状态。

此时我们也能执行ls, pwd,因为-bash此时在前台执行。
然后我们就有一个问题:我们如何终止进程?现在^C只能输入给-bash,而不能给进程,所以我们不能使用 ^C终止进程,我们有两个解决方法:
kill -9 [pid];jobs查看后台进程,fg [code]将后台进程转为前台进程,再终止进程。
2.3、为什么要有后台
不需要键盘键入的进程,也需要运行。所以后台的存在是为了提高效率。
比如,我们yum/apt下载安装软件包的时候,就可以设置yum/apt指令为后台执行,
- 后台执行,我们就可以执行其它指令,提高效率;
- 我们还可以把下载安装输出的字符文本写入一个叫
null的字符文件中,它会帮我们自动处理掉字符文本。
指令为:sudo apt install -y [软件包] > /dev/null &
3、T状态
在S状态关于前台的学习中,我们发现:当进程被设置为后台运行,但是scanf函数强制读取键盘数据,此时操作系统就会将进程设置为T状态。
像这样,当进程做不符合规定的工作的时候(比如后台进程企图读取键盘),操作系统又不会将进程终止,那么操作系统就会将进程设置为T状态。
T状态下,就算进程想强制执行某些不合法的操作,操作系统直接将进程设置为T暂停状态。也就是说T状态也属于阻塞状态。
使进程进入T状态,有两种方法:
- (这种方法我们用过了)后台执行包含scanf函数代码的进程;
kill -19 [pid]
对于kill指令法:

我们再执行kill -18 [pid],进程又恢复运行,但是进程还是处于后台:

4、t状态
t状态发生在调试的时候。
c
// test2.c
#include <stdio.h>
int Sum(int n)
{
int ret = 0;
for (int i = 1; i <= n; ++i)
{
ret += i;
}
return ret;
}
int main()
{
int i = 10;
printf("i = %d\n", i);
int ret = Sum(i);
printf("ret = %d", ret);
return 0;
}
我们使用cgdb调试,在Sum函数里一开始就设置断点,调试执行一进入Sum函数就停下。

此时由于调试启动的进程,就处于t状态。遇到断点的本质就是进程暂停。t状态也属于阻塞状态。
小贴士
其实调试的本质也是创建子进程。
我们仔细观察,就能在这张图中,找到"父子关系"(ppid与pid)。
小贴士2
路径一定不要有中文,否则cgdb会有问题。
5、D状态
D状态源于下面一个场景。
假设没有D状态。当前有一个进程正在向磁盘做写入,向磁盘做写入为冯诺依玛体系中的Output,进程需要等待磁盘响应,那么进程大部分时间就会处于S状态。
就在这时,内存资源已经严重不足,操作系统为了自保,强制终止此进程;磁盘也受到了牵连,进程写入磁盘失败。
进程写入磁盘的,往往是非常重要的数据,比如银行交易记录。为了防止类似问题的发生,开发者新增了一个D状态 ,D状态属于阻塞状态。D状态通常只会在等待磁盘、NFS 等 I/O 时短暂出现,我们也可以理解为D状态是操作系统出于保护I/O流目的而设置的状态。然而D状态是深度睡眠状态,用户只能等待进程自己从D状态"醒来"。所以D状态也是一个非常"危险"的状态。
6、X状态与Z状态
小贴士3
进程不能直接到X死亡状态,必须先由Z僵尸状态,再到X死亡状态。
X死亡状态的持续时间很短。
Z状态,又叫僵尸状态,是怎么一回事呢?
大街上突然出现一具尸体,帽子叔叔不能直接处理尸体,必须先调查清除死亡原因。
同理,创建进程是为了完成具体的任务,当进程退出或者死亡,
- 任务完成得怎么样;
- 进程是如何退出的。
这两条信息,都必须让父进程知道。所以父进程就必须读取(子)进程task_struct内部的退出信息。
**进程已经退出,但是task_struct必须维持一段时间,因为内部的退出信息需要等待父进程读取。等待的过程中,进程就处于僵尸状态。**所以僵尸状态存在的理由,是因为父进程需要读取子进程的退出信息,从而管理子进程。
6.1、观察僵尸进程
我们可以这样设计代码,让子进程正常退出,而父进程不退出,也就不会读取子进程的推出信息。
c
// test3.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5;
while (cnt--)
{
printf("我是子进程。pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
printf("子进程已退出。\n");
}
else if (id > 0)
{
// parent
while (1)
{
printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
当子进程退出,而父进程还在循环,子进程就进入了僵尸状态:

6.2、父进程永远不读取?
如果父进程永远不读取子进程的退出信息,结果会怎样?答案是内存泄漏。
父进程不读取,意味着子进程的task_struct永远不会被销毁,积少成多内存就会逐渐减少。
小贴士4
进程退出,手动申请的堆空间(malloc, new)就算不手动归还,操作系统也会自动归还。
内存泄漏对于会退出的进程,不是很大的问题,进程退出就归还空间了;而对于永远不退出的进程(常驻进程),内存泄漏才是大问题。而我们使用的软件,本质上都是一个死循环,所以我们要重视内存泄露的问题。
6.3、父进程如何读取?
父进程如何读取子进程的退出信息?这个问题我们以后再聊。我们现在只需要知道方法是使用wait waitpid方法即可。

7、孤儿进程
如果父进程退出了,而进程还没退出,结果会怎样?答案是子进程会变成孤儿进程。
我们可以模拟孤儿进程的产生:
c
// test4.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
while (1)
{
printf("我是子进程。pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else if (id > 0)
{
// parent
int cnt = 5;
while (cnt--)
{
printf("我是父进程。pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
printf("父进程已退出。\n");
}
return 0;
}

我们会发现,子进程被pid为1的进程"领养"了,而且领养后的子进程变成了后台进程,所以我们无法^C终止,只能使用kill指令。
我们一般认为,pid为1的进程就是操作系统本身。
孤儿进程必须要被领养,因为孤儿进程未来也是要退出的。为了使孤儿进程的退出信息被回收,而不让孤儿进程变成僵尸进程,进而引发内存泄漏问题,孤儿进程就必须被领养。
小贴士5
Linux下似乎没有设置挂起 状态?
实际上,挂起操作是操作系统自动帮用户做的(辗转腾挪),用户并不需要知道操作系统具体的挂起操作。


