[toc]

kernel/sched/syscalls.c 调度相关的系统调用(Scheduler-Related System Calls) 用户空间与调度器交互的接口

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/sched/syscalls.c 这个文件本身并不是一种“技术”,而是用户空间与内核调度器进行交互的官方API层。它的存在是为了解决一个至关重要的问题:如何为一个用户空间的应用程序提供一个稳定、受控且安全的接口,使其能够查询或影响自身以及其他进程的调度行为。

内核调度器(fair.c, rt.c, deadline.c等)是一个极其复杂的内部子系统。如果没有syscalls.c这一层“防火墙”和“翻译官”,就会出现以下问题:

  1. 缺乏控制:用户程序将无法请求更高的执行优先级(如实时音频应用)、降低后台任务的影响(如批处理任务),或将任务绑定到特定的CPU核心以优化性能。
  2. 安全漏洞:任何程序都可以随意修改系统上所有进程的优先级,一个普通用户进程就能通过提升自己为最高优先级的实时任务来饿死所有其他进程,导致系统完全锁死。
  3. 缺乏抽象:应用程序需要直接理解内核调度器内部复杂的数据结构和算法,这会使得应用程序与特定版本的内核紧密耦合,极难维护。
  4. ABI不稳定:内核内部的调度器实现可以自由地演进和重构,如果应用程序直接依赖这些实现,那么每次内核更新都可能导致整个用户空间崩溃。

syscalls.c通过提供一组标准的系统调用(System Calls),完美地解决了这些问题。它充当了一个策略执行和权限检查的关卡。

它的发展经历了哪些重要的里程碑或版本迭代?

该文件的演进历史就是Linux调度器功能不断丰富并向用户空间开放的历史。

  • 经典Unix调用:最初,它实现了从传统Unix继承而来的基本调用,如nice()(调整进程的“友好度”以影响其调度优先级)、getpriority()setpriority()
  • POSIX实时支持:随着内核支持SCHED_FIFOSCHED_RR等实时调度策略,syscalls.c中加入了sched_setscheduler()sched_getscheduler()sched_get_priority_max/min()等一套完整的POSIX.1b API,这是Linux实时能力的一个重要里程碑。
  • SMP(多核)支持:为了适应多核处理器的普及,sched_setaffinity()sched_getaffinity()系统调用被加入。这允许程序将一个进程或线程“钉”在特定的一个或多个CPU核心上运行,这对于HPC(高性能计算)和延迟敏感的应用至关重要。
  • 现代化的sched_attr接口:当SCHED_DEADLINE等更复杂的调度策略被引入后,仅靠一个“优先级”数字已经不足以描述其调度参数。因此,内核引入了一对新的、更具扩展性的系统调用:sched_setattr()sched_getattr()。它们使用一个结构体struct sched_attr来传递所有参数,为未来添加更多新的调度策略和参数预留了空间。
  • 协作式调度sched_yield()系统调用也被实现,它允许一个进程自愿放弃CPU,让调度器选择另一个进程运行。

目前该技术的社区活跃度和主流应用情况如何?

syscalls.c是Linux内核ABI(应用程序二进制接口)中最核心、最稳定的部分之一。

  • 主流应用:它是所有需要进行调度控制的用户空间工具和库的基石。例如:
    • 系统工具nice, renice, taskset, chrt都是这些系统调用的直接命令行封装。
    • pthreads等多线程库在设置线程优先级和亲和性时会调用它们。
    • systemd在管理服务单元(service units)时,会使用它们来设置服务的调度策略和优先级。
    • 所有容器运行时(Docker, Podman)在配置容器的CPU资源时也会间接用到这些接口。

核心原理与设计

它的核心工作原理是什么?

syscalls.c的本质是一张系统调用表的实现。文件中使用SYSCALL_DEFINE*宏定义的每一个函数,都对应一个用户空间可以直接调用的系统调用。

其工作流程可以概括为**“校验-分发”**模型:

  1. 入口:当用户空间程序发起一个调度相关的系统调用时(例如chrt -f 90 my_program),CPU会陷入内核态,并根据系统调用号找到syscalls.c中对应的入口函数(如sys_sched_setscheduler)。
  2. 参数校验:内核首先会从用户空间拷贝参数到内核空间,并进行严格的合法性检查。例如,检查请求的调度策略是否有效,优先级是否在合法范围内。
  3. 权限检查:这是最关键的一步。内核会使用能力(Capabilities)框架来检查当前进程是否拥有执行该操作的权限。例如,改变其他进程的优先级通常需要CAP_SYS_NICE,而设置实时调度策略则需要CAP_SYS_ADMIN。如果权限不足,系统调用会立即返回错误。
  4. 目标定位:系统调用需要找到要操作的目标进程(task_struct)。它会根据传入的pid在进程表中查找。
  5. 调用核心逻辑:在所有检查都通过后,该函数会获取必要的锁(如目标进程的运行队列锁rq->lock),然后调用内核调度器的内部核心函数(通常位于kernel/sched/core.c中,例如sched_setscheduler())来执行真正的调度策略变更。
  6. 返回结果:核心逻辑执行完毕后,释放锁,并将执行结果(成功或错误码)返回给用户空间。

它的主要优势体现在哪些方面?

  • 清晰的抽象层:将复杂的调度器内部实现与简单的用户API完全分离开。
  • 安全:集中的权限检查点确保了系统的稳定和安全,防止恶意或有bug的程序破坏调度公平性。
  • 稳定ABI:提供了一组向后兼容的接口。即使内核调度器内部发生了翻天覆地的重构(如从O(1)切换到CFS),这些系统调用接口依然保持不变,保护了用户空间程序的兼容性。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 接口的刚性:一旦一个系统调用被定义并发布,其参数和基本行为就很难再改变,因为这会破坏ABI。这就是为什么内核倾向于添加新的、更具扩展性的系统调用(如sched_setattr),而不是修改旧的。
  • 控制而非数据:这些系统调用是为低频率的“控制平面”操作设计的。它们不适合用于高频率的数据交换,因为每次调用都涉及上下文切换和锁操作,开销较大。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是用户空间程序控制进程级调度行为唯一标准方案

  • 提升实时性能:一个音频处理程序或工业控制应用会使用sched_setscheduler()将自己设置为SCHED_FIFO,以获得最低的延迟。
  • 降低后台任务影响:一个执行数据备份或科学计算的后台程序,会使用nice()setpriority()降低自己的优先级,以免影响前台的用户交互。
  • 优化HPC性能:一个大规模并行计算任务,会使用sched_setaffinity()将不同的计算线程绑定到不同的CPU核心,以避免线程在核心间迁移造成的缓存失效。
  • 系统诊断top, htop等工具使用getpriority()来显示进程的nice值。

是否有不推荐使用该技术的场景?为什么?

  • 应用内部的协作式调度:如果你的目的是在应用内部的多个任务(如协程/纤程)之间进行调度,那么使用这些系统调用就太“重”了。应该使用用户空间的协程库(如Boost.Context, libco)。sched_yield()偶尔可用于提示内核,但频繁使用通常意味着设计不佳。
  • 全局调度策略调优:这些系统调用主要影响单个进程。如果要调整整个系统的调度器行为(如CFS的目标延迟),应该通过/proc/sys/kernel/下的sysctl接口。

对比分析

请将其 与 其他相似技术 进行详细对比。

在Linux中,有多种方式可以影响调度行为,它们处于不同的层次。

特性 调度系统调用 (syscalls.c) 控制组 (cgroups - CPU Controller) sysctl (/proc/sys/kernel/)
作用域 单个进程/线程 一组进程/线程 系统全局
控制方式 命令式 (Imperative):程序主动调用函数来改变自己或其他进程的策略。 声明式 (Declarative):管理员为cgroup配置资源限制(如CPU份额、配额),内核对组内所有进程强制执行。 声明式:管理员配置全局调度器参数。
主要用途 为特定应用设置调度策略、优先级和亲和性。 在多租户环境(如容器、虚拟机)中分配和限制CPU资源。 调整内核调度器的全局行为和默认参数。
使用示例 chrt -f 90 my_prog echo 512 > /sys/fs/cgroup/cpu/my_group/cpu.shares sysctl -w kernel.sched_min_granularity_ns=10000000
关系 互补。一个进程的最终调度行为是其自身调度策略和其所属cgroup资源限制的共同结果。 互补。cgroup为一组进程设置了“天花板”和“地板”,而组内的进程仍可使用系统调用来微调它们在该组内的相对优先级。 定义了调度器的基础行为框架。

set_user_nice 设置用户的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void set_user_nice(struct task_struct *p, long nice)
{
bool queued, running;
struct rq *rq;
int old_prio;

if (task_nice(p) == nice || nice < MIN_NICE || nice > MAX_NICE)
return;
/* 使用 CLASS(task_rq_lock, rq_guard)(p) 锁定任务的运行队列(rq),
确保在多核环境下对任务和运行队列的操作是线程安全的。 */
CLASS(task_rq_lock, rq_guard)(p);
rq = rq_guard.rq;
/* 更新运行队列的时钟信息,确保调度器的时间信息是最新的 */
update_rq_clock(rq);

/*
* 实时任务的特殊处理: 如果任务是实时任务(SCHED_DEADLINE、SCHED_FIFO 或 SCHED_RR),
nice 值的改变不会影响调度优先级,但仍然更新任务的静态优先级(static_prio),然后直接返回
*/
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
p->static_prio = NICE_TO_PRIO(nice);
return;
}

queued = task_on_rq_queued(p);
running = task_current_donor(rq, p);
if (queued)
/* 任务在运行队列中 将其从队列中移除*/
dequeue_task(rq, p, DEQUEUE_SAVE | DEQUEUE_NOCLOCK);
if (running)
/* 任务正在运行 调用 put_prev_task 将其从当前 CPU 上移除 */
put_prev_task(rq, p);

p->static_prio = NICE_TO_PRIO(nice);
/* 更新任务的负载权重,以反映新的优先级 */
set_load_weight(p, true);
old_prio = p->prio;
/* 更新任务的动态优先级 */
p->prio = effective_prio(p);

if (queued)
/* 如果任务之前在运行队列中,将其重新加入 */
enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK);
if (running)
/* 任务正在运行,将其设置为下一个运行的任务 */
set_next_task(rq, p);

/*
* 触发重新调度: 如果任务的优先级提高,或者任务正在运行且优先级降低,
* 则调用调度类的 prio_changed 方法,通知调度器重新评估任务的调度。
*/
p->sched_class->prio_changed(rq, p, old_prio);
}
EXPORT_SYMBOL(set_user_nice);

__setscheduler_params 将sched_attr中的参数应用到task_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
* 这是一个静态函数,负责将sched_attr中的参数应用到task_struct。
* @p: 指向目标任务的task_struct。
* @attr: 指向包含新调度属性的sched_attr结构体。
*/
static void __setscheduler_params(struct task_struct *p,
const struct sched_attr *attr)
{
int policy = attr->sched_policy;

/*
* SETPARAM_POLICY是一个特殊值,来自于旧的sched_setparam()系统调用,
* 它表示只修改参数,不改变策略。
*/
if (policy == SETPARAM_POLICY)
/* 如果是这种情况,则使用任务当前的策略。*/
policy = p->policy;

/* 将最终确定的策略写入任务的task_struct。*/
p->policy = policy;

/* 根据策略类型,调用相应的辅助函数来设置特定于该类的参数。*/
if (dl_policy(policy))
/* 如果是截止时间策略,调用__setparam_dl设置runtime, deadline, period。*/
__setparam_dl(p, attr);
else if (fair_policy(policy))
/* 如果是公平策略,调用__setparam_fair设置nice值。*/
__setparam_fair(p, attr);

/*
* 注释:实时策略任务没有定时器松弛度。
* 定时器松弛度允许内核将相近的定时器事件聚合,以减少唤醒和功耗。
* 实时任务需要精确唤醒,所以不应有松弛。
*/
if (rt_or_dl_task_policy(p)) {
/* 如果任务的新策略是实时或截止时间策略,则将其定时器松弛度设为0。*/
p->timer_slack_ns = 0;
} else if (p->timer_slack_ns == 0) {
/*
* 注释:当从实时策略切换回非实时策略时,恢复定时器松弛度。
* 如果之前的松弛度为0(因为是实时任务),现在需要恢复到它的默认值。
*/
p->timer_slack_ns = p->default_timer_slack_ns;
}

/*
* 注释:__sched_setscheduler()函数确保了当策略不是实时策略时,
* attr->sched_priority为0。总是设置这个值可以确保像
* getparam()/getattr()这样的函数对于非实时任务不会报告无意义的值。
*/
/* 将attr中的实时优先级写入p->rt_priority。*/
p->rt_priority = attr->sched_priority;
/*
* 调用normal_prio(),这是一个宏,它会调用__normal_prio()函数,
* 根据任务所有新的调度属性,重新计算其内核内部统一的优先级值。
*/
p->normal_prio = normal_prio(p);
/*
* 根据新的标准优先级,重新计算任务的负载权重(load_weight)。
* 这个权重是CFS进行公平调度和负载均衡计算的基础。
*/
set_load_weight(p, true);
}

__sched_setscheduler 内核中统一的调度策略设置后端函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
/*
* __sched_setscheduler - 内核中统一的调度策略设置后端函数。
* @p: 指向目标任务的task_struct。
* @attr: 指向包含所有新调度属性的sched_attr结构体。
* @user: 布尔值,若为true,表示请求来自用户空间,需要权限检查。
* @pi: 布尔值,若为true,表示需要考虑优先级继承(PI)的影响。
*/
int __sched_setscheduler(struct task_struct *p,
const struct sched_attr *attr,
bool user, bool pi)
{
int oldpolicy = -1, /* 用于在recheck机制中,保存进入函数时任务的原始策略。初始化为-1,作为一个特殊标记,表示尚未保存。 */
policy = attr->sched_policy; /* 用于存储本次操作的目标调度策略 */
int retval, /* return value”的缩写。作为函数内部统一的返回值容器 */
oldprio, /* 用于保存在修改之前,任务的旧优先级 */
newprio, /* 用于存储根据新策略和参数计算出的新基准优先级 */
queued, /* 记录在执行出队操作前,任务是否在运行队列中 */
running; /* 用于记录在执行出队操作前,任务是否就是当前CPU正在运行的任务 */
const struct sched_class *prev_class, /* 用于保存任务在变更前的旧调度类 */
*next_class; /* 用于保存根据新策略计算出的新调度类 */
struct balance_callback *head; /* 用于临时存储从运行队列中取出的负载均衡回调链表的头指针 */
struct rq_flags rf; /* “runqueue flags”的缩写。这是一个结构体,用于在调用task_rq_lock()时,保存被禁用的中断状态。当后续调用task_rq_unlock()时,需要将这个结构体传回,以正确地恢复之前的中断状态 */
int reset_on_fork; /* 存储从attr->sched_flags中解析出的SCHED_FLAG_RESET_ON_FORK状态。它决定了当该任务fork一个子进程时,子进程是否要恢复到默认的调度策略 */
/* 用于在调用dequeue_task和enqueue_task时,控制其行为的一组标志。
DEQUEUE_SAVE: 表示在出队时,需要保存任务的负载贡献等信息。
DEQUEUE_MOVE: 表示这是一次任务迁移(可能在不同运行队列或调度类之间),而不仅仅是睡眠。
*/
int queue_flags = DEQUEUE_SAVE | DEQUEUE_MOVE | DEQUEUE_NOCLOCK;
struct rq *rq; /* 将用于指向目标任务当前所在的运行队列 */
bool cpuset_locked = false; /* 记录是否已经获取了cpuset_lock。因为这个锁是根据策略条件性获取的,需要一个标志来确保在函数的所有退出路径上,如果锁被获取了,就一定会被释放 */

/* 优先级继承(PI)相关的代码期望中断是开启的。*/
BUG_ON(pi && in_interrupt());
recheck:
/*
* Step 1: 参数预处理与验证 (无锁阶段)
*/
/* 如果policy为负,表示这是一个查询或保持当前策略的请求。*/
if (policy < 0) {
reset_on_fork = p->sched_reset_on_fork;
policy = oldpolicy = p->policy;
} else { /* 否则,这是一个变更请求。*/
reset_on_fork = !!(attr->sched_flags & SCHED_FLAG_RESET_ON_FORK);
if (!valid_policy(policy)) /* 检查策略是否合法。*/
return -EINVAL;
}
/* 检查所有标志位是否合法。*/
if (attr->sched_flags & ~(SCHED_FLAG_ALL | SCHED_FLAG_SUGOV))
return -EINVAL;
/* 检查优先级、实时参数等是否与策略匹配且在有效范围内。*/
if (attr->sched_priority > MAX_RT_PRIO-1)
return -EINVAL;
if ((dl_policy(policy) && !__checkparam_dl(attr)) ||
(rt_policy(policy) != (attr->sched_priority != 0)))
return -EINVAL;

/* 如果请求来自用户空间,执行权限和安全检查。*/
if (user) {
/* 第一道关卡:通用权限检查
* 验证调用者是否有权修改目标任务p的调度策略。其内部逻辑遵循最小权限原则:
* 权限判断: 通常情况下,一个进程只能修改自己的调度策略。要修改另一个进程的调度策略,调用者必须拥有CAP_SYS_NICE这个capability(能力)。
* 特权策略限制: 即使有CAP_SYS_NICE,普通用户进程也不能将自己或其他进程的策略设置为实时策略(如SCHED_FIFO, SCHED_RR, SCHED_DEADLINE)。
* 要设置这些可能影响系统稳定性的实时策略,调用者必须拥有最高的CAP_SYS_ADMIN权限,或者属于一个被配置了实时调度预算的cgroup。
* 优先级范围检查: 该函数还会检查请求的优先级是否在允许的范围内。例如,非特权用户不能设置一个高于其当前允许值的实时优先级
*/
retval = user_check_sched_setscheduler(p, attr, policy, reset_on_fork);
if (retval)
return retval;

/* 第二道关卡:特定标志检查
* 这个标志与内核的schedutil CPU频率调控器(governor)相关,它允许调度器根据任务的利用率信息去“建议”CPU调整频率。
* 这是一个内部的、由内核其他部分(如功耗管理)使用的标志,绝不允许由用户空间直接设置。
* 作用: 这条检查确保了用户空间无法通过系统调用来干预底层的CPU频率选择机制,维护了内核模块间的接口隔离 */
if (attr->sched_flags & SCHED_FLAG_SUGOV)
return -EINVAL;

/* 第三道关卡:Linux安全模块(LSM)检查
* LSM是什么: LSM是一个允许集成不同安全策略(如SELinux, AppArmor, Smack)的框架。它在内核的各个关键操作点(如文件打开、任务创建、策略设置)都埋下了钩子函数。
* 作用: security_task_setscheduler会调用当前系统启用的安全模块提供的回调函数。这个回调函数会根据该安全模块自身的策略规则,来判断本次调度策略变更是否被允许。
* 补充安全层: 这个检查提供了一个独立于传统UNIX权限和capabilities的、更强大和灵活的强制访问控制(MAC)层*/
retval = security_task_setscheduler(p);
if (retval)
return retval;
}
/* 验证并更新CPU利用率限制(uclamp)。*/
/* 目的: uclamp机制允许用户或系统管理员为一个任务(或cgroup)设置一个**最小(min)和最大(max)**的CPU利用率期望值。这个值是一个在 0 到 1024(代表100%)之间的整数。
对调度器的“提示”: 这些值不是硬性的资源限制,而是给内核中其他子系统,特别是**调度器(scheduler)和CPU频率调控器(cpufreq governor,如schedutil)**的强大“提示”。
调度器: 当进行任务放置(决定将任务放在哪个CPU上)时,调度器会倾向于将一个有最小利用率要求的任务,放置到一个性能足够强(即CPU capacity足够大)的CPU上,以满足其性能下限。
CPU频率调控器: schedutil调控器会根据CPU上所有任务的聚合利用率来决定CPU应该运行在哪个频率。一个任务设置的util_min会抬高这个聚合值的“水位”,可能促使CPU提升到更高的频率以保证性能。反之,util_max则可以限制频率的无谓拉高。
应用场景: 这在异构计算平台(如ARM big.LITTLE架构,包含性能不同的大小核)和需要精细化功耗-性能管理的场景中尤其重要。例如,可以将一个对延迟敏感的前台任务的util_min设得高一些,确保它总能被调度到大核上并以较高频率运行。 */
if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP) {
retval = uclamp_validate(p, attr);
if (retval)
return retval;
}
/*
* Step 2: 加锁,准备进入临界区
*/
/* 如果涉及SCHED_DEADLINE,需要锁定cpuset以保证带宽计算的稳定性。*/
// if (dl_policy(policy) || dl_policy(p->policy)) {
// cpuset_locked = true;
// cpuset_lock();
// }
/* 锁定任务所在的运行队列(rq),并禁用中断/抢占。这是最重要的锁。*/
rq = task_rq_lock(p, &rf);
update_rq_clock(rq);

/*
* Step 3: 在锁保护下,执行核心的状态迁移
*/
/* 不允许修改stop线程的调度策略。*/
if (p == rq->stop) {
retval = -EINVAL;
goto unlock;
}

// retval = scx_check_setscheduler(p, policy);
// if (retval)
// goto unlock;

/*
* 如果请求的策略(policy)与任务当前的策略(p->policy)完全相同,
* unlikely()提示编译器,这种情况不常发生(相对于策略确实改变的情况)。
*/
if (unlikely(policy == p->policy)) {
/*
* 进入此块,说明主策略未变,现在需要检查子参数。
* 如果任何一个子参数变了,就需要跳转到'change'标签,
* 执行完整的状态变更流程。
*/

/* 检查是否为公平调度策略(SCHED_NORMAL/BATCH)。*/
if (fair_policy(policy) &&
/* 检查新的nice值是否与旧的不同,或者新的runtime值是否与旧的slice不同。*/
(attr->sched_nice != task_nice(p) ||
(attr->sched_runtime != p->se.slice)))
goto change; /* 如果有变动,则跳转到慢速路径。*/

/* 检查是否为实时调度策略(SCHED_FIFO/RR)。*/
if (rt_policy(policy) && attr->sched_priority != p->rt_priority)
goto change; /* 如果实时优先级有变动,则跳转。*/

/* 检查是否为截止时间调度策略(SCHED_DEADLINE)。*/
if (dl_policy(policy) && dl_param_changed(p, attr))
goto change; /* 如果DL参数有变动,则跳转。*/

/* 检查是否请求了uclamp(CPU利用率限制)的变更。*/
if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP)
goto change; /* 如果有,则跳转。*/

/*
* 如果代码执行到这里,说明主策略和所有相关的子参数都完全没有变化。
* 这是真正的“无事发生”情况。
*/

/* 唯一可能需要更新的是sched_reset_on_fork这个标志,因为它不影响当前任务。*/
p->sched_reset_on_fork = reset_on_fork;
/* 设置返回值为0(成功)。*/
retval = 0;
/* 直接跳转到unlock标签,释放锁并返回,跳过所有耗时的出队入队操作。*/
goto unlock;
}

change: // 这是慢速路径的起点标签
/* 如果是用户请求,执行更多与资源限制相关的检查(如RT组调度带宽)。*/
if (user) {
/* ... */
}

/* recheck机制:如果在持锁后发现任务策略已被并发修改,则解锁重试。*/
/*
* Re-check policy now with rq lock held:
* 注释:现在,在持有rq锁的情况下,重新检查策略。
*/
if (unlikely(oldpolicy != -1 && oldpolicy != p->policy)) {
/*
* unlikely()提示编译器,这个分支不常发生,是处理异常情况的。
*
* oldpolicy != -1: 这个条件判断确保我们只在“不改变策略,只改变参数”
* 的模式下进行重试检查。oldpolicy是在函数开头,当policy参数为负时,
* 被设置为p->policy的快照。如果policy参数是明确的策略,oldpolicy
* 保持为-1,这个检查不生效。
*
* oldpolicy != p->policy: 这是核心检查。它比较我们当初在无锁阶段
* 记录下的策略快照(oldpolicy),和现在持锁后看到的任务实时策略(p->policy)。
* 如果两者不相等,就说明在“间隙”中,p的策略已经被其他执行单元改变了。
*/

/*
* 如果检查发现不一致,我们需要放弃本次操作,从头再来。
*/

/* 将policy和oldpolicy重置为-1,这会使下一次goto recheck时,
* 重新从任务p的当前状态读取最新的策略。*/
policy = oldpolicy = -1;

/* 按照与加锁相反的顺序,释放所有持有的锁。*/
task_rq_unlock(rq, p, &rf);
if (cpuset_locked)
cpuset_unlock();

/* 无条件跳转到函数开头的recheck标签处,重新开始整个验证和加锁流程。*/
goto recheck;
}

/* 如果涉及SCHED_DEADLINE,检查系统总带宽是否足够。*/
if ((dl_policy(policy) || dl_task(p)) && sched_dl_overflow(p, policy, attr)) {
retval = -EBUSY;
goto unlock;
}

/* --- 核心状态变更开始 --- */
p->sched_reset_on_fork = reset_on_fork;
oldprio = p->prio;
/* 根据新策略和参数,计算出新的基准优先级。*/
newprio = __normal_prio(policy, attr->sched_priority, attr->sched_nice);
/* 如果需要考虑PI,计算出最终的有效优先级。*/
if (pi) {
/*
* 考虑优先级提升的任务。如果新的有效优先级未改变,
* 我们只存储新的正常参数,而不触及调度器类和运行队列。这将在任务自身降级时完成。
*/
newprio = rt_effective_prio(p, newprio);
//优先级相同则不需要移动任务
if (newprio == oldprio)
queue_flags &= ~DEQUEUE_MOVE;
}

/* 获取任务变更前和变更后的调度类。*/
prev_class = p->sched_class;
next_class = __setscheduler_class(policy, newprio);

/* 从运行队列中取出任务。*/
/*
* 使用task_on_rq_queued()检查任务p是否在运行队列的某个链表上
* (即是否处于可运行状态)。结果存入布尔变量queued。
*/
queued = task_on_rq_queued(p);
/*
* 使用task_current_donor()检查任务p是否就是当前CPU上正在运行的任务。
* donor这个词与优先级继承有关,但在简单情况下,它等同于 task_current(rq) == p。
* 结果存入布尔变量running。
*/
running = task_current_donor(rq, p);
/*
* 如果任务在运行队列中...
*/
if (queued)
/*
* ...则调用dequeue_task()将其从队列中移除。
* queue_flags参数(DEQUEUE_SAVE | DEQUEUE_MOVE | DEQUEUE_NOCLOCK)
* 指示了出队的具体行为:
* - DEQUEUE_SAVE: 保存任务的负载贡献等信息,因为我们稍后还会把它放回去。
* - DEQUEUE_MOVE: 表明这是一次“迁移”,而不是因为任务要睡眠。
* - DEQUEUE_NOCLOCK: 指示出队操作不需要更新运行队列的时钟。
*/
dequeue_task(rq, p, queue_flags);
/*
* 如果任务当前正在CPU上运行...
*/
if (running)
/*
* ...则调用put_prev_task()。这个函数会将当前运行任务(即p)
* 告知其所属的调度类,调度类会更新其运行时间统计等信息。
* 逻辑上,这相当于在切换走之前,完成对p作为“前一个任务”的 finalization。
*/
put_prev_task(rq, p);

/* 将新的参数和调度类应用到task_struct。*/
/*
* SCHED_FLAG_KEEP_PARAMS是一个特殊标志,如果设置了它,
* 意味着调用者只想改变调度类,而不想改变与该类相关的参数
* (例如,从SCHED_NORMAL切换到SCHED_FIFO时,保持nice值不变)。
*
* 检查这个标志是否被设置。
*/
if (!(attr->sched_flags & SCHED_FLAG_KEEP_PARAMS)) {
/*
* 如果没有设置该标志(这是常规情况),则应用所有新参数。
*/
/* 调用辅助函数,将attr中的nice, rt_priority, dl参数应用到p。*/
__setscheduler_params(p, attr);
/* 将任务的调度类指针更新为新的调度类。这是最核心的变更。*/
p->sched_class = next_class;
/* 将任务的基准优先级更新为新计算出的优先级。*/
p->prio = newprio;
}
/*
* 不论SCHED_FLAG_KEEP_PARAMS是否设置,uclamp的更新是独立的,
* 只要之前验证通过,这里就进行更新。
*/
// __setscheduler_uclamp(p, attr);
/* 通知调度类将要切换到另一个调度类 */
check_class_changing(rq, p, prev_class);


/*
* 将更新后的任务重新放入运行队列。
* queued是在出队前保存的状态。
*/
if (queued) {
/*
* 注释:当任务的优先级提高时(用户空间视图),我们将其加入队列头部。
*
* 在Linux内核中,优先级值越小,
* 实际优先级越高。所以 oldprio < p->prio 实际上意味着
* 实际优先级被“降低”了。
*
* 当新优先级值大于旧值时,加ENQUEUE_HEAD标志。
*/
if (oldprio < p->prio)
queue_flags |= ENQUEUE_HEAD;

/*
* 调用enqueue_task()将任务p重新放入运行队列rq。
* 它会调用新的p->sched_class的enqueue_task方法,
* 将任务插入到正确的子队列(如CFS红黑树或RT优先级链表)的正确位置。
*/
enqueue_task(rq, p, queue_flags);
}

/*
* 如果任务在出队前正在运行(running为真)。
*/
if (running)
/*
* 调用set_next_task()。这个函数会通知调度器,当前CPU的下一个
* 运行任务仍然是p。这确保了在没有更高优先级任务抢占的情况下,
* p能够继续运行。
*/
set_next_task(rq, p);
/* 通知调度类从一个调度类切换到另一个调度类。*/
check_class_changed(rq, p, prev_class, oldprio);

/*
* Step 4: 解锁与后处理
*/
/* 在解锁rq前,先禁用抢占,防止rq被释放。*/
preempt_disable();
// head = splice_balance_callbacks(rq);
task_rq_unlock(rq, p, &rf);

/* 如果涉及PI,调整实时互斥锁。*/
if (pi) {
// if (cpuset_locked)
// cpuset_unlock();
rt_mutex_adjust_pi(p);
}
/* ... (执行负载均衡回调) ... */
// balance_callbacks(rq, head);
preempt_enable();

return 0;

unlock: /* 统一的错误退出路径 */
task_rq_unlock(rq, p, &rf);
// if (cpuset_locked)
// cpuset_unlock();
return retval;
}

_sched_setscheduler 设置任务的调度策略和优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
* 这是一个静态函数,作为传统sched_setscheduler系统调用的后端实现。
* @p: 指向目标任务的task_struct。
* @policy: 要设置的调度策略,如SCHED_NORMAL, SCHED_FIFO等。
* @param: 指向传统的sched_param结构体,其中包含优先级。
* @check: 一个布尔值,指示是否需要进行权限检查。
*/
static int _sched_setscheduler(struct task_struct *p, int policy,
const struct sched_param *param, bool check)
{
/* 在函数栈上创建一个新的、扩展的调度属性结构体sched_attr。*/
struct sched_attr attr = {
/* 将传入的policy直接赋值给attr的对应字段。*/
.sched_policy = policy,
/* 将传入的param中的优先级赋值给attr的对应字段。*/
.sched_priority = param->sched_priority,
/*
* 从任务当前的静态优先级(static_prio)反向计算出其nice值。
* 这是因为传统的接口不直接传递nice值。
*/
.sched_nice = PRIO_TO_NICE(p->static_prio),
};

/* 如果任务的CFS调度实体中有一个自定义的时间片长度。*/
if (p->se.custom_slice)
/* 将这个自定义时间片作为SCHED_DEADLINE的runtime参数传递,
* 这是一种兼容或特性复用。*/
attr.sched_runtime = p->se.slice;

/*
* 注释:修复遗留的SCHED_RESET_ON_FORK“黑客”用法。
* 在旧API中,这个标志位是和policy值“或”在一起的。
*/
/* 检查policy值是否包含SCHED_RESET_ON_FORK标志。
* SETPARAM_POLICY是一个特殊值,表示这是一个sched_setparam调用,此时不处理此标志。*/
if ((policy != SETPARAM_POLICY) && (policy & SCHED_RESET_ON_FORK)) {
/* 如果包含,则在新的attr.sched_flags中设置对应的标志位。*/
attr.sched_flags |= SCHED_FLAG_RESET_ON_FORK;
/* 从policy整数值中清除这个标志位,使其变回一个纯粹的策略值。*/
policy &= ~SCHED_RESET_ON_FORK;
/* 更新attr.sched_policy为纯净的策略值。*/
attr.sched_policy = policy;
}

/*
* 调用真正、统一的后端函数__sched_setscheduler,
* 传递转换后的、新的sched_attr结构体。
* true参数表示这是一个来自遗留API的调用。
*/
return __sched_setscheduler(p, &attr, check, true);
}

sched_setscheduler_nocheck 从内核空间更改线程的调度策略和/或实时优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* sched_setscheduler_nocheck - 从内核空间更改线程的调度策略和/或实时优先级。
* @p: 相关任务。
* @policy: 新的策略。
* @param: 包含新实时优先级的结构体。
*
* 与 sched_setscheduler 类似,只是不检查当前上下文是否具有权限。
* 例如,这在 stop_machine() 中是必要的:我们创建临时的高优先级工作线程,
* 但调用者可能没有该能力。
*
* 返回: 成功返回 0,否则返回错误代码。
*/
int sched_setscheduler_nocheck(struct task_struct *p, int policy,
const struct sched_param *param)
{
return _sched_setscheduler(p, policy, param, false);
}