当前位置: 首页 > article >正文

Linux:进程状态

目录

1. 进程状态

 1.1 并行和并发

1.2 时间片

1.3 运行状态

1.4 阻塞(等待)状态

1.5 挂起状态

2. Linux的进程状态

2.1 运行状态

2.2 sleep状态

2.3 Stop状态

2.4 X和Z状态

2.5 孤儿进程

 


1. 进程状态

如果你看任何一本关于操作系统的教材,都会有类似下面进程状态转换图。但具体的操作系统有Linux,windows等,所以操作系统教材需要上升到一个更高的维度,抽象出所有操作系统的共性并阐述它。因此,我们难以理解下面的进程状态转换图,许多概念很陌生。

本文会从Linux操作系统入手,讲解其进程的状态。

067a8aa59472461193fddf11dbcb273d.png

 

 1.1 并行和并发

并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。

并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发。

单个CPU执行进程代码,不是把进程代码执行完毕,才开始执行下一个进程代码。而是给每一个进程分配一个时间片,基于时间片,进行调度轮转

  • 当您使用C/C++编写一个死循环代码并运行时,您可能会观察到程序并不会导致整个系统卡顿。这是因为现代操作系统的调度机制发挥了作用。具体来说,CPU在执行该死循环程序时,并不是无休止地持续运行,而是在用完了分配给该程序的时间片之后,操作系统会包括保存当前程序的状态(代码和数据),然后调度另一个进程来执行。

 

1.2 时间片

时间片(Time Slice)是一种在操作系统中使用的调度技术。在多任务操作系统中,为了能够公平且高效地共享处理器(CPU)时间,操作系统将一个进程的运行时间分割成许多小的时间段,每一个时间段就是一个时间片。

  • 那如果我在听音乐和看视频,为什么我们不会感到卡顿?因为每个时间片通常极为短暂,仅存在于毫秒级别,而CPU的处理速度则是惊人的,每秒能够执行几亿甚至几十亿次基本操作。这样的高速处理能力,使得CPU在各个任务之间迅速切换,而用户几乎无法察觉到这种频繁的交接。

 

上面基于时间片轮转(time-slicing)技术的操作系统,叫做分时操作系统,主要应用于民用级别计算机。因为这类操作系统的进程调度优先级没有明显的区分,所以要求能调度多个进程,并追求调度任务公平

与之相对的就是实时操作系统,它更注重任务的及时性和可靠性。主要应用于那些对时间敏感、对任务完成时间有严格要求的领域,如工业控制、航空航天、医疗设备、汽车电子等。

 

1.3 运行状态

ca32f4f7806941d28370408cb5f735b6.png

进程 = 内核数据结构(task_struct) + 程序的代码和数据

task_struct结构体对象不仅记录了进程的各种属性,还有某种数据结构来连接多个进程,完成管理所有进程的工作。

6ec03bdfcf9e49c08d8f014b0840a253.png

当一个程序启动变成进程,先会创建task_struct结构体对象,再将该程序的代码和数据从磁盘加载到内存当中。该进程的pid为1.

此时,操作系统会有一个运行队列结构体,叫做runqueue。它里面有许多属性,其中有个变量类型是task_struct结构体类指针。该指针会连接内存中的task_struct对象。

如果还有更多程序启动,假设里面存在结构体指针,那么新进程结构体对象会被已存在进程对象中结构体指针连接。这样就被runqueue结构体对象管理起来。

当CPU要调度pid为1的进程时,我们假设使用最简单的FIFO(先进先出)调度算法,那么CPU会到runqueue队列中找到第一个task_struct对象,获取该进程main函数地址和数据,放到CPU的寄存器中,并执行该进程。

85180c12d6904213ab0764f15ada028c.png

当CPU调度时间到达该pid为1进程的时间片,该进程会自动记录执行到那行代码和数据,操作系统会将该结构体对象从runqueue队列中剥离下来,连接到最后一个task_struct结构体对象的后面。之后,CPU获取队头的进程代码和数据,执行该程序。

如此循环往复,就完成了基于时间片的轮转调度。

我们也可以得出一个结论,在Linux操作系统中,只要进程在运行队列中,该进程的状态就叫做运行状态。此时表明进程已经准备好了,可以随时被CPU调度。所以不是进程正在被执行,进程状态才是运行状态。一般来说,就绪状态和执行状态可以合二为一,叫做运行状态。

 

1.4 阻塞(等待)状态

除了CPU内部设备,计算机还有许多外部设备,如磁盘、显示器、键盘和网卡等。

  • 操作系统作为管理计算机软硬件资源的核心软件,在处理各类对象时,始终遵循“先描述,后组织”的基本原则。为此,操作系统定义了一个名为“device”的结构体,该结构体内包含了设备的类型、状态等关键属性。
  • 同时,为了有效地组织和管理这些设备,结构体还会包含某种数据结构。我们这里结社结构体中还包括了一个指向同类结构体的指针,通过链表这种数据结构,将所有设备对应的结构体对象串联起来,形成一个有序的设备管理链。

c482cb689da647668c63ce7d98a6615a.png
如图所示,当CPU调度到标识为PID 1的进程时,若该进程执行到scanf函数并等待用户从键盘输入数据,此时进程将处于等待键盘输入的状态。由于用户输入数据所需的时间通常会远远超过该进程分配的时间片,CPU不可能因此暂停其他进程的调度。因此,该进程会变为阻塞状态,那怎么理解阻塞状态呢?

 

2244475f8aea4714b239a70eaba94b11.png

上面我们解释了操作系统管理硬件时,也会为硬件创建一个结构体。该结构体中会包含一个task_struct结构体指针,叫做waitqueue,即等待队列。操作系统会把等待键盘输入的进程结构体对象从runqueue剥离下来,键盘结构体对象的waitqueue指针会连入该task_struct对象。

如果有其他进程需要等待某个硬件设备的数据,该硬件设备结构体中会存在一个等待队列,连接进程的结构体对象。

因此,我们可以得出以下结论:当进程处于运行状态或阻塞状态时,其对应的PCB将被置于特定的队列中。本质上,进程始终在访问某种硬件资源,当它正在使用CPU资源时,我们称其为运行状态;而当它等待其他外部设备资源时,则处于阻塞状态。

 

1.5 挂起状态

c2e1889c54b640dd921bcb3062fbc585.png

当内存资源严重不足时,操作系统会将在处于阻塞状态的进程直接放到磁盘中的一个区域,这个区域叫做swap分期,此时进程的状态叫做阻塞挂起状态。

但是由于处于运行状态的进程太多而导致内存资源不足,而运行队列尾部的进程一段时间都不会被调度,操作系统就会将这些进程放到磁盘中的swap分区,此时进程状态叫做运行挂起状态。这是极端的操作,风险较大,一般不会出现。

 

 

2. Linux的进程状态

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

上面是Linux源代码,表示进程的状态。分别有“R”“S”“D”“T”等状态。将会一个个展示。

 

 

2.1 运行状态

#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {}

    return 0;
}

5c5afa2abae5486088a3155706ee6539.png

e994a125ad1a4c0587b8ba3c116f3282.png

我们写个死循环,正常来说应该是运行状态。使用ps指令查询myproc进程的状态,会发现进程状态就是R+,其中“+”符号表示在前台运行。

 

 

2.2 sleep状态

#include <stdio.h>

int main()
{
    int cnt = 0;
    while(1)
    {
        scanf("%d", &cnt);
    }

    return 0;
}

4826ec861b444e55b43f07272f0ab43f.png

fe071652e5a04e34aa51a6fdab07c533.png

当我们写一个scanf函数,进程会等待键盘数据,此时的进程状态是阻塞等待状态。使用ps指令查询myproc进程,会发现myproc程序的状态是S。对应最开始给出的Linux进程状态的解释是sleeping,即休眠。所以,Linux中的阻塞状态叫做休眠。

 

 

18fa678ff6194e72873a20745fdb7a9a.png

5809c5ae2c654ebfa0787da374e2b31f.png

当我们想终止休眠状态下的进程,我们可以使用kill指令,发送信号终止进程。这种休眠状态叫做可中断睡眠,也叫浅睡眠。

与之相对的就是不可中断睡眠,即深度睡眠,就是disk sleep状态。disk sleep意思是磁盘休眠状态。当系统处在磁盘级拷贝,会进行大量的读写操作,此时无法使用kill指令终端进程。

 

 

2.3 Stop状态

#include <stdio.h>
#include <unistd.h>

int main()
{
    int cnt = 0;
    while(1)
    {
        printf("hello world, cnt: %d\n", cnt++);
        sleep(1);
    }

    return 0;
}

5209567f0ef14dae96a2ddb03927fb3c.png

79b88837d1a644f588f8b866d8230854.png

 我们写一个死循环打印变量的程序,运行起来,会发现处于S状态。这是因为printf本质是向内存缓冲区写入数据,再输出到显示器上,而输入输出的时间相比于毫秒级的时间片很慢,所以百分制九十九以上的时间都在显示器的等待队列中。

 

2ba264934f0c42c5a9a14e9bf2a4249c.png

ac7814f142b549efb4236db47fec1d98.png

21e8369cc403448d8cf59c0f3c76a795.png

我们可以使用kill指令,其中19号选项就是发送暂停信号。查看进程状态就是T,运行的进程也会出现终端,有Stopped提示。

 

4083ccc3a2a14521a89dcf3754bb8870.png

e18ae1ba65314171bc293542458a19ac.png

如果想继续运行进程,可以使用kill指令的18选项,使暂停的进程继续启动。

 

8f341e0c365242d183fa6ee337f9f95d.png

48082b70adf1445baf06ef4044548671.png

但是你会发现该进程的状态变量了S,没有“+”符号,说明进程在后台运行。此时我们无法使用Ctrl+C终止程序,但可以使用kill指令9号选项终止程序。而一般出现T状态,说明进程做了非法饭不知名的操作。

后台进程是指在操作系统中,在后台独立运行的程序,它们不直接与用户交互。在Linux系统中,后台进程通常是那些不需要即时用户输入且执行时间较长的任务。这些进程在执行时,用户可以继续使用终端进行其他操作。相对应地,在Windows操作系统中,当用户将一个耗时的任务,如下载大文件,最小化其窗口后,该任务便在后台运行。此时,用户可以切换到其他应用程序,进行不同的工作,而下载任务则在后台安静地继续进行

 

2.4 X和Z状态

X状态指的是dead,就是死亡状态,顾名思义,指的是进程被终止了。

5cd75a9180b7401092e7480c4efe62cd.png

创建一个进程,是为了完成一个任务。父进程是要知道他的子进程完成任务的情况,子进程在退出时会记录一个退出信息。在Linux中,查看一个进程退出信息,可以使用echo $?指令获取。

执行ls指令,相当于执行一个程序,查看退出信息为0,说明进程执行任务成功。当使用ls查看不存在的文件,会报查不到该文件的错误,此时再查看退出信息就是非0的。我们所写的main函数一般都会返回0,就是告诉系统该任务执行成功。

 

 

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("父进程运行,pid: %d,ppid: %d\n",  getpid(), getppid());
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        int cnt = 10;
        while(cnt)
        {
            printf("我是子进程,我的pid: %d,ppid: %d, cnt: %d\n",getpid(), getppid(), cnt);
            sleep(2);
            cnt--;
        }
        
    }
    else 
    {
        //父进程
        while(1)
        {
            printf("我是父进程,我的pid: %d,ppid: %d\n", getpid(), getppid());
        }
        sleep(1);
    }


    return 0;
}

我们写一个代码,使用fork函数创建子进程,子进程执行几十秒就结束,父进程不退出。

 

 

c026a3b225fc4aceaac6787712f5e17f.png

cf676e714dfa4f429bfbd73284f3dc50.png 我们执行该程序,并使用一个循环指令查看相关进程信息。发现父子进程都处于S状态。

 

fcf085cba6c34aa0a0f95a61e37e4d68.png

f1c0e8268d8449ae89f702ab8852e78a.png

当子进程执行完毕退出时,他的状态变量Z状态。其中Z状态指的是zombie,即僵尸状态。当一个程序退出,代码和数据都在内存都销毁了,还会剩下task_struct对象,等着父进程回收查看。

退出的子进程还剩下结构体对象,如果没有父进程接受管理,它会一直是僵尸状态,不断消耗内存,会造成内存泄漏!

 

 

2.5 孤儿进程

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("父进程运行,pid: %d,ppid: %d\n",  getpid(), getppid());
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        while(1)
        {
            printf("我是子进程,我的pid: %d,ppid: %d\n",getpid(), getppid());
            sleep(1);
        }
        
        
    }
    else 
    {
        //父进程
        int cnt = 10;
        while(cnt > 0)
        {
            printf("我是父进程,我的pid: %d,ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        sleep(1);
    }

    return 0;
}

我们写一份代码,父进程退出,子进程死循环不退出。

 

 

196dfb65cc27428e951e9c49ec2892a6.png

402b7766a4e049eb956e868c2910db8c.png

06d46cdeab524df3be921a0428d657e3.png

父进程没退出前,父子进程都处于S状态。当父进程退出,子进程还在运行,状态没变,但是PPID为1,说明他的父进程变为pid为1的进程。

 

6e576276f87f42eb9f18d297d61b2f6a.png

通过top指令查询进程信息,可以看到pid为1的进程,指令叫做systemd,即系统。说明子进程被系统进程领养,我们称这种进程为孤儿进程。

 

 


创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!

db7aef8aedd94befa53ef752e3298159.png

 

 


http://www.kler.cn/news/359542.html

相关文章:

  • Unity 同项目多开
  • 数智合同 | 业财一体与履约联动的数字化转型
  • RabbitMQ系列学习笔记(七)--RabbitMQ交换机
  • 【数据结构与算法】插入排序、希尔排序
  • 2024年软件设计师中级(软考中级)详细笔记【6】(下午题)试题6 Java 23种设计模式解题技巧(分值15)
  • 前端自动化测试框架Jest
  • 数据结构与算法--返回袋子数
  • Spring-Bean的实例化和依赖注入方式
  • Spring Boot 2.7=>3.0 升级整理
  • 【进阶OpenCV】 (12)--人脸检测识别
  • 基于SpringBoot+Vue的旅游服务平台【提供源码+答辩PPT+参考文档+项目部署】
  • 智能优化算法-生物地理学算法(BBO)(附源码)
  • 学习ROS系列 python语言
  • 起吊机革新:协议转换器解锁安全与效率
  • 提示词高级阶段学习day2.1-在提示词编写中对{}的使用教程
  • Python机器学习中的主成分分析(PCA)全面解析与应用
  • 深度学习 自动求梯度
  • kubernetes(k8s)面试之2024
  • Spring Boot:中小型医院网站开发新趋势
  • react18中如何实现同步的setState来实现所见即所得的效果