You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

绑定至同一CPU的SCHED_IDLE线程为何在存在其他可调度线程时仍被调度?

为什么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, &param);
    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, &param);
    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 12:42:40