【Linux进程】进程状态:运行阻塞挂起、僵尸孤儿进程

目录

一、前言

二、什么是进程状态

 三、运行状态

四、阻塞状态

五、挂起状态

六、Linux中特定的进程状态

1.R/S状态

2.S/D状态

3.t/T状态

4.僵尸状态

5.孤儿进程

七、理解内核链表


一、前言

本文介绍了什么是进程状态,大致分为运行、阻塞、挂起三种状态,随后介绍了在特定环境:linux中有的几种进程状态,分别是R运行状态,S睡眠状态,D磁盘休眠状态,T暂停状态,Z僵尸状态和孤儿进程,详细解析其产生原因和例子,最后介绍了Linux中内核链表的底层实现加深理解。


二、什么是进程状态

进程状态的本质其实就是PCB结构体中的一个变量,可能是用宏定义实现,如下代码:

#define NEW 1
#define RUNNING 2
#define BLOCK 3
strcut PCB
{
	...其他信息
	int state;//进程状态
}
if(PCB->state==NEW)
{将进程放入运行队列}
else if(PCB->state==BLOCK)
{将进程放入阻塞队列}
......

 三、运行状态

处理器有多核的,也就是有多个CPU,但每个CPU都有一个对应的运行队列,CPU会在该队列中寻找数据来处理。

所以只要一个进程在运行队列中,那么它的状态就是运行状态,无论它是否正在被处理。 


四、阻塞状态

当CPU去处理运行队列中的PCB时,运行程序的代码中或多或少会有些去访问外设的操作,例如键盘,磁盘等,就像C语言中的scanf,程序执行到此时系统会等待用户输入,只要用户不输入程序就无法接着往下运行。

此时该进程的PCB就会从运行队列移动到等待设备的等待队列中,再将运行状态改为阻塞状态,此时这个进程就处于阻塞状态了。这个过程大致如下图:


五、挂起状态

如果有很多进程都处于阻塞状态,而处于阻塞状态的进程因为它所等待的资源又始终无法调度,始终占用着资源,此时恰好OS的内存资源已经严重不足,那么就需要让这些进程挂起,也就是将其PCB和对应的代码和数据移动到磁盘中,为内存腾出一部分空间来缓解压力,此时进程就是处于挂起状态。

注意:swap分区是磁盘中真实存在的,它的大小往往是很小的,这个部分专门用于内存严重不足时和内存进行交互,并且当内存情况缓解后,曾经被置换出去的代码和数据又会重新加载进来。


六、Linux中特定的进程状态

Linux中有常见的六种状态:

  • 运行状态R:并不代表进程一定在运行,而是表明进程要么在运行中要么在运行队列里
  • 睡眠状态S:意味着进程在等待事件完成
  • 磁盘休眠状态D:也叫不可中断睡眠状态,进程通常会等待IO结束
  • 停止状态T:通过发送SIGSTOP信号停止进程时的状态,通过SIGCONT信号继续进行
  • 死亡状态X:这只是个返回状态,不会在任务列表中看到这个状态
  • 僵尸状态Z

1.R/S状态

我们自己写一个程序,然后用ps ajx来查看该进程的状态:

按照常理来说,我们写的程序是一个死循环,应该是一直处于运行状态(R)的,可为什么我们手动去查看时却是S状态呢?

 因为有printf时,进程状态是在运行队列和阻塞队列里频繁切换的,而IO输出是很慢的,进程大部分时间都排在显示器的等待队列中,执行printf的时间却很短,因此我们去查看进程状态时多半是S状态。若没有printf,只有while,那么程序就会一直在R状态。


2.S/D状态

这里还一个和S睡眠状态类似的D磁盘休眠状态,两者存在区别:

如果我们要将一个非常大的重要文件拷贝到磁盘中,需要用比较长的时间,但此时OS的内存不足,需要干掉一些进程来维持OS存活,若此时杀掉了该拷贝进程,那么这个文件就拷贝失败了,如果这个文件非常重要,此时OS就要背大锅了,因此OS中存在这么一个D状态

  • S:浅度休眠,可以被终止
  • D:深度休眠,为了防止向磁盘写入重要资源时被杀掉而专门创建的一个分类

3.t/T状态

t是一种被追踪的暂停状态,只有收到某个命令后,进程才会继续执行。例如将test.c编译为debug版本然后用gdb进行调试,跳到断点处,此时该进程属于t状态。

T和上述状态不同,通过 kill -19 pid 给进程发送SIGSTOP(19)信号可以让进程处于该暂停状态T,并且进程会从前台转到后台运行,若想要从T状态恢复,需要使用 kill -18 pid 给进程发送SIGCONT(18)继续信号。


4.僵尸状态

当一个人正常死亡后,不会立即拖到火葬场火化,而是要先调查其死因,给社会一个交代。同理,在Linux中一个进程结束后不会立即销毁,而是会将退出信息返回给父进程,让父进程知道子进程完成对应任务的情况。

例如C语言中的return 0就是退出信息,0代表程序正常进行,其他代表异常

如果子进程退出并且父进程没有读取到子进程退出的返回代码,那么该子进程就会进入僵尸状态,僵尸进程会以终止的状态保持在进程表中,并且会一直等待父进程来读取退出状态代码,这样相应的资源空间会一直被占据而无法释放,造成内存泄漏,危害甚大。

下面是一个僵尸状态的例子:父进程不读取子进程退出信息


5.孤儿进程

从名字上听就知道孤儿进程大概是父进程挂了的进程,其定义其实是当一个子进程还没有退出时,父进程先退出了,那么这个子进程就被称为孤儿进程。

当一个进程被称为孤儿进程了,那么这就意味着它没有父进程为它“收尸”了,不回收就会造成内存泄漏,决不能放任这种事情发生,因此OS会让孤儿进程找一个“干爹”来充当其父进程,来为其“收尸”,这个所谓的干爹其实就是1号进程,即操作系统本身。

接下来就来实现一个孤儿进程:

可以发现当父进程退出后,子进程的ppid就变为1了,也就是被1号进程领养了,此时该孤儿进程是后台进程,无法通过CRTL + C杀掉,只能用kill -9指令。


七、理解内核链表

我们之前学习过的双链表都是在节点内部定义一个指向下一个节点的next指针和prev指针,内核中的链表也是双链表,但有所不同,内核是通过一个list_head的结构体,包含指向下一个list_head的next和prev指针构成的。

那么这样的结构放在struct task_struct中又该如何通过list_head的地址来找到其对应task_struct中的其他成员呢?这考验我们的C语言功底了--使用偏移量

注意:结构体的地址就是结构体第一个元素的地址;结构体内部元素地址依次增大


首先,我们使用(struct task_struct*)0->links,将0强转为该结构体地址,并访问其list_head成员links,这里仅是访问不会报错,然后&((struct task_struct*)0->links),对其取地址,这里得到的就是偏移量,即该task_struct结构体从开头到list_head中间间隔的地址大小,随后用得到的、实际存在的next/list (是list_head类型),减去偏移量,即:next/list - &((struct task_struct*)0->links) ,就能得到该next/list对应的task_struct的起始地址,随后就可以访问在内的其他成员。

那么这种写法相比于直接在task_struct内部定义task_struct* next/prev有什么好处呢?

那就是我们可以在一个task_struct中存放许多组list_head,每一个links都可以和任意的links链接起来,所以这些PCB既可以属于全局链表,也可以属于运行队列、阻塞队列。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值