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

Linux 进程状态

上章我们讲到进程的基本概念,本章我们来讲解进程的状态,将从所有操作系统的进程状态共性和Linux特有的状态讲解。

一.进程状态

1.概念导入

每一个活着的人,都有自己的状态,而我们也因不同的状态做着不同的事。操作系统中的进程也是如此,操作系统会根据进程的不同状态进行处理,状态也决定了进程要做什么动作。上回我们讲到,Linux中的PCB的名字叫做task_struct,而进程的状态由task_struct中的一个int类型数组维护,不同的数值代表着不同的状态。

1.我们先对课本上对进程状态的描述进行提炼。

2.具体讲解

进程能运行,本质在于cpu在维护一个调度队列,选取PCB,再根据PCB中的内存指针找到代码和数据

基本调度结构

这里我们遗留一个大大的问题:

PCB只有一份,却可以同时隶属于多种数据结构,这是怎么做到的?

2.运行态与阻塞态

运行:只要进程在调度队列或者占用cpu资源运行时,就算做运行状态

阻塞:有什么现象?用scanf或者cin做例子。代码一跑起来就是一个进程,当他遇到这两个函数时程序就会停下来,停下来干什么?等待用户输入(等待键盘文件就绪)。所以阻塞,就是等待设备或某种资源就绪。

更详细的说,管理硬件设备,就是先描述再组织。操作系统可以维护设备队列和运行队列等等数据结构。

每一个设备,还可以有自己的等待队列

假如当前的调度队列中执行程序,执行到scanf,需要等待键盘输入,于是OS转而去查找device队列查看特殊硬件键盘的状态,假如这时的键盘为不活跃状态,就会把当前这个task从调度队列中移动到设备队列的wait queue

此时的task不在调度队列,就不会被cpu调度,就处于阻塞状态。也就是说,阻塞就是把PCB从调度队列链接到其他的队列中。

那么此时键盘被按下时,当前的task能知道吗?其实不知道。

键盘按下,它的状态改变,操作系统一定会第一时间知道,所以此时OS会查看对应设备结点的device结构体,并将状态修改为活跃,查看等待队列不为空,所以OS就把该task重新链接到调度队列。注意此时数据还未从键盘读取到程序中,进入调度队列,task被调度时就会重新执行scanf从键盘中读取数据。

也就是说,从阻塞回到运行,就是找到PCB,并将他链接回运行队列。

那么我们能得出一条原理:

进程状态的变化,表现之一就是在不同的队列中流动。

在键盘中,可以有很多进程想用键盘,就会链接到键盘device的等待队列中。

3.挂起态

挂起态是用来处理一种极端的情况的。当内存资源不足时,操作系统会将一些进程的代码和数据调出内存,转而调到磁盘的swap分区暂存。这就是课本中提到的阻塞挂起

还有更极端的情况:如果内存资源严重不足,甚至可能把正在运行队列末端的task代码和数据换入swap分区。这就是课本中提到的运行挂起

4.理解内核链表

在这里我们将回答我们上面的问题:

PCB只有一份,却可以同时隶属于多种数据结构,这是怎么做到的?

我们常见的链表设计可能是这样的:每个节点都有指向下一个和指向前一个的指针next和prev,并且这些指针都是指向一整个Node结构的(struct Node*)。

而内核链表的设计却大有乾坤:一个task_struct结点内包含一个list_head成员,而每个list_head都指向下一个task_struct结点的list_head,而不是整个task_struct结点。

所以要遍历进程,就用一个struct list_head*遍历即可。

但是这样访问到的也只是list_head这一个属性,那怎么访问一个task_struct的其他属性呢?

结构体的整体地址,和第一个成员的地址,在数值上是相等的。一个结构体内的所有成员地址,应该是逐步递增的。也就是说,我们目前知道的是一个task_struct内list_head的地址。所以要访问任意一个task_struct内部的其他成员,就需要:

1.得到links相对于结构体初始位置的偏移量

&((struct task_struct*)0->links)

2.得到task_struct的起始地址

next指针减去这个偏移量,再强转为task_struct*类型

(struct task_struct*) (next - &((struct task_struct*)0->links))

这样我们就可以通过对这个结果解引用得到task_struct所有的成员了。

那么上面的问题就得到解决:可以在一个task_struct定义一个结构的list_head,就可以定义很多个结构的list_head。

此时,当前进程可以用不同字段,不同数据结构进行链接。这样就可以让OS仅管理一个struct_list,就做到struct_list即属于运行队列,又属于全局链表,还可以在二叉树里...

这样,对于一个在运行队列的PCB,若要将它断链进入阻塞队列,也依然可以在全局的链表内找到它,就是这个原因。PCB只有一份,却可以同时隶属于多种数据结构。之前的普通类型的链表,是无法实现这种结构的。

查看Linux内核源码,发现它的做法正是如此。

二.Linux中的进程

讲完了大部分操作系统的共性,我们来看看Linux的进程状态有什么特别之处。

1.运行态和阻塞态

我们先写一个简单的程序,目的让它跑起来并查看状态。

运行起来状态如下:

用命令查看状态:

我们先查看一次

ps ajx| head;ps ajx|grep myprocess

然后再时隔一秒监控一次进程状态

while true; do
ps -C myprocess -o pid,state,%cpu,%mem,cmd --headers
echo "----------------------------------------"
sleep 1
done

为什么是阻塞(sleep)而不是运行(run)?

  1. I/O 操作导致睡眠

    • 代码中的 printf("hello process!\n") 是一个I/O操作

    • 当进程执行I/O操作(如向终端输出)时,它需要等待I/O设备准备就绪

    • 在此期间,内核会将进程置为睡眠状态(S),直到I/O操作完成

  2. 缓冲机制的影响

    • printf 使用缓冲输出,默认情况下当输出到终端时是行缓冲

    • 每次调用 printf 后,进程可能会短暂等待缓冲区刷新

  3. 时间片调度

    • 即使没有I/O,进程也不会100%时间处于运行状态

    • Linux调度器会给每个进程分配时间片,时间片用完后进程会被挂起

    • 使用 ps 命令捕捉到的瞬间,进程可能正好处于两次运行之间的间隔

代码中若IO操作占比较大时,就会将大部分时间用于等待IO。

如果我们这里将printf的代码注释,就会显示进程处于运行态

wujiahao@VM-12-14-ubuntu:~$ while true; dops -C myprocess -o pid,state,%cpu,%mem,cmd --headersecho "----------------------------------------"sleep 1
donePID S %CPU %MEM CMD673691 R  105  0.0 ./myprocess
----------------------------------------PID S %CPU %MEM CMD673691 R  104  0.0 ./myprocess
----------------------------------------PID S %CPU %MEM CMD673691 R  104  0.0 ./myprocess

tips:这里的状态带+,是因为在前台启动,在启动可执行程序时带上&就会在后台启动。

注意:后台的程序可以通过kill指令杀死。

2.阻塞态

由之前的讲解我们得知,我们可以用scanf函数来演示阻塞态。

我们修改代码

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>  2 3 int main(){4     printf("我是一个进程,我的pid:%d\n",getpid());5 6     int x;7     scanf("%d",&x);                                                                                                                                                                                           8    // while(1){9        // printf("hello process!\n");10    // }11     return 0;12 }

运行可执行程序,并查看状态。

可以看到此时进程处于阻塞态。

3.追踪(tracing stop)

追踪状态(tracing stop):被debug,由于断点进程被暂停。

暂停:不同于阻塞,他并不是因为等待资源而停止运行,而是因为某种特殊条件或者操作。是Linux特有的一种状态。操作系统觉得这个进程可能有问题,由用户决定是否继续运行。

我们修改Makefile文件,演示这种情况。

myprocess:myprocess.cgcc -o $@ $^ -g                           
.PHONY:clean
clean:rm -f myprocess

修改源文件

#include<stdio.h>                                                                                  2 #include<sys/types.h>3 #include<unistd.h>4 int main(){5     printf("我是一个进程,我的pid:%d\n",getpid());6 7   //  int x;8    // scanf("%d",&x);9     while(1){10         printf("hello process!\n");11     }12     return 0;13 }

生成可执行文件之后用cgdb打开。

打好断点之后,不输入运行,可以看到此时程序一直处于t状态。

如果将源程序直接Ctrl+Z暂停,程序的状态会显示T:暂停。

4.深度休眠(Disk sleep)

我们之前讲到的阻塞(sleep),可以说是浅度睡眠——可中断休眠。我们可以对这些进程执行kill -9指令kill掉它们,操作系统也会响应我们的操作。

而这里说的Disk sleep,是一种不可中断的休眠。我们用写磁盘数据为例讲解:

假如进程是一般的sleep状态:操作系统说,现在内存资源太紧张了,你去休息吧!进程:好吧。。但这时你的写磁盘操作已经进行了90%,你就已经跑路了。磁盘:数据还没发完呢,你怎么不说话了?剩下的10MB还传不传了?我这边也很忙,我就先去干其他活了。。

就这样:你的这些数据,会在你不可知的情况下丢失。

所以,操作系统设置了一种深度睡眠的阻塞。操作系统是无法杀掉正在进行磁盘IO的进程的。

处于这种状态的进程只能等进程自己醒来,甚至只能重启或断电才行(甚至都不行)。

我们可以构建一个块级IO观察这个现象

dd if=/dev/zero of=~/test.txt bs=4096 count=100000

这样就会创建一个每块4096比特,十万块的空间。

kill用于向进程发信号。

5.僵尸(zombie)态和结束态

僵尸态可能看字面意思难以理解,我们就举一个例子。

比如现在,你目睹了一个案发现场。此时你要做的肯定不是直接把ta搬走,而是:保护现场,拨打电话,等待专业人员采集各种信息,再搬走。

僵尸进程也是这种逻辑。我们创建一个子进程,是为了让它去完成某种工作。子进程死亡之前,父进程需要得知子进程的各种信息,否则子进程会一直处于僵尸态。而这些信息会被保存在task_struct中。

这里我们用代码示例。我们的预期是只有子进程执行这个会结束的循环,而父进程一直在执行无限循环,到时候子进程就会变成僵尸进程

#include<stdio.h>2 #include<sys/types.h>3 #include<unistd.h>4 int main(){5     pid_t id=fork();6     if(id==0)7     {8         //child9         int count = 5;10         while(count--){11             printf("我是子进程,我正在运行:%d\n",count);12             sleep(1);13         }14     }15     else{16         //father17         while(1){18             printf("我是父进程,我正在运行...\n");19             sleep(1);20         }21     }                                                                                              22   //  int x;23    // scanf("%d",&x);24     while(1){25         printf("hello process!\n");26     }27     return 0;28 }

此时观察进程状态

defunct:失效的。

那么父进程一直不管,不回收,不获取子进程的退出信息,那么子进程一直会处于Z状态。

长久以来,Z状态的子进程PCB一直占用内存,会造成内存泄漏。

处理僵尸进程的方法,之后会单独讲解。

6.关于内存泄漏

问:进程退出后,内存泄漏问题还在不在?

答:不存在。此时由内存泄漏问题new出来的空间会自动被系统回收

那么什么样的进程具有内存泄漏问题,会很麻烦?就是哪些常驻内存的进程

僵尸进程,也是内存泄露的一种潜在风险。未来需要用户去亲自解决这个问题,因为僵尸进程就是一种常驻内存的进程

反复的申请释放进程需要开销。由此可以引出一种节省这种消耗的结构——进程池

http://www.dtcms.com/a/392223.html

相关文章:

  • 基于自然语言处理的文本敏感内容检测系统的设计与实现
  • JDBC小白入门项目创建 IDEA 空项目+模块配置 JavaWeb MySQL
  • 笔记 Docker(离线)安装(24.0.9)
  • Docker-Android+cpolar:移动开发的环境革命
  • uniapp首先对战匹配简单实现
  • [bitcoin白皮书_2] 隐私 | 计算
  • 【杂谈】-重构注意力经济:人工智能重塑短视频内容生态
  • 【杂谈】Godot 4.5下载指南
  • CICD工具选型,Jenkins VS Arbess哪一款更好用?
  • iOS 26 续航测试实战,如何测电池掉电、Adaptive Power 模式功耗、新系统更新后的耗电差异与 App 续航优化指南
  • 数据挖掘与KDD:从理论到实践的最佳流程解析
  • 深入理解Linux网络中的Socket网络套接字——基础概念与核心实现
  • Spark专题-第二部分:Spark SQL 入门(4)-算子介绍-Exchange
  • Spark专题-第二部分:Spark SQL 入门(3)-算子介绍-Aggregate
  • Go基础:Go语言中集合详解(包括:数组、切片、Map、列表等)
  • 《算法闯关指南:优选算法--滑动窗口》--09长度最小的子数串,10无重复字符的最长字串
  • 请卸载xshell,一款国产的终端工具,界面漂亮,功能强大,支持win,mac,linux平台,安全免费
  • 用批处理文件实现Excel和word文件的重造
  • unseping(反序列化漏洞)
  • 麒麟系统 word转为pdf
  • 【Codex CLI 配置指南(小白速通版)】
  • R及RStudio的配置与安装
  • 深度解析:基于 ODBC连接 KingbaseES 数据库的完整操作与实践
  • springboot川剧科普平台(代码+数据库+LW)
  • Vue中的监听方式
  • CentOS 7系统解决yum报错
  • GD32VW553-IOT V2开发版【温湿度检测】
  • Perplexica - 开源AI搜索引擎,让搜索更智能
  • Windows在VSCode Cline中安装Promptx
  • 深入解析 Spring AI 系列:解析返回参数处理