免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 955 | 回复: 0
打印 上一主题 下一主题

nptl_design [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2007-01-22 18:06 |只看该作者 |倒序浏览

Linux原生POSIX线程库
               
                Ulrich
Drepper          Ingo Molnar
                Red
Hat, Inc.           Red Hat, Inc.
                drepper@redhat.com      mingo@redhat.com
               
                            2005/9/21
   
    在这篇文档被用来描述当前实现的缺陷的时候,它肯定完全过时了。每个引用这篇文档来描述NPTL缺点的人,要么是没有完成他/她的家庭作业、调查问题的笨蛋,要么是故意误导大家,就像从西北部来的恶棍经常说的那样(as it happens too often with publications done by the Evil from the
North-West)。

    今天,作为linux系统组成部分的POSIX线程库实现--LinuxThreads线程库,已经不能很好的满足人们对于线程的需求。它不是利用我们现在已经有的,以及不久的将来也可用的linux扩展实现的。它没有与时俱进(it does not scale),也没有考虑现代处理器的架构。一个全新的设计是必要的,这篇论文就描述了我们提出的设计。
                    
1. 第一个实现
LinuxThreads作为现在的Linux标准POSIX线程库实现,遵循代码所写时(1996)内核开发者提出的原则。其基本假定是,相关过程之间的上下文切换足够快,快到可以用一个内核线程处理每个用户级线程。内核进程可以有不同的关联度。POSIX线程规范要求可以共享几乎所有的资源。

    对于目标架构缺少thread_aware的ABI,因此这个设计没有使用线程寄存器。线程局部存储也被替换为利用栈指针和线程描述符位置之间的确定关系来定位。

    另外,一个管理线程(manager thread)必须被创建,以便实现关于信号,线程创建,以及其他许多进程管理的正确语义。

    或许最大的问题是,内核中缺少可用的同步原语,这就强制实现去使用信号。这,以及当时内核版本中内核线程组(kernel thread group)概念的缺失,一起致使线程库不兼容、脆弱的信号处理。

2. 不断地改进
在接下来的六年中,线程库的代码明显的改进了。改进来自两个方面:ABI 和 内核。
    新定义的ABI扩展允许使用线程寄存器或者能够像线程寄存器一样工作的构件。这是一个本质上的改进,因为现在线程局部数据的定位不是很费时的操作了。定位线程局部数据对于线程库的任何操作来说都是很基本的。其他的运行时环境和用户应用同样需要。

    对于许多架构来说,改变是很容易的。其中一些架构有专用的寄存器;另一些架构具有特殊的处理器特性,允许在执行上下文中保存值。但是还有一些架构因为没有这些特性而被忽略了。这些架构仍然依赖于利用栈地址来计算线程局部数据的方法。这不仅是一个很慢的计算,而且还意味着那些允许程序员选择栈地址和大小的API无法在这些架构上实现。当大量线程被使用时,不管是同时使用还是依次使用,这些借口都是非常重要的。

    有必要解释一下用于IA-32的解决方案,因为它不是straight-forward,并且毋庸置疑,它是最重要的架构,这影响了设计。IA-32架构,being registor starved,有两个寄存器不会被ABI用到:段寄存器%fs和%gs。虽然不能用来存储任意的数据,但是却可以用来访问进程的虚拟寄存器空间中有确定偏移值的任意位置的存储。段寄存器的值可以用来访问内核为处理器存取而创建的数据结构,数据结构中保存着每个有效的段索引的基址。利用不同的基址,可以用相同的偏移来访问虚拟地址空间中的不同部分,而这正是访问线程局部数据所需要的。


    这个方法的问题在于,段寄存器必须被处理器使用的那些数据结构支持。段基址保存在描述符表中。对这些数据结构的访问仅限于操作系统自己,而非用户级的代码。这意味着修改描述符表的操作是很慢的。因为每次上下文切换中必须重新加载额外的数据结构,不同进程的上下文切换也变得更慢了。另外,内核必须处理描述符表的存储分配问题,而由于所需内存的本质(它一定在很难到达的固定的映射存储位置——it must be in
permanently mapped memory locations which can be difficult to achieve),当描述符表中需要很多表项时,这会成为麻烦。另外,段寄存器可以存储的不同值的数量,以及相应的可以表示的不同地址的数量,被限制在8192。

    总之,利用“线程寄存器”带来了速度、弹性以及API完整性,但是它限制了线程的数量,对于系统的性能也有负面作用。

    内核发展到2.4版的时候,库的改变包括功能的稳定,使得允许在IA32架构上使用线程寄存器;以及clone()——用于创建内核线程的系统调用——的改进。这些变化消除了一些对管理者线程的存在性的需求,同时提供了进程ID的正确语义,也就是说,对于所有的线程来说,进程ID是相同的。

    由于一些原因,管理者线程并没有完全消除。其中一个原因是栈存储的释放不能够由正在使用存储的线程来进行。另一个原因是,为了避免僵死的内核线程,要终止的线程必须等待。

    虽然这些以及其他一些问题没有解决,却并没有考虑重写线程库以利用新的、可用的特性的优点。

3. 现有的实现的问题
现有的实现在许多应用中都执行的很好,但是仍然有很多问题,特别是当压力很大时:
l        
管理线程的存在引起了一些问题。如果管理线程被杀死了,那么进程的其他部分就陷入了必须手工清理的状态。管理线程的一些处理操作,例如线程创建和清理,使得它成为瓶颈。
l        
信号系统被严重破坏了。它不符合POSIX标准所规定的行为,向进程整体发送信号没有实现。
l        
使用信号实现同步原语引发了非常多的问题。操作的潜在开销是很昂贵的,线程库中本来就复杂的信号处理甚至变得更加复杂。虚假的唤醒无时不在发生,并且到处被处理(and must be worked around)。除了曲解了唤醒,这还加重了内核信号系统的负担。
l        
作为被破坏的信号处理的显著特例,SIGSTOP和SIGCONT的不正确实现需要提及。缺少内核对于这些信号的正确处理,用户就没法停止一个多线程的进程(e.g.在具有作业处理支持的终端里按Ctrl-Z)。只有一个线程会被停掉。调试器也有同样的问题。
l        
每个线程具有不同的进程ID导致与其它POSIX线程库实现的兼容问题。由于信号也不能很好的使用,这点倒也不那么令人担心,不过仍然值得注意。
l        
IA-32架构上对于线程数的限制(最多只能有8192线程,除去一个管理线程),对于一些人来说也是问题。虽然这种情形下线程经常被滥用,那些令人厌恶的程序倒也可以在其他平台上工作。

内核也有一些问题:
l        
拥有成百上千个线程的进程使得/proc文件系统几乎不可用。每个线程都表现为一个独立的进程。
l        
信号实现的问题主要归咎于缺少内核的支持。特殊的信号,例如SIGSTOP,必须由内核来处理,并且要为所有的线程处理。
l        
为了实现同步原语而带来的对信号的误用使问题更加严重。为确保同步而发送信号是一种很笨重的方式。
   
4.新实现的目标
试图修正现有实现的问题是不值得的。当时全部的设计都集中于Linux内核的限制。一个完全重写的版本是必要的。目标是ABI兼容(受惠于POSIX线程API的设计方式,这个目标是可以实现的)。必须要重新评估已做出的每个设计决策。做出正确的决策意味着要理解实现的需求。已被修正的需求包括:
    POSIX兼容     符合最新的POSIX标准,与其它平台做到源代码兼容是最高目标。这并不意味着不能添加对POSIX标准的扩展。
    有效利用SMP    使用线程的一个主要目标是提供利用多处理器系统能力的方法。理论上,将工作分配到各个CPU上并行运行能够提供线性加速。
    低启动成本     创建新的线程应该只有很低的成本,使得即使很小的工作也可以使用多线程。
    低链入(Link-in)成本   那些(直接或间接地)链接了线程库却没有使用线程的程序不应该为此负担太多的代价。
    二机制兼容     新的实现与原来的Linux-Threads实现应该是二进制兼容的。一些语义方面的差异是不可避免的:因为Linux-Threads实现不符合POSIX标准,这些方面的改变是必要的。
    硬件可伸缩性       线程实现应该在大量的处理器上运行良好。随着处理器数量的增加,管理成本不应该增长太多。
       软件可伸缩性             线程的另一个应用是解决用户程序在独立的执行上下文中的小问题。在java中,由于缺少异步操作,线程用于实现设计环境(to implement the
programming environment)。对于新线程库也是一样:数量巨大的线程可以被创建。新线程库对于线程或者其他任何东西的数量不应该有确定的限制。
       机器架构支持             为支持大型机而作的设计要比支持主流的机器复杂的多。对于这些机器的有效支持需要内核和跟OS密切相关的用户级代码理解机器架构的细节。例如大型机的处理器经常分为独立的节点,使用其他节点的资源是很昂贵。
       NUMA支持                  一类逐渐兴起的特殊机器是基于非一致存储访问架构(NUMA)的。像线程库这种代码在设计时要时刻记住:当在这种机器上使用线程时应该充分利用这种架构优势。数据结构的设计是很重要的。
       与C++的集成                     C++定义了异常处理,当异常抛出时,作用域内的对象会被自动清理。线程的取消跟异常类似,期望取消时调用必要的对象销毁器是合理的。

5.设计决策
在开始实现之前,要做许多基本的决策。它们从根本上影响到实现。

5.1 1-on-1 或 M-on-N
需要做的最基本的决策是关于内核线程与用户级线程的对应关系。毫无疑问,内核线程是必须要用到的。一个纯粹的用户级实现不能利用多处理器机器的好处。而前面说过,充分利用多处理器是我们的目标。整个线程库只是内核函数之上的简单扩展。
       另一个选择是库遵循M-on-N的模型,内核线程跟用户级线程不一定是确定的对应关系。这种实现在空闲的内核线程上调度用户线程。因此,就有两个调度器在工作。如果它们不能协调调度策略就会造成性能的降低。过去几年中,许多用于协调的方案被提出来。最有前途,也是用的最多的一个是调度器激活(Scheduler Activation)。在这里,两个调度器密切的合作:当内核调度器将它的策略通知给用户级调度器时,用户级调度器会作出应答。
       内核开发者一致认为M-on-N模型的实现不符合Linux内核的概念。需要添加的一个必要的基础设施会有十分高昂的代价。用户级调度器中的上下文切换经常需要从内核空间复制寄存器内容。
       另外,用户级调度可以防止的许多问题对内核来说都不是真正的问题。因为调度器和其他所有的核心例程具有常量执行时间(O(1))而不是关于活动的进程和线程数的线性时间,因此大量的线程不是问题。
       最后,为M-on-N实现维护额外的必要代码的代价也不能忽略。尤其是对线程库这种高复杂度的代码,一个干净精致的实现更不应忽略这些额外代价(Especially for highly complicated code like a thread library a lot can be
said for a clean and slim implementation.)。
      
5.2信号处理
使用M-on-N模型的另一个原因是简化内核中的信号处理。不管信号是否被忽略,当信号处理器以及与此相关的行为具有进程范围(process-wide)属性时,信号掩码是线程粒度的。当进程中有数以千计的线程时,这意味着内核潜在的需要检查每个线程的信号掩码来决定信号是否可以派发。如果可以通过M-on-N模型保持低数量的内核线程,信号派发工作就可以在用户级完成。
       但是在用户级完成最终信号的派发有许多问题。一个不期待某个特定信号的线程不会注意它接收到一个信号。只有当线程的栈用于发送信号,以及线程从系统调用收到一个EINTR错误的时候,信号才会被注意到。前一个问题通过预留一个用于信号发送的栈来避免。但是信号栈对于用户来说也是可用的。允许两者存在是很困难的。为了防止系统调用返回的不可接受的EINTR结果,系统调用包装器(wrapper)必须被扩展,这就会给普通操作带来额外的负担。
       对于信号发送有两种选择:
1.  信号被发送到一个专门的进程,这个进程不执行用户代码(或者至少没有不期望接受所有可能信号的代码)。这种方式的缺点是附加的线程带来的开销,还有更重要的是,信号的序列化。后者意味着,即使专用的信号线程将信号分发到其他线程,信号首先要以漏斗状的方式经过信号线程。这违背了POSIX线程模型的真正意图(但不是字面上的),POSIX线程模型允许并行处理信号。如果对信号的响应时间成为问题,应用可能就会创建多个线程用于信号处理。这会因为信号线程的使用而失败。
2.  
信号可以用另一种不同的方式传递到用户级。不使用单独的信号处理器,取而代之的是单独向上呼叫的机制(a separate upcall)。基于信号激活的实现使用的就是这种方式。代价是内核中复杂性增加,内核必须实现信号的二次分发机制,用户级代码也必须模拟一些信号功能。例如,如果所有的线程阻塞在read调用上,一个信号想通过返回EINTR唤醒一个线程,这种情况下,必须能够继续工作。

总之,在用户级实现M-on-N模型的信号处理是可能的,不过很困难。需要更多的代码,这会减慢正常操作的速度。
另一种选择是内核实现信号处理。这种情况下内核必须处理大多数的信号掩码,不过这是唯一的问题。因为信号只会发送到一个线程,如果线程没有阻塞、信号发生过程中没有不多余的中断。内核也能够工作在更好的状态去判断那个线程最适合接受信号。很明显,只有使用1-on-1模型,这种方式才会有用。

5.3 是否使用协助/管理线程
在老的线程库中,必须使用一个所谓的管理线程来处理内部工作。管理线程从来不会执行代码。而是由其他线程向他发送诸如“创建新线程”之类的请求,管理线程持续的围绕这些请求工作。对于实现一些问题的正确语义来说,这是必要的。
l        
为了能够响应致命(fatal)信号杀死整个进程,线程的创建者必须经常地监视所有的子线程。在内核不接管这个工作的情况下,除非有一个管理线程,否则是不可能实现的。
l        
作为栈空间使用的内存必须线程结束后被销毁。因此线程不能自己做这些工作。
l        
要中止的线程必须等待,以防止进入僵死状态。
l        
如果主线程调用pthread_exit,进程不能中止;主线程进入休眠状态,进程终止后,管理线程负责唤醒它。
l        
许多情况下,线程需要帮助,以便处理semaphore操作。
l        
线程局部数据的释放需要遍历所有的线程,这需要由管理线程来做。

         这些问题中都不必然意味着需要使用管理线程。借助内核的支持,管理线程根本就不需要。如果在内核中有POSIX信号处理的正确实现,第一个问题就解决了。第二个问题可以通过让内核执行释放(不管在实现中这意味着什么)来解决。第三个问题通过让内核自动处理结束的线程来解决。其他的问题也有解决方案,或者在内核里,或者在线程库里。
         不强制诸如线程创建之类重要而经常的操作序列化,将会有明显的性能收益。管理线程只能在一个CPU上运行,因此任何的同步在SMP系统上都会引起严重的伸缩性(scalability)问题,而在NUMA系统上,这个问题会更加严重。过度依赖管理线程也会给上下文切换带来明显增加的代价。在任何情况下,没有管理线程会简化设计。因此,新实现的目标应该是避免管理线程。

5.4 线程列表
旧的线程库会维护一个所有正在运行的线程的列表,这个列表偶尔会被遍历以便在所有线程上执行操作。最重要的应用是当进程结束的时候杀死所有的线程。如果进程结束时内核负责杀死线程,就可以避免使用列表。
         这个列表还被用来实现pthread_key_delete函数。如果一个key被pthread_key_delete删除了,随后当调用pthread_key_create返回同样的key时被重新使用,实现必须保证对于所有的线程,与key关联的值为NULL。旧的实现通过在key被删除时,主动清理线程私有(thread_specific)内存数据结构中的对应项来实现这种功能。
         这不是唯一可用的实现方式。如果线程列表(或者遍历它的动作)不得不避免,那么必须能够确定是否需要调用一个析构子(destructor)。一种实现方式是使用生成计数器。每个为线程局部存储所用的key,以及在线程结构中为其分配的存储,都有一个计数器。当一个key被分配的时候,这个key的生成计数器的值就会增加,同时新的值也会被赋值到线程数据结构中与key相关的计数器。删除一个key会导致与此key关联的线程计数器的值再次增加。当线程结束时,只有其关联的key的计数器值与线程数据结构中计数器值相符的线程的析构子才会被执行。Key的删除变成了一个简单的增加操作。
         维护线程列表并不能完全避免。为了实现无内存——这些内存用于线程栈以及保存其他内部信息——泄露的fork函数,维护线程列表示有必要的。除非调用fork的线程是再生的。这种情况下内核无法提供帮助。

5.5 同步原语
对于像互斥体,读写锁,条件变量,semaphore,以及barrier等同步原语的实现需要许多形式的内核之持。因为线程有许多优先级,因此忙等待不是一个接受的选择(除了浪费CPU周期)。同样的论点对于sched_yield的独占(exclusive)使用是不适用的。线程会阻塞在内核知道被线程唤醒。这个方法在由应用中伪唤醒引起的速度和可靠性以及信号处理质量的下降方面有很严重的缺点。
       幸运的是一些新的功能被添加到内核中来实现同步原语:futex。隐藏其后的原理很简单,但是足够强大,能适应各种应用。调用者可以阻塞在内核,可以由于中断被显式地唤醒,或者由于超时而唤醒。
       互斥体可以利用这个快速的方式用半打的指令完全在用户级实现。等待队列由内核维护。在取消的时候不再需要维护和清理更多的用户级数据结构。其他的三种同步原语也可以用futex很好的实现。
       Futex方法的另一个很大的好处是它工作在共享内存区。因此,futex可以被访问同一块共享内存区的进程共享。这,以及完全由内核处理的等待队列,正是POSIX进程间同步原语所需要的。因此,现在实现经常被问及的PTHREAD_PROCESS_SHARED选项成为可能。

5.6 内存分配
库的一个目标是对于线程有低的启动成本。内核之外与时间相关的最大问题是线程数据结构,线程局部存储,以及栈所需的内存。优化内存分配通过下面两个步骤实现:
l        
必要的内存块被合并。例如,线程数据结构和线程数据存储放置到栈上。可用的栈从这两者内存下面(或者如果栈是向上生长的则在上面)开始。
在ELF gABI定义的线程局部存储ABI只需要一个额外的数据结构:DTV(Dynamic
Thread Vector)。它需要的内存可能会改变,因此不能在线程启动的时候静态地分配。
l        
存储处理,特别是释放,是很慢的。因此如果整体分配可以避免,那就可以达到减少存储处理代价的目的。如果线程结束时内存块不直接释放而是保留下来,而这就是现状。栈帧的munmap操作会引起昂贵的TLB操作,例如,在IA32架构上,会引起全局TLB表的刷新,而且可能会广播到其他的CPU上。因此,对栈帧作缓存是pthread-create和pthread_exit具有更高性能的关键。

另一个好处是,当线程结束的时候,线程描述符里保存的信息还处于有用的状态,当描述被被再次使用的时候,不需要重新初始化。

特别是在32位机器上,由于其受限的地址空间,因此不可能保持无限的内存已备重新使用。内存高速缓存需要尽可能大的空间。这是一个可调整的量,在64位机器上可能也需要一个很大的绝不会被超过的值。

一个进程里面的线程只有有限个不同尺寸的栈,因此这种模式在大多数时候都可以工作的很好。
这种模式的唯一缺点是,因为线程句柄只是一个指向线程描述符的指针,后续生成的线程可能会有相同的句柄。这可能会隐藏bug并导致意想不到的结果。如果这确实成为一个问题,那么线程描述符的分配可以转入到调试模式,这种模式可以避免生成同样的线程句柄。这不会成为标准运行时环境的问题。

6.内核改进
甚至在linux内核的2.5.x开发版本的早期都没有提供好的线程库实现所需的所有功能。从那以后,官方内核版本开始改变,包括下述的各方面。作为这个工程的一部分,所有这些改变都是由Ingo Molnar在2002年的8、9月份完成的。内核功能和线程库的设计一直并肩进行,以确保这两个组件之间的最佳界面。
l        
通过引入TLS系统调用,在IA32和x86-64上支持任意数量的线程局部存储区域。这个系统调用允许分配一个或者多个GDT表项,这些表项可跟一个指定的偏移一起用于访问内存。这是线程寄存器的有效的替代品。每个CPU对应一个GDT数据结构,每个线程对应GDT中一个表项。调度器保存当前线程表项。

这个修补使1-on-1模型的实现不用受限于线程的数量,以前所用的方法(通过LDT)是每个进程只能有8192个线程。如果没有新的系统调用,要达到最大的可伸缩性,必须使用M-on-N模型。
l        
Clone系统调用被扩展以优化线程创建,并有助于在没有其他线程帮助下终止线程(以前的实现中管理线程充当了这个角色)。在新的实现中,如果CLONE_PARENT_SETTID标志被设置,内核在一个特定的区域存放新线程的线程ID;和/或如果CLONE_CLREATID标志被设置,一旦线程终止,内核会清理同一块内存。这可用于用户级内存管理功能来分辨未用的内存块。这有助于用户级内存管理的实现,而不需要内核干预。

另外,内核对线程ID作futex唤醒。这可以用于pthread_join的实现。
      
       另一个重要的改进是添加了线程寄存器加载的信号安全支持。因为信号可能在任何时候到达。Clone调用执行的时候它们必须被禁止,或者内核必须利用加载完的线程寄存器启动新线程。后一种情况是对clone的另一个扩展:CLONE_TLS标志。这个参数传递给内核的具体形式是架构相关的。
l        
多线程进程的POSIX信号处理在内核中实现。发送到进程的信号被传递给进程的一个可用线程。致命信号结束整个进程。停止和继续信号影响整个进程;这使得多线程进程的作业控制成为可能,在以前的实现中这些严重缺失了。共享的未决信号也被支持了。
l        
引入了另一个不同的exit系统调用:exit_group。原先的调用仍用来结束当前线程。新的系统调用用来结束整个进程。

同时,exit的实现显著的增强了。现在结束掉一个很多线程的进程所花的时间只是以前的零头。在一个起/停100000个线程的实例中,以前的实现花了15分钟,现在花了2秒。
l        
新的exec系统调用提供原先进程的ID给新创建进程。在新的进程映像获得控制之前,其他的线程被终止。
l        
现在,向父进程作的资源(CPU,各种时间,页错误等) 报告是关于整个进程的资源使用,而不是初始线程。
l        
/proc目录的内容现在只保存初始线程,而不是所有线程。初始线程代表进程。这是必要的方法,否则包含数以千、万计的进程会极大的造成/proc的使用性能下降。
l        
支持分离(detached)线程,不需要连接线程对这种线程执行wait,连接通过内核在线程退出之上完成的futex唤醒实现。
l        
内核维持初始线程直到所有的线程结束,这保证了/proc中进程的可见性,也保证了信号传送。
l        
内核被扩展以处理任意数量的线程。PID空间被扩展到在IA32架构上支持最多20亿个进程,巨大线程(massively-threaded)工作负载的可伸缩性也显著加强。/proc文件系统被修正以支持多于64k个进程。
l        
内核信号终止线程的方式使得,在子线程死掉的时候,pthread_join能够返回。例如,所有的TSD析钩子运行并且栈内存可以被重用。当栈是由用户分配的时候,这非常重要。

7.结果
这一节展示两个完全不同的测试的结果。第一个是线程创建和销毁时间的测试;第二个测试关注的是锁竞争(lock contention)的处理。

线程创建和销毁时间测定
这里简单地测量在不同的条件下,线程创建和销毁的时间。能确定的只有,在一个时刻,不定数量的线程存在。如果达到了最大的并行线程数,程序会等到有线程结束了再创建新的线程。这使得资源保持在可管理的水平。可能有多于个线程创建新线程;精确的线程数是测试中的第二个变量。

测试如下进行:
       有1到20个顶层线程创建新线程
       每个线程创建的线程数从1到10逐渐增多

       我们重复100000次线程创建操作——这只是为了得到一个可测定的时间,因此不能与前述的启动100000个并行线程的测试混淆。
       结果统计了200次。每次都是根据顶层线程数和每个顶层线程所能创建的最大线程数索引的。被创建的线程什么都不做,只是结束掉。
       我们将基准结果总结在两个表里。在两种情况下,都用一个最小函数平整/单调化测试结果矩阵的一个维度。
       图1展示了我们计算的不同数量顶层线程创建的实际线程数量结果。这里使用的值是运行所有能够并行运行的不同数量的线程需要的最小时间。
       我们可以看到,相对于LinuxThreads来说,NGPL确实有显著的提高,速度是LT的两倍。LT的线程创建确实是复杂耗时的。令人惊讶的是跟它与NTPL的差距这么大(1/4)。
       第二个总结看起来相似。图2展示了顶层线程数量不同时所需的时间。每个顶层线程所用的最佳线程数量决定了时间。
       在这个图中我们看到了规模伸缩性的影响。当有太多的线程试图并行创建新的线程时,不管哪个实现都或多或少的受到影响。

竞争处理
图3展示了对于创建了32个线程和不定数量临界区——线程试图进入其中——的程序的时间测试,一共进行的50000次。更少的临界去会导致竞争的可能性增大。
       图中展示了明显的不同,即便结果是六次运行的平均值。这些不同是由影响了所有程序的调度结果引起的,这些程序不作实际的工作,而是把所有时间花在制造调度情形上(例如阻塞在互斥体上)。

两个不同版本内核的结果表明:
l        
NTPL所用的各项时间明显少于LT所用的时间。
l        
2.4.20-2.21内核有一个调度器用来处理新的频繁的futex创建的情形。相似的改变也应用到2.5版的开发内核中。关于这个开发版本的信息是内核调度器的调整,这是有必要的,也带来了明显的收益。没有原因去相信2.4.20-2.21版本的内核处于最理想的情形。
l        
可以看到期望的渐进行为。


  
  
  
  
  
  
  
  
  
  
  
  







8.剩下的挑战
在达到100%的POSIX兼容之前还有许多挑战。这将依赖于解决方案适合linux实现的程度。
       setuid和setgid家族的系统调用必须能够影响整个进程,而不是初始线程。
       nice级别是一个进程宽度(process_wide)的属性。调整之后,进程的所有线程都会受到影响。
       CPU使用限制——可以利用setrlimit来选择——用来限制进程的所有线程总共使用的CPU时间。
       实时支持是库实现缺少的最主要的部分。用于选择调度参数的系统调用都是可用的,但是没有任何效果。这种情况的原因是内核的大部分都不遵循实时调度的原则。唤醒等待某个futex的线程不是通过查看线程的优先级实现的。
       还有其他一些在内核中没有提供合适的实时支持的地方。正是因为这个原因,现在的实现没有被对某些方面无法完成的支持而拖减慢。
       库实现中还有许多地方是可以调整的。在真实世界中,必须要确定合理的缺省值。

附录   参考
这篇论文可以在
http://people.redhat.com/drepper/nptl-design.pdf
找到。
[Futex] Fuss, Futexes and Furwocks: Fast Userlevel Locking in Linux, Hubertus
Franke,
Rusty Russell, Matthew Kirkwood, Proceedings of the Ottawa Linux Symposium,
2002.
[TLS] ELF Handler For Thread-Local Storage, Ulrich Drepper, Red Hat,
Inc.,
http://people.redhat.com/drepper/tls.pdf
[csfast] csfast5a at
http://www-106.ibm.com/developerworks/linux/library/l-rt10/ index.
Html?t=gr,lnxw01=ConSwip2
感觉还不太流畅,以后再改吧。

               
               
               

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/7753/showart_236211.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP