【问题标题】:Context switch between kernel threads vs user threads内核线程与用户线程之间的上下文切换
【发布时间】:2019-12-16 00:18:50
【问题描述】:
复制粘贴自this链接:
- 线程切换不需要内核模式权限。
- 用户级线程可以快速创建和管理。
- 创建和管理内核线程的速度通常比用户线程慢。
- 在同一进程中将控制从一个线程转移到另一个线程需要将模式切换到内核。
我在阅读标准操作系统参考书时从未遇到过这些要点。尽管这些观点听起来合乎逻辑,但我想知道它们如何反映在 Linux 中。准确地说:
谁能给出用户线程和内核线程之间上下文切换的详细步骤,以便我找出两者之间的步骤差异。
谁能解释一下与实际上下文切换示例或代码的区别。可能涉及系统调用(如果内核线程之间的上下文切换)和线程库调用(如果用户线程之间的上下文切换)。
有人可以将我链接到处理上下文切换的 Linux 源代码行(比如在 github 上)吗?
我也怀疑为什么内核线程之间的上下文切换需要更改为内核模式。我们不是已经处于第一个线程的内核模式了吗?
【问题讨论】:
标签:
linux
unix
linux-kernel
operating-system
kernel
【解决方案1】:
谁能给出用户线程和内核线程之间上下文切换的详细步骤,以便我找出两者之间的步骤差异。
让我们假设一个线程需要从一个文件中读取数据,但是该文件没有缓存在内存中并且磁盘驱动器很慢,所以线程必须等待;为简单起见,我们还假设内核是单片的。
对于内核线程:
线程调用库或其他东西中的“read()”函数;这必须至少导致切换到内核代码(因为它将涉及设备驱动程序)。
内核将 IO 请求添加到磁盘驱动程序的“可能有许多未决请求的队列”;意识到线程需要等待直到请求完成,将线程设置为“阻塞等待 IO”并切换到不同的线程(可能属于完全不同的进程,取决于全局线程优先级)。内核返回到它切换到的任何线程的用户空间。
稍后;磁盘硬件导致一个 IRQ 导致切换回内核代码中的 IRQ 处理程序。磁盘驱动程序完成了它必须为(当前阻塞的)线程执行的工作并解除阻塞该线程。此时内核可能决定切换到“现在未阻塞”的线程;并且内核返回到“现在未阻塞”线程的用户空间。
对于用户线程:
线程调用库或其他东西中的“read()”函数;这必须至少导致切换到内核代码(因为它将涉及设备驱动程序)。
内核将 IO 请求添加到磁盘驱动程序的“可能有许多未决请求的队列”;意识到线程需要等到请求完成但无法处理,因为有些傻瓜决定通过在用户空间进行线程切换来使一切变得更糟,因此内核返回用户空间并显示“IO请求已排队" 状态。
在切换回用户空间的无意义的额外开销之后;用户空间调度程序执行内核可以完成的线程切换。此时,用户空间调度程序要么告诉内核它无事可做,要么你将有更多毫无意义的额外开销切换回内核;或用户空间调度程序将线程切换到同一进程中的另一个线程(这可能是错误的线程,因为不同进程中的线程具有更高的优先级)。
稍后;磁盘硬件导致一个 IRQ 导致切换回内核代码中的 IRQ 处理程序。磁盘驱动程序完成了它必须为(当前阻塞的)线程做的工作;但是内核无法通过线程切换来解除线程阻塞,因为某些傻瓜决定通过在用户空间进行线程切换来使一切变得更糟。现在我们遇到了一个问题——内核如何通知用户空间调度程序 IO 已经完成?为了解决这个问题(没有任何“运行零线程的用户空间调度程序不断轮询内核”的疯狂),您必须有某种“内核将 IO 完成通知放在某种队列上,并且(如果进程空闲)唤醒进程up”这(单独)将比仅在内核中进行线程切换更昂贵。当然,如果进程不是空闲的,那么用户空间中的代码将不得不轮询其通知队列以找出“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 状态加载”的异常处理程序,...) ;这可能加起来相当于分布在 50 个文件中的 10000 行代码。
我也怀疑为什么内核线程之间的上下文切换需要更改为内核模式。第一个线程不是已经处于内核模式了吗?
是的;当你发现需要线程切换时,通常你已经在内核代码中了。
很少/有时(主要是由于属于同一进程的线程之间的通信 - 例如,同一进程中的 2 个或多个线程试图同时获取相同的互斥体/信号量;或线程相互发送数据和等待彼此的数据到达)不涉及内核;并且在某些情况下(几乎总是大规模的设计失败 - 例如极端的锁争用问题,未能使用“工作线程池”来限制所需的线程数量等)这可能是线程切换的主要原因,因此,在用户空间中进行线程切换可能是有益的(例如,作为大规模设计失败的解决方法)。
【解决方案2】:
不要将自己限制在 Linux 甚至 UNIX 上,它们既不是系统或编程模型的第一名,也不是最后一名。同步执行模型可以追溯到计算的早期阶段,并不是特别适合更大规模的并发和反应式编程。
例如,Golang 采用了大量轻量级用户线程(goroutines),并将它们多路复用到一组较小的重量级内核线程上,以产生更引人注目的并发范式。其他一些编程系统采用类似的方法。