56、Linux CPU调度器解析:原理与机制

Linux CPU调度器解析:原理与机制

1. 调度代码的运行上下文

调度代码总是由当前正在执行内核代码的进程上下文运行,也就是 current 。这里需要强调Linux内核的一个重要规则:调度器绝不能在任何原子上下文(包括中断上下文)中运行。因为原子上下文代码必须保证无阻塞和原子性,要能完整运行而不被中断。所以,不能在原子上下文中调用 schedule() ,例如在原子上下文中使用 GFP_KERNEL 标志调用 kmalloc() 是错误的,因为该标志可能会导致阻塞;而使用 GFP_ATOMIC 标志则没问题,它指示内核内存管理代码不会阻塞。在可抢占内核中,调度代码路径运行时会禁用内核抢占。

2. schedule() 何时运行

操作系统调度器的任务是仲裁对处理器(CPU)资源的访问,在竞争使用CPU的线程之间进行资源共享。为确保任务间公平共享CPU资源,调度器需要定期在处理器上运行。一种看似合理的方法是让操作系统在启动时挂钩到定时器芯片的中断,并在定时器中断触发时的中断处理程序“内务处理”中调用调度器。定时器中断每秒触发 CONFIG_HZ 次(在x86_64 Ubuntu上通常为250,在x86_64 Fedora上为1000)。但根据规则,不能在定时器中断内调用调度器代码路径,否则会导致内核错误。

3. thread_info 结构基础

为了更好地理解后续内容,需要了解 thread_info 这个与架构相关的每个线程的数据结构。该结构较小,包含几个关键的“热点”成员,其存在主要是为了提高性能,查找 thread_info 内容比查找大的任务结构内容快得多,因为它更小,通常设计为能放入单个CPU缓存行(在x86_64和AArch64上大小为24字节,在AArch32中还用于计算 current )。 thread_info 结构的位置历史多样,早期Linux中它是独立实体,从2.6版本开始,32位平台上它位于每个线程的内核模式栈中,在较新的Linux版本(4.0或4.4及以后),对于某些架构,当 CONFIG_THREAD_INFO_IN_TASK=y 时,它成为任务结构的一个成员。

对于我们当前的讨论, thread_info 结构中一个有意义的成员是位掩码 unsigned long flags ,其中所有标志值都以 TIF_<FOO> 的形式存在( TIF thread_info flag 的缩写)。所有 TIF_* 标志宏的定义可在代码库中找到。在任务调度讨论中,关键的标志是 TIF_NEED_RESCHED ,如果该标志被设置,意味着内核“需要尽快重新调度”;如果清除,则不需要重新调度。

4. TIF_NEED_RESCHED 位的设置与检查
  • TIF_NEED_RESCHED 位的设置
    • 定时器中断内务处理 :在每次定时器中断(技术上是在定时器软中断代码路径中),会进行与调度器相关的“内务处理”。其中一个关键问题是判断当前进程是否需要被抢占,如果需要,则设置 TIF_NEED_RESCHED 位,但不会调用 schedule()
    • 任务唤醒 :当一个任务被唤醒时,它会被加入到适当的运行队列中。如果确定它必须抢占当前进程,则设置 thread_info.flags:TIF_NEED_RESCHED 位。
  • TIF_NEED_RESCHED 位的检查 :在特定设计的进程上下文“机会点”,当前进程会检查 thread_info.flags:TIF_NEED_RESCHED 位是否被设置。如果设置,则调用调度代码路径;否则,继续正常运行。典型的“机会点”包括:
    • 从系统调用代码路径返回时。
    • 从中断代码路径返回时。
    • 内核中从不可抢占模式切换到可抢占模式时(例如解锁自旋锁时)。
5. 定时器中断内务处理中设置 TIF_NEED_RESCHED

在定时器中断( kernel/sched/core.c:scheduler_tick() 代码中,本地核心上的中断被禁用)期间,内核会进行必要的元工作(内务处理)以确保调度顺利运行,包括持续更新每个CPU的运行队列、进行任务负载均衡等。这里不会调用实际的 schedule() 函数,最多会调用调度类钩子函数(如果非空)。例如,对于公平(CFS)类的线程,会在 task_tick_fair() 钩子函数中更新 vruntime 成员(虚拟运行时间)和任务在处理器上花费的(有优先级偏差的)时间。

在定时器中断(软中断)上下文中,通过检查以下条件来决定是否需要抢占当前进程:
- 当前进程是否超过了其时间片,并且超过邻居的阈值是否足够大(由 /sys/kernel/debug/sched/min_granularity_ns 可调参数决定,x86_64 Ubuntu 22.04上默认值为3 ms,Fedora 38/39上为2.25 ms)。
- 新诞生或最近唤醒的任务(在该CFS运行队列上)是否比当前进程优先级更高。
- CFS红黑树中是否有任务的 vruntime 比当前进程低(即当前进程是否不再是树中最左边的叶子节点)。

如果内核代码路径判断新任务更值得使用CPU,不会调用 schedule() ,而是设置 thread_info->flags TIF_NEED_RESCHED 位,标记需要尽快重新调度。新任务会被放入适当的运行队列,等待下一个调度机会。

6. 进程上下文部分:检查 TIF_NEED_RESCHED

系统运行时,定时器中断(软中断)部分的调度内务处理工作会持续进行,可能会设置 thread_info:TIF_NEED_RESCHED 位来“通知”内核尽快调用调度代码。而检查该位是否设置并在设置时调用 schedule() 的操作,有以下特点:
- 仅在进程上下文中执行。
- 仅在散布于内核代码路径中的特定“机会点”执行。

典型的检查 thread_info->flags.TIF_NEED_RESCHED 的“机会点”如下表所示:
| 机会点 | 说明 |
| ---- | ---- |
| 从系统调用代码路径返回 | 线程从用户空间发起系统调用进入内核模式,系统调用完成后,在返回用户空间的路径上检查该位 |
| 从中断代码路径返回 | 处理完硬件中断及相关软中断处理程序后,检查该位 |
| 内核模式切换 | 从不可抢占模式切换到可抢占模式时,如解锁自旋锁时检查该位 |

当线程在用户空间发起系统调用进入内核模式,系统调用结束后,会有一个返回路径回到用户模式继续执行。在这个内核返回路径上,会检查 TIF_NEED_RESCHED 位,如果设置,则由进程上下文调用 schedule() 激活调度器。

另外,还有其他可以调用 schedule() 的情况:
- 任何显式(或隐式)调用 schedule() (例如发出阻塞调用时)。
- 调用 cond_resched*() 函数可能会导致 schedule() 被调用。内核提供的“条件调度”API(如 cond_resched() )允许驱动检查是否占用过多CPU,如果是则让出CPU,只有在 current->ti->flags.TIF_NEED_RESCHED 位设置时才会调用 schedule()

以下是关于从内核返回用户模式代码路径中激活调度代码路径的代码:

// include/linux/entry-common.h
#define EXIT_TO_USER_MODE_WORK                      \
    (_TIF_SIGPENDING | _TIF_NOTIFY_RESUME | _TIF_UPROBE |       \
     _TIF_NEED_RESCHED | _TIF_PATCH_PENDING | _TIF_NOTIFY_SIGNAL |  \
     ARCH_EXIT_TO_USER_MODE_WORK)

kernel/entry/common.c
static void exit_to_user_mode_prepare(struct pt_regs *regs)
{
  …
  ti_work = read_thread_flags();
  if (unlikely(ti_work & EXIT_TO_USER_MODE_WORK))
        ti_work = exit_to_user_mode_loop(regs, ti_work);
[ … ]
static unsigned long exit_to_user_mode_loop(struct pt_regs *regs,
                        unsigned long ti_work)
{
    /*
     * Before returning to user space ensure that all pending work
     * items have been completed.
     */
    while (ti_work & EXIT_TO_USER_MODE_WORK) {
        local_irq_enable_exit_to_user(ti_work);
        if (ti_work & _TIF_NEED_RESCHED)
            schedule();
        [ … ]
        if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
            arch_do_signal_or_restart(regs);
        [ … ]

处理硬件中断(及相关软中断处理程序)后也会出现类似情况。处理硬件中断时内核处于不可抢占模式,处理完成后切换回可抢占模式,此时会检查 TIF_NEED_RESCHED 位,如果设置则调用 schedule() 。解锁自旋锁时也会出现同样的调度机会。

下面用mermaid流程图展示 TIF_NEED_RESCHED 位的设置与检查流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([系统运行]):::startend --> B(定时器中断触发):::process
    B --> C{是否需要抢占当前进程?}:::decision
    C -->|是| D(设置TIF_NEED_RESCHED位):::process
    C -->|否| E(不设置TIF_NEED_RESCHED位):::process
    F(任务唤醒):::process --> G{是否抢占当前进程?}:::decision
    G -->|是| D
    G -->|否| E
    D --> H(等待机会点):::process
    I(机会点到达):::process --> J{检查TIF_NEED_RESCHED位是否设置?}:::decision
    J -->|是| K(调用schedule()):::process
    J -->|否| L(继续正常运行):::process

综上所述,Linux CPU调度器通过合理设置和检查 TIF_NEED_RESCHED 位,在不同的上下文和机会点进行调度操作,确保了CPU资源的公平共享和系统的稳定运行。

Linux CPU调度器解析:原理与机制

7. 调度机会点的详细分析
  • 从系统调用代码路径返回
    当用户空间的线程发起系统调用时,会从用户模式切换到内核模式,以获得内核的特权来执行相应的操作。系统调用的执行时间是有限的,完成后会通过特定的返回路径回到用户模式继续执行。在这个返回路径上,会检查 thread_info->flags 中的 TIF_NEED_RESCHED 位。如果该位被设置,说明内核需要尽快重新调度,此时进程上下文会调用 schedule() 函数来激活调度器,选择下一个合适的任务来执行。
  • 从中断代码路径返回
    硬件中断会打断当前正在执行的任务,内核会进入中断处理程序来处理这些中断。在处理中断的过程中,内核处于不可抢占模式,以确保中断处理的完整性。当中断处理完成后,内核会从不可抢占模式切换回可抢占模式,此时就是一个调度机会点。内核会检查 TIF_NEED_RESCHED 位,如果该位被设置,就会调用 schedule() 函数进行重新调度。
  • 内核模式切换
    内核中存在从不可抢占模式到可抢占模式的切换情况,例如解锁自旋锁时。自旋锁是一种用于保护共享资源的同步机制,当一个线程持有自旋锁时,其他线程会忙等待,此时内核处于不可抢占模式。当自旋锁被解锁后,内核切换回可抢占模式,这也是一个调度机会点,会检查 TIF_NEED_RESCHED 位并根据情况调用 schedule() 函数。
8. 调度器相关函数及代码分析
  • schedule() 函数
    schedule() 函数是调度器的核心函数,用于选择下一个要执行的任务并进行上下文切换。它不能在原子上下文(包括中断上下文)中调用,因为原子上下文要求代码必须是无阻塞和原子性的,而 schedule() 函数可能会导致当前任务进入睡眠状态,不符合原子上下文的要求。
  • need_resched() 辅助函数
    通常用于检查 thread_info->flags.TIF_NEED_RESCHED 位是否被设置。在各个调度机会点,会通过调用 need_resched() 函数来判断是否需要调用 schedule() 函数进行重新调度。
  • cond_resched*() 函数
    内核提供的“条件调度”API,如 cond_resched() 。这些函数允许驱动程序或其他内核代码检查当前是否占用了过多的CPU资源,如果是,则让出CPU。只有当 current->ti->flags.TIF_NEED_RESCHED 位被设置时,调用 cond_resched*() 函数才会导致 schedule() 函数被调用。

下面是一个简单的表格总结这些函数的作用:
| 函数名 | 作用 | 调用条件 |
| ---- | ---- | ---- |
| schedule() | 选择下一个任务并进行上下文切换 | 在进程上下文的调度机会点,且 TIF_NEED_RESCHED 位被设置 |
| need_resched() | 检查 TIF_NEED_RESCHED 位是否设置 | 在调度机会点进行检查 |
| cond_resched*() | 条件性地调用 schedule() | TIF_NEED_RESCHED 位设置且满足特定条件 |

9. 调度器的性能优化考虑
  • thread_info 结构的设计
    thread_info 结构的设计是为了提高性能。它包含了一些关键的“热点”成员,由于其体积较小,通常可以放入单个CPU缓存行中,这样在查找这些成员时速度会比查找大的任务结构快得多。这减少了CPU访问内存的时间,提高了调度器的效率。
  • 避免在原子上下文调用调度器
    严格遵守不能在原子上下文(包括中断上下文)中调用调度器的规则,确保了系统的稳定性和性能。在原子上下文中调用调度器可能会导致内核崩溃或出现不可预期的行为,因为原子上下文要求代码必须是无阻塞和原子性的。
  • 合理设置调度参数
    例如 /sys/kernel/debug/sched/min_granularity_ns 参数,它决定了CFS调度器的有效时间片。合理设置这个参数可以平衡不同任务之间的CPU资源分配,提高系统的整体性能。不同的系统和应用场景可能需要不同的参数值,需要根据实际情况进行调整。
10. 总结与展望

Linux CPU调度器通过一系列复杂的机制和规则,确保了CPU资源在不同任务之间的公平共享和高效利用。通过合理设置和检查 TIF_NEED_RESCHED 位,在不同的上下文和调度机会点进行调度操作,保证了系统的稳定性和性能。

未来,随着计算机硬件的不断发展和应用场景的日益复杂,CPU调度器可能需要进一步优化和改进。例如,对于多核处理器和异构计算环境,调度器需要更好地处理任务的分配和负载均衡;对于实时性要求较高的应用,调度器需要提供更精确的调度策略。同时,随着人工智能和机器学习等新兴技术的发展,调度器也可能需要结合这些技术来实现更智能的调度决策。

下面用mermaid流程图展示调度器的整体工作流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([系统启动]):::startend --> B(进程/线程创建):::process
    B --> C(进程/线程运行):::process
    C --> D{是否进入调度机会点?}:::decision
    D -->|是| E{检查TIF_NEED_RESCHED位?}:::decision
    E -->|是| F(调用schedule()):::process
    E -->|否| C
    D -->|否| C
    G(定时器中断):::process --> H{是否需要抢占当前进程?}:::decision
    H -->|是| I(设置TIF_NEED_RESCHED位):::process
    H -->|否| C
    J(任务唤醒):::process --> K{是否抢占当前进程?}:::decision
    K -->|是| I
    K -->|否| C
    F --> L(选择下一个任务):::process
    L --> M(上下文切换):::process
    M --> C

通过对Linux CPU调度器的深入理解,我们可以更好地优化系统性能,确保系统在各种负载下都能稳定运行。同时,这也为我们进一步研究和开发更高效的调度算法和机制提供了基础。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值