Linux:进程状态

Linux:进程状态


进程状态

当一个可执行程序,被载入内存,获得自己的PCB,那么其就可以变成一个进程。也许你学习过一些进程状态的相关知识,其中最常见的就是以下图片:

我们把进程状态分为了运行阻塞挂起三个状态。但是这个图片过于笼统了,相信你学习的时候,也不是很能理解这三个状态到底是啥。本博客将深入讲解Linux中的进程状态,把每一个状态都深入展开讲述。

在Linux中,进程状态的本质其实就是一个整型变量,Linux通过管理进程的PCB结构体,来管理进程,而PCB内部就有一个整型变量来表示进程。

我们不妨看看Linux源码怎么说:

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 */
};

其中包含了RSD等等状态,后面都标识了其对应的整型变量。

第一行中,用bitmap来描述这个数组,也就是一个位图,你会发现每个状态,都是2的次幂,也就是说只有一位是1,当全0就是R运行状态。

比如S状态对应的位图就是00000001T状态对应的位图就是00000100

所以在源码中,进程状态就是用一个整型来描述的。

那么进程的状态有什么意义呢?

比如说,当你敲完一天的代码,你突然对别人说:"我饿了!",其中饿了就是你当前的状态,这个状态意味着,你即将要去吃饭。

因此,进程状态标识着这个进程的后续动作!

比如一个简单的程序:

cpp 复制代码
#include <stdio.h>

int main()
{
	int a;
	scanf("%d", &a);
	
	return 0;
}

当这个程序运行到一半,就会暂停运行,等待用户输入一个数字,此时进程就进入了一个等待的状态,其标识着这个程序在等待用户输入。


运行状态

也许你听说过,进程是可以排队的,最常听的莫过于运行队列,那么进程是如何排队的呢?

进程排队的本质,是进程的PCB在排队,一个PCB可以在多个队列中。

比如下图:

这就是一个运行队列的视图,操作系统给CPU维护了一个运行队列,当有进程要进入运行队列时,就把PCB连入这个链表中,然后CPU遍历链表,一个一个执行。

不过通过上图可以看出,PCB进入链表中,其实不是把整个PCB都连入链表,而是PCB中有多个链表节点的成员,把这个链表节点的成员连入运行队列中。这个链表节点成员是一个简单的链表节点指针List Node*,然后该指针指向下一个节点。

这样操作的好处在于,一个PCB可以连入多个链表,处于多个不同队列中。而想要通过这个小的链表节点找回整个PCB,只需要调用C语言提供的宏offsetof即可。

每个软硬件都有自己的等待队列,比如CPU的等待队列就可以叫做运行队列,各个进程在等待CPU执行它们。比如键盘也有等待队列,各个进程在等待从键盘中提取数据。

当一个进程需要运行,就把它链接到CPU的等待队列中,当一个进程需要网络请求,就把它链接到网卡的等待队列中。对于PCB的状态改变,以及把PCB放到哪一个队列中,都是由OS来执行的。


R状态

R状态就是运行状态running,只要一个进程处于CPU的运行队列中,它就是R状态。

也就是说,运行状态不一定是当前进程正在使用CPU,也许CPU还没有运行当前进程,只是这个进程在CPU的运行队列中,马上要被执行了。

我们看到以下程序test.exe

cpp 复制代码
#include <stdio.h>    
#include <unistd.h>    
#include <stdbool.h>    
    
int main()    
{    
    while(true)    
    {    
        printf("hello world!\n");    
    }    
    
    return 0;    
}                                                                                                           

这是一个死循环的程序,其会一直输出hello world字符串,现在我们让其运行起来,然后使用执行ps ajx来查看这个进程的状态。

先执行程序:./tets.exe

由于是一个死循环,而且没有sleep,会一直高速刷屏,我们再开一个窗口输入ps ajx | head -1 && ps ajx | grep test.exe | grep -v grep

我先简单说明该指令,ps ajx用于列出所有进程相关数据,其第一行用于表示各个数据的含义,因此我们先用ps ajx | head -1保留第一行数据。

由于系统中进程太多了,直接ps ajx会大量刷屏,我们只查找进程test.exe,所以用grep test.exe筛选。

由于grep本身也是一个进程,而且输入了选项test.exe,如果筛选test.exe,会把grep自己也筛选出来,所以我们要把grep自己删掉:grep -v grep

输出结果:

STAT栏表示进程状态,我们这个test.exe明明一直在执行,最后的状态不是R,而是一个S+,这是为什么?

因为我们的进程中,有一个printf语句,其需要向屏幕输出,而CPU计算的速度远大于向屏幕输出的速度,因此这个进程99%的时间都在显示屏的等待队列中,只有很小一段时间在CPU的运行队列中,所以我们很难看到R状态。

现在我们删掉printf,再看看:

cpp 复制代码
#include <stdio.h>    
#include <unistd.h>    
#include <stdbool.h>    
    
int main()    
{    
    while(true)    
    {}    
    
    return 0;    
}                                                                                                           

执行结果:

此时光标一直卡顿,说明这个进程开始执行,陷入死循环了。

再用ps ajx | head -1 && ps ajx | grep test.exe | grep -v grep查看:

此时进程test.exe就进入R+的运行状态了!现在我们成功证明,并且观察到了R状态的存在,那么R++是什么意思呢?

进程状态后面的+代表这个进程是一个前台运行的进程,我们这个进程一旦运行,我们就不可以在这个窗口再输入命令了,这就算一个前台进程。

如果想让一个进程在后台运行,只需要在指令的最后面加上一个&符号即可,./test.exe &

可以看到,现在这个指令一执行完,给我们返回了一个PID,我们就可以再次使用命令行了,现在这个test.exe的进程就在后台运行了。

再用ps ajx | head -1 && ps ajx | grep test.exe | grep -v grep查看:

现在test.exe的状态就是R状态,而不是R+了,说明这是一个后台运行的进程。

那么我们要如何关闭这个进程呢?

  • 对于前台进程,可以通过ctrl + c或者输入指令kill -9 PID来关闭
  • 对于后台进程,只能通过kill -9 PID来关闭

对于刚刚的后台进程,我们就要输入kill -9 15738来关闭进程。


阻塞状态

S状态

S状态,即sleep休眠状态,属于阻塞状态,其一般处于等待资源的状态,比如等待scanf输入,printf输出等。

比如我们刚刚的死循环程序:

cpp 复制代码
#include <stdio.h>    
#include <unistd.h>    
#include <stdbool.h>    
    
int main()    
{    
    while(true)    
    {    
        printf("hello world!\n");    
    }    
    
    return 0;    
}                                                                                                           

此时就可以观察到S状态了:

此处的printf一直在等待显示器的输出,也就是等待显示器资源,所以会处于S状态。

这个S状态,也可以叫做可中断睡眠或者浅度睡眠状态,我们可以在其等待资源的时候,通过ctrl + c直接中断程序。


D状态

D状态,disk sleep,属于阻塞状态,也叫做不可中断睡眠或者深度睡眠状态。

OS过于繁忙的时候,内存中可能会有大量进程,此时OS就会选择直接杀掉某些进程。比如你使用低配置的手机打高能耗的游戏的时候,程序就有可能会直接闪退,这个过程就是OS直接杀掉程序的过程。因为该进程已经让操作系统过于繁忙了,操作系统会直接将其杀掉,防止崩溃。

D状态,是一个免死金牌,如果某些进程正在处理很重要的内容,我们不希望OS主动把它杀掉,此时给这个进程一个D状态,操作系统就不会主动杀掉该进程。

由于出现D状态时,操作系统已经处于很危险的状态了,所以其不好观察,这里就不观察了。


T状态

T状态,属于阻塞状态,即进程处于暂停状态。

想要出现T状态,我们可以通过kill给进程发送信号,其中19号状态,就是SIGSTOP暂停的信号。

现在我们再次运行刚刚的./test.exe死循环:

现在其处于R+状态,也就是前台运行的状态,然后我们给其发信号kill -19 18280

现在test.exe显示了一个stopped,即被暂停了,我们再通过ps ajx观察:

此时进程就处于T状态了,而且会被从前台转到后台运行。

如果想要从T状态恢复,可以使用kill -18信号,其代表SIGCONT,即继续进程。

不过就算从T状态恢复了,其也依然是一个后台进程。


t状态

t状态也是一个暂停状态,但是其是一种被追踪的暂停状态,属于阻塞状态。

什么叫做被追踪呢?其实就是只有收到某个命令后,进程才会继续执行。

比如说使用调试程序的时候,当进程到某个断点处停止了,此时进程就属于t状态。

现在我们将代码编译为debug版本,命名为test-debug.exe,然后用gdb调试,gdb test-debug.exe

随便打一个断点后,让程序运行到断点时停止,然后使用指令ps ajx | head -1 && ps ajx | grep test-debug.exe | grep -v grep

输出结果:

此时可以看到,出现了两个进程,一个是gdb调试器进程,另外一个就是test-debug.exe,该进程的状态就是t状态了。


挂起状态

挂起状态就是当内存资源不足时,OS将数据和代码交换到磁盘中挂起,此时内存中只有PCB

当内存吃紧了,此时如果有进程处于阻塞状态,OS检测到该进程暂时不会得到数据,于是把该进程的代码和数据放到磁盘的swap分区中,这个过程叫做换出,于是就有内存空出来放其它的内容了。

这个过程中,PCB一直保留在内存中,因为PCB要去排队,比如这个进程在等待网卡资源,那么PCB就会一直处于网卡的等待队列,直到获得资源,再把磁盘中的代码和数据拷贝回来,这个过程叫做换入,然后再运行程序。

当代码和数据处于磁盘中,而PCB还在队列中等待资源,这就算一个挂起状态。因此挂起状态是一个特殊的阻塞状态。

举个例子,我们在下载软件时,下载这个进程就会被加载到内存中,此时如果突然断网了,就会阻塞(缺少网卡资源),由于这里进程暂时不会被运行,资源放在内存中就会浪费空间,OS就会将代码和数据(进程)暂时存放到磁盘中,等到有网了再从磁盘中拿取,这个过程就是挂起状态


僵尸进程 & 孤儿进程

X状态

X状态表示死亡状态,此时进程已经死亡了,但是该状态只保留非常非常短暂的时间,因为很快该进程的PCB就被释放了。所以该进程很难观察到,我们只需要知道确实有该状态存在即可。


Z状态

当一个进程退出时,其代码和数据会被立马释放,但是其PCB会被保留,因为我们需要通过这个进程的PCB来判断这个进程的执行情况。

就好像法医需要对死者验尸,来确定该死者的伤亡情况。进程在执行结束后,也要进行一个"验尸"的操作。

当一个进程执行完毕,PCB还被保留,等待别人读取的时候,该进程就处于Z僵尸状态(zombie)。所有进程结束后,都必须经过Z状态。

这个读取PCB的任务,是父进程执行的。

我们看到这样一个程序:

cpp 复制代码
#include <stdio.h>                                                                                          
#include <unistd.h>
#include <stdbool.h>
#include <sys/types.h>
#include <stdlib.h>

int main()    
{    
    pid_t id = fork();

    if(id == 0)//子进程
    {
        int cnt = 3;
        while(cnt--)
        {
            printf("I am child, pid = %d, ppid = %d\n", getpid(), getppid());
            sleep(1);
        }
        exit(0);
    }

    while(true)
    {
        printf("I am father, pid = %d, ppid = %d\n", getpid(), getppid());
        sleep(1);                                                                                           
    }                                                                  
                                                                       
    return 0;                                                          
}

该进程通过fork创建了一个子进程,子进程三秒后exit退出,而父进程一直运行。此时子进程死亡,但是父进程依然运行还没有读取子进程的PCB,子进程就处于Z状态了。

执行程序:

三秒已过,此时查看子进程状态,ps ajx | head -1 && ps ajx | grep 20152 | grep -v grep

子进程就是Z+状态了,也就是僵尸进程。子进程此时在等待父进程结束后,回收其PCB,此时进程才算真正退出。


孤儿进程

如果一对父子进程,父进程先退出了,子进程后退出,那么子进程的PCB就无法被父进程回收,因为父进程已经先退出了。

对于这种父进程先退出的进程,叫做孤儿进程

而只要僵尸进程的PCB不被回收,其就会一直留在操作系统中等待回收。那么就到造成内存泄漏的问题,Linux是如何解决这个问题的?

我们看到以下代码:

cpp 复制代码
#include <stdio.h>                                                                                          
#include <unistd.h>
#include <stdbool.h>
#include <sys/types.h>
#include <stdlib.h>

int main()      
{               
    pid_t id = fork();      
                            
    if(id == 0)//子进程      
    {                        
        while(true)    
        {    
            printf("I am child, pid = %d, ppid = %d\n", getpid(), getppid());    
            sleep(1);    
        }    
    }    
    
    int cnt = 10;    
    while(cnt--)                                                                                            
    {               
        printf("I am father, pid = %d, ppid = %d\n", getpid(), getppid());    
        sleep(1);                                                             
    }                
         
    return 0;    
}       

该程序中,父进程执行十秒后退出,而子进程一直死循环,那么此时子进程就失去了父进程,这该怎么办?

输出:

此时子进程的PID = 21350,我们前十秒观察该PID的子进程:

此时PPID = 21349,也就是进程test.exe

十秒后再观察子进程:

此时PPID = 1,Linux中1号进程代表OS操作系统。

也就是说:孤儿进程会被1号进程操作系统认领,随后PCB由操作系统亲自回收。


相关推荐
fnd_LN几秒前
Linux文件目录 --- mkdir命令,创建目录,多级目录,设置目录权限
linux·运维·服务器
会飞的土拨鼠呀10 分钟前
Flannel是什么,如何安装Flannel
运维·云原生·kubernetes
木与子不厌12 分钟前
微服务自定义过滤器
运维·数据库·微服务
析木不会编程17 分钟前
【C语言】动态内存管理:详解malloc和free函数
c语言·开发语言
达帮主18 分钟前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
行思理30 分钟前
Linux 下SVN新手操作手册
linux·运维·svn
茶猫_39 分钟前
力扣面试题 39 - 三步问题 C语言解法
c语言·数据结构·算法·leetcode·职场和发展
初学者丶一起加油42 分钟前
C语言基础:指针(数组指针与指针数组)
linux·c语言·开发语言·数据结构·c++·算法·visual studio
沛沛老爹1 小时前
CI/CD是什么?
运维·git·ci/cd
一只搬砖的猹1 小时前
cJson系列——常用cJson库函数
linux·前端·javascript·python·物联网·mysql·json