Linux进程状态
1、操作系统的进程状态
Linux内核中每一个进程唯一对应一个task_struct结构体,结构体中存储进程的状态,进程状态的本质是int类型整数,进程状态决定了进程接下来要执行什么任务,进程的状态有7种:
- 创建状态:创建进程
- 就绪状态:进程已经拿到所有资源,只差 CPU 时间片,随时可被调度运行
- 运行状态:调度器选中该进程,CPU 正在执行它的代码
- 阻塞状态:主动脱离调度队列,挂载到对应硬件的等待队列wait_queue;事件完成后回到就绪
- 结束状态:进程执行完毕return/exit(),释放资源
1.1 运行状态
一般计算机中只有一个CPU,但进程很多,CPU遵循先进先出(FIFO)的规则,所以CPU会维护一个运行队列,以队列的形式调度进程,队列的每个成员都是进程的PCB对象,凡是在运行队列中的进程,所处状态都是运行状态

1.2 阻塞状态
写一段代码并运行:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int a = 0;
scanf("%d",&a);
printf("a = %d\n",a);
return 0;
}

运行程序后执行到scanf时程序会停下等待键盘输入,此时进程阻塞
常见的阻塞是进程等待键盘输入数据的过程,键盘是一种硬件,操作系统对硬件的管理方式是先描述再组织,所以每一个硬件在内核中都有一个自己的结构体对象,结构体对象中会维护一个等待队列,用来存放所有等待该硬件事件的阻塞进程
所以阻塞本质是将当前进程从CPU的运行队列摘除,挂载到了硬件结构体对象中的等待队列
当用户敲击键盘输入时,操作系统从键盘的等待队列中取出改进程,放回CPU运行队列
所以阻塞与运行的本质:看你的task_struct在谁提供的队列中
1.3 挂起状态
- swap
swap是磁盘上划分的一块专用分区,作用是内存不足时,把进程的代码或数据临时存到磁盘,用磁盘的空间换内存的空间,过度的swap会导致系统变慢
swap out表示把进程代码段、数据段 写入磁盘 swap 分区
swap in表示把 swap 分区里的进程代码或数据重新加载回物理内存,进程恢复普通就绪或阻塞状态
当内存空间不足时,操作系统会把没有被CPU调度的代码和数据放到swap分区,只留PCB在队列中排队,这种进程就处于挂起状态,如果进程处于阻塞状态时被挂起成我阻塞挂起,如果进程处于运行状态时被挂起成我运行挂起,如果内存还是不够,那么操作系统会选择性杀掉特定的进程
2、Linux中具体的进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
2.1 状态在kernel源代码里定义
cpp
/*
* 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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
2.2 查看进程状态
指令:ps ajx | head -1 && ps ajx | grep myprocess
先写一段代码
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("我是一个进程:%d\n", getpid());
}
return 0;
}
运行后查看进程的状态

从上面可以看出代码执行时处于S睡眠状态,将printf打印语句删除后再运行
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1);
return 0;
}

此时进程状态变成了R状态
因为CPU的速度很快,在持续输出时,进程的大部分时间都用来等显示器输出,所以最终是睡眠状态,去掉printf后,进程不会去访问显示器,进程始终运行,所以最终是R状态
另外加号表示在前台运行,没有+表示在后台运行
在./myprocess后加上& 可以切换到后台运行

如果想停止在后台运行的进程可以用指令kill -9 + 进程PID 结束进程
2.3 D(disk sleep)不可中断休眠
如果进程需要磁盘写入数据,那么进程把数据交给磁盘后要等磁盘把数据全部写入,而在等待时间内进程不能结束,但操作系统检测到该进程在等待磁盘后,为了腾出更多的内存空间,会直接终止该进程,此时如果磁盘写入数据失败,磁盘找不到进程,无法告诉用户数据写入结果,磁盘会直接把数据丢掉,造成数据丢失
所以D状态存在的意义是让进程等待磁盘 IO 硬件操作时,屏蔽所有信号,避免操作系统打断读写磁盘的进程,避免造成数据损坏
2.4 T(stopped)停止状态
进程被外部信号冻结,完全停止执行,不等待资源
使用指令:kill -19 进程PID 可以使进程变成T状态

执行命令后,进程由R+状态变为T状态,并且进程会到后台运行
使用指令kill -18 进程PID 可以使进程结束T状态

结束停止状态的进程会到后台运行,此时只能通过kill -9 + 进程PID 指令将进程终止
T状态和S状态的不同之处是S状态下,资源就绪会自动唤醒,而T状态不会自动恢复,只能手动操作
3、僵尸进程
当一个进程结束时,退出信息有两种:main函数的返回值或收到的信号值,那么进程退出后退出的信息保存在哪里?答案是保存在进程自己的task_struct内核结构体 里,结构体中有两个关键成员:exit_state:标记进程状态,exit_code、exit_signal:存储退出码、杀死它的信号值
当一个子进程终止时,父进程没有读取子进程结构体中的退出信息,此时子进程就会进入僵尸状态
操作系统不会直接销毁它的task_struct,会把退出信息(exit_codeexit_signal)保存在这个结构体里,专门留给父进程读取。
只要父进程不去读取这份信息,task_struct就不会被内核释放,进程将一直处于僵尸状态
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
printf("子进程退出\n");
return 0; // 子进程退出,变僵尸
}
sleep(30); // 父进程休眠30秒,不调用wait回收
return 0;
}
当程序运行时,查看程序的运行状态

当子进程结束后,子进程的用户态代码内存全部释放,父进程卡在sleep(30),没有读取子进程退出状态,子进程无法被清理,成为僵尸状态
僵尸进程的危害:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程将一直处于 Z 状态
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
- 那一个父进程创建了很多子进程,就是不回收,会造成内存资源的浪费因为数据结构对象本身就要占用内存
- 造成内存泄漏
4、孤儿进程
僵尸进程是让子进程先退出,父进程运行,那么孤儿进程是让父进程先退出,子进程运行
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程代码段
while(1)
{
printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
// 父进程代码段
int cnt = 5;
while(cnt)
{
cnt--;
printf("我是父进程, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
}
}
return 0;
}
当父进程运行时查看运行状态

父进程和子进程都处于S状态
当父进程退出后查看运行状态

当父进程退出后,父进程被bash回收,所以不会出现僵尸状态,当父进程结束后,子进程的父进程会变成1号进程,这种进程就是孤儿进程,孤儿进程会被系统领养,给孤儿进程退出善后,孤儿进程会自动变成后台进程
补充:前后台进程的区分:谁能从键盘获取数据输入,谁就是前台,键盘只有一个,所以在获取输入时,只能有一个进程获取,前台进程任何时刻只能有一个,后台进程可以有多个,所以后台进程不能获取键盘数据,不能用ctrl + c终止,根据这一点可以区分前后台进程