【Linux】进程的状态和优先级

进程的状态和优先级

书接上回进程的概念,进程 = PCB + 代码和数据。PCB 中存放着进程的信息,是进程属性的集合,其中包括进程的状态

Linux的进程状态


话不多说,我们直接来看在 Linux 都有哪些状态:

  1. R - running
  2. S - sleeping
  3. D - disk sleep
  4. T - stopped
  5. t - tracing stop
  6. X - dead
  7. Z - zombie

下面我们来通过代码实际演示,来看看这些状态到底是怎样的。其中 D 状态由于机器限制,X 状态又是瞬时的,这里无法演示

R - running

running,顾名思义,就是运行状态。当进程被 CPU 调度时,进程就处于运行状态

例如,有如下 test.c 文件,写一个死循环,方便我们观察进程的状态

c 复制代码
int main()
{
	while(1)
	{
	}
	return 0;
}

编译得到 procStatus 文件,执行

再使用如下命令查看进程的状态:

shell 复制代码
while :; do ps ajx | head -1 && ps ajx | grep procStatus | grep -v grep; sleep 1; done

最终我们可以看到的结果:

我们的 procStatus 进程确实在运行中,处于 R 状态,加号不用管

S - sleeping

进程没有在运行,处于睡眠状态,这是一种进程等待资源就绪的状态

以下是进行测试的 test.c 文件

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
	while(1)
	{
		printf("I am a process, pid: %d\n", getid());
	}
	return 0;
}

依然是编译并运行,同时使用 ps 命令查看进程的状态:

可以看到,虽然我们的进程在不停地输出,但是我们查到的进程状态却还是处于睡眠状态。原因如下:

进程是在 CPU 上跑的,而 printf 输出的对象是屏幕,也就是外设。输出的信息需要由 CPU 先传到内存中,进而输出到外设

CPU 的处理速度是比外设快得多得多的,这就导致 CPU 上的进程输出了一大堆信息到外设,外设还在慢慢处理,那进程就只能等待外设就绪后再运行了

这种睡眠状态是可以中断的,所以是可中断睡眠。想要中断进程,我们可以:

  1. CTRL + c
  2. 使用信号命令,kill -9

D - disk sleep

由于机器限制,我这里就不再演示了,只说理论

D 状态是 Linux 操作系统特有的一种状态,可以应对下面这种情况:

操作系统运行于内存中,现有一个进程 A 也在内存中,它的任务是把一份非常重要的数据交给磁盘,由于磁盘的处理速度较慢,进程 A 需要等待磁盘的反馈信息,进入了 S 状态。

而此时内存压力比较紧张,操作系统快顶不住了,为了保证操作系统的正常运行,操作系统有权杀掉一些进程来释放空间。所以操作系统将处于 S 状态的进程A 杀掉了,之后磁盘恰巧写入失败,来找进程A 反馈信息,但是进程A 已经没了

磁盘不只有进程A 的任务要处理,还有其他进程的任务,所以只能将这份数据抛弃掉,处理其他任务去了。这就导致了数据的丢失,如果这份数据非常重要,如银行用户转账记录,那么损失就大了

所以为了避免这种情况发生,就有了 D 状态,这也是一种睡眠状态,但是不可中断。想要中断有两种办法:

  1. 等待进程自己醒来
  2. 重启大法

T - stopped

T/t 状态都是使进程暂停,等待进一步的唤醒。要想将进程暂停,我们可以使用信号命令,使用如下命令来查看可以使用的信号

shell 复制代码
kill -l

19号信号可以帮我们暂停进程

演示如下,当我们使用kill -19后,进程的状态由 S 变为了 T

我们还可以使用 18 号信号,唤醒处于 T 状态的进程

t - tracing stop

这种暂停状态,我们平时也会使用到,就是调试。当我们调试程序时,进程就会启动,我们给程序打断点,就会使进程暂停

我们先编译 .c 文件,让程序可以调试

接着进入调试,并查看进程

可以看到有了gdb 进程,但还没有我们的程序进程。接下来给我们的程序打个断点

然后输入 r,让我们的程序跑起来

此时可以看到,我们的程序进程,处于 t 状态。

接着输入 c,到下个断点处

程序进程还是处于 t 状态

Z - zombie 与 X - dead

当一个进程结束后,并不是直接退出。一般是将代码与数据 先释放掉,留下 PCB。PCB 中存着进程退出的信息,将一直处于 Z 状态,直到父进程将这些信息读取,才会退出 Z 状态,变为 X 状态,继而进程将完全退出

僵尸进程和孤儿进程


僵尸进程

当一个进程结束时,需要维持自己的退出信息,在自己的 task_struct(Linux 中的 PCB) 记录相关信息,未来让父进程读取,之后进程就会完全退出

下面我们用代码开一个父进程和一个子进程,让父进程无限循环,子进程运行一会就结束,进程运行期间我们可以查看它们的状态

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
 
int main()
{
	pid_t id = fork();
	
	if (id == 0)
	{
		// child
		int cnt = 5; // 运行5秒
		while(cnt--)
		{
			sleep(1);
			printf("I am child, pid: %d\n", getpid());
		}
	}
	else
	{
		// parent
		while(1)
		{
			sleep(1);
			printf("I am parent, pid: %d\n", getpid());                        
		}
	}
	return 0;
}

一开始,两个进程都处于 S 状态

当运行 5 秒后,子进程结束,进入 Z 状态,等待父进程读取退出信息

父进程结束时,会读取子进程的信息,子进程也就会完全退出

如果父进程不读取子进程的退出信息,那么子进程的 PCB 占用的空间就会一直不释放,造成内存泄漏问题

孤儿进程

如果父进程先退出,那么子进程就会成为孤儿进程

还是用代码开一个父进程和一个子进程,让父进程运行 5 秒,子进程无限循环

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
 
int main()
{
	pid_t id = fork();
	
	if (id == 0)
	{
		// child
		while(1)
		{
			sleep(1);
			printf("I am child, pid: %d\n", getpid());
		}
	}
	else
	{
		// parent
        int cnt = 5; // 运行5秒
		while(cnt--)
		{
			sleep(1);
			printf("I am parent, pid: %d\n", getpid());                        
		}
	}
	return 0;
}

可以看到,前五秒有父进程 6315 和子进程 6316,五秒后父进程退出,只剩下子进程,此时子进程就成为了孤儿进程

孤儿进程没有父进程,所以退出信息就没人读取,会造成内存泄露问题。为了避免这种情况,孤儿进程一般都是由1号进程(操作系统)领养,这样就可以保证子进程被正常回收

运行 阻塞 挂起


运行

进程是运行在 CPU 上的,而一个 CPU 同时只能运行一个进程,如果有多个进程都想要运行,那么就只能排队了

每个 CPU 都要维护一个运行队列,运行队列是一个数据结构,里面至少有一个指针 task_struct* head 指向进程队列头部

系统将位于队列头部的进程的代码数据等相关信息加载到 CPU 的寄存器中,CPU 就可以调度进程了

一般来说,当进程被放到 CPU 中时,这个进程的状态就是 R 状态了,也就是运行状态。而在很多教材中,只要进程进入了运行队列,就可以被称为 R 状态,其含义是:进程已经准备好了,可以随时被调度

进程的切换问题

当一个进程被 CPU 调度时,会一直运行到进程结束吗?不会的,如果写一个死循环的程序,那其他进程岂不是永远轮不到了。所以有这样一种调度算法:基于时间片的轮转调度

一段时间内 ,给运行队列的每个进程分配特定运行时间,一旦时间到了,不管进程有没有结束,都要从 CPU 剥离下来,放到运行队列的尾部,然后 CPU 调度下一个进程。这样让多个进程以切换的方式进行调度,在一段时间内得以同时推进代码 的做法,就叫做并发

如果有多个 CPU 在工作,就存在多个运行队列,不同队列可以同时运行不同的进程 ,这样在任意时刻 ,真的有不同进程运行的做法,叫做并行

阻塞

下面我们还是通过代码来理解阻塞态

c 复制代码
#include <stdio.h>

int main()
{
	int a = 0;
	scanf("%d", &a);
	printf("%d\n", a);
	return 0;
}

编译运行,执行到 scanf 时,程序就会暂停住,如下

此时进程处于阻塞态,对应 Linux 中的睡眠状态,在等待键盘资源的就绪。在冯诺依曼体系中,键盘是属于外设的,进程是软件,它是如何等待外设的呢?

键盘属于外设层,它的上一层是驱动层,再上一层是操作系统层,如图

操作系统是对软硬件进行管理的,如何管理,我们可以先描述,再组织

硬件设备也可以描述成一个数据结构 struct device,其属性如下

c 复制代码
struct device
{
	int type; // 设备类型
	int status; // 设备是否已经就绪
	// ...设备其他属性
	struct device* next; // 指向下一个设备
}

每一个设备在内核中都拥有自己的 struct device,不同设备的 struct device 互相链接在一起,组织成一个设备链表

在每个设备的 struct device 中,也存在一个指针,指向等待此设备的进程 ,这样的进程可能不止一个,所以和运行队列 一样,也存在等待队列 wait_queue

c 复制代码
struct device
{
	int type; // 设备类型
	int status; // 设备是否已经就绪
	// ...设备其他属性
	struct device* next; // 指向下一个设备
	task_struct* wait_queue; // 指向等待设备的进程
}

当进程等待设备资源就绪时,就会从运行队列剥离,链入到设备的等待队列。此时进程的状态就不是运行态了,而是阻塞态,不可被调度。直到设备资源就绪,如按下键盘,进程就会被唤醒到运行队列

进程在运行态和阻塞态之间切换时,往往伴随着 PCB 链入不同的队列中。入队列的不是进程的代码和数据,而是进程的 PCB

挂起

我们在装系统的时候,一般都会给磁盘分区,而磁盘中存在一个独立的分区:swap 分区,它的大小一般和系统内存相同,或者是内存的 2 倍,不同的人可能分配的空间大小不同,但是 swap 分区不宜太大也不宜太小

假设现在内存中运行着很多进程,操作系统的内存压力很大,即将调度不过来了,这时有一个进程 1 处于阻塞状态,在 Linux 就是 S 状态或者 D 状态,总之就是这个进程的代码和数据 暂时不会被调度。那么就可以把进程 1 的代码和数据唤出 到 swap 分区,这样就可以腾出一些空间,解决燃眉之急。此时进程 1 就是处于挂起态 ,严格来说是阻塞挂起态,当进程 1 需要调度时,再将其代码数据唤入到内存中

虽然 swap 分区可以减轻内存压力,但不宜设置太大。因为唤入和唤出是有消耗的,是一种用效率换取空间的做法。所以为了操作系统的效率,不宜频繁唤入唤出

进程的切换


在运行部分,我们已经知道,进程的切换是基于时间片的轮转调度,如果一个进程还没运行完毕,时间片结束,如何保证此进程下次运行的时候从中断点继续运行呢?

在 CPU 中,有非常多的寄存器,它们可以保存一些临时数据,如下面这个函数

c 复制代码
int add(int a, int b)
{
	int c = a + b;
	return c;
}

int ret = add(1, 1);

c 是临时变量,函数结束就会销毁。实际上,c 的值会被临时保存在寄存器中,这样就可以返回给 ret 了

而在 CPU 上运行的进程也是这样的,寄存器会保存进程的临时数据。CPU 内部所有寄存器中的临时数据,叫做进程的上下文

假设进程 1 正在 CPU 上运行,时间片到了,还没运行完,代码运行到了 20 行,为了保证进程 1 下次运行时从 20 行开始,进程 1 需要将寄存器中的上下文数据存入到 task_struct 中。轮到进程 2 运行时,如果不是第一次运行,就需要将 task_struct 中的上下文数据恢复到寄存器当中,继续运行

所以,进程的切换,重要的是上下文数据的保存与恢复

进程的优先级


什么是优先级

简单来说,优先级就是操作系统中指定进程获取某种资源的先后顺序。在进程的 task_struct 中,进程的优先级用数字表示,或者一个,或者多个

c 复制代码
struct task_strcut
{
	// ...
	int prio; // 优先级用数字表示
}

在 Linux 中,优先级数字越小,表示优先级越高

这时可能会有人觉得优先级权限的意思有点相像,其实是不一样的

  • 权限决定的是能不能获取某资源
  • 优先级是在已经的前提下,获取资源的先后顺序

为什么要有优先级

我们在食堂打饭时通常要排队,是为什么?因为人很多,饭是有限的,排队打饭是相对公平的分配方式

进程调度也是同理,进程要访问的资源(CPU等)通常都是有限的,而进程相对来说是很多的。我们所使用的操作系统 CPU 调度通常都是基于时间片轮转的,也就是分时操作系统,这样可以相对公平地给进程分配系统资源

如果在我们打饭时,不断有人破环规则而插队,那么有人就会因为迟迟打不打饭而饥饿。同样在进程的运行队列中,如果有进程不按特定顺序来访问 CPU 资源,就会导致有的进程一直访问不到 CPU,造成饥饿问题

Linux中的优先级

我们可以使用 ps -al命令查看进程的情况,可以看到两个属性:PRI 和 NI

  • PRI 就是进程的优先级,数字越小,代表进程优先级越高
  • NI 是进程优先级的修正数值,被称为 nice 值

PRI 是进程的默认优先级,若想修改进程的优先级,一般是修改 NI 的值,新的优先级 = PRI + NI

下面我们写一个程序并运行,修改一下它的优先级看看

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

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

启动我们的程序后,相应的进程优先级 PRI 为 80,NI 为 0,下面我们来修改进程的优先级

  • 输入 top 命令 -> 按下 r -> 输入进程的 pid -> 输入要修改的 NI 值 -> 输入 q 退出

我们先随便输入一个 100 试试,然后查看我们进程的优先级

为什么我们输入的是 100,但是 NI 值却是 19 呢?这是因为 NI 值的范围是 [-20, 19] ,一共40个数。下面我们将 NI 改为 -10

注意,普通用户不能频繁改变进程的优先级,需要 root

为什么进程的优先级不是 99 -10 = 89,而是 70 呢?这是因为每次调整优先级,都是从 80 开始的

通过上面的演示我们可以发现,Linux 中的进程优先级可以调整,但是存在限制,可以调整的范围很小。而进程的优先级也确实不应该轻易人为调整,这些应该交给系统

相关推荐
九河云23 分钟前
AWS账号注册费用详解:新用户是否需要付费?
服务器·云计算·aws
Lary_Rock28 分钟前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
幺零九零零1 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
云飞云共享云桌面2 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮4 小时前
Linux 使用中的问题
linux·运维
dsywws5 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零6 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
wclass-zhengge6 小时前
Docker篇(Docker Compose)
运维·docker·容器
李启柱6 小时前
项目开发流程规范文档
运维·软件构建·个人开发·设计规范