【Linux】进程(3)状态

hello~ 很高兴见到大家! 这次带来的是Linux系统中关于进程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙


文章目录

  • 一、理论上的操作系统状态
      • [1.1 运行状态](#1.1 运行状态)
      • [1.2 阻塞状态](#1.2 阻塞状态)
      • [1.3 挂起状态](#1.3 挂起状态)
  • 二、Linux操作系统状态说明
      • [2.1 查看进程状态 ps axj / ps aux](#2.1 查看进程状态 ps axj / ps aux)
      • [2.2 初识前后台进程和运行状态R](#2.2 初识前后台进程和运行状态R)
      • [2.3 休眠状态S和深度休眠状态D](#2.3 休眠状态S和深度休眠状态D)
      • [2.4 暂停T和追踪暂停状态t](#2.4 暂停T和追踪暂停状态t)
      • [2.5 死亡X](#2.5 死亡X)
  • 三、僵尸进程Z
      • [3.1 介绍](#3.1 介绍)
      • [3.2 僵尸进程危害](#3.2 僵尸进程危害)
  • 四、孤儿进程

一、理论上的操作系统状态

  1. 观察下图,能够看到不同的操作系统状态以及它们直接的关系,比如状态是如何改变的。接下来会对这些状态做一个基本的讲解。

1.1 运行状态

  1. cpu是如何处理一个又一个进程的呢?是通过调度队列,一个cpu一个调度队列。比如先进先出FIFO(first in first out)调度队列:链表的起始位置直接指向最先进入就绪态的 struct task_struct(进程控制块PCB),而这个struct task_struct则会链接到后面进入的struct task_struct结构体,如此形成一条队列。
  1. 放入运行队列里面的我们统一都认为是进入了运行状态,如果细分的话(对应上面的图),正在被cpu执行的就是运行状态,而在队伍里排队但还没有被cpu执行的进程就是就绪状态。

1.2 阻塞状态

  1. 当代码里有scanf()这样需要从键盘里读取设备的函数,进程状态会变得怎么样呢?执行到scanf所在代码时,如果我们迟迟不在窗口里面输入数据,那么就会一直卡在这个阶段,不会执行接下来的代码。这是在等待硬件键盘的输入,而硬件就绪好之后操作系统一定会是最先知道的。操作系统毕竟是硬件的管理者,而它如何进行管理?类似于PCB,先描述(硬件的各种属性)再组织(构成链表),之后直接对链表进行操作就能完成管理。

  2. 这时候会发生的事情如下图,这个执行到scanf的进程会从调度队列里面撤下来,进入键盘设备对应的等待队列里面,等待键盘的就绪。这个等待队列每一个控制硬件的struct device里面都会有一个。

  3. 只有当键盘就绪即数据输入完成之后,这个进程就会离开这个struct_device的等待序列,重新回到运行队列中去。因为是FIFO,不考虑优先级的话,它会回到队尾。

  4. cpu在这个进程离开的时间里也不会闲着,它会执行没有发生阻塞状态的下一个进程。

  5. 可以说,阻塞和运行的本质就是看某个进程task_struct在谁的队列里面

1.3 挂起状态

  1. 要知道,无论是运行还是阻塞状态它们的test_task和代码数据可是一直都在内存里面存着,而内存的空间有限,如果进程太多,内存满了该怎么办?
  2. 这个时候,就得提到磁盘里面会存在的一块区域swap分区,当内存满了的时候,操作系统就会把长期处于阻塞状态的进程的代码数据丢到swap分区里面,等硬件就绪的时候,再把这些代码和数据拿回来;如果还是不够,就会把长期处于就绪状态的进程的代码数据丢到swap分区里面,同样也是等到要执行的时候再把它们拿回来。前者叫做阻塞挂起状态,后者就叫做运行挂起状态
  3. swap分区的大小一般跟内存大小差不多或者是1.5倍左右,它不会太大。这是因为不能太依赖swap分区,代码数据进出swap分区是有时间损耗的,如果进出swap分区次数太多,效率就会下降,导致系统变慢。本质是利用时间换空间。
  1. 操作系统在内存不足的时候,会先尝试挂起,如果尽可能的挂起进程结果内存还是不够,那么操作系统就会直接杀掉特定的进程节省空间。

二、Linux操作系统状态说明

以下是Linux系统里对于状态的定义

cpp 复制代码
static const char * const task_state_array[] = {
	"R (running)"     //运行
	"S (sleeping)"    //休眠
	"D (disk sleep)"  //深度休眠
	"T (stopped)"     //暂停
	"t (tracing stop)"//追踪暂停
	"X (dead)"        //死亡
	"Z (zombie)",     //僵尸
};

2.1 查看进程状态 ps axj / ps aux

  1. -a: 显示⼀个终端所有的进程,包括其他用户的进程。
  2. -x: 显示无控制终端的进程,比如后台进程。
  3. -j: 显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
  4. -u: 以 "用户导向" 的格式输出,含资源占用。

2.2 初识前后台进程和运行状态R

  1. 只要是放在运行队列里的进程,都是运行状态,它不一定正在运行,也可能在等待。
  2. 进程也分前后台进程,若在启动命令后面加上符号&,进程就会在后端运行,否则会进入前台。
cpp 复制代码
//代码如下:
#include <stdio.h>

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


  1. 可以看到,如果加&放在后台的进程状态后面是不会有+号的,只有放在前台的进程状态后面会有+号。

2.3 休眠状态S和深度休眠状态D

  1. S和D状态其实都是对于上面提到过的阻塞状态的定义。它们都是在等待事件的完成。
  2. S和D状态的不同之处在于:S状态是可以打断的,可以发送信号kill -9也可以ctrl+c结束进程。而D状态是无法被打断的,它会等待IO信号输入的完成,在此期间不会相应任何外部信号
S状态
  1. 对于只有输入输出的代码,它的状态大部分时间会是S,而不是R,这是因为cpu的速度非常快(纳秒级),而外设的速度相对很慢(毫秒/秒级),大部分的时间都是进程在等待输入和输出的数据,这个时候就是休眠状态,也就是对应的S状态。

输出:

等待显示屏的就绪,task_struct在显示屏struct device的等待队列里等待。
输入:


等待键盘的就绪,task_struct在键盘struct device的等待队列里等待。

D状态
  1. 为什么会有深度休眠状态D?设想一个场景,一个进程将在对磁盘做输出,而要输出的文件非常大,这需要时间,在这期间task_struct只能在内存里面等待,等待磁盘输出完成的信号。而此时内存又非常紧张,操作系统已经挂起了很多进程的代码数据了,操作系统这个时候看到没有运行的task_struct就在那里什么事都不干,很有可能就会发信号一下子把这个进程干掉。而假设磁盘读取过程中出现问题,想要告诉task_struct,结果根本找不着,之后就只能将这份未传输成功的数据删除掉。而万一这份数据它很重要,比如是银行一个小时的转账记录,那么就会有很大的损失。
  2. 但在这个过程中,操作系统、进程和磁盘的行为都是没有问题的。操作系统要清理内存,进程只能等磁盘,磁盘在卖力输入。为了不让某些进程被草率的杀掉,就有了这个深度休眠状态D,它不会被外部的信号影响到,也就不会被干掉。

2.4 暂停T和追踪暂停状态t

  1. 可以发送信号来使进程进入停止状态,也可以继续发送信号让进程恢复之前状态。T和t状态没有本质的区别,只是t状态是调试时打断点后运行到断点处会出现的状态。

kill -19 进程PID 暂停进程

kill -18 进程PID 恢复进程之前状态

kill - 9 进程PID 干掉进程

用的也是一个简单无限循环代码。

  1. 放在前台运行,这个时候是R+状态,我们发送暂停信号之后,会变成T状态,而且会把这个进程从前台给它放到后台中去,即便是发送恢复信号也不能从后台移动到前台了。
  1. 停止状态并不属于阻塞状态,阻塞状态的S和D是停下来等待资源的输入输出没有办法运行,而停止状态则是被信号叫停。
  2. 若把有scanf输入的进程放在后台,执行到scanf时,进程会进入暂停状态。



  1. 这是因为后台进程无法从键盘读取数据,系统就强制给进入暂停状态。我们无法用ctrl+c去终止一个后台进程也是这个原因,因为它根本就收不到。只能用发送信号来完成。
  2. 这也变相指明了前后台进程的区别:能够从键盘里读取数据的就是前台进程,否则就是后台进程。我们只有一个键盘,也就是说只能有一个前台进程能够读取数据。这也说明同时只能有一个前台进程,而后台则可以有多个

当运行到断点的时候,状态就会变成ts+,s是首个会话进程,+是前台进程。

不只是t的原因:ts+:因为我用了 CGDB 调试(终端前台运行),GDB 为 test 进程创建了独立会话,且占用终端前台 → 三个标记同时触发。

2.5 死亡X

  1. 死亡状态我们是无法通过任务列表看到的,这是因为死亡的进程是不占用系统资源的,会被操作系统进行回收。

三、僵尸进程Z

3.1 介绍

  1. 进程执行完之后退出并不会直接进入死亡状态,而是进入僵尸状态Z,在这个状态里面,代码和数据会离开内存,但是进程对应的部分task_struct(退出信息)会等待父进程的回收,在此之前会一直留在内存
  2. 为什么要有这个Z状态?这是因为,所有关于进程的信息都是被保存在task_struct里面的。当一个进程结束后,我们想要知道这个进程结束的如何就必须想办法获取task_struct里面的信息。如果直接进入死亡状态X,那么代码数据还有task_struct一个都不会留下,也就没有办法获得进程的退出信息。
  3. 由于直接在终端创建可执行文件会是bash的子进程,执行完之后会自动被bash回收。这里我们在代码里创建一个子进程来进行测试。这个父进程不会回收子进程,函数waitpid才是用来回收子进程的。
c 复制代码
	#include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 int main()
  5 {
  6   pid_t p = fork();
  7   if (p == 0)
  8   {
  9     while(1)
 10     {
 11       printf("我是子进程,PID:%d, PPID: %d\n", getpid(), getppid    ());
 12       sleep(1);
 13     }
 14   }
 15   else
 16   {
 17     while(1)
 18     {
 19       printf("我是父进程,PID:%d\n", getpid());
 20       sleep(1);                                                  
 21     }
 22   }
 23   return 0;
 24 }
  1. 可以看到在发送信号给27556之后,这个子进程并没有直接消失,他依旧可以显示在任务列表,是Z状态。

3.2 僵尸进程危害

  1. 僵尸进程的危害之一是内存占用:这些僵尸进程不能被回收的话,它那拥有退出信息的部分task_struct就会一直留在内存里面,没有办法清理掉。
  2. 也会造成PID资源资源泄漏,因为PID的数量是有限的。如果僵尸进程数量太多,那么PID可能就会被消耗光,这会导致新进程无法创建。
  3. 如果退出程序,内存泄漏自然也就没有了,比如像qq这样的常驻进程,如果变得一卡一卡的,退出重进之后就会流畅很多。

四、孤儿进程

  1. 僵尸进程是子进程被干掉,父进程还不进行回收的情况。如果保留子进程,让其正常运行,转而把它的父进程干掉,那么这个子进程就会变成孤儿进程。之后这个孤儿进程会被系统给领养。
  1. 系统为什么要领养孤儿进程?这是因为孤儿进程没有了父进程,也就没有对其进行回收的进程。当孤儿进程执行完毕的时候就会变成僵尸进程,会有危害。而把这些孤儿进程交给系统,当孤儿进程退出的时候系统就可以将其回收。系统领养孤儿进程的目的就是:避免孤儿进程终止后成为 "无人回收的僵尸进程"

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

相关推荐
恒创科技HK1 小时前
香港云大宽带服务器常见的配置及价格参考
运维·服务器
Ling_Ze1 小时前
mysql和postgressql数据库在服务器中容器创建和工具连接
服务器·数据库·mysql
Live in Shanxi.1 小时前
Prometheus监控服务器及K8s集群资源
服务器·kubernetes·prometheus
vortex51 小时前
浅谈Linux文件读取类漏洞的额外攻击面
linux·安全·web安全
_OP_CHEN1 小时前
【Git原理与使用】(三)Git 分支管理终极指南:从基础操作到企业级实战,解锁高效协作密码
linux·运维·git·git分支管理·企业级组件·企业协作
meng_ser1 小时前
基于Linux内核模块的进程与内存监控工具(CentOS 7实现)
linux·运维·centos
regret~1 小时前
【笔记】创建systemctl服务
linux·服务器·笔记
Heavydrink1 小时前
Java项目部署云服务器详细教程
java·服务器·开发语言
水天需0101 小时前
ps 命令全面详解
linux·服务器·网络