Linux CPU调度器深入解析
1. CPU调度器入口点总结
CPU调度器的
schedule()
函数在以下几种情况下会被调用:
-
显式阻塞调用
:当进程或线程进行显式的阻塞调用时,由于调用者需要等待某个事件,阻塞API最终会调用
schedule()
。当事件发生时,内核或使其进入睡眠状态的驱动程序会将其唤醒。
-
用户模式定时器管理抢占
:满足以下三个条件之一时会触发:
- 当前任务是否超过了其时间片(有效时间片),超过量达到
min_granularity_ns
。
- 新创建或刚唤醒的任务优先级是否高于当前正在执行内核代码路径的任务。
- CFS红黑树中是否有任务的虚拟运行时间(vruntime)低于当前任务,即当前任务是否不再是该树的最左叶子节点。
-
定时器(软中断)管理
:在定时器软中断上下文函数
scheduler_tick()
中检查是否需要设置
TIF_NEED_RESCHED
位,但不会在此处调用
schedule()
。
-
机会点检查
:在以下进程上下文的“机会点”检查
TIF_NEED_RESCHED
位是否设置:
- 系统调用返回路径。
- 中断返回路径。
-
其他调度激活点
:
- 任何对
schedule()
的调用。
- 任何可能导致调用
schedule()
的
cond_resched*()
调用。
- 内核中从不可抢占模式切换到可抢占模式的情况。
-
任务唤醒
:当有任务被添加到运行队列时,判断该任务是否需要抢占当前任务。如果需要,设置
TIF_NEED_RESCHED
,并在下次机会点调用
schedule()
:
-
可抢占内核
:在任何内核抢占点(如调用
preempt_enable()
,如解锁自旋锁),包括系统调用返回和硬件中断返回时。
-
不可抢占内核
:在系统调用返回用户空间、中断返回用户空间、任何
cond_resched*()
调用或显式调用
schedule()
时。
以下是这些情况的总结表格:
| 触发情况 | 详细描述 |
| — | — |
| 显式阻塞调用 | 进程或线程进行阻塞调用,等待事件时调用
schedule()
|
| 用户模式定时器管理抢占 | 满足三个特定条件之一时触发 |
| 定时器(软中断)管理 | 检查
TIF_NEED_RESCHED
位,但不调用
schedule()
|
| 机会点检查 | 系统调用和中断返回路径检查
TIF_NEED_RESCHED
位 |
| 其他调度激活点 | 调用
schedule()
、
cond_resched*()
或模式切换时 |
| 任务唤醒 | 根据内核类型在不同时机调用
schedule()
|
2. 核心调度器代码简述
核心调度器代码由当前线程在进程上下文中运行,最终通过上下文切换将自己从CPU上移除。
__schedule()
函数中有两个重要的局部变量
prev
和
next
,
prev
指向当前任务,
next
指向即将进行上下文切换并运行的下一个任务。以下是
__schedule()
函数的部分代码:
static void __sched notrace __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
unsigned long prev_state;
struct rq_flags rf;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr; << this is current ! >>
[ ... ]
next = pick_next_task(rq, prev, &rf);
<< Here we 'pick' the task to run next in an 'object-
oriented' manner, as discussed earlier in detail... >>
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
[ ... ]
if (likely(prev != next)) { << switching to another thread's likely…
>>
[ ... ]
/* Also unlocks the rq: */
rq = context_switch(rq, prev, next, &rf);
[ ... ]
}
3. 上下文切换
上下文切换的工作是在切换到下一个任务之前,保存当前正在执行任务的状态。在Linux中,
kernel/sched/core.c:context_switch()
内联函数完成上下文切换工作,由当前任务(即“前一个”任务)运行其代码。上下文切换主要分为两个与架构密切相关的阶段:
-
内存(MM)切换
:将特定架构的CPU寄存器指向
next
任务的内存描述符结构(
struct mm_struct
)。在x86[_64]架构中,该寄存器是CR3(控制寄存器3);在ARM(AArch32)架构中,是TTBR0(转换表基址寄存器0)。这样做是为了让MMU在
next
任务执行地址转换时引用其页表。
-
实际CPU切换
:通过保存
prev
任务的栈和CPU寄存器状态,并将
next
任务的栈和CPU寄存器状态恢复到处理器,在
switch_to()
宏中完成。这使得
next
任务能够从上次中断的位置继续执行。
以下是上下文切换的mermaid流程图:
graph TD;
A[开始] --> B[内存(MM)切换];
B --> C[实际CPU切换];
C --> D[结束];
4. 处理器隔离
内核提供了一种方法,通过指定内核参数
isolcpus=[flag-list,]<cpu-list>
来隔离一组处理器,使其不受调度器类和SMP负载均衡的影响。
flag-list
默认值为
domain
,指定后,
cpu-list
中的所有CPU核心将与“通用SMP平衡和调度算法”隔离,这对某些实时应用程序很有用。不过,
isolcpus=
内核参数现已被弃用,建议使用更强大灵活的cpusets cgroups(v2)控制器。
5. CPU亲和性掩码的理解、查询和设置
5.1 CPU亲和性掩码概述
任务结构中有一个重要成员——CPU亲和性位掩码(
cpumask_t *cpus_ptr
),它表示线程被允许运行的CPU核心。默认情况下,所有CPU亲和性掩码位都被设置,即线程可以在任何核心上运行。例如,在一个有8个CPU核心的系统中,每个线程的默认CPU亲和性位掩码为二进制
1111 1111
(十六进制
0xff
)。
以下是一个8核系统中CPU亲和性位掩码的示例表格:
| CPU核心编号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| — | — | — | — | — | — | — | — | — |
| 亲和性位 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
此示例中,CPU位掩码值为
0x3f
(二进制
0011 1111
),表示该线程可以在CPU核心0到5上调度,但不能在核心6和7上运行。
5.2 显式设置CPU亲和性掩码的好处
- 减少缓存失效 :确保线程始终在同一CPU核心上运行,可大大减少频繁的缓存失效和不愉快的缓存“跳动”。
- 消除线程迁移成本 :有效消除核心之间的线程迁移成本。
- CPU预留 :保证某个核心专门用于一个线程,确保其他线程不能在该核心上执行,这在一些对时间要求严格的实时系统中很有用。
5.3 查询和设置CPU亲和性掩码的代码实现
可以使用系统调用
sched_getaffinity()
和
sched_setaffinity()
来查询和设置线程的CPU亲和性掩码。以下是示例代码:
#define _GNU_SOURCE
#include <sched.h>
int sched_getaffinity(pid_t pid, size_t cpusetsize,
cpu_set_t *mask);
int sched_setaffinity(pid_t pid, size_t cpusetsize,
const cpu_set_t *mask);
cpu_set_t
类型用于表示CPU亲和性位掩码,需要先使用
CPU_ZERO()
宏将其初始化为零。以下是查询和设置CPU亲和性掩码的具体代码:
// 查询CPU亲和性掩码
static int query_cpu_affinity(pid_t pid)
{
cpu_set_t cpumask;
CPU_ZERO(&cpumask);
if (sched_getaffinity(pid, sizeof(cpu_set_t), &cpumask) < 0) {
perror("sched_getaffinity() failed");
return -1;
}
disp_cpumask(pid, &cpumask, numcores);
return 0;
}
// 设置CPU亲和性掩码
static int set_cpu_affinity(pid_t pid, unsigned long bitmask)
{
cpu_set_t cpumask;
int i;
printf("\nSetting CPU affinity mask for PID %d now...\n", pid);
CPU_ZERO(&cpumask);
for (i = 0; i < sizeof(unsigned long) * 8; i++) {
if ((bitmask >> i) & 1)
CPU_SET(i, &cpumask);
}
if (sched_setaffinity(pid, sizeof(cpu_set_t), &cpumask) < 0) {
perror("sched_setaffinity() failed");
return -1;
}
disp_cpumask(pid, &cpumask, numcores);
return 0;
}
需要注意的是,任何人都可以查询任何任务的CPU亲和性掩码,但只有拥有任务所有权、具有root权限或
CAP_SYS_NICE
能力的用户才能设置它。
Linux CPU调度器深入解析
6. 线程调度策略和优先级的查询与设置
在Linux系统中,线程的调度策略和优先级是影响其执行顺序和资源分配的重要因素。以下是关于查询和设置线程调度策略及优先级的详细介绍。
6.1 相关系统调用
Linux提供了一系列系统调用用于查询和设置线程的调度策略和优先级,其中常用的有
sched_getscheduler
和
sched_setscheduler
。
#include <sched.h>
// 获取线程的调度策略和优先级
int sched_getscheduler(pid_t pid);
// 设置线程的调度策略和优先级
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
-
sched_getscheduler:该函数用于获取指定线程(通过pid指定)的当前调度策略。 -
sched_setscheduler:该函数用于设置指定线程的调度策略和优先级。policy参数指定调度策略,param参数是一个指向sched_param结构体的指针,该结构体包含线程的优先级信息。
6.2 调度策略和优先级的操作步骤
以下是查询和设置线程调度策略及优先级的具体操作步骤:
-
查询线程的调度策略和优先级
-
调用
sched_getscheduler函数获取线程的当前调度策略。 - 根据返回的调度策略值,进一步处理和显示相关信息。
-
调用
#include <stdio.h>
#include <sched.h>
int main() {
pid_t pid = getpid(); // 获取当前线程的PID
int policy = sched_getscheduler(pid);
if (policy == -1) {
perror("sched_getscheduler");
return 1;
}
printf("Current scheduling policy: %d\n", policy);
return 0;
}
-
设置线程的调度策略和优先级
-
定义
sched_param结构体,并设置所需的优先级。 -
调用
sched_setscheduler函数设置线程的调度策略和优先级。
-
定义
#include <stdio.h>
#include <sched.h>
int main() {
pid_t pid = getpid(); // 获取当前线程的PID
struct sched_param param;
param.sched_priority = 50; // 设置优先级为50
int policy = SCHED_FIFO; // 设置调度策略为SCHED_FIFO
if (sched_setscheduler(pid, policy, ¶m) == -1) {
perror("sched_setscheduler");
return 1;
}
printf("Scheduling policy and priority set successfully.\n");
return 0;
}
7. cgroups简介
cgroups(Control Groups)是Linux内核提供的一种机制,用于限制和监控一组进程的资源使用情况,如CPU、内存、磁盘I/O等。通过cgroups,可以对不同的进程组进行资源分配和限制,从而提高系统的稳定性和性能。
7.1 cgroups的基本概念
- 控制组(Control Group) :一组进程的集合,这些进程可以共享相同的资源限制和监控信息。
- 子系统(Subsystem) :用于实现特定资源管理功能的模块,如CPU子系统、内存子系统等。
- 层级(Hierarchy) :cgroups以层级结构组织,每个层级可以包含多个控制组,子控制组可以继承父控制组的资源限制。
7.2 cgroups的使用步骤
以下是使用cgroups进行资源限制的基本步骤:
-
创建cgroup层级
-
使用
cgcreate命令创建一个新的cgroup层级。
-
使用
cgcreate -g cpu,memory:/mygroup
上述命令创建了一个名为
mygroup
的cgroup层级,同时启用了CPU和内存子系统。
-
将进程添加到cgroup
-
使用
cgclassify命令将指定进程添加到创建的cgroup中。
-
使用
cgclassify -g cpu,memory:/mygroup <pid>
其中
<pid>
是要添加到cgroup的进程的PID。
-
设置资源限制
- 通过修改cgroup目录下的相应文件来设置资源限制。例如,设置CPU使用率限制:
echo 50000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us
上述命令将
mygroup
控制组的CPU使用率限制为50%(假设CPU周期为100000微秒)。
8. 运行Linux作为实时操作系统(RTOS)简介
在某些对时间要求严格的应用场景中,需要将Linux系统作为实时操作系统(RTOS)来运行,以确保任务能够在规定的时间内完成。以下是关于将Linux作为RTOS运行的一些关键要点。
8.1 实时调度策略
Linux提供了几种实时调度策略,如
SCHED_FIFO
和
SCHED_RR
,用于满足实时任务的需求。
-
SCHED_FIFO:先进先出调度策略,具有相同优先级的任务按照到达顺序依次执行,高优先级任务可以抢占低优先级任务。 -
SCHED_RR:轮转调度策略,具有相同优先级的任务在时间片内轮流执行,时间片用完后,任务会被放到队列尾部。
8.2 实时内核配置
为了使Linux系统具备更好的实时性能,需要进行一些内核配置。以下是一些常见的配置选项:
- 启用实时补丁 :使用PREEMPT_RT补丁可以将Linux内核转换为实时内核,增强其抢占能力。
-
调整调度参数
:通过修改内核参数,如
kernel.sched_rt_runtime_us和kernel.sched_rt_period_us,可以调整实时任务的时间片和周期。
9. 杂项调度相关主题
除了上述内容外,还有一些其他的调度相关主题值得关注。
9.1 调度器的负载均衡
在多CPU系统中,调度器的负载均衡功能非常重要,它可以确保各个CPU核心的负载均匀分布。调度器会根据任务的优先级、CPU使用率等因素,动态地将任务分配到不同的CPU核心上。
9.2 调度器的性能优化
为了提高调度器的性能,可以采取以下措施:
- 减少调度开销 :通过优化调度算法和数据结构,减少调度过程中的时间和空间开销。
- 提高缓存命中率 :合理安排任务的执行顺序,提高CPU缓存的命中率,减少缓存失效带来的性能损失。
10. 总结
本文深入探讨了Linux CPU调度器的多个方面,包括CPU调度器的入口点、核心调度器代码、上下文切换、处理器隔离、CPU亲和性掩码的查询和设置、线程调度策略和优先级的操作、cgroups的使用、将Linux作为RTOS运行以及杂项调度相关主题。通过对这些内容的学习,我们可以更好地理解Linux系统的调度机制,优化系统性能,满足不同应用场景的需求。
在实际应用中,我们可以根据具体需求选择合适的调度策略和配置,以提高系统的稳定性、实时性和性能。同时,不断关注Linux内核的发展和更新,学习新的调度技术和优化方法,将有助于我们更好地应对各种挑战。
以下是一个总结表格,概括了本文的主要内容:
| 主题 | 主要内容 |
| — | — |
| CPU调度器入口点 | 包括显式阻塞调用、用户模式定时器管理抢占等多种情况 |
| 核心调度器代码 |
__schedule()
函数的工作原理 |
| 上下文切换 | 内存切换和实际CPU切换两个阶段 |
| 处理器隔离 | 使用
isolcpus
参数或cpusets cgroups控制器 |
| CPU亲和性掩码 | 查询和设置线程的CPU亲和性掩码 |
| 线程调度策略和优先级 | 使用系统调用进行查询和设置 |
| cgroups | 限制和监控进程的资源使用 |
| 运行Linux作为RTOS | 实时调度策略和内核配置 |
| 杂项调度相关主题 | 负载均衡和性能优化 |
希望本文对您理解Linux CPU调度器有所帮助,如有任何疑问或建议,请随时留言。
超级会员免费看
958

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



