上次讲了进程这些内容:Linux:进程概念(二.查看进程、父进程与子进程、进程状态详解)
文章目录
- 1.Linux中的进程状态
- 1.1前台进程和后台进程
- 运行状态
- 睡眠状态
- 磁盘休眠状态
- 停止状态
- kill指令—向进程发送信号
- 死亡状态
- 2.僵尸进程
- 2.1僵尸状态
- 2.2僵尸进程
- 2.3僵尸进程危害
- 3.孤儿进程
- 4.进程的优先级
- 概念
- 查看进程优先级
- PRI(优先级)和NI(nice)
- 更改nice来间接改变优先级
- 5.进程其他重要概念
- 6.进程的切换与调度
- 进程切换
- 进程调度
1.Linux中的进程状态
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): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
Z僵尸状态(zombie):下面详讲它
1.1前台进程和后台进程
在Linux中,可以将进程分为前台进程和后台进程,它们的区别在于与终端的交互方式和执行状态。
-
前台进程:会有
+
- 前台进程是当前正在与用户交互的进程,它会占用终端的输入和输出。
- 当用户在终端启动一个程序时,该程序通常成为前台进程,用户可以看到程序的输出,并且可以与程序进行交互。
- 前台进程会阻塞终端,直到该进程执行完毕或者暂停。
- 用户可以通过按下Ctrl + C来中断前台进程的执行。
-
后台进程:没有
+
- 后台进程是在后台执行的进程,不会占用终端的输入和输出。
- 用户可以在命令行中在命令后面加上"&"符号,将进程放入后台执行。
- 后台进程不会阻塞终端,用户可以继续输入其他命令。
- 用户可以使用命令
bg
将一个前台进程转为后台进程,或者使用命令jobs
查看当前所有的作业(包括前台和后台)。 - 使用
kill 进程ID
命令关闭对应的后台进程,比如kill 1234
运行状态
运行状态(Running)是进程可以被调度执行的状态。当一个进程处于运行状态时,它的代码正在被 CPU 执行,即正在运行指令并处理各种任务。在 Linux 中,通常用
R
表示进程处于运行状态。
int main()
{
while (1)
{
;
}
return 0;
}
睡眠状态
就是我们上次学习的阻塞状态,在Linux中,进程的睡眠状态(Sleeping)是指进程因等待某些事件而暂时停止执行。这个状态有时也被称为可中断睡眠(Interruptible Sleep),因为进程在这种状态下可以被中断,例如通过接收信号来唤醒。
睡眠状态的进程通常在等待某些事件的完成,例如:
- 等待某个I/O操作的完成,比如从磁盘读取数据。
- 等待某个信号的到达,例如等待用户输入或其他进程发送的信号。
- 等待某个条件的满足,比如等待某个锁的释放或某个共享资源的可用性。
需要注意的是,睡眠状态的进程是可以被中断的,也就是说,在等待事件的过程中,如果接收到一个信号,进程可能会被唤醒并处理该信号,之后可能会继续等待或执行其他操作。我们使用
Ctrl+c
可以中断进程,因此,这种状态也称为可中断睡眠。
#include<stdio.h>
#include<unistd.h>
int main()
{
int a=0;
while(1)
{
printf("%d",a);
sleep(2);
}
return 0;
}
在这个程序中,主循环是一个无限循环
while(1)
,它不会主动放弃 CPU,因此进程会一直处于运行状态(R)。但是,在每次循环迭代中,程序会调用printf
函数打印a
的值,并使用sleep(2)
函数让进程休眠 2 秒。在
sleep(2)
调用期间,进程暂时停止执行,等待指定的时间结束后再继续执行。虽然进程在休眠期间处于不活动状态,但是它并没有主动释放 CPU。因此,进程会被标记为睡眠状态(S),表示它正在等待特定事件的完成,即等待sleep
函数定时器计时结束。
但是如果我们把sleep()
去掉后,会发现还是S
状态
在这样的程序中,主循环是一个无限循环
while(1)
,它不会主动放弃 CPU,因此进程会一直处于运行状态(R)。但是,由于printf
函数涉及输出操作,这可能会导致进程在等待标准输出设备的 I/O 操作完成时陷入睡眠状态(S)。当程序运行时,
printf
函数将数据输出到标准输出设备(通常是终端),并且在数据传输过程中,可能需要等待设备的响应。在这段等待期间,进程暂时停止执行,处于睡眠状态。因此,即使主循环一直在运行,但是由于进程在某些时刻需要等待设备响应,因此会被标记为睡眠状态(S)。(CPU执行是很快的)
磁盘休眠状态
也是阻塞状态。D磁盘休眠状态(Disk sleep)是Linux系统中的一种进程状态,有时也称为不可中断睡眠状态(uninterruptible sleep)。进程进入这种状态通常是因为正在等待某些IO操作的完成,比如磁盘读写操作,网络请求等。在D状态下的进程是无法被中断或者唤醒的,直到IO操作完成为止。
- 进程状态:
- 当一个进程被阻塞在等待IO操作完成的情况下,它会被标记为D状态。这种状态下的进程无法响应信号,也无法被中断。
- 原因:
- 进程进入D状态通常是因为正在等待硬件设备的响应。比如,一个进程正在等待硬盘读取数据,但是硬盘响应较慢,导致进程无法继续执行。
- 解决方法:
- 通常情况下,D状态的进程会在IO操作完成后自动恢复,进程会从D状态转为可运行状态。如果进程长时间处于D状态,可能需要检查硬件设备是否正常,或者尝试重新启动系统。
也可以理解磁盘休眠状态的进程是有免死金牌的,能防止CPU因为资源不足而删除这个正在等待的进程
因此,即使系统资源紧张或CPU负载高,磁盘休眠状态下的进程仍然会被系统保留,不会被强制删除。这种机制确保了IO操作的完整性和系统的稳定性。
停止状态
在Linux系统中,当一个进程接收到
SIGSTOP
信号时,它会被暂停(停止)执行,进入停止状态。在这种状态下,进程的执行被暂时挂起,不会继续执行,也不会被调度到CPU上运行。停止状态下的进程不会消耗CPU资源,也不会响应任何信号,直到接收到
SIGCONT
信号后才会继续执行。停止状态的进程可以通过ps
命令或者类似的工具查看,通常会显示为T
状态。要将一个进程从停止状态恢复到运行状态,可以向该进程发送
SIGCONT
信号。这样进程就会从停止状态恢复到运行状态,继续执行
kill指令—向进程发送信号
在Linux系统中,kill
指令用于向进程发送信号。通过kill
指令,可以向指定的进程发送不同的信号,从而影响进程的行为。常用的kill
指令格式如下:
kill [options] <PID>
其中,<PID>
是要发送信号的进程的进程ID(Process ID)。可以使用ps
指令或者pgrep
指令来查找进程的进程ID。
-
-9
:发送SIGKILL信号,强制终止进程。 -
-15
(或不加选项):发送SIGTERM信号,请求进程正常终止。 -
kill -l
是用来列出系统支持的信号列表的命令 -
-19
SIGSTOP
(编号为19):发送SIGSTOP
信号会使进程停止执行,进程将被挂起,直到接收到SIGCONT
信号继续执行。SIGSTOP
信号不能被捕获、忽略或阻塞,是一种强制停止进程的信号。 -
-18
SIGCONT
(编号为18):发送SIGCONT
信号会使之前被停止的进程继续执行。这个信号用于恢复被SIGSTOP
或者类似信号暂停的进程的执行。
死亡状态
对应的就是我们之前讲解的终止状态
在Linux系统中,"死亡状态(dead)"通常指的是进程已经终止(terminated)并且退出,但其进程描述符(process descriptor)还未被释放。这种状态通常在进程终止后,其父进程还未对其进行处理或回收资源时出现。
当一个进程终止后,其进程描述符会保留一段时间,直到父进程调用
wait()
或waitpid()
等系统调用来回收子进程的资源。在这段时间内,进程的状态会被标记为"死亡状态(dead)"。一旦父进程回收了子进程的资源,进程描述符就会被释放,进程完全被清除。因为是一个瞬间的动作,我们很难看到该状态
2.僵尸进程
2.1僵尸状态
在Linux系统中,当一个进程终止后,其进程描述符==(PCB并不会立即被释放)。此时,该进程会变成一个僵尸进程(Zombie Process)。僵尸进程是指一个进程已经终止==,但其父进程还未对其进行处理或回收资源。
在Linux系统中,当父进程读取了子进程的退出状态后,子进程的状态会从僵尸状态(Zombie)变为终止状态(Terminated),通常用X表示。这意味着父进程已经处理了子进程的退出状态信息,并且子进程的资源已经被释放,不再占用系统资源。因此,及时处理子进程的退出状态是非常重要的,可以避免僵尸进程的积累,提高系统的稳定性和性能。
而bash会自动读取子进程的退出状态
2.2僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
2.3僵尸进程危害
- 进程的退出状态对于父进程来说很重要,因为它告诉父进程子进程执行任务的结果。如果父进程不读取子进程的退出状态,子进程就会一直处于僵尸(Zombie)状态。
- 退出状态是需要被维护的数据,保存在进程控制块(task_struct或PCB)中。因此,即使进程处于僵尸状态,其退出状态信息也需要被维护(PCB不会被释放,进程控制块(task_struct或PCB)中保存了进程的重要信息)。
- 如果一个父进程创建了很多子进程但不回收它们,会导致内存资源的浪费。因为每个进程都有自己的进程控制块,不回收子进程会导致内存泄漏。
3.孤儿进程
孤儿进程是指父进程先于子进程结束而结束,导致子进程成为孤儿进程。在Linux系统中,孤儿进程会被init进程(进程ID为1的进程)接管。当父进程先于子进程结束时,子进程的父进程ID会被修改为1,即init进程的进程ID,这样子进程就成为了孤儿进程。
- 孤儿进程的父进程ID会被修改为1,即init进程的进程ID。
- 孤儿进程会被init进程接管,init进程会负责回收孤儿进程的资源。
- 孤儿进程的父进程结束后,其父进程ID会被修改为1,但其依然可以正常运行,直到自己结束或被init进程接管。
- 这种领养机制保证了即使父进程终止,子进程仍然能够正常运行并被系统管理
孤儿进程的产生通常发生在父进程没有等待子进程结束就提前结束的情况下。为了避免产生孤儿进程,父进程在创建子进程后应该等待子进程结束,并及时处理子进程的终止状态。这样可以确保子进程在父进程结束时能够正常退出,而不会成为孤儿进程。
4.进程的优先级
概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
系统通常会根据进程的实际需求和系统负载来动态调整优先级。较高优先级的进程将更频繁地获得CPU时间片,从而更快地执行,而较低优先级的进程可能会等待更长的时间才能获得CPU执行时间。
- Linux中优先级默认是80
- Linux优先级是可以被修改的,Linux的优先级的范围 [60,99],
- 数字越小,优先级越高
task_struct
{
//...
int PRI;
int nice;
//...
};
查看进程优先级
ps -al
-a
选项:显示所有进程,包括其他用户的进程。默认情况下,ps
命令只显示当前用户的进程,使用-a
选项可以显示所有用户的进程。
-l
选项:以长格式显示进程信息。长格式包括更多的字段,如进程状态、进程 ID、父进程 ID、优先级、CPU 使用情况、内存使用情况等。
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI(优先级)和NI(nice)
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
nice
值的确是影响进程优先级的修正因子,通过调整nice
值,可以间接地影响进程的优先级,从而影响其在CPU上的执行顺序。PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
为什么要有-20到19的这个限制?
- 如果没有这个限制,那么就有人会把自己使用的优先级调的特别特别高,把别人的进程调低,这样显然不好
- 进程饥饿问题是指在多任务系统中,优先级较高的进程持续占用CPU资源,导致优先级较低的进程无法及时获得CPU时间片,从而长时间处于等待状态,无法执行。这种情况下,低优先级的进程可能会长时间等待CPU资源,无法完成其任务,造成资源浪费和系统性能下降
更改nice来间接改变优先级
top:进入top后按“r”–>输入进程PID–>输入nice值
使用
top
命令可以实时监视系统的运行情况,包括进程的资源占用情况、进程列表等。按下r
键后,可以对指定进程的优先级进行调整。在按下
r
键后,按照提示输入要调整优先级的进程的PID,然后输入要为其设置的新的nice
值。输入完毕后,top
将会尝试修改指定进程的优先级,根据新的nice
值重新计算其优先级。这样可以实现对指定进程执行优先级的调整。
5.进程其他重要概念
-
竞争性:指系统中进程的数量多于可用的CPU资源。由于资源有限,进程之间会竞争CPU、内存、IO等资源。为了高效地完成任务,进程之间需要根据优先级进行竞争,以便合理地分配资源并提高系统性能。
-
独立性:多进程运行时,每个进程都有自己的内存空间和执行环境,彼此之间相互独立,互不干扰。这意味着一个进程的错误或异常不会直接影响其他进程的正常运行,提高了系统的稳定性和可靠性。
-
并行:多个进程在多个CPU上同时执行(我们一般遇不到),每个CPU负责处理一个或多个进程。这样可以加快任务的执行速度,提高系统的吞吐量和性能。
-
并发:多个进程在单个CPU上交替执行,通过进程切换的方式,让多个进程在一段时间内都得以推进。虽然在任意时刻只有一个进程在CPU上执行,但由于进程切换的快速性,给人的感觉就像是多个进程在同时执行一样。并发可以提高系统的响应速度和资源利用率。
现代操作系统采用时间片轮转的方式来调度进程执行,而不是等待一个进程的代码完全执行完毕后再切换到下一个进程。这种方式能够实现多任务的并发执行,提高系统的响应速度和资源利用率。
- 时间片(Time Slice): 时间片是操作系统分配给每个进程的执行时间段。一旦一个进程的时间片用完,操作系统就会将CPU的控制权交给另一个就绪状态的进程,从而实现进程的切换和并发执行。
- 轮转调度(Round-Robin Scheduling): 时间片轮转调度算法是一种简单而高效的调度算法,它会将就绪队列中的进程按照先来先服务的原则排队,并为每个进程分配一个固定长度的时间片。当一个进程的时间片用完后,操作系统会将其移到队列的末尾,然后继续执行下一个就绪进程。
6.进程的切换与调度
进程的切换与调度是操作系统中非常重要的部分,它涉及到如何有效地利用CPU资源,保证系统的响应速度和吞吐量。
进程切换
进程切换指的是从一个正在执行的进程切换到另一个进程的过程。当操作系统决定将CPU的控制权从当前进程转移到另一个进程时,就需要进行进程切换。进程切换包括以下几个关键步骤:
-
上下文保存: 当操作系统决定要切换到另一个进程时,首先需要保存当前进程的上下文信息,包括程序计数器、寄存器内容、栈指针等。这些信息存储在进程的控制块(PCB)中。
-
选择新进程: 在确定要切换到哪个新进程之前,操作系统会根据调度算法从就绪队列中选择一个合适的进程。这个选择可能基于进程的优先级、先到先服务(FIFO)、轮转法等。
-
加载新进程的上下文: 一旦确定了新进程,操作系统就会从其对应的PCB中恢复该进程的上下文信息。这包括将新进程的程序计数器值加载到CPU中,以便执行新进程的代码。
进程调度
进程调度是操作系统根据一定的调度策略从就绪队列中选择下一个要执行的进程的过程。调度策略的选择会影响系统的性能、响应速度和资源利用率。
-
进程队列数组
queue[140]
:这个数组用于存储不同优先级的进程队列。每个队列按照先进先出(FIFO)规则进行排队调度。数组的下标表示进程的优先级,因此可以直接根据优先级来访问对应的进程队列,提高了访问效率。 -
进程队列状态位图
bitmap[5]
:为了快速判断哪些队列是非空的,使用了一个位图来表示每个队列的状态。每个比特位对应一个队列,如果该队列非空,则对应的比特位为1;否则为0。这样,查找非空队列的操作变得高效,时间复杂度为常数级别。 -
active指针和expired指针:这两个指针用于指示当前活跃队列和过期队列。随着调度的进行,它们的内容可以交换,从而实现活跃队列和过期队列的动态切换。
-
活跃队列和过期队列:活跃队列中包含当前活跃的进程,而过期队列包含一段时间内未被调度的进程。Linux 内核根据需要从活跃队列和过期队列中选择进程进行调度,以平衡优先级和资源利用效率。
活动队列
时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
从该结构中,选择一个最合适的进程,过程是怎么样呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程、
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
- O(1) 调度算法:Linux 内核的调度器通常采用 O(1) 调度算法(使用了位图(bitmap)来实现),该算法在常数时间内选择下一个要执行的进程,而不受进程数量的影响。这确保了调度器的高效性,使得系统在任何负载情况下都能快速响应。
好啦,进程相关的知识,也大概梳理完毕了。