【Linux进程状态】

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

一、直接谈论Linux的进程状态

看看Linux内核源代码怎么说

  • 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)。
  • 下面的状态在kernel源代码里定义:
 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 */
};

操作系统要更改一个进程的状态,它的原理其实就是更改 task_struct{}内部的状态属性(status)。

1.1、R状态 -----> 进程运行的状态

vim Makefile
testStatus:testStatus.c
        gcc -o $@ $^ -g
.PHONY:clean
clean:
        rm -f testStatus

这儿有一个替换方式:

:%s/myprocess/testStatus/  
//将 myprocess替换成 testStatus的可执行程序
vim testStatus.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        while(1)
        {
                printf("i am a process,pid:%d\n",getpid());
        }
        return 0;
}

我们还可以用shell命令来一直刷新观察

while :; do ps ajx | head -1 && ps ajx | grep testStatus | grep -v grep; sleep 1; done

它其实一直处于一种休眠状态。

我们将打印的代码注释掉,再来看一下程序:

vim testStatus.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        while(1)
        {
                //printf("i am a process,pid:%d\n",getpid());
        }
        return 0;
}

此时的代码就是一个非常纯粹的循环代码,没有打印。

重新 make 编译一下:

那么为什么我们加上 printf 的代码,就看到了大量的 S(休眠状态)呢?

printf的本质是往显示器上打印;而程序的运行是在千里之外的云服务器上跑;根据冯诺依曼体系结构,显示器是一个外设,所以CPU在跑当前的程序时,把数据写入到我们当前的内存当中,打印数据的顺序:先写入到内存里,再刷新到外设里。可是我们无法保证每次打印的时候,显示器的状态都是就绪的,因为程序是CPU跑的,CPU的运算速度要比显示器本身的速度要快的多,所以进程在被调度的时候,要访问显示器的资源,因为资源要一直在显示器上打,所以大部分时间,相比较CPU来讲,大部分时间,我们对应的进程都在等待我们的设备资源是否就绪。就比如:代码执行到 printf 的时候,CPU是几纳秒,而数据刷新到显示器的时间是几毫秒,其余大部分时间都在等待中,而这等待的时间,就是(S)休眠状态。

1.2、S状态 -----> 休眠状态(进程在等待“资源”就绪)

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

int main()
{
      while(1)
      {
             sleep(10);
             //printf("i am a process,pid:%d\n",getpid());
      }
      return 0;
}

可中断睡眠:处于S睡眠状态,依旧可以随时被外部信息打断,比如:ctrl + c,就醒来了。

进程在运行时 + &(取地址符);那么进程就默认在后端运行。

此时 R 后面就没有 + 了,+ 代表进程是在前端运行的,还是在后端运行的。

1.3、T状态 -----> 暂停状态

T/t :让进程暂停,等待被进一步唤醒。

1.4、t状态 ------> 当前的进程因为被追踪而暂停了

1.5、D状态

  • D:Linux系统比较特有的一种进程状态。
  • 当A进程需要大量的数据写入磁盘的时候,等待磁盘写入数据,此时A进程的状态就是休眠状态,当磁盘写入数据完成或者写入失败的话,都要向A进程汇报情况。
  • 但是有一个场景:系统中内存严重不足时,操作系统有一个特权,Linux操作系统有权力杀掉进程来释放空间。那么此时你的A进程被操作系统杀掉了,用来释放空间,过来3、4秒之后,磁盘已经写入了1GB的数据了,还想要再次写入A进程的数据,但是失败了,失败之后,要汇报情况,可是A进程已经被操作系统杀掉了;此时B进程也要向磁盘写入数据,那么磁盘就去照顾B进程去了,那么已经写入A进程的那1GB数据就丢失了(假如那1GB的数据是转账记录的话,那事故是很严重的),那该怎么办呢?
  • 为了避免这种情况,就可以把等待磁盘数据写入后,需要拿到磁盘返回结果的进程状态,设为D状态。D:不可被杀,深度睡眠,不可中断睡眠。

消除D状态有两种情况:

1、让进程自己醒来;

2、重启,不行的话,就得断电了。

1.6、僵尸状态(Z)

僵尸状态也叫僵死状态,它是在进程在死亡状态之前的状态。当一个进程运行完毕、出现问题或者被杀掉以后,它所占用的内存资源和退出状态没有被它的父进程回收,此时这个进程的状态就称为僵尸状态。

1.7、死亡状态(X)

当一个进程执行结束或者是被操作系统杀掉,它的PCB被操作系统删除,并且对应加载到磁盘上的二进制代码也被删除,此时这个进程就处于死亡状态了。

当一个进程占有内存的所有资源被回收以后,这个进程就处于死亡状态。进程的死亡状态是看不到的,因为只有在回收完成的那一刻才会出现,PCB不存在也就搜索不到这个进程,所以也无法演示。

二、僵尸进程

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

Linux中的进程退出的时候,会将自己的退出信息保留在自己的PCB当中,如果没有人读取PCB中进程退出的消息,那么进程就会一直不释放;一般会把进程的代码和数据结果释放掉,但是PCB的数据结构不会释放,直到将来对进程进行等待,如果不等待,那么进程会一直处于僵尸状态。如果把进程的退出信息读取了和等待了,那么这个进程才会变成X状态,从而将进程的所有资源全部释放。

一个进程退出的时候,它不会立即退出,而是会现处于一种僵尸状态,如果父进程不会对我们这个进程进行回收或者等待的话,那么对应的进程会一直处于僵尸状态。

下面我们创建一个进程来看一看

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        int cnt = 5;
        while(cnt)
        {
            printf("I am child, cnt: %d, pid: %d\n", cnt, getpid());
            sleep(1);
            cnt--;
        }
    }
    else
    {
        // parent
    
        while(1)
        {
            printf("I am parent, running always!, pid: %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

子进程已经运行完毕,但是需要维持自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程进行读取。

如果没有父进程读取,僵尸进程会一直存在(内核数据结构task_struct会一直存在),进程的代码和数据会释放。

将来子进程被 waitpid(系统调用接口) 等待方式读取了,那么这个子进程会由Z状态变为X状态,之后由OS来释放。

僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 内存泄漏?是的!
  • 如何避免?后面讲。

三、孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”。
  • 孤儿进程被1号init进程领养,当然要有init进程回收喽。

下面我们创建一个进程来看一看

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        int cnt = 5;
        while(cnt)
        {
            printf("I am child, cnt: %d, pid: %d\n", cnt, getpid());
            sleep(1);
            //cnt--;
        }
    }
    else
    {
        // parent
        int cnt = 5
        while(cnt)
        {
            printf("I am parent, running always!, pid: %d\n", getpid());
            sleep(1);
            cnt--;
        }
    }
    return 0;
}

我们已经启动的所有的进程,我们怎么从来没有关心过僵尸进程(内存泄漏)呢?

直接在命令行中启动的进程,它的父进程是bash,bash会自动回收新进程的Z(僵尸进程)。

四、运行状态

操作系统为了合理分配CPU以及各种硬件资源,保证各个进程的正常运行。操作系统会为CPU创建一个进程队列,也为每一个硬件都创建一个等待队列。

而某一个进程处于运行状态本质上就是操作系统将该进程对应的PCB放入CPU的运行队列中,然后再将PCB中维护进程状态的变量修改为相应的值。

PCB里面有进程的各种属性值,以及对应的代码的地址。所以CPU从运行队列中找到PCB取出数据后,可以根据数据得到进程的各种属性值和指令,然后执行相应代码。进程处于运行状态并不意味着该进程此时一定正在被运行,只要该进程处于CPU的运行队列中即可。

注意:CPU是处理数据的速度在纳秒级,运算速度非常快,所以只要进程处于CPU的运行队列中,我们就可以认为该进程正在被运行。

一个进程一旦持有CPU,会一直运行到这个进程结束吗?

不会。CPU基于时间片进行轮转调度的。(Linux不是这样调度的,这只是OS教材调度方法之一)

让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码,就叫做并发。

每一个CPU都要有一个自己的运行队列,有两个CPU的话,会存在真正意义上的有两个调度队列。任何时刻,都同时有多个进程在真的同时运行,我们叫做并行。

五、阻塞状态

CPU处理数据的速度极快,是我们计算机中的各种硬件处理数据的万倍,比如:一个磁盘或者一个网卡同时只能为一个进程服务,但是在计算机中需要使用这些硬件资源(磁盘等)的进程会有很多。

如果在硬件为一个进程服务时,有其他运行中的进程也需要使用该硬件资源,操作系统就会将该进程的PCB放入硬件的等待队列中,进程会等待硬件来为自己提供服务。

不是只有CPU才有运行队列,各种硬件设备也有自己的等待队列。

由于多个进程需要访问某种硬件,进程PCB在硬件等待队列中等待硬件服务自己的状态就被称为阻塞状态。阻塞状态在本质上就是将进程的PCB从CPU的运行队列中,放入硬件的等待队列中,然后将PCB中维护进程状态的变量也会修改为相应的值。当该进程获得对应的硬件资源服务时,再将该进程放回CPU的运行队列中。

注意:并不是只有等待硬件资源的进程才处于阻塞状态,一个进程等待另一个进程就绪、一个进程等待软件资源就绪等也是阻塞状态。

六、挂起状态

上面我们学习了阻塞状态,处于阻塞状态的进程由于需要等待某种资源,所以它对应的代码和数据在短期内不会被处理(这里的短期指的是对于操作系统而言)。但它们的数据仍储存在内存中,占用存储空间但是不执行,相当于浪费了内存资源。而如果当前操作系统处于高IO(大量向内存输入和向外部设备输出数据)的情况下,可用的内存空间不足,操作系统就会选择性的将这些处于阻塞状态的进程对应的代码和数据转移到磁盘中,从而节省出内存空间。

这种由于内存空间不足,操作系统将在等待资源的进程对应的代码和数据放到磁盘中以节省内存空间的状态就被称为挂起状态。挂起状态不会移动进程的PCB,只会移动进程对应的代码和数据。

挂起并不是释放进程,因为对应的PCB仍然在硬件的等待队列中,当该进程获得对应的资源服务以后,操作系统仍然可以将该进程对应的代码和数据从磁盘加载到内存中来继续运行,其本质是对内存数据的唤入唤出。

七、进程切换的话题

A进程在CPU运行队列中运行的时间片到了之后,A进程会脱离CPU的运行进程;B进程会进入CPU的运行队列中运行,那么比如:A进程在CPU中已经运行到了第50行代码后,时间片到了,退出CPU,当A进程再次进入CPU的时候,是从新开始运行呢?还是接上上回第50行的代码继续运行呢?

CPU里面有一套寄存器,会保留A进程运行的数据,当A进程退出CPU的时候,A进程在CPU中已经运行的数据,将由 task_struct 这个结构体来保存,当A进程再次进入CPU时,数据还会回到寄存器原来的位置,所以A进程会接着第50行继续运行。

进程的切换,最重要的一件事情是:上下文数据的保护和恢复。

CPU内的寄存器:

寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套!!!CPU内部的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据。

寄存器 != 寄存器的内容


总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

相关推荐

  1. Linux进程状态

    2024-03-16 22:56:04       39 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-16 22:56:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-16 22:56:04       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-16 22:56:04       82 阅读
  4. Python语言-面向对象

    2024-03-16 22:56:04       91 阅读

热门阅读

  1. 共享库的创建gcc选项“-shared -fPIC -WI”

    2024-03-16 22:56:04       42 阅读
  2. perl 用 XML::Parser 解析 XML文件,访问哈希

    2024-03-16 22:56:04       44 阅读
  3. redis的过期策略以及内存淘汰机制

    2024-03-16 22:56:04       47 阅读
  4. 【Android】TextView前增加红色必填项星号*

    2024-03-16 22:56:04       47 阅读
  5. Vue3.0+vite vite.config.ts配置与env

    2024-03-16 22:56:04       42 阅读
  6. 【嵌入式——QT】线程同步

    2024-03-16 22:56:04       37 阅读
  7. Qt是什么?

    2024-03-16 22:56:04       38 阅读
  8. 第1章第2节:SAS语言基础

    2024-03-16 22:56:04       41 阅读
  9. 3月16日ACwing每日一题

    2024-03-16 22:56:04       47 阅读
  10. View UI清除表单

    2024-03-16 22:56:04       38 阅读
  11. 构建专业聊天软件:C#编程深度解析

    2024-03-16 22:56:04       42 阅读
  12. 树莓派开机自动播放U盘里的照片和视频

    2024-03-16 22:56:04       68 阅读