【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 杀死该进程。

为什么要被系统领养?

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

相关推荐
AI 智能服务3 分钟前
第6课__本地工具调用(文件操作)
服务器·人工智能·windows·php
码农小韩1 小时前
基于Linux的C++学习——指针
linux·开发语言·c++·学习·算法
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]seq_file
linux·笔记·学习
Jay Chou why did2 小时前
wsl安装完无法进入wsl
linux
石头5303 小时前
Rocky Linux 9.6 docker k8s v1.23.17 kubeadm 高可用部署文档
linux
松涛和鸣3 小时前
49、智能电源箱项目技术栈解析
服务器·c语言·开发语言·http·html·php
凉、介3 小时前
SylixOS 中的 Unix Socket
服务器·c语言·笔记·学习·嵌入式·sylixos
RisunJan3 小时前
Linux命令-ipcs命令(报告进程间通信(IPC)设施状态的实用工具)
linux·运维·服务器
春日见4 小时前
控制算法:PP(纯跟踪)算法
linux·人工智能·驱动开发·算法·机器学习