内核线程与用户线程之间的上下文切换

时间:2019-08-08 10:02:19

标签: linux unix linux-kernel operating-system kernel

this链接粘贴的副本:

  
      
  • 线程切换不需要内核模式特权。
  •   
  • 用户级线程可以快速创建和管理。
  •   
  • 内核线程通常比用户线程创建和管理慢。
  •   
  • 在同一进程中将控制权从一个线程转移到另一个线程需要将模式切换到内核。
  •   

在阅读标准操作系统参考书时,我从未遇到过这些问题。尽管这些观点听起来合乎逻辑,但我想知道它们如何反映在Linux中。准确地说:

  1. 有人可以给出涉及用户线程和内核线程之间的上下文切换的详细步骤,以便我可以找到两者之间的步骤差异。

  2. 有人可以解释与实际上下文切换示例或代码的区别。可能涉及系统调用(如果在内核线程之间进行上下文切换)和涉及线程库的调用(如果需要在用户线程之间进行上下文切换)。

  3. 有人可以将我链接到处理上下文切换的Linux源代码行(例如github上)。

  4. 我还怀疑为什么内核线程之间的上下文切换需要更改为内核模式。我们不是已经在内核模式下使用第一个线程了吗?

2 个答案:

答案 0 :(得分:1)

  

有人可以给出在用户线程和内核线程之间进行上下文切换的详细步骤,以便我可以找到两者之间的步骤差异。

让我们想象一个线程需要从文件中读取数据,但是该文件没有缓存在内存中,并且磁盘驱动器很慢,因此线程必须等待;为了简单起见,我们还假设内核是整体的。

对于内核线程:

  • 线程在库等中调用“ read()”函数;至少必须切换到内核代码(因为它将涉及设备驱动程序)。

  • 内核将IO请求添加到磁盘驱动程序的“可能有许多未决请求的队列”中;意识到线程将需要等待直到请求完成,将线程设置为“阻塞的等待IO”并切换到其他线程(取决于全局线程优先级,该线程可能属于完全不同的进程)。内核返回到它切换到的任何线程的用户空间。

  • 稍后;磁盘硬件会引起IRQ,IRQ会导致切换回内核代码中的IRQ处理程序。磁盘驱动程序完成了对(当前被阻止的)线程所做的工作,并取消了对该线程的阻止。在这一点上,内核可能决定切换到“现在不受阻塞”的线程。然后内核返回到“现在不受阻塞”线程的用户空间。

对于用户线程:

  • 线程在库等中调用“ read()”函数;至少必须切换到内核代码(因为它将涉及设备驱动程序)。

  • 内核将IO请求添加到磁盘驱动程序的“可能有许多未决请求的队列”中;意识到线程将需要等待直到请求完成,但是无法解决这个问题,因为某些傻瓜决定通过在用户空间中进行线程切换来使一切变得更糟,因此内核返回“ IO请求已排队,并返回用户空间” ”状态。

  • 切换回用户空间无意义的额外开销;用户空间调度程序执行内核可能完成的线程切换。此时,用户空间调度程序将告诉内核它无事可做,而切换回内核将产生更多毫无意义的额外开销。或用户空间调度程序将在同一进程中将线程切换到另一个线程(这可能是错误的线程,因为不同进程中的线程具有更高的优先级)。

  • 稍后;磁盘硬件会引起IRQ,IRQ会导致切换回内核代码中的IRQ处理程序。磁盘驱动程序完成了对(当前阻塞的)线程必须执行的工作;但是内核无法执行线程切换来解除线程阻塞,因为有些傻瓜决定通过在用户空间中进行线程切换来使一切变得更糟。现在我们有一个问题-内核如何通知用户空间调度程序IO已完成?要解决此问题(没有任何“运行零线程的用户空间调度程序不断轮询内核”的混乱),您必须具有某种“内核将IO完成的通知放在某种队列中,并且(如果进程处于空闲状态)唤醒进程” ”(单独)将比仅在内核中执行线程切换要昂贵。当然,如果进程不是空闲的,那么用户空间中的代码将不得不轮询其通知队列,以了解“ IO完成通知”是否到达/何时到达,这将增加延迟和开销。无论如何,经过大量愚蠢的,毫无意义的和可避免的开销之后;用户空间调度程序可以执行线程切换。

  

有人可以解释与实际上下文切换示例或代码的区别。可能涉及系统调用(如果在内核线程之间进行上下文切换)和涉及线程库的调用(如果需要在用户线程之间进行上下文切换)。

实际的低级上下文切换代码通常以类似以下内容的开头:

  • 根据堆栈上的调用约定保存“调用者保留的”任何寄存器

  • 将当前堆栈顶部保存在属于旧线程的某种“线程信息结构”中

  • 从属于新线程的某种“线程信息结构”中加载新的堆栈顶部

  • 根据调用约定弹出“保留调用者”的寄存器

  • 返回

但是:

  • 通常(对于现代CPU)存在相对大量的“ SIMD寄存器状态”(例如,对于支持AVX-512的80x86,我认为它已经超过4 KiB了)。 CPU制造商通常有一种机制来避免保留状态的一部分(如果未更改),并且(可选)将状态的(部分)加载推迟到实际使用为止(如果不实际使用,则完全避免使用)。所有这些都需要内核。

  • 如果这是一个任务切换器,而不仅仅是用于线程切换,那么您可能需要在其顶部添加某种“如果虚拟地址空间需要更改{更改虚拟地址空间}”

  • 通常,您希望跟踪统计信息,例如线程已使用了多少CPU时间。这需要某种“ thread_info.time_used += now() - time_at_last_thread_switch;”;当“进程切换”与“线程切换”分开时,这变得很困难/难看。

  • 通常情况下,可能需要在线程切换期间保存/加载其他状态(例如,指向线程本地存储的指针,用于性能监视和/或调试的特殊寄存器...)。通常,无法通过用户代码直接访问此状态。

  • 通常,您还希望将计时器设置为在线程使用过多时间时到期;要么是因为您正在执行某种“时间复用”(例如循环调度程序),要么是因为它需要一个协作的调度程序,在这种情况下,您需要某种“在无响应的情况下5秒钟后终止此任务,以防它进入永远无限循环”。

  • 这仅仅是隔离中的低级任务/线程切换。几乎总是有更高级别的代码来选择要切换的任务,处理“线程使用了过多的CPU时间”等。

  

有人可以将我链接到处理上下文切换的Linux源代码行(在github上说)

有人可能做不到。这不是一行。每个不同的体系结构都有许多行汇编,外加额外的高级代码(用于计时器,支持例程,“选择要切换到的任务”代码,用于异常处理程序以支持“惰性SIMD状态加载”的...) ;大概所有这些加起来大约有1万行代码,分布在50个文件中。

  

我还怀疑为什么内核线程之间的上下文切换需要更改为内核模式。我们不是已经在内核模式下使用第一个线程了吗?

是;通常,当您发现需要进行线程切换时,您已经在内核代码中。

很少/有时(主要是由于属于同一进程的线程之间的通信-例如,同一进程中的2个或更多线程试图同时获取相同的互斥体/信号量;或者线程互相发送数据,并且不涉及等待彼此的数据到达)内核;在某些情况下(这几乎总是大规模的设计失败,例如极端的锁争用问题,无法使用“工作线程池”来限制所需的线程数等),这可能是导致线程切换的主要原因,因此在用户空间中进行线程切换可能是有益的(例如,作为大规模设计失败的解决方法)。

答案 1 :(得分:0)

不要将自己限制在Linux甚至UNIX上,它们既不是系统也不是编程模型的头等舱。同步执行模型的历史可以追溯到计算的早期,并不是特别适合大规模并发和反应式编程。

例如,

Golang使用了许多轻量级用户线程-goroutines-并将它们多路复用到较小的一组重量级内核线程上,以产生更具吸引力的并发范例。其他一些编程系统也采用类似的方法。