【问题标题】:Where does the scheduler run?调度程序在哪里运行?
【发布时间】:2017-08-14 01:09:51
【问题描述】:

刚读完一本关于comp的书。架构,我发现自己并没有完全弄清楚调度程序在哪里运行。

我希望澄清的是调度程序在哪里运行 - 它是否有自己的核心分配来运行它而不是其他任何东西,或者“调度程序”实际上只是一个更模棱两可的算法,它在每个正在执行的线程 - 例如。抢占线程后,是否运行 swithToFrom() 命令?

我不需要根据 windows x/linux x/mac os x 的细节,只是一般。

【问题讨论】:

  • 通用调度器分为三种:作业调度器也称为长期调度器、短期调度器也称为CPU调度器和中期调度器。在一本操作系统书籍中,它显示了这些调度程序进出的状态的一个很好的自动机。作业调度程序将作业队列中的内容放入就绪队列,CPU 调度程序将就绪队列中的内容放入运行状态。该算法就像任何其他软件一样,它必须在 cpu/core 上运行,它很可能是某个地方的内核的一部分。
  • 我想补充一下,如果你找到那些工作/就绪队列等的位置,那么你也知道调度程序代码的位置。当然,让代码运行的是 CPU,不一定是它自己的核心。
  • 因此,与调度程序关联的所有数据都可能在 cpu 缓存或我想的 ram 中。但是调度程序在调度队列之外运行是我正在寻找的吗?或者调度程序也可以被抢占并暂停?这样做真的没有意义。
  • 换句话说我的问题是,当内核必须调度一个新任务时,内核是自己调度将任务分配到调度队列中,还是在不调度自己的情况下获得cpu时间?
  • 就像你说的那样,调度程序可以被抢占是没有意义的。队列中的作业可以在运行时被抢占,用于 I/O 等。不,内核不必自己调度来分配任务,它只是获得 cpu 时间而无需自己调度。是的,很可能数据在内存中,不确定是否值得存储在 cpu 缓存中。

标签: scheduler cpu-architecture


【解决方案1】:

不,调度程序没有在它自己的核心中运行。事实上,在多核 CPU 普及之前,多线程就很普遍了。

了解调度程序代码如何与线程代码交互的最佳方式是从一个简单、协作的单核示例开始。

假设thread A 正在运行,而thread B 正在等待一个事件。 thread A 发布该事件,这导致 thread B 变为可运行。事件逻辑必须调用调度程序,并且出于本示例的目的,我们假设它决定切换到thread B。此时调用堆栈将如下所示:

thread_A_main()
post_event(...)
scheduler(...)
switch_threads(threadA, threadB) 

switch_threads会将CPU状态保存在栈上,保存thread A的栈指针,并用thread B的栈指针的值加载CPU栈指针。然后它将从堆栈中加载剩余的 CPU 状态,其中 堆栈 现在是堆栈 B。此时,调用堆栈已变为

thread_B_main()
wait_on_event(...)
scheduler(...)
switch_threads(threadB, threadC)

换句话说,线程 B 现在已经唤醒,处于它之前将控制权交给线程 C 时的状态。当switch_threads() 返回时,它会将控制权返回给thread B

这种对堆栈指针的操作通常需要一些手工编码的汇编程序。

添加中断

Thread B 正在运行并且发生定时器中断。调用堆栈现在是

thread_B_main()
foo()   //something thread B was up to
interrupt_shell
timer_isr()

interrupt_shell 是一个特殊函数。它不被称为。它由硬件抢先调用。 foo() 没有调用interrupt_shell,所以当interrupt_shell 将控制权交还给foo() 时,它必须准确地恢复CPU 状态。这与普通函数不同,后者根据调用约定返回离开 CPU 状态。由于interrupt_shell 遵循与调用约定不同的规则,因此也必须用汇编程序编写。

interrupt_shell 的主要工作是识别中断源并调用适当的中断服务例程 (ISR),在本例中为timer_isr(),然后将控制权返回给正在运行的线程。

添加抢占式线程切换

假设timer_isr() 决定是时间片的时候了。线程 D 将获得一些 CPU 时间

thread_B_main()
foo()   //something thread B was up to
interrupt_shell
timer_isr()
scheduler()

现在,scheduler() 无法调用 switch_threads(),因为我们处于中断上下文中。但是,它可以很快被调用,通常就像interrupt_shell 所做的最后一件事一样。这使得thread B 堆栈保存在此状态

thread_B_main()
foo()   //something thread B was up to
interrupt_shell
switch_threads(threadB, threadD)

添加延迟服务例程

某些操作系统不允许您执行复杂的逻辑,例如在 ISR 中进行调度。一种解决方案是使用延迟服务例程 (DSR),它以高于线程但低于中断的优先级运行。使用这些是为了虽然scheduler() 仍需要保护以免被 DSR 抢占,但可以毫无问题地执行 ISR。这减少了内核必须屏蔽(关闭)中断以保持其逻辑一致的位置数量。

我曾经将一些软件从具有 DSR 的操作系统移植到没有 DSR 的操作系统。对此的简单解决方案是创建一个比所有其他线程运行的优先级更高的“DSR 线程”。 “DSR 线程”只是替换了其他操作系统使用的 DSR 调度程序。

添加陷阱

您可能已经在我目前给出的示例中观察到,我们正在从线程和中断上下文中调用调度程序。有两条进路和两条出路。它看起来有点奇怪,但它确实有效。然而,向前看,我们可能希望将线程代码与内核代码隔离开来,我们使用陷阱来做到这一点。这是使用陷阱重做的事件发布

thread_A_main()
post_event(...)
user_space_scheduler(...)
trap()
interrupt_shell
kernel_space_scheduler(...)
switch_threads(threadA, threadB) 

陷阱导致中断或类似中断的事件。在 ARM CPU 上,它们被称为“软件中断”,这是一个很好的描述。

现在所有对switch_threads() 的调用都在中断上下文中开始和结束,这通常发生在特殊的 CPU 模式下。这是迈向特权分离的一步。

如您所见,日程安排不是一天完成的。你可以继续:

  • 添加内存映射器
  • 添加进程
  • 添加多个核心
  • 添加超线程
  • 添加虚拟化

阅读愉快!

【讨论】:

    【解决方案2】:

    每个内核单独运行内核,并通过读/写共享内存与其他内核协作。内核维护的共享数据结构之一是准备运行的任务列表,并且只是等待时间片运行。

    内核的进程/线程调度程序运行在需要弄清楚接下来要做什么的内核上。这是一个distributed algorithm,没有单一的决策线程。

    通过确定应该在哪个其他 CPU 上运行什么任务来进行调度不起作用。它的工作原理是根据准备运行的任务确定这个 CPU 现在应该做什么。每当线程用完其时间片或进行阻塞的系统调用时,都会发生这种情况。在 Linux 中,even the kernel itself is pre-emptible,因此即使在需要大量 CPU 时间来处理的系统调用中间,也可以运行高优先级任务。 (例如,检查 open("/a/b/c/d/e/f/g/h/file", ...) 中所有父目录的权限,如果它们在 VFS 缓存中很热,因此不会阻塞,只会占用大量 CPU 时间)。

    我不确定这是否通过在(由)open()“手动”调用schedule() 中的目录遍历循环来完成,以查看当前线程是否应该被抢占。或者也许只是唤醒任务会设置某种硬件时间来触发中断,如果使用CONFIG_PREEMPT 编译,内核通常是可抢占式的。

    有一个处理器间中断机制要求另一个内核自行安排某些事情,所以上面的描述过于简单了。(例如,对于 Linux run_on 来支持 RCU 同步点,当另一个内核上的线程使用munmap 时,TLB 被击落)。但确实没有一个“主控程序”;通常,每个内核上的内核决定该内核应该运行什么。 (通过在准备好运行的任务的共享数据结构上运行相同的 schedule() 函数。)


    调度器的决策并不总是像把任务放在队列最前面那么简单:一个好的调度器会尽量避免线程从一个核心跳到另一个核心(因为它的数据在上次运行的核心,如果那是最近的)。因此,为了避免缓存抖动,调度程序算法可能会选择不在当前内核上运行准备好的任务,如果它只是在不同的内核上运行,而是将其留给其他内核稍后处理。这样一来,简短的中断处理程序或阻塞系统调用就不会导致 CPU 迁移。

    这在NUMA 系统中尤其重要,在“错误”内核上运行会长期变慢,即使缓存已填充。

    【讨论】:

    • 关于“[每个 CPU 都弄清楚] 这个 CPU 现在应该做什么”的部分是真的吗?如果一个线程/进程解除了另一个线程/进程的阻塞(例如,退出它正在等待的互斥锁),那么活动线程是否本质上不会启动“另一个”CPU 上的新可用工作?当然,它不会等待空闲 CPU 上的下一个计时器滴答来确定有准备好的工作。我不确定有多少调度程序在活动的“启动线程”与“启动线程”中运行(例如,也许前者只唤醒后者而几乎没有做其他事情),但肯定涉及启动 CPU。
    • @BeeOnRope:问得好。 IDK 如果它在这种情况下发送 IPI(处理器间中断),如果它可以看到另一个内核正在做一些它应该中断的事情。嗯,在 x86 上,内核以mwait 休眠。也许它实际上使用monitor 来设置一个地址,以便来自另一个内核的写入将其唤醒,作为 IPI 的替代方案。我几乎可以肯定当前 CPU 的决策是在 当前 CPU 上完成的。我认为提示睡眠 CPU 到 schedule() 更有可能在互斥解锁代码中完成,而不是在调度程序本身(在 Linux 上)。
    • 假设其他核心都在休眠。使用mwaithlt 或其他。当然,至少部分是用户模式代码的mutex-unlock 代码不会告诉另一个 CPU 唤醒:这些东西与“线程”(或跨进程构造的进程)抽象一起工作,而不是 CPU 的.他们只是告诉内核“取消驻留该线程”,然后内核决定该 thread 现在是否应该开始在 CPU 上运行(例如,这就是 FUTEX_WAKE 会做的事情内核端 - 我什至找不到 pre-futex calls?)。
    • 因此保证“调度程序”至少部分在唤醒线程上运行,以便决定 (a) 是否应唤醒睡眠核心或是否应中断活动核心和 (b ) 来实际发送“信号”(可以是 IPI 或写入mwait 地址或立即触发定时器中断)。因此,调度程序肯定涉及 CPU,而不是最终运行任务。唯一的问题是,被唤醒的 CPU 是否真的会重新计算所有内容,然后通常只运行它被唤醒的线程,或者它是否更像是一个切换。
    • @BeeOnRope:stackoverflow.com/questions/23908711/…的结尾简要描述了解锁过程。在用户空间中,您所知道的是其他线程正在等待内核在锁可用时唤醒它们,因此您进行系统调用。我认为在那个系统调用中,内核会做任何事情来唤醒它们以及它们应该运行的内核。这段代码可能必须了解调度器的设计和数据结构,但严格来说,它不是schedule() 函数或调度器本身的一部分。
    【解决方案3】:

    通用调度器有三种类型:

    作业调度器也称为长期调度器。

    短期调度器也称为 CPU 调度器。

    中期调度器,主要用于交换作业,因此可以进行非阻塞调用。这通常是因为没有太多或很少的 I/O 作业。

    在一本操作系统书籍中,它显示了这些调度程序往返的状态的一个很好的自动机。作业调度程序将作业队列中的内容放入就绪队列,CPU 调度程序将就绪队列中的内容放入运行状态。该算法就像任何其他软件一样,它必须在 cpu/core 上运行,它很可能是某个地方的内核的一部分。

    调度程序可以被抢占是没有意义的。队列中的作业可以在运行时被抢占,用于 I/O 等。不,内核不必自己调度来分配任务,它只是获得 cpu 时间而无需自己调度。是的,很可能数据在内存中,不确定是否值得存储在 cpu 缓存中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-02-17
      • 1970-01-01
      • 2013-01-23
      • 2012-03-27
      • 2021-01-23
      • 2016-10-12
      • 2012-08-23
      • 1970-01-01
      相关资源
      最近更新 更多