[toc]
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/sched/syscalls.c
这个文件本身并不是一种“技术”,而是用户空间与内核调度器进行交互的官方API层。它的存在是为了解决一个至关重要的问题:如何为一个用户空间的应用程序提供一个稳定、受控且安全的接口,使其能够查询或影响自身以及其他进程的调度行为。
内核调度器(fair.c
, rt.c
, deadline.c
等)是一个极其复杂的内部子系统。如果没有syscalls.c
这一层“防火墙”和“翻译官”,就会出现以下问题:
- 缺乏控制:用户程序将无法请求更高的执行优先级(如实时音频应用)、降低后台任务的影响(如批处理任务),或将任务绑定到特定的CPU核心以优化性能。
- 安全漏洞:任何程序都可以随意修改系统上所有进程的优先级,一个普通用户进程就能通过提升自己为最高优先级的实时任务来饿死所有其他进程,导致系统完全锁死。
- 缺乏抽象:应用程序需要直接理解内核调度器内部复杂的数据结构和算法,这会使得应用程序与特定版本的内核紧密耦合,极难维护。
- ABI不稳定:内核内部的调度器实现可以自由地演进和重构,如果应用程序直接依赖这些实现,那么每次内核更新都可能导致整个用户空间崩溃。
syscalls.c
通过提供一组标准的系统调用(System Calls),完美地解决了这些问题。它充当了一个策略执行和权限检查的关卡。
它的发展经历了哪些重要的里程碑或版本迭代?
该文件的演进历史就是Linux调度器功能不断丰富并向用户空间开放的历史。
- 经典Unix调用:最初,它实现了从传统Unix继承而来的基本调用,如
nice()
(调整进程的“友好度”以影响其调度优先级)、getpriority()
和setpriority()
。
- POSIX实时支持:随着内核支持
SCHED_FIFO
和SCHED_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*
宏定义的每一个函数,都对应一个用户空间可以直接调用的系统调用。
其工作流程可以概括为**“校验-分发”**模型:
- 入口:当用户空间程序发起一个调度相关的系统调用时(例如
chrt -f 90 my_program
),CPU会陷入内核态,并根据系统调用号找到syscalls.c
中对应的入口函数(如sys_sched_setscheduler
)。
- 参数校验:内核首先会从用户空间拷贝参数到内核空间,并进行严格的合法性检查。例如,检查请求的调度策略是否有效,优先级是否在合法范围内。
- 权限检查:这是最关键的一步。内核会使用能力(Capabilities)框架来检查当前进程是否拥有执行该操作的权限。例如,改变其他进程的优先级通常需要
CAP_SYS_NICE
,而设置实时调度策略则需要CAP_SYS_ADMIN
。如果权限不足,系统调用会立即返回错误。
- 目标定位:系统调用需要找到要操作的目标进程(
task_struct
)。它会根据传入的pid
在进程表中查找。
- 调用核心逻辑:在所有检查都通过后,该函数会获取必要的锁(如目标进程的运行队列锁
rq->lock
),然后调用内核调度器的内部核心函数(通常位于kernel/sched/core.c
中,例如sched_setscheduler()
)来执行真正的调度策略变更。
- 返回结果:核心逻辑执行完毕后,释放锁,并将执行结果(成功或错误码)返回给用户空间。
它的主要优势体现在哪些方面?
- 清晰的抽象层:将复杂的调度器内部实现与简单的用户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 = rq_guard.rq; update_rq_clock(rq);
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(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);
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
|
static void __setscheduler_params(struct task_struct *p, const struct sched_attr *attr) { int policy = attr->sched_policy;
if (policy == SETPARAM_POLICY) policy = p->policy;
p->policy = policy;
if (dl_policy(policy)) __setparam_dl(p, attr); else if (fair_policy(policy)) __setparam_fair(p, attr);
if (rt_or_dl_task_policy(p)) { p->timer_slack_ns = 0; } else if (p->timer_slack_ns == 0) {
p->timer_slack_ns = p->default_timer_slack_ns; }
p->rt_priority = attr->sched_priority;
p->normal_prio = normal_prio(p);
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
|
int __sched_setscheduler(struct task_struct *p, const struct sched_attr *attr, bool user, bool pi) { int oldpolicy = -1, policy = attr->sched_policy; int retval, oldprio, newprio, queued, running; const struct sched_class *prev_class, /* 用于保存任务在变更前的旧调度类 */ *next_class; struct balance_callback *head; struct rq_flags rf; int reset_on_fork;
int queue_flags = DEQUEUE_SAVE | DEQUEUE_MOVE | DEQUEUE_NOCLOCK; struct rq *rq; bool cpuset_locked = false;
BUG_ON(pi && in_interrupt()); recheck:
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) {
retval = user_check_sched_setscheduler(p, attr, policy, reset_on_fork); if (retval) return retval;
if (attr->sched_flags & SCHED_FLAG_SUGOV) return -EINVAL;
retval = security_task_setscheduler(p); if (retval) return retval; }
if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP) { retval = uclamp_validate(p, attr); if (retval) return retval; }
rq = task_rq_lock(p, &rf); update_rq_clock(rq);
if (p == rq->stop) { retval = -EINVAL; goto unlock; }
if (unlikely(policy == p->policy)) {
if (fair_policy(policy) && (attr->sched_nice != task_nice(p) || (attr->sched_runtime != p->se.slice))) goto change;
if (rt_policy(policy) && attr->sched_priority != p->rt_priority) goto change;
if (dl_policy(policy) && dl_param_changed(p, attr)) goto change; if (attr->sched_flags & SCHED_FLAG_UTIL_CLAMP) goto change;
p->sched_reset_on_fork = reset_on_fork; retval = 0; goto unlock; }
change: if (user) { }
if (unlikely(oldpolicy != -1 && oldpolicy != p->policy)) {
policy = oldpolicy = -1; task_rq_unlock(rq, p, &rf); if (cpuset_locked) cpuset_unlock(); goto recheck; }
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); 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);
queued = task_on_rq_queued(p);
running = task_current_donor(rq, p);
if (queued)
dequeue_task(rq, p, queue_flags);
if (running)
put_prev_task(rq, p);
if (!(attr->sched_flags & SCHED_FLAG_KEEP_PARAMS)) {
__setscheduler_params(p, attr); p->sched_class = next_class; p->prio = newprio; }
check_class_changing(rq, p, prev_class);
if (queued) {
if (oldprio < p->prio) queue_flags |= ENQUEUE_HEAD;
enqueue_task(rq, p, queue_flags); }
if (running)
set_next_task(rq, p); check_class_changed(rq, p, prev_class, oldprio);
preempt_disable(); task_rq_unlock(rq, p, &rf);
if (pi) { rt_mutex_adjust_pi(p); } preempt_enable();
return 0;
unlock: task_rq_unlock(rq, p, &rf); 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
|
static int _sched_setscheduler(struct task_struct *p, int policy, const struct sched_param *param, bool check) { struct sched_attr attr = { .sched_policy = policy, .sched_priority = param->sched_priority,
.sched_nice = PRIO_TO_NICE(p->static_prio), };
if (p->se.custom_slice)
attr.sched_runtime = p->se.slice;
if ((policy != SETPARAM_POLICY) && (policy & SCHED_RESET_ON_FORK)) { attr.sched_flags |= SCHED_FLAG_RESET_ON_FORK; policy &= ~SCHED_RESET_ON_FORK; attr.sched_policy = policy; }
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
|
int sched_setscheduler_nocheck(struct task_struct *p, int policy, const struct sched_param *param) { return _sched_setscheduler(p, policy, param, false); }
|