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