绑定至同一CPU的SCHED_IDLE线程为何在存在其他可调度线程时仍被调度?
我最近碰到一个特别困惑的问题:我把两个线程都绑定到CPU 0,其中一个用SCHED_IDLE调度策略,另一个是纯CPU密集型线程——要么用普通优先级的SCHED_OTHER,要么用实时的SCHED_FIFO策略。按我的理解,SCHED_IDLE的设计初衷就是只有当系统没有其他可运行线程时才会被调度,但实际测试中,这个低优先级线程却一直在被调度执行,甚至还能持续打印输出。
对于SCHED_FIFO的情况,我一开始以为是sched(7)文档里提到的防系统锁死机制("Limiting the CPU usage of real-time and deadline processes")在起作用,但即使我通过sysctl把kernel.sched_rt_period_us和kernel.sched_rt_runtime_us设为-1关闭了这个限制,问题依然存在。更奇怪的是,当用SCHED_OTHER普通线程时,SCHED_IDLE线程还是会被调度,这完全没法用实时线程的保护机制来解释。
我检查了SCHED_FIFO线程的反汇编代码,它就是一个无系统调用、无睡眠的死循环,理论上应该一直占据CPU,没有任何理由会被SCHED_IDLE线程抢占:
Dump of assembler code for function spin_loop_hint: 0x00000000000011d9 <+0>: push %rbp 0x00000000000011da <+1>: mov %rsp,%rbp 0x00000000000011dd <+4>: nop 0x00000000000011de <+5>: nop 0x00000000000011df <+6>: pop %rbp 0x00000000000011e0 <+7>: ret Dump of assembler code for function infinite_loop: 0x000000000000146d <+0>: push %rbp 0x000000000000146e <+1>: mov %rsp,%rbp 0x0000000000001471 <+4>: call 0x11d9 <spin_loop_hint> 0x0000000000001476 <+9>: jmp 0x1471 <infinite_loop+4>
完整测试代码
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <pthread.h> #include <sched.h> #include <sys/mman.h> #include <unistd.h> static void spin_loop_hint(void) { // Attempt at preventing optimization steps. asm("nop"); } static void pin_to_cpu0(void) { cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); int ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset); if (ret != 0) { fprintf(stderr, "pthread_setaffinity_np failed: %d\n", ret); } } static void set_sched_idle(void) { struct sched_param param; param.sched_priority = 0; /* must be 0 for SCHED_IDLE */ int ret = sched_setscheduler(0, SCHED_IDLE, ¶m); if (ret != 0) { int err = errno; fprintf(stderr, "sched_setscheduler(SCHED_IDLE) failed: ret=%d, errno=%d\n", ret, err); } } static int lock_all_memory(void) { int flags = MCL_CURRENT | MCL_FUTURE; int ret = mlockall(flags); if (ret != 0) { /* mlockall returns -1 on error and sets errno */ perror("mlockall"); return -1; } return 0; } static void set_sched_fifo(void) { struct sched_param param; param.sched_priority = 99; int ret = sched_setscheduler(0, SCHED_FIFO, ¶m); if (ret != 0) { int err = errno; fprintf(stderr, "sched_setscheduler(SCHED_FIFO) failed: ret=%d, errno=%d\n", ret, err); } } static void idle_loop() { unsigned long long i = 0; for (;;) { spin_loop_hint(); i++; if (i % 100000ULL == 0ULL) { printf("Why is SCHED_IDLE ever given any CPU?\n"); } } } static void *idle_thread_func(void *arg) { (void)arg; if (lock_all_memory() != 0) { fprintf(stderr, "lock_all_memory() failed in idle thread\n"); exit(EXIT_FAILURE); } pin_to_cpu0(); set_sched_idle(); printf("SCHED_IDLE thread running on CPU 0\n"); idle_loop(); /* Unreachable, but required for pthread signature */ return NULL; } void infinite_loop() { for (;;) { spin_loop_hint(); } } int main(void) { if (lock_all_memory() != 0) { fprintf(stderr, "lock_all_memory() failed in main\n"); return EXIT_FAILURE; } /* SCHED_IDLE spinning thread, also on CPU 0 */ pthread_t idle; int ret = pthread_create(&idle, NULL, idle_thread_func, NULL); if (ret != 0) { fprintf(stderr, "pthread_create failed: %d\n", ret); return EXIT_FAILURE; } /* "Normal" spinning thread (actually SCHED_FIFO here), pinned to CPU 0 */ pin_to_cpu0(); set_sched_fifo(); printf("Normal thread running on CPU 0\n"); infinite_loop(); /* Unreachable */ return 0; }
运行输出
$ gcc -g t.c -o t && sudo ./t Normal thread running on CPU 0 SCHED_IDLE thread running on CPU 0 Why is SCHED_IDLE ever given any CPU? Why is SCHED_IDLE ever given any CPU? [… 持续输出该信息]
问题分析与解决思路
首先,咱们得先纠正一个对SCHED_IDLE的常见误解:它确实是Linux里优先级最低的调度策略,但"只有无其他可运行线程时才调度"的规则,在实际场景中会被一些细节打破。下面咱们一步步拆解可能的原因:
1. 先明确调度器的优先级层级
Linux调度器的优先级顺序是:实时调度类(SCHED_FIFO/SCHED_RR) > CFS调度类(SCHED_OTHER) > IDLE调度类(SCHED_IDLE)。理论上,只要实时线程处于就绪状态(没被阻塞、没主动放弃CPU),调度器绝对不会轮到SCHED_IDLE线程;对于普通线程,只有当所有CFS队列的线程都被调度过(时间片用完且无就绪普通线程),才会考虑SCHED_IDLE。
那你的测试里为什么会出现异常?可能是以下几个点:
2. 可能的触发原因
(1) 编译器优化把死循环"优化没了"
你加了asm("nop")试图阻止优化,但默认的-g编译选项虽然关闭了大部分优化,某些编译器版本还是可能把无意义的空循环(只做nop)优化成等效于CPU空闲的指令(比如hlt)。一旦CPU进入空闲状态,SCHED_IDLE线程就会被调度。
验证方案:强制关闭所有优化,用gcc -g -O0 -fno-inline t.c -o t重新编译,再测试。
(2) 实时线程的组级限制没关闭
你修改了全局的kernel.sched_rt_runtime_us,但如果内核启用了CONFIG_RT_GROUP_SCHED,还存在cgroup级别的实时线程时间限制。即使全局限制关闭了,组限制依然会限制实时线程的CPU占用时间,导致调度器不得不调度其他线程。
验证与解决:
# 检查组级实时限制 cat /sys/fs/cgroup/cpu/cpu.rt_runtime_us # 如果不是-1,修改为无限制 echo -1 > /sys/fs/cgroup/cpu/cpu.rt_runtime_us
(3) 线程亲和性绑定失败
虽然你的代码调用了pthread_setaffinity_np,但有可能绑定操作没成功(比如CPU 0不存在,或者权限问题),导致两个线程跑到了不同的CPU上——比如SCHED_FIFO线程在CPU 0,SCHED_IDLE线程在CPU 1,那SCHED_IDLE自然会被调度。
验证方案:在pin_to_cpu0函数里添加检查:
static void pin_to_cpu0(void) { cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); int ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset); if (ret != 0) { fprintf(stderr, "pthread_setaffinity_np failed: %d\n", ret); } // 检查绑定是否成功 CPU_ZERO(&cpuset); ret = pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset); if (ret == 0) { printf("Thread bound to CPU %d\n", CPU_ISSET(0, &cpuset) ? 0 : -1); } }
(4) 输出操作的隐性干扰
你的SCHED_IDLE线程里调用了printf,这会触发write系统调用。当线程从内核态返回用户态时,调度器会重新选择线程。如果此时SCHED_FIFO线程因某种微小延迟(比如中断处理后的调度延迟)没被及时标记为就绪,SCHED_IDLE线程可能会被短暂调度。不过这种情况应该只会产生零星输出,而不是持续打印。
3. 终极验证方法
为了排除输出操作的干扰,你可以修改SCHED_IDLE线程的逻辑,去掉printf,改用一个内存计数器,然后用另一个工具(比如top、perf)检查CPU占用率和线程状态:
- 修改
idle_loop:把打印逻辑改成对一个全局变量的递增。 - 用
top -H -p <pid>查看两个线程的CPU占用率,如果SCHED_FIFO线程的CPU占用率不是100%,说明它确实没有持续运行。
内容的提问来源于stack exchange,提问作者Thomas

