文章目录
前言
进程的概念
- 课本概念:程序的一个执行示例,正在执行的程序等。
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
操作系统是怎么进行进程管理的呢
一、先描述
-
进程创建与撤销
• 创建
• 当有新的程序需要运行或者系统进行初始化时,操作系统就开始了进程的创建工作。就像盖房子一样,首先要为这个“进程房子”规划出一块合适的“土地”,也就是分配内存空间。然后,还要为这个进程建立一个详细的“档案”,即初始化进程控制块(PCB),它记录了进程的所有关键信息,比如进程的身份证号(标识符)、当前的“人生状态”(状态)、在众多进程中的“优先级”、执行到了哪一步(程序计数器)、各个“器官”(寄存器)的状态以及能活动的“范围”(内存界限)等。
• 撤销
• 当进程完成了它的使命,或者在执行过程中出现了严重错误,操作系统就要进行“销户”操作。这就像拆除一个已经废弃的建筑,不仅要收回这个进程曾经占用的内存“土地”,还要关闭它打开的文件(相当于这个进程的“门”和“窗”)以及其他资源,最后把这个进程从系统的“户籍簿”(进程列表)中删除。 -
进程调度
• 调度算法选择
• 操作系统就像一个交通警察,指挥着众多进程车辆在 CPU 这条道路上行驶。为了保证交通的有序和高效,它采用了不同的调度算法。比如先来先服务(FCFS),就像排队买东西,先到的先被服务;短作业优先(SJF),则是优先照顾那些执行时间短的进程;时间片轮转就像给每个进程分配一个固定的开车时间,时间一到就换其他进程;优先级调度则是给重要的进程赋予更高的优先级,让它们优先通过。
• 上下文切换
• 当操作系统决定切换进程时,就像在舞台上换演员一样,需要进行上下文切换。首先要把当前正在表演的演员(进程)的表演状态(上下文,包括程序计数器、寄存器值等)保存起来,然后把下一个演员(进程)的表演状态加载进来,这样新的演员(进程)就可以在舞台(CPU)上继续表演了。 -
进程状态管理
• 进程状态定义
• 进程在它的“生命旅程”中会经历不同的状态。就绪状态就像是运动员已经做好了准备,站在起跑线前,只等发令枪响(CPU 资源分配)就可以开始奔跑;运行状态就是运动员正在赛道上奋力奔跑;阻塞状态则是运动员在跑步过程中遇到了一些问题(比如等待 I/O 操作完成),暂时停下来等待问题解决。
• 状态转换机制
• 操作系统时刻关注着进程的状态变化。比如当一个进程等待的 I/O 操作完成了,就像运动员解决了阻碍他前进的问题,操作系统会把这个进程从阻塞状态转换为就绪状态;当一个正在运行的进程的时间片用完了,就像运动员跑了规定的时间,它就会从运行状态转换为就绪状态。 -
进程同步与互斥
• 同步机制
• 多个进程之间的协作就像一场交响乐演奏,需要同步机制来协调它们的执行顺序。比如信号量和管程就是指挥棒,在生产者 - 消费者问题中,信号量可以确保生产者在缓冲区未满时才能生产,消费者在缓冲区非空时才能消费,这样整个演奏过程才能和谐有序。
• 互斥机制
• 当多个进程都想使用同一个共享资源时,就像多个人都想使用同一把钥匙打开一扇门,这时就需要互斥机制来保证它们不会相互冲突。互斥锁就像是这把钥匙,当一个进程获取了互斥锁后,其他进程就只能等待,直到这个进程释放锁。
二、再组织
操作系统对进程的管理是一个复杂但有序的过程:
在进程的整个生命周期中,创建和撤销是起始和结束的关键环节。创建时精心分配资源和初始化信息,撤销时彻底回收资源和清理痕迹。
进程调度是核心,通过合理选择调度算法,确保 CPU 资源能被高效利用,而上下文切换保证了不同进程之间的无缝切换,让每个进程都能适时得到执行机会。
进程状态管理明确了进程在不同阶段的状态,并且通过状态转换机制,根据进程执行过程中的不同情况(如 I/O 操作完成、时间片用完等)灵活调整状态。
最后,进程同步与互斥机制确保了多个进程在协作和共享资源时的正确性和稳定性。同步机制协调执行顺序,互斥机制避免资源冲突,它们共同保证了系统的正常运行。这几个方面相互配合,构成了操作系统对进程进行有效管理的整体框架。
对于操作系统是如何对进程进行管理的,简单来说就是:先描述,再组织。
描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct
task_struct是PCB的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
以下是一个简化的task_struct结构体的定义示例,展示了一些关键字段:
struct task_struct {
volatile long state; // 进程状态
pid_t pid; // 进程标识符
pid_t tgid; // 线程组标识符
unsigned int flags; // 进程标志
int prio; // 动态优先级
int static_prio; // 静态优先级
int normal_prio; // 正常优先级
unsigned int rt_priority; // 实时优先级
struct mm_struct *mm; // 内存描述符
struct task_struct *parent; // 父进程
struct list_head children; // 子进程列表
struct list_head sibling; // 兄弟进程列表
// ... 其他字段
};
- task_struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据(这个在后面介绍进程替换的时候会着重介绍一下)。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
介绍完上面的内容,以下是对进程更进一步的理解:

所有运行在系统里的进程都以task_struct链表的形式存在内核里。
查看进程
介绍了上面的理论知识,下面来在Linux中具体看看进程:
进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

大多数进程信息同样可以使用top和ps这些用户级工具来获取,下面写一代码,我们查看一下它的进程标识符。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// 获取该进程的PID
pid_t id = getpid();
// 获取该进程父进程的PID
pid_t fid = getppid();
while (1)
{
printf("I am a process , pid = %d, ppid = %d\n", id, fid);
sleep(1);
}
return 0;
}

这里演示了一遍进程的创建到运行再到终止的过程。

从这里可以看出,每次运行进程的时候,进程的PID都不会相同,这是因为每次创建进程都需要将程序重新加载到内存中,所以就得重新分配资源。我们运行一遍程序,看看它在 /proc下是否被创建:

再看看它里面包含了哪些信息:

图只截了其中一部分,可以看到,在我们运行一个程序时,系统会在在/proc中以该进程的PID为名,创建一个目录,该目录下存放着管理该进程的信息。这里介绍一下两个比较好理解的:

- cwd
当前工作目录:记录了该进程的路径
举个例子:在C语言中,进行文件操作时:fopen("file.txt", "w")如果该文件不存在,就会在当前路径下创建一个文件。这里的当前路径指的就是cwd,如果我们更改了当前工作目录,那么文件就会创建在我们修改的路径下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = getpid();
printf("pid = %d\n", id);
printf("15秒后修改工作路径\n");
sleep(15);
// 将当前路径修改为上级目录下的lesson5路径下
chdir("../lesson5");
printf("5秒后创建文件\n");
sleep(5);
FILE* fp = fopen("file.txt", "w");
if (fp == NULL)
return 1;
return 0;
}

- exe
记录了当前进程是由那个程序加载到内存中的
下面我做一个测试,该进程还在运行,我将该进程在磁盘中进行删除:

可以看到,当我们在磁盘中将可执行程序进行删除以后,在运行的进程并不会受到影响,只不过当我们再参看 /proc/21108 时,发现exe那一行提示我们可执行程序已被删除。
这里可以看出,其实我们在运行的进程是由磁盘中的可执行程序拷贝到内存中运行的,所以我们在删除磁盘中的可执行文件时,进程还能够继续运行。
从上面可以看到,虽然进程的PID一直都在改变,但是PPID却没有改变,我们可以看一下PID为 PPID 的进程是什么:

关于bash在后面再介绍,这里先说一下,bash进程其实就是与用户交互的命令行(是shell的其中一种)。
通过系统调用创建进程
初识fork调用
fork 是 Unix 和 Linux 操作系统中一个非常重要的系统调用,用于创建一个新的进程。新创建的进程称为“子进程”(child process),而调用 fork 的进程称为“父进程”(parent process)。fork 是在多任务操作系统中实现并发执行的基础。
基本概念
fork 系统调用的作用是创建一个新的进程,它会复制调用进程的整个执行环境,包括内存、文件描述符、进程状态等,因此子进程与父进程几乎完全相同。
当调用 fork() 时,会发生以下几点:
- 创建一个新的子进程,该子进程是父进程的副本。
- 子进程和父进程在代码上完全相同,只是子进程有一个新的进程 ID(PID)。
- 子进程将从
fork()返回的地方开始执行。 - 在父进程中,
fork()返回子进程的 PID。 - 在子进程中,
fork()返回 0。
fork() 的返回值
fork() 的返回值可以分为三种情况:
- 负值:如果返回负值,表示
fork()失败,没有创建新进程。 - 0:在子进程中,
fork()返回 0,表示当前是子进程。 - 正值:在父进程中,
fork()返回子进程的 PID(正整数),表示当前是父进程。
fork 的工作原理
当 fork 被调用时,操作系统会做以下工作:
- 为新进程分配一个唯一的进程 ID。
- 复制父进程的虚拟内存空间,创建子进程的地址空间。
- 复制父进程打开的文件描述符、堆栈、代码段等,子进程的这些资源与父进程相互独立。
- 返回不同的值以区分父进程和子进程。
- 子进程是父进程的几乎完全副本,但有一些不同之处:
- 子进程有自己的进程 ID。
- 子进程的 ppid(父进程 ID)是父进程的 PID。
- 子进程和父进程的资源(如内存)是独立的。
举个例子:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid;
// 调用 fork() 创建子进程
pid = fork();
// fork之后通常使用if语句来进行分流
if (pid < 0) {
// fork 失败
fprintf(stderr, "Fork Failed\n");
return 1;
}
else if (pid == 0) {
// 这是子进程
printf("This is the Child Process. PID: %d\n", getpid());
}
else {
// 这是父进程
printf("This is the Parent Process. PID: %d, Child PID: %d\n", getpid(), pid);
}
return 0;
}
- pid = fork() 调用后,如果 pid 小于 0,表示 fork 失败。
- 如果 pid == 0,表示当前是在子进程中执行的代码。
- 如果 pid > 0,表示当前是在父进程中执行的代码。
进程状态
进程排队
在操作系统中,进程排队是一种调度机制,用于管理系统中大量等待执行的进程。排队机制能够合理地分配有限的 CPU 资源,使得多个进程在多任务环境中有序地执行。进程排队通常通过不同的队列来实现,这些队列按照进程状态和优先级等因素进行组织。
进程排队的类型
根据进程的状态和调度的阶段,排队通常可以分为以下几种常见的队列:
就绪队列(Ready Queue)
- 描述:存放所有已经准备好但尚未分配 CPU 的进程。这些进程在等待 CPU 调度器的选择。
- 特点:当 CPU 空闲时,就绪队列中的某个进程会被选中进入运行状态。
- 位置:一般在内存中维护。
运行队列(Running Queue)
- 描述:包含当前正在运行的进程。通常,系统中同时运行的进程数等于 CPU 核心数。
- 特点:进程在运行时会占用 CPU,时间片耗尽或者被阻塞后会离开运行队列。
在Linux中,运行队列和就绪队列就相当于就合二为一了,都是等待排在CPU的资源队列中,等待CPU进行调度

阻塞队列(Wait Queue 或 Blocked Queue)
- 描述:存放所有处于等待某个事件(如 I/O 完成、资源可用)的进程。
- 特点:当等待的事件完成时,进程从等待队列移回就绪队列。
- 位置:也在内存中维护。
阻塞队列一般是在排在除了CPU外的其他设备的队列中,等待资源。

挂起队列(Suspended Queue)
描述:存放被暂停的进程,这些进程不会立即被调度运行。
特点:通常是由于资源限制(如内存不足)或者用户手动暂停。
位置:进程可能会被移动到磁盘上存储。
状态类型
在进程状态这里,首先先介绍一下总体的三大类状态是什么样的:
运行状态
运行状态表示进程正在使用 CPU 进行执行,或者已经准备好运行,只要 CPU 空闲下来就会执行这个进程。它是进程调度中最活跃的状态之一。
条件:
- 进程已经获得 CPU 的调度权,正在被 CPU 执行。
- 进程准备好运行(在运行队列中的进程),等待被调度器分配 CPU。
运行状态的切换:
- 进入运行状态:进程被创建并准备好运行,或者从其他状态(例如阻塞状态或挂起状态)转换到运行状态。
- 退出运行状态:进程的时间片耗尽、被高优先级进程抢占,或者进程进入睡眠等待某个事件。
阻塞状态
阻塞状态表示进程在等待某个外部事件的发生(例如 I/O 操作完成、资源可用等),而无法继续执行。这是一种被动的状态,表示进程暂停执行,直到所等待的条件满足。
条件:
- 进程等待外部事件(如磁盘 I/O、网络 I/O)的完成。
- 进程请求某些资源(如内存、锁)而资源暂时不可用。
阻塞状态的切换
- 进入阻塞状态:当进程发起 I/O 请求、请求资源或等待某个条件时。
- 退出阻塞状态:当等待的条件满足(例如 I/O 完成、资源可用)时,进程会从阻塞状态转为就绪状态,等待 CPU 调度
挂起状态
挂起状态表示进程被暂停或停止执行,但它的当前状态和资源仍然保存在内存中。挂起状态通常是由用户或调试器手动触发,用于在某些情况下临时中止进程执行。
条件:
- 进程接收到暂停信号(例如 SIGSTOP)。
- 用户在终端中使用 Ctrl + Z,将进程从前台移到后台并暂停。
- 调试器在调试进程时,暂停进程以检查或更改状态。
挂起状态的切换:
- 进入挂起状态:进程收到 SIGSTOP 或其他暂停信号。
- 退出挂起状态:进程收到恢复信号(如 SIGCONT),将返回之前的状态(通常是运行状态或阻塞状态)。
每种状态触发的条件
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 或 Runnable)
- 描述:进程正在使用 CPU 或者准备使用 CPU 进行执行。
- 状态标记:R
- 触发条件:
- 进程创建后进入就绪队列:当进程被创建并准备好执行时,它会被放入就绪队列,标记为 R。
- 进程获得 CPU 调度:当调度器选择一个就绪的进程并为其分配 CPU 时,进程状态变为 R。
- 等待事件完成后:某些等待的事件完成后,进程会从等待状态(S 或 D)变为就绪状态,准备运行。
S - 可中断睡眠状态(Sleeping)
- 描述:进程在等待某个事件(如 I/O 操作或资源)完成,可以被信号打断。
- 状态标记:S
- 触发条件:
- 等待 I/O 操作:进程发起 I/O 操作(如读取文件或等待网络数据)时会进入 S 状态。
- 等待资源:进程请求某些资源(如内存、锁)但资源不可用时,会进入 S 状态等待资源释放。
- 系统调用阻塞:某些阻塞型系统调用(如 sleep())会让进程进入 S 状态。
D - 不可中断睡眠状态(Uninterruptible Sleep)
- 描述:进程在等待无法被信号中断的事件(通常是内核层面的硬件操作或磁盘 I/O 操作)。
- 状态标记:D
- 触发条件:
- 硬件操作:进程在等待某些硬件设备操作(如磁盘 I/O)的完成,这些操作不能被打断。
- 内核资源锁定:进程持有内核资源锁时进入 D 状态,以确保某些关键操作的原子性和一致性。
T - 停止状态(Stopped 或 Traced)
- 描述:进程被暂停或停止执行,通常是通过接收特定信号。
- 状态标记:T
- 触发条件:
- 接收到 SIGSTOP 信号:当进程收到 SIGSTOP 信号时,会立即停止执行,状态变为 T。
- 接收到 SIGTSTP 信号:例如用户在终端中按下 Ctrl + Z,会发送 SIGTSTP 信号,导致前台进程暂停。
- 被调试器暂停:进程被调试器(如 gdb)控制和跟踪时,会进入 T 状态。
t - 被跟踪或调试状态(Traced)
- 描述:进程被调试器(如 gdb)暂停,通常用于调试目的。
- 状态标记:t
- 触发条件:
- 进入调试模式:当进程被调试器跟踪时,状态会变为 t。
- 进程暂停等待调试命令:当调试器发出暂停命令或在断点处暂停时,进程状态为 t。
- 调试器检查状态:进程在等待调试器的进一步指令时,会保持在 t 状态。
X - 终止状态(Dead 或 Exit)
- 描述:进程已经结束并退出,但可能由于异常原因,进程没有正常退出。这种状态通常不会在 ps 等常用命令中显示。
- 状态标记:X
- 触发条件:
- 进程异常终止:进程由于严重的系统错误或程序异常而突然终止时,可能会进入 X 状态。
- 内核错误:进程在内核模式下发生致命错误(如非法访问内存)时,可能导致 X 状态。
Z - 僵尸状态(Zombie)
- 描述:进程已经结束运行,但其进程表条目仍保留,等待父进程读取其退出状态。
- 状态标记:Z
- 触发条件:
- 进程正常终止:进程完成执行并调用 exit() 终止,但父进程尚未调用 wait() 或 waitpid() 获取其退出状态。
- 父进程未处理子进程的状态:当父进程没有及时获取子进程的退出状态时,子进程会进入 Z 状态,成为僵尸进程。
僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
- 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
下面用一段代码来测试一下:


可以看到进程开始运行时就创建出了两个进程,目前两个进程都再运行

5秒过后,根据代码子进程要退出了,但父进程还在继续执行,没有去读取子进程的退出状态,所以导致子进程在这里一直处于僵尸状态。
僵尸进程的危害:
-
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),父进程交给子进程的任务,子进程办的怎么样了,需要给父进程返回一下。可父进程如果一直不读取,那子进程就一直处于Z状态
-
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
-
父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,所以如果一直不处理僵尸进程就会导致内存泄漏
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init进程领养,当然要由init进程回收喽。
我们写段代码来看一下:


进程开始运行时,父子进程同时运行,5秒过后,父进程退出,子进程继续执行

此时可以看到子进程的ppid变成了1,说明它的父进程变为了init进程。
进程优先级
基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
查看进程优先级的命令

我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
PRI & NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice(此处的PRI(old)代表的是默认PRI,并不是修改前的上一次PRI值)
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别,那么PRI的取值范围就是[60,99]
- 需要注意的是,进程的nice值不是进程的优先级,它们不是一个概念,但是进程的 nice 值会影响到进程的优先级变化。可以理解为 nice 值是进程优先级的修正数据
用top命令更改已存在进程的nice值:
- top
- 进入top后按 “r” 再输入进程PID 再输入nice值,就能更改进程的优先级了

不过这里需要注意的是,修改优先级得需要拥有超级用户的权限。
进程优先级的影响
- CPU 使用时间:高优先级进程会更频繁地被 CPU 调度,因此会获得更多的 CPU 时间。
- 响应时间:高优先级进程会更快响应用户或系统的请求。
- 系统负载:如果大量高优先级进程占用 CPU,低优先级进程可能得不到足够的执行机会,甚至导致“饥饿”问题。
- 公平性:完全公平调度器(CFS)尝试平衡 CPU 时间,使得低优先级进程也能获得执行机会。
上下文切换
在操作系统中,进程上下文切换(context switch)指的是当 CPU 从一个进程切换到另一个进程时,保存当前进程的状态并加载另一个进程的状态,以便新进程可以从中断的地方继续执行。这种上下文切换确保了多任务操作系统可以在多个进程之间公平地分配 CPU 资源。
上下文数据的内容
上下文数据包含了进程的执行状态和当前 CPU 运行环境的内容,包括:
- 通用寄存器:如程序计数器(PC)、栈指针(SP)、基址指针(BP)、以及其他通用寄存器。
- 程序状态字(PSW):用于存储 CPU 的状态信息,如条件标志(例如溢出、进位、零标志等)。
- 内存管理信息:如页表基址寄存器,用于虚拟内存映射。
- 其他控制信息:如 I/O 状态、文件描述符表等。
进程上下文切换的步骤
-
保存当前进程的上下文:
- 当进程的时间片用完、被其他进程抢占或进入阻塞状态时,内核会先保存该进程的上下文。
- 内核将当前进程的寄存器值、程序计数器等信息保存到其进程控制块(PCB)中。
-
加载新进程的上下文:
- 通过调度算法(如 CFS、O(1) 等),内核选择下一个准备运行的进程。
- 内核从该进程的 PCB 中读取并恢复其上下文数据,将各寄存器和程序计数器等恢复到新进程的状态。
-
执行新进程:
- 通过恢复程序计数器(PC)等关键信息,CPU 开始从新进程的中断点处继续执行。
上下文切换的机制
- 中断或系统调用触发:上下文切换通常由中断、系统调用或进程自愿让出 CPU(如 sleep)触发。中断发生后,控制权从用户态转移到内核态,内核可以选择进行上下文切换。
- 进程控制块(PCB):每个进程都有一个 PCB,用于存储进程状态信息。PCB 是上下文切换的关键数据结构,包含了进程的寄存器内容、程序计数器、栈指针等。
- 硬件支持:现代 CPU 提供专门的指令(如 save 和 restore)用于快速保存和恢复寄存器,提升上下文切换的效率。
上下文切换的性能影响
- 上下文切换是有开销的,因为它需要 CPU 花费时间来保存和恢复状态。频繁的上下文切换会导致 “上下文切换开销” 问题,从而影响系统的整体性能。为此,操作系统会尽量减少不必要的上下文切换,比如通过优先级调度策略来减少频繁切换带来的开销。
- 通过 PCB 的使用和硬件支持,操作系统能够在进程切换时保证上下文数据的完整性,使得每个进程可以独立地从其停止的地方继续执行。
Linux实现进程调度的算法(了解)
Linux 的 O(1) 调度器是一种早期的进程调度算法,在 2.6 内核版本中作为默认调度器使用。它的主要特点是能够在常数时间内完成调度决策,因此其时间复杂度为 O(1)。这种调度器设计的目的是在不论系统中运行多少进程的情况下,都能提供高效的调度性能。
O(1) 调度器的主要功能是通过优先级队列结构来实现快速的进程调度,以适应多任务环境。

O(1) 调度器的基本结构
-
优先级队列:O(1) 调度器使用 140 个优先级级别对所有进程进行分类,其中:
-
0 到 99 是 实时进程,优先级越高(数值越小),进程获得的调度优先权越高。
100 到 139 是 普通进程,使用 Nice 值来确定优先级(Nice 值越低,优先级越高)。
双队列机制:为了管理进程的时间片,O(1) 调度器使用了 活动队列(Active Queue) 和 过期队列(Expired Queue)。 -
活动队列:存放所有时间片未耗尽的进程,调度器从该队列中选择下一个要执行的进程。
-
过期队列:存放所有时间片耗尽的进程。当活动队列中的进程时间片用完后会被移至过期队列。
-
时间片分配:进程的时间片根据优先级分配,优先级越高的进程时间片越长,从而可以获得更多的 CPU 资源。
O(1) 调度器的主要工作流程
- 初始化进程的时间片
当一个进程被创建时,O(1) 调度器会根据其优先级初始化其时间片。
对于普通进程,Nice 值影响其时间片长度。Nice 值越低,分配的时间片越多,优先级越高。
实时进程的时间片固定,但调度优先级高于普通进程。 - 选择下一个运行的进程
调度器在活动队列中查找具有最高优先级的进程,并将其调度到 CPU 上执行。
由于每个优先级都有一个独立的队列,调度器可以在 O(1) 时间内找到最高优先级的队列,直接将该优先级的第一个进程选为运行进程。 - 时间片耗尽或进程阻塞
如果进程的时间片用尽或它被阻塞(如等待 I/O 事件),O(1) 调度器会将其移到过期队列。
如果进程主动进入睡眠(如等待 I/O 操作完成),则在重新唤醒时会被重新分配时间片并移回到活动队列中。 - 活动队列与过期队列的交换
当活动队列中的所有进程都已用完时间片(即活动队列为空),调度器会交换活动队列和过期队列。此时,所有进程都重新获得分配的时间片。
这种双队列结构确保每个进程都有公平的机会被调度。 - 优先级调整
对于普通进程,调度器可能根据其等待时间调整其优先级,以平衡系统的响应时间。
例如,进程等待的时间越长,调度器可能会略微提升其优先级,增加其获得 CPU 的机会,以防止“饥饿”现象。
O(1) 调度器的调度策略
O(1) 调度器支持两种调度策略:实时调度 和 普通调度。
实时调度策略
FIFO(First In, First Out):实时进程按照优先级排队,一旦获得 CPU 将一直运行到其主动释放或被更高优先级的实时进程抢占。
RR(Round Robin):实时进程按照优先级进行轮转,每个进程在其优先级队列中按照固定时间片轮流执行。
普通调度策略
普通进程使用优先级调度和时间片轮转调度相结合的策略。Nice 值的高低会影响普通进程的优先级,从而影响它们的时间片分配。
O(1) 调度器的优缺点
优点
高效的调度决策:由于使用了优先级队列和双队列机制,O(1) 调度器可以在常数时间内完成调度决策,适用于多任务场景。
公平性:通过动态调整进程的优先级,防止进程长期得不到 CPU,避免饥饿现象。
双队列管理:活动队列和过期队列的设计确保了进程有足够的时间片进行轮转,保证了所有进程有公平的执行机会。
缺点
公平性不足:在多核系统和负载较高的环境中,O(1) 调度器的公平性和效率可能不足,特别是在需要高响应性的场景中。
实时性不够强:尽管支持实时进程的 FIFO 和 RR 调度,但 O(1) 调度器难以满足严格的实时性需求,无法处理复杂的优先级调整和时间片分配。
设计复杂:双队列的设计较为复杂,在某些情况下会增加系统负担。
24万+

被折叠的 条评论
为什么被折叠?



