【Linux系统编程】进程概念(三)进程状态

【Linux系统编程】进程概念(三)进程状态

  • [1. 操作系统教材中的进程状态](#1. 操作系统教材中的进程状态)
    • [1.1 运行状态](#1.1 运行状态)
    • [1.2 阻塞状态](#1.2 阻塞状态)
    • [1.3 挂起状态](#1.3 挂起状态)
  • [2. Linux操作系统的进程状态](#2. Linux操作系统的进程状态)
    • [2.1 R运行状态](#2.1 R运行状态)
    • [2.2 S睡眠状态](#2.2 S睡眠状态)
    • [2.3 磁盘休眠状态](#2.3 磁盘休眠状态)
    • [2.4 T停止状态](#2.4 T停止状态)
    • [2.5 t追踪停止状态](#2.5 t追踪停止状态)
    • [2.6 进程状态查看](#2.6 进程状态查看)
    • [2.7 前台和后台进程](#2.7 前台和后台进程)
    • [2.8 X死亡状态](#2.8 X死亡状态)
  • [3 僵尸进程(Z僵尸状态)](#3 僵尸进程(Z僵尸状态))
    • [3.1 僵尸进程的危害](#3.1 僵尸进程的危害)
  • [4. 孤儿进程](#4. 孤儿进程)

1. 操作系统教材中的进程状态

别看图中写了很多状态,其实主要就是三种状态:运行状态、阻塞状态、挂起状态。

下面具体说说这三种状态。

1.1 运行状态

  • 一个CPU对应一个运行队列,在该运行队列中的进程,遵循FIFO(先进先出)的原则,排在前面的进程先被运行。
  • 凡是在这个队列中的进程,状态都是运行状态。

1.2 阻塞状态

当我们运行一个有scanf的C程序的,程序会卡在scanf那里,它需要键盘上面的数据才能运行,否则就会一直卡在这里,键盘是硬件,而操作系统是硬件的管理者,操作系统肯定不能一直等这个进程,它还有别的进程需要调度,所以操作系统就会把该进程先托管到该硬件上,等该硬件就绪,这里就是键盘数据输入完成,再把该进程重新加入到运行队列中。

c 复制代码
int main()
{
	int a = 0;
	scanf("%d", &a);
	printf("a = %d\n", a);
	return 0;
}
  • 阻塞与运行的本质:看你的task_struct在谁提供的队列中。

1.3 挂起状态

如果操作系统(OS)发现自己的内存空间不足了,那OS就会把一些进程的代码和数据交换到swap分区(磁盘),即swap out(换出),此时该进程处于阻塞挂起状态,如果这样做还是不行,LinuxOS就会选择性的杀掉特定的进程,等到OS发先自己的内存足够了就会把swap分区中该进程的代码和数据拿回来,即swap in(换入)。

  • 磁盘中的swap分区存在的本质是用时间换空间。
  • swap分区的大小一般是内存的1.5~2倍,不建议swap分区太大,这样会导致OS会过度依赖swap分区,只要有进程就把它的代码和数据交换到swap分区,而过度的swap out/in,会导致系统变慢。

2. Linux操作系统的进程状态

c 复制代码
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,僵死
 };

2.1 R运行状态

R运行状态:并不意味着进程一定在运行中,它表明进程要么在CPU运行中,要么在CUP对应的运行队列里。

我们以下面的代码为例,讲解一下R运行状态。

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

int main()
{
	while(1)
	{}
	return 0;
}

运行结果:

我们看到该进程的STAT是R+,R就是运行状态,+表示该进程在前台,什么也没有表示该进程在后台,后面会讲前台和后台。

结合我们最开始讲的运行状态,思考一下为什么该进程处于运行状态?

因为该进程一直在死循环重复执行代码,占用CPU资源,并且它不满足访问外设,例如显示屏、键盘等,进入阻塞状态。

2.2 S睡眠状态

S睡眠状态(sleeping):意味着进程在等待事件完成,同时也叫做可中断睡眠,浅度睡眠,即睡眠期间可以被唤醒,对应操作系统学科中的阻塞状态。

我们以下面的代码为例,讲解一下S睡眠状态。

c 复制代码
#include <stdio.h>    
    
int main()    
{    
    while(1)    
    {    
        printf("I am process\n");                                                                 
    }                                   
    return 0;                           
}   

运行结果:

我们看到该进程的STAT是S+,此时该进程就处于睡眠状态。

那么我们思考一下为什么该进程会处于睡眠状态?

由于我们一直用printf打印,要访问外设显示器,在写入显示器完成的一瞬间将其加入到运行队列,等待运行,此时该程序处于运行状态,只不过时间太短,我们很难查看到,之后再托管到显示器中进行写入,所以我们查看的时候一般都会显示该进程处于休眠状态。

但是如果是下面的程序,只要我们不在键盘中输入数据,该进程就一直处于睡眠状态。

c 复制代码
int main()
{
	int a = 0;
	scanf("%d", &a);
	printf("a = %d\n", a);
	return 0;
}


2.3 磁盘休眠状态

D磁盘休眠状态(disk sleep):也叫做深度睡眠,不可中断睡眠状态,Linux特有的状态,处于这个状态的进程通常会等待IO的结束,不响应任何请求,同时其也对应操作系统学科上的阻塞状态的一种特殊情况,处于深度睡眠的进程不可被信号,或者OS杀掉。

想象这样一个场景:一个进程正准备向磁盘写入一批重要数据。

  1. 危机潜伏:磁盘的剩余空间可能不足以容纳这批数据,但进程并不知情,它发起了写入请求后,便进入等待状态,期盼着磁盘的"操作成功"回复。
  2. 系统发难 :恰在此时,操作系统发现内存资源告急 ,整个系统濒临卡顿。为了自救,它开始清理"占用资源却不干活"的进程。它一眼就看到了这个正在"悠闲"等待磁盘回复的进程,心想:"内存都快耗尽了,你居然还在空等?" 于是,操作系统手起刀落,强制终止了这个进程,并回收了其占用的所有内存,其中自然也包括那批等待写入的数据。
  3. 磁盘的抉择 :几乎在同一时间,磁盘正在处理写入请求。它发现空间确实不够,无法完成这个任务。磁盘心想:"这批数据太大了,我写不进去。但我剩余的空间还可以服务其他进程的小数据请求,不能让它堵在这里。" 于是,磁盘丢弃了 这批无法写入的数据,转而处理其他任务。当它回过头来,准备给刚才的进程回复一个"写入失败"时,却发现------那个进程已经消失了

结果:数据彻底丢失了。进程的数据在内存中被系统释放,在磁盘端又被丢弃。用户的重要资料就此不翼而飞。

责任在谁?

  • 操作系统:为了保障系统整体稳定而清理资源,看似无可厚非。
  • 磁盘:为了不阻塞其他任务而丢弃无法处理的数据,似乎也情有可原。
  • 进程:只是一个等待结果的"受害者"。

这成了一笔糊涂账。但用户绝不接受这种结果,频繁的数据丢失将是灾难性的。

解决方案:引入"深度睡眠"状态

为了解决这一致命问题,深度睡眠状态 应运而生。当进程在执行诸如关键数据写入这类不容中断的任务时,它会被标记为此状态。

一旦进入深度睡眠,进程便获得了一把"尚方宝剑":

  • 对操作系统的清理指令置若罔闻 ,即使是强大的 kill -9 信号也无法将其终止。
  • 它仿佛进入了一个受保护的"结界",其代码和数据会一直被保留在内存中,纹丝不动

进程会一直维持这种"不死"的沉睡,直到它等待的事件发生------也就是磁盘确实给出了最终回复(无论成功与否)------它才会被唤醒并做出响应。

这样一来,在上述场景中,即使内存紧张,操作系统也无法杀死这个进程。磁盘在丢弃数据后发出的"失败"回应,最终也能顺利送达这个仍在等待的进程。进程收到回应后,便可以由内核将其正常唤醒和终止,或者尝试其他错误处理策略,从而从根本上避免了数据的无声丢失

2.4 T停止状态

T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。

c 复制代码
#include <stdio.h>      
#include <sys/types.h>      
#include <unistd.h>      
      
int main()      
{      
    while(1)      
    {      
        printf("I am process, pid:%d,ppid: %d\n", getpid(), getppid());      
        sleep(1);                                                                       
    }                                                             
    return 0;                                                     
}       


2.5 t追踪停止状态

t追踪停止状态(tracing stop):在调试过程中的进程停止。


2.6 进程状态查看

ps aux / ps axj 命令

  • a: 显示一个终端所有的进程,包括其他用户的进程。
  • x: 显示没有控制终端的进程,例如后台运行的守护进程。
  • j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
  • u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。

2.7 前台和后台进程

区分前后台进程:

谁能从键盘获取数据输入,谁就是前台。

键盘只有一个,所以在获取输入的时候,只能有一个进程在获取键盘数据。

前台进程任何时刻只能有一个,后台进程可以有很多个。

例如你自己的手机打开很多app,谁在你的屏幕上谁就是前台,其他的都在后台。

2.8 X死亡状态

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

3 僵尸进程(Z僵尸状态)

僵尸进程(zombies):子进程退出的时候,如果父进程没有主动回收子进程的信息,那么子进程会让自己一直处于Z僵尸状态,即对应子进程相关资源尤其是task_struct结构体不能释放。

我们以下面的代码演示一下僵尸进程。

c 复制代码
#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    
    
int main()    
{    
    printf("我是父进程:%d\n", getpid());    
    sleep(3);    
    
    pid_t id = fork();    
    
    if(id == 0)    
    {    
        //child    
        while(1)    
        {    
            printf("我是子进程:%d,我的父进程是:%d\n", getpid(), getppid());    
            sleep(3);    
        }    
    }    
    else    
    {    
        while(1)    
        {    
            printf("我是父进程:%d\n", getpid());    
            sleep(3);    
        }    
    } 
	return 0;
}

输出结果:

每3秒打印一次状态。

杀死子进程,此时父进程还在运行,无法收回子进程中的信息,导致子进程处于Z僵尸状态。

3.1 僵尸进程的危害

1. 进程退出了,退出信息是什么?

main函数的返回值or收到的信号值,该信号值在该进程的task_struct结构体中。

2. 进程退出了,退出信息保存在哪里?

进程自己的task_struct结构体中。

w
3. 检测Z状态进程,回收Z状态进程,本质是在做什么?

获取子进程的退出数据。

4. 具体怎么回收?谁来回收?

父进程系统调用(OS)wait命令来回收子进程的信息。

5. 子进程必须回收!!!

假如有一个常驻进程一直在malloc空间,如果我一直不回收该进程,它就会一直处于Z状态,它就会占据大量内存空间,task_struck也不会被释放,从而导致内存泄漏。

如果对应的进程结束了,内存泄漏还在吗?

进程结束,系统会自动回收该进程,不会存在内存泄漏。但在实践中,进程一般都是死循环,即不会结束的,所以内存泄漏问题很重要。

4. 孤儿进程

孤儿进程:在父子进程中,父进程提前退出,子进程的父进程会被改为1号进程(操作系统),我们称该子进程被操作系统领养了,此时这个子进程(孤儿进程)就变成了后台进程,无法从键盘中获取数据,那么Ctrl + C就无法退出,只能用信号kill -9 PID将子进程杀死。

我们以下面的代码为例,讲解孤儿进程

c 复制代码
#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    
    
int main()    
{    
    
    // 创建子进程        
    pid_t id = fork();        
        
    if(id == 0)        
    {        
        //child        
        while(1)    
        {    
            printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());    
            sleep(3);    
        }                                                                                                           
    }    
    else    
    {    
        //parent        
        int cnt = 5;    
        while(cnt)    
        {    
            cnt--;    
            printf("我是父进程, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);    
            sleep(3);    
        }    
    
    }    
    return 0;
}

父进程退出,子进程被1号进程领养,并变成后台进程。

后台进程无法获取键盘中的数据,所以Ctrl + C不能结束该进程。

使用信号kill -9 PID 杀死该进程。

为什么要被系统领养?

给孤儿进程退出进行善后,系统自动回收。

相关推荐
国际云,接待4 小时前
出海东南亚无忧:腾讯云如何凭借本地合作与全球节点,保障游戏和电商业务合规流畅?
大数据·服务器·网络·云计算·腾讯云
ejinxian4 小时前
Linux 虚拟化技术 KVM/ESXI/Docker
linux·运维·docker·qemu·openvz
z202305084 小时前
linux之arm SMMUv3 故障和错误(4)
linux·运维·arm开发
攒钱植发4 小时前
嵌入式Linux——解密 ARM 性能优化:LDR 未命中时,为何 STR 还能“插队”?
linux·arm开发·c++·性能优化
是孑然呀4 小时前
【钉钉多元表格(自动化)】钉钉群根据表格 自动推送当天值日生信息
运维·自动化·钉钉
天外飞雨4 小时前
各传感器消息解析
linux
一匹电信狗4 小时前
【C++】哈希表详解(开放定址法+哈希桶)
服务器·c++·leetcode·小程序·stl·哈希算法·散列表
逐风&者5 小时前
CentsOS 7 “Could not resolve host: mirrorlist.centos.org; 未知的错误”问题解决
linux·运维·centos
路由侠内网穿透.5 小时前
本地部署网站流量分析工具 Matomo 并实现外部访问
运维·服务器·远程工作