内核和用户线程之间的关系
内核和用户线程之间是否有关系?
一些操作系统教科书说“ 将一个(许多)用户线程映射到一个(许多)内核线程”。 地图在这里意味着什么?
当他们说map时,它们表示每个内核线程被分配给一定数量的用户模式线程。
内核线程用于为应用程序(如系统调用)提供特权服务。 它们也被内核用来跟踪系统上正在运行的是什么,有多less资源分配给哪个进程,以及调度它们。
如果您的应用程序大量使用系统调用,则每个内核线程的用户线程数会增加,而您的应用程序将运行得更慢。 这是因为内核线程将成为瓶颈,因为所有的系统调用都会通过它。
另一方面,如果程序很less使用系统调用(或其他内核服务),则可以将大量用户线程分配给内核线程,而不会牺牲性能,而不是开销。
你可以增加内核线程的数量,但是这会增加内核的总体开销,所以当单个线程对系统调用的响应更快时,整个系统会变慢。
这就是为什么在每个内核线程的内核线程数量和用户线程数量之间find一个很好的平衡。
http://www.informit.com/articles/printerfriendly.aspx?p=25075
在用户空间中实现线程
有两种主要的方式来实现一个线程包:在用户空间和内核中。 这个select适度有争议,混合实现也是可能的。 现在我们将描述这些方法,以及它们的优点和缺点。
第一种方法是将线程包完全放在用户空间中。 内核对它们一无所知。 就内核而言,它正在pipe理普通的单线程进程。 第一个也是最明显的优点是用户级线程包可以在不支持线程的操作系统上实现。 所有使用的操作系统都属于这个类别,甚至现在有些还在做。
所有这些实现具有相同的总体结构,如图2-8(a)所示。 线程运行在一个运行时系统之上,这是一个pipe理线程的过程的集合。 我们已经看到了其中的四个:thread_create,thread_exit,thread_wait和thread_yield,但通常还有更多。
当用户空间中pipe理线程时,每个进程都需要自己的专用线程表来跟踪该进程中的线程。 这个表类似于内核的进程表,除了它只跟踪每个线程的属性,比如每个线程的程序计数器,堆栈指针,寄存器,状态等。线程表由运行时系统pipe理。 当线程移动到就绪状态或阻塞状态时,重新启动所需的信息将存储在线程表中,与内核存储有关进程表中进程的信息的方式完全相同。
当一个线程执行某些可能导致它在本地被阻塞的情况时,例如,等待进程中的另一个线程完成一些工作,它会调用一个运行时系统过程。 这个过程检查线程是否必须进入阻塞状态。 如果是这样,则将线程的寄存器(即,它自己的)存储在线程表中,在表中寻找准备好的线程来运行,并用新线程的保存值重新加载机器寄存器。 一旦堆栈指针和程序计数器切换,新线程就会自动重新生效。 如果机器有一个存储所有寄存器的指令,另一个指令将所有的寄存器全部加载,那么整个线程切换可以通过一些指令完成。 像这样做线程切换至less比陷入内核快一个数量级,并且是支持用户级线程包的有力论据。
但是,与stream程有一个关键的区别。 当一个线程完成运行的时候,例如,当它调用thread_yield时,thread_yield的代码可以将线程的信息保存在线程表本身中。 此外,它可以调用线程调度器来select另一个线程来运行。 保存线程状态和调度程序的过程只是本地过程,因此调用它们比调用内核要有效得多。 在其他问题中,不需要陷阱,不需要上下文切换,不需要刷新内存caching等等。 这使得线程调度非常快。
用户级线程也有其他优点。 它们允许每个进程拥有自己的自定义调度algorithm。 对于某些应用程序,例如那些带有垃圾回收器线程的应用程序,不用担心线程在不方便的时候被停止是一个好的select。 由于内核线程在内核中总是需要一些表空间和堆栈空间,所以它们也可以更好地扩展,如果线程数量非常大,这可能是一个问题。
尽pipe它们有更好的性能,用户级线程包还是有一些主要的问题。 其中首先是阻止系统调用的问题。 假设一个线程在任何键被击中之前从键盘读取。 让线程实际上使系统调用是不可接受的,因为这将停止所有的线程。 首先有线程的主要目标之一是允许每个人使用阻塞调用,但是阻止一个阻塞线程影响其他线程。 通过屏蔽系统调用,很难看到如何轻松实现这个目标。
系统调用可以全部更改为非阻塞(例如,如果没有字符已经被缓冲,则键盘上的读取将仅返回0字节),但是要求改变操作系统是没有吸引力的。 另外,用户级线程的一个参数就是它们可以在现有的操作系统上运行。 另外,改变读取的语义将需要改变许多用户程序。
如果可以预先知道呼叫是否被阻止,另一个可能的scheme是可能的。 在某些版本的UNIX中,存在一个系统调用select,它允许调用者判断预期的读取是否会被阻塞。 当这个调用存在时,读取的库过程可以被一个新的replace为一个新的调用,然后只有在读取调用是安全的(即不会被阻塞)时才执行。 如果读取呼叫将被阻止,则不会进行呼叫。 相反,另一个线程运行。 下一次运行时系统得到控制,它可以再次检查是否读取现在是安全的。 这种方法需要重写系统调用库的一部分,效率低下,不够优雅,但是没有多lessselect。 放在系统调用周围的代码被称为夹克或包装。
有点类似于阻止系统调用的问题是页面错误的问题。 我们将在Chap中学习。 4.目前来说,计算机可以这样设置,即不是所有的程序都在主存中。 如果程序调用或跳转到不在内存中的指令,则会发生页面错误,操作系统将从磁盘中获取缺less的指令(及其邻居)。 这被称为页面错误。 当必要的指令被定位并读入时,该进程被阻塞。如果一个线程导致页面错误,内核甚至不知道线程的存在,自然会阻塞整个进程直到磁盘I / O完成,甚至尽pipe其他线程可能是可运行的。
用户级线程包的另一个问题是,如果一个线程开始运行,除非第一个线程自愿放弃CPU,否则该进程中的其他线程将不会运行。 在一个单一的过程中,没有时钟中断,使得不可能安排循环stream程(轮stream)。 除非线程进入自由意志的运行时系统,否则调度程序将永远不会有机会。
永久运行线程问题的一个可能的解决scheme是让运行系统每秒钟请求一个时钟信号(中断)来控制它,但是这同样也是粗糙和杂乱的编程。 更高频率的周期性时钟中断并不总是可能的,即使它们是,总开销可能是相当大的。 此外,一个线程可能还需要一个时钟中断,干扰运行时系统对时钟的使用。
另一种可能是对用户级线程最具破坏性的论点是,程序员通常希望线程能够精确地在线程阻塞的应用程序中,例如在multithreadingWeb服务器中。 这些线程不断地进行系统调用。 一旦内核出现陷阱来执行系统调用,如果内核已经被阻塞,内核切换线程几乎没有任何工作,并且让内核这样做,这就消除了不断地进行select系统调用的需要检查看看系统调用是否安全。 对于基本上完全受CPU约束且很less阻塞的应用程序,线程究竟有什么意义? 没有人会认真地提出计算前n个素数或使用线程下棋,因为这样做没有什么可以获得的。
用户线程在用户空间中进行pipe理 – 这意味着调度,交换等不是来自内核。
由于操作系统内核最终负责在“执行单元”之间进行上下文切换,因此用户线程必须与内核调度对象(内核线程[+1])关联(即“映射”)。
所以,给定N个用户线程 – 你可以使用N个内核线程(1:1映射)。 这允许你利用内核的硬件多处理(在多CPU上运行),并且是一个相当简单的库 – 基本上只是将大部分工作推迟到内核。 但是,它确实使你的应用程序可以在操作系统之间移植,因为你不直接调用内核线程函数。 我相信POSIX线程( PThreads )是首选的* nix实现,它遵循1:1映射(使其几乎等同于内核线程)。 然而,这并不能保证,因为它会依赖于实现(使用PThreads的主要原因是内核之间的可移植性)。
或者,您可以只使用1个内核线程。 这将允许您在非多任务操作系统上运行,或者完全负责调度。 Windows的用户模式调度是这个N:1地图的一个例子。
或者,您可以映射到任意数量的内核线程 – N:M映射。 Windows有Fibers ,它允许你将N个光纤映射到M个内核线程,并合作安排它们。 一个线程池也可以是这样一个例子 – N个M线程的工作项。
[+1]一个进程至less有一个内核线程,它是实际的执行单元。 另外,一个内核线程必须包含在一个进程中。 操作系统必须安排线程运行 – 而不是进程。
- 这是关于线程库实现的一个问题。
- 在Linux中,线程(或任务)可以在用户空间或内核空间中。 当进程通过系统调用(read,write或ioctl)请求内核执行某些操作时,进程会进入内核空间。
- 还有一个所谓的内核线程,它总是在内核空间中运行,并不代表任何用户进程。
根据Wikipedia和Oracle ,用户级线程实际上是在内核线程上挂载的一个图层; 不是内核线程与用户级线程一起执行,而是一般来说,处理器/操作系统实际执行的唯一实体是内核线程。
例如,假设我们有一个带有2个用户级线程的程序,它们都映射到(即分配)相同的内核线程。 有时,内核线程运行第一个用户级线程(并且据说当前这个内核线程被映射到第一个用户级线程),有时候内核线程运行第二个用户级线程。 所以我们说我们有两个用户级线程映射到同一个内核线程。
作为澄清 :
一个操作系统的核心被称为内核 ,所以内核级别的线程(即内核知道和pipe理的线程)被称为内核线程,调用OS核心的服务可以称为内核调用。 .. 内核 事物之间唯一确定的关系是它们与OS核心密切相关,仅此而已。