【Linux】进程状态
📝前言:
前两篇文章Linux进程概念(一),Linux进程概念(二)我们初步了解了Linux进程概念,这篇文章我们来讲讲Linux进程状态:
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
- 一,操作系统的进程状态
- 二,Linux进程状态
- (1)R 运行状态
- 1.1 运行队列
- (2)阻塞状态
- 设备等待队列
- PCB中如何存储指针
- 阻塞状态
- S 状态和 D 状态
- 挂起状态
- (3)X 死亡状态
- (4)T 状态和 t 状态
- (5)Z 僵尸状态
- 内存泄漏
- 内核结构申请
- (6)孤儿状态
- 终端,前台与后台
一,操作系统的进程状态
在操作系统这门学科(书本)里面,主要强调:进程在就绪、运行和阻塞三种状态之间转换:
下面先笼统的讲一下,这三中状态(在Linux的进程状态中再结合Linux系统具体讲Linux的进程状态):
ready
就绪状态:进程创建后,系统会为其分配必要的资源,并将其放入就绪队列,此时进程进入就绪状态。running
运行状态:进程在运行或者在运行队列中blocked
阻塞状态:进程因某一时间暂停执行,如:等待 I/O 事件(等待键盘输入,等待写入操作完成…)会从运行状态进入阻塞状态
二,Linux进程状态
操作系统学科中的进程状态是一个较为抽象和通用的概念,而 Linux 进程状态则是基于操作系统原理在 Linux 系统中的具体实现。
Linux的进程状态是用一个变量来表示的,这个变量是一个在task_struct
里面的一个整型变量
下面是状态在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 */
};
- R运行状态(running): 进程在运行中或在运行队列里
- S睡眠状态(sleeping):意味着进程在等待事件完成,可中断睡眠
- D磁盘休眠状态(Disk sleep):也是进程在等待事情完成,不可中断睡眠
- T停止状态(stopped):主要是用户/操作系统暂停的,用户可以通过发送SIGSTOP 信号暂停进程,也可以通过发送SIGCONT信号,让进程继续运行
- t停止状态((tracing stop)):debug的时候,在断点处停止时,就是t状态
- X死亡状态(dead):程序运行完,且资源回收了(这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态)
- Z僵尸状态(zombie):当程序运行完,但是资源还未回收时,处于的状态
(1)R 运行状态
当进程在运行中或在运行队列里,就是R状态
在运行中就是进程被加载到CPU了,那什么是运行队列呢?
1.1 运行队列
运行队列(调度队列):是Linux操作系统用来管理后面几个要运行的进程的PCB的数据结构。CPU 通过从运行队列中获取进程的相关信息来决定下一步要执行的进程。
(2)阻塞状态
设备等待队列
首先,我们要知道,Linux里面的设备也是被描述了,然后把特性提取出来,用结构体储存,再用数据结构组织起来的。
并且,这些设备结构体中会有一个成员叫做:设备等待队列。当进程需要访问某个设备,但设备暂时不可用或处于忙碌状态时,进程(PBC)会被放入该设备的等待队列中。
看到这里你会不会思考,为什么PBC即可以在运行队列又可以在设备的等待队列中呢?
这就要谈谈PCB中的指针了。
PCB中如何存储指针
我们以前学的队列/链表中的节点:
结构体内直接存储指针变量,那当然一个结构体里面如果只有这一组pre和next,那就只能存放在一个数据结构体了。
除非你这样(再存一个指针变量,需要放在多少个不同的数据结构里,你就搞多少个不同的指针):
但是这样有点挫,你不同数据结构的next和prev还得区分。为此,Linux内把prev
和next
先封装到了一个结构体list_head
里面。
这个list_head
就只存储next
和prev
指针,然后PCB里面存储的就是list_head
这一个成员。
这时候,不同的list_head
就相当于不同数据结构中的两个指针,实现了PBC存在不同数据结构里:
但是list_head
不是一个指向结构体的指针,没有结构体指针,那我们怎么访问结构体成员呢?
别怕,我们已知,list_head
成员的地址,自然可以获取到这个成员和结构体“开始地址”的偏移量。
&((struct struct_task* )0->list_head)
:这样就可以得到list_head
距离该结构体开始地址的偏移量。
C语言标准库 <stddef.h>
中定义的 offsetof
宏就可以帮我们解决这个问题。
阻塞状态
我们通过一个程序执行的过程,感受什么时候出现阻塞状态
- 我们写了一个程序,程序里有一句
scanf
- 当程序运行到
scanf
的时候,就需要从键盘读取数据,进程就会等待用户输入 - 如果用户没有按下键盘,键盘就一直处于未就绪状态,原来在运行的进程(PCB)就会从运行队列中被移动到键盘设备的等待队列,这个时候,进程就从运行状态,变成了阻塞状态
- 直到用户输入完了,键盘就绪了,这个时候操作系统会立刻知道硬件的变化,然后操作系统就会查看就绪设备的节点,将PCB的进程状态更改(从阻塞状态改回运行状态),把PCB从设备等待队列移动回调度队列
S 状态和 D 状态
S 状态和 D 状态都是阻塞状态,它们的区别是:S 可以被中断,D不可以。
怎么理解这个意义呢?讲个例子
假如现在有一个进程中有个操作要往磁盘里面写数据,那么磁盘写数据有成功也有失败,这个进程就需要等到磁盘写入完成后,才能继续下一步操作,在这个等待的过程中就是阻塞状态(假如就是 S 状态,可以被中断)
但是,假如现在的操作系统内存严重不足,操作系统就可能会把这个处于S 状态的进程直接杀掉来腾空间。直接杀掉以后,如果磁盘写入失败了,这时候磁盘回头看,已经找不到原来的进程了,那么要写入的这份数据,就会被磁盘丢弃,但是这个行为用户是不知道的,就造成了数据丢失。
为了解决这个问题,就出现了 D 状态。如,当执行写入这种高I/O的操作的时候,进程就会被设置成 D 状态,无法被杀掉。
状态演示:
S 状态:
myproc.c
文件:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{while(1){printf("我是一个进程\n");sleep(1);}return 0;
}
运行并查看进程信息:
while true; do ps -axj | head -1; ps -axj | grep myproc; sleep 1; done
结果:
这里一直在输出printf
所以是S。那为什么程序运行不显示R状态呢?
因为在这里printf
的运行时间占程序的很大比重(比如99%),我们在这里每隔一秒钟打印,刚好遇到R的状态概率比较小,大部分时候都在IO
D 状态不容易观察,因为磁盘的写入速度其实也很快,如果出现秒级以上的D状态的显示出现,那可能是机器要挂了,或者磁盘出问题了。这里就不做演示了
挂起状态
这个状态Linux中没有具体的状态对应,因为挂起的操作是操作系统自己完成的,我们不用关心。
挂起分为阻塞挂起和运行挂起:
- 阻塞挂起就是:当操作系统内存不够了,OS把不会被调度的进程先交换到磁盘上(进程是数据和代码挂到磁盘上),腾出空间。后续换回操作就是:OS通过指针找到对应的数据和代码再换回来
- 运行挂起:内存实在不够了,把运行队列末端的进程也挂出去
(3)X 死亡状态
死亡状态就是指程序运行完毕退出了,且资源也被回收完了。X状态也无法被显示观察到。
(4)T 状态和 t 状态
T 状态和 t 状态都是暂停状态。
t 状态
:在程序debug的时候,在断点位置处的暂停就会进入t状态
状态演示(在第8行打一个断点,然后运行):
T 状态
:主要是通过用户发信号的暂停,或者是操作系统的暂停。用户发信号的暂停就是通过:kill -信号编号 进程PID
。系统的暂停的就是当操作系统怀疑程序有问题时,暂停程序(而不是直接结束),然后给用户,让用户自己排查问题,相当于一种“止损”操作。-19
是暂停,-20
是重新启动
状态演示:
我们让进程运行起来,然后手动暂停
状态变化:
(5)Z 僵尸状态
Z僵尸状态就是:当进程已经结束,但是还未回收资源(PCB)。
子进程在运行结束以后,代码和数据都会被释放,但是进程的运行结果信息会被保存在task_struct
留给父进程(父进程需要知道子进程运行的怎么样)。【也就是说PCB是不会直接在结束的时候释放的,这时候就进入了僵尸状态】
父进程可以在这个状态内获取子进程的退出信息,并且需要回收子进程的PCB(如何回收先不讨论)
如果没有回收,那么子进程的PCB就一直不会被释放,就会造成内存泄漏
示例(我们让子进程运行完,然后父进程一直运行,使得父进程没办法回收子进程的PBC):
myproc.c
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{int ret = fork();if(ret == 0){int count = 5;while(count){count--;printf("我是一个子进程,我的PID是:%d\n",getpid());sleep(1);}}else{// father 进程while(1){printf("我是一个父进程, 我的PID是:%d\n", getpid());sleep(1);}}return 0;
}
运行效果:
当子进程运行结束,父进程还没有结束
状态变成了Z,< defunct >
表示无用的
内存泄漏
简单聊一下内存泄漏:
如果进程X了,还会有内存泄漏吗?
答案是:如果程序退出了,那new/malloc的那些空间都会被自动回收,此时就不存在内存泄漏。
但是,大部分的软件都是一直运行的,是常驻内存的软件(比如操作系统),这时候就要考虑内存泄漏问题。
你可以会想到自己的电脑关机,然后操作系统也关了,但是服务器上的呢?
内核结构申请
Linux中有一个用来专门储存已经无用的task_struct
的链表(相当于:数据结构对象的缓存),这样就可以加速未来申请同类对象的速度。
(6)孤儿状态
孤儿进程:当子进程没执行完,父进程已经执行完,先退出了。
这时候子进程的PPID变成1
,这个1
号进程就是操作系统,也就是说孤儿进程被操作系统领养了。
为什么要领养?因为不领养就没有父进程帮它释放PBC了,那就会内存泄漏。
同时注意:领养以后的子进程就变成后台进程了(Ctrl+c
就杀不了了)
演示(让父进程运行完,子进程孩子还在运行):
myproc.c
文件代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{int ret = fork();if(ret == 0){while(1){printf("我是一个子进程,我的父进程PID是:%d\n",getppid());sleep(1);}}else{// father 进程int count = 3;while(count){printf("我是一个父进程, 我的PID是:%d\n", getpid());sleep(1);count--;}}return 0;
}
运行效果:
并且这时候,子进程被领养以后就跑后台去了,在终端Ctrl+c
杀不掉
只能:kill -9 78793
杀掉
终端,前台与后台
进程状态有+
的是前台进程,当我们执行命令(进程)时,在后面+ &
代表在后台执行。
- 终端:一个你与计算机的“对话窗口”
- 前台:前台进程是在终端前台运行的进程,它会占用当前终端的输入输出。
- 后台:一般与终端分离,即使关闭了启动它的终端,后台进程也可以继续运行。
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!