首页 > 代码库 > LinuxThreads 和 NPTL

LinuxThreads 和 NPTL

LinuxThreads 项目最初将多线程的概念引入了 Linux?,可是 LinuxThreads 并不遵守 POSIX 线程标准。虽然更新的 Native POSIX Thread Library(NPTL)库填补了一些空白,可是这仍然存在一些问题。

本文为那些须要将自己的应用程序从 LinuxThreads 移植到 NPTL 上或者仅仅是希望理解有何差别的开发者介绍这两种 Linux 线程模型之间的差别。

当 Linux 最初开发时。在内核中并不能真正支持线程。

可是它的确能够通过 clone() 系统调用将进程作为可调度的实体。

这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享同样的地址空间。

LinuxThreads 项目使用这个调用来全然在用户空间模拟对线程的支持。不幸的是。这样的方法有一些缺点。尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合 POSIX 的要求。

要改进 LinuxThreads。很明显我们须要内核的支持。而且须要重写线程库。有两个相互竞争的项目開始来满足这些要求。一个包含 IBM 的开发者的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同一时候。Red Hat 的一些开发者开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域全然留给了 NPTL。

虽然从 LinuxThreads 到 NPTL 看起来似乎是一个必定的过程。可是假设您正在为一个历史悠久的 Linux 发行版维护一些应用程序,而且计划非常快就要进行升级,那么怎样迁移到 NPTL 上就会变成整个移植过程中重要的一个部分。另外,我们可能会希望了解二者之间的差别,这样就行对自己的应用程序进行设计。使其可以更好地利用这两种技术。

本文具体介绍了这些线程模型各自是在哪些发行版上实现的。

LinuxThreads 设计细节

线程 将应用程序划分成一个或多个同一时候执行的任务。线程与传统的多任务进程 之间的差别在于:线程共享的是单个进程的状态信息,并会直接共享内存和其它资源。

同一个进程中线程之间的上下文切换通常要比进程之间的上下文切换速度更快。因此。多线程程序的长处就是它能够比多进程应用程序的执行速度更快。

另外,使用线程我们能够实现并行处理。这些相对于基于进程的方法所具有的长处推动了 LinuxThreads 的实现。

LinuxThreads 最初的设计相信相关进程之间的上下文切换速度非常快,因此每一个内核线程足以处理非常多相关的用户级线程。这就导致了一对一 线程模型的革命。

让我们来回想一下 LinuxThreads 设计细节的一些基本理念:

  • LinuxThreads 很出名的一个特性就是管理线程(manager thread)。管理线程能够满足下面要求:

    • 系统必须可以响应终止信号并杀死整个进程。

    • 以堆栈形式使用的内存回收必须在线程完毕之后进行。因此。线程无法自行完毕这个过程。

    • 终止线程必须进行等待。这样它们才不会进入僵尸状态。
    • 线程本地数据的回收须要对全部线程进行遍历;这必须由管理线程来进行。

    • 假设主线程须要调用 pthread_exit(),那么这个线程就无法结束。主线程要进入睡眠状态,而管理线程的工作就是在全部线程都被杀死之后来唤醒这个主线程。
  • 为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。
  • 原语的同步是使用信号 来实现的。比如,线程会一直堵塞,直到被信号唤醒为止。
  • 在克隆系统的最初设计之下,LinuxThreads 将每一个线程都是作为一个具有惟一进程 ID 的进程实现的。
  • 终止信号能够杀死全部的线程。

    LinuxThreads 接收到终止信号之后,管理线程就会使用同样的信号杀死全部其它线程(进程)。

  • 依据 LinuxThreads 的设计,假设一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程。假设这个线程如今堵塞了这个信号。那么这个信号也就会被挂起。这是由于管理线程无法将这个信号发送给进程。相反。每一个线程都是作为一个进程在运行。
  • 线程之间的调度是由内核调度器来处理的。

LinuxThreads 的设计通常都能够非常好地工作;可是在压力非常大的应用程序中,它的性能、可伸缩性和可用性都会存在问题。

以下让我们来看一下 LinuxThreads 设计的一些局限性:

  • 它使用管理线程来创建线程。并对每一个进程所拥有的全部线程进行协调。这添加了创建和销毁线程所须要的开销。
  • 因为它是环绕一个管理线程来设计的,因此会导致非常多的上下文切换的开销,这可能会妨碍系统的可伸缩性和性能。
  • 因为管理线程仅仅能在一个 CPU 上执行,因此所执行的同步操作在 SMP 或 NUMA 系统上可能会产生可伸缩性的问题。
  • 因为线程的管理方式。以及每一个线程都使用了一个不同的进程 ID,因此 LinuxThreads 与其它与 POSIX 相关的线程库并不兼容。
  • 信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。

    因此,这并不遵守 POSIX 中处理信号的方法。

  • LinuxThreads 中对信号的处理是依照每线程的原则建立的。而不是依照每进程的原则建立的。这是由于每一个线程都有一个独立的进程 ID。由于信号被发送给了一个专用的线程,因此信号是串行化的 —— 也就是说,信号是透过这个线程再传递给其它线程的。这与 POSIX 标准对线程进行并行处理的要求形成了鲜明的对照。比如,在 LinuxThreads 中。通过 kill() 所发送的信号被传递到一些单独的线程,而不是集中总体进行处理。

    这意味着假设有线程堵塞了这个信号,那么 LinuxThreads 就仅仅能对这个线程进行排队,并在线程开放这个信号时在运行处理,而不是像其它没有堵塞信号的线程中一样马上处理这个信号。

  • 因为 LinuxThreads 中的每一个线程都是一个进程,因此用户和组 ID 的信息可能对单个进程中的全部线程来说都不是通用的。比如,一个多线程的 setuid()/setgid() 进程对于不同的线程来说可能都是不同的。

  • 有一些情况下。所创建的多线程核心转储中并没有包括全部的线程信息。相同,这样的行为也是每一个线程都是一个进程这个事实所导致的结果。假设不论什么线程发生了问题,我们在系统的核心文件里仅仅能看到这个线程的信息。只是,这样的行为主要适用于早期版本号的 LinuxThreads 实现。
  • 因为每一个线程都是一个单独的进程。因此 /proc 文件夹中会充满众多的进程项。而这实际上应该是线程。

  • 因为每一个线程都是一个进程。因此对每一个应用程序仅仅能创建有限数目的线程。比如,在 IA32 系统上,可用进程总数 —— 也就是能够创建的线程总数 —— 是 4,090。
  • 由于计算线程本地数据的方法是基于堆栈地址的位置的,因此对于这些数据的訪问速度都非常慢。另外一个缺点是用户无法可信地指定堆栈的大小,由于用户可能会意外地将堆栈地址映射到本来要为其它目的所使用的区域上了。

    按需增长(grow on demand) 的概念(也称为浮动堆栈的概念)是在 2.4.10 版本号的 Linux 内核中实现的。在此之前,LinuxThreads 使用的是固定堆栈。

关于 NPTL

NPTL。或称为 Native POSIX Thread Library。是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同一时候也符合 POSIX 的需求。

与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。

与 LinuxThreads 一样。NPTL 也实现了一对一的模型。

Ulrich Drepper 和 Ingo Molnar 是 Red Hat 參与 NPTL 设计的两名员工。他们的整体设计目标例如以下:

  • 这个新线程库应该兼容 POSIX 标准。
  • 这个线程实现应该在具有非常多处理器的系统上也能非常好地工作。
  • 为一小段任务创建新线程应该具有非常低的启动成本。
  • NPTL 线程库应该与 LinuxThreads 是二进制兼容的。注意。为此我们能够使用 LD_ASSUME_KERNEL,这会在本文稍后进行讨论。
  • 这个新线程库应该能够利用 NUMA 支持的长处。

NPTL 的长处

与 LinuxThreads 相比,NPTL 具有非常多长处:

  • NPTL 没有使用管理线程。管理线程的一些需求,比如向作为进程一部分的全部线程发送终止信号,是并不须要的。由于内核本身就能够实现这些功能。内核还会处理每一个线程堆栈所使用的内存的回收工作。

    它甚至还通过在清除父线程之前进行等待,从而实现对全部线程结束的管理,这样能够避免僵尸进程的问题。

  • 因为 NPTL 没有使用管理线程,因此其线程模型在 NUMA 和 SMP 系统上具有更好的可伸缩性和同步机制。
  • 使用 NPTL 线程库与新内核实现,就能够避免使用信号来对线程进行同步了。

    为了这个目的,NPTL 引入了一种名为 futex 的新机制。futex 在共享内存区域上进行工作,因此能够在进程之间进行共享,这样就能够提供进程间 POSIX 同步机制。我们也能够在进程之间共享一个 futex。这样的行为使得进程间同步成为可能。实际上,NPTL 包括了一个 PTHREAD_PROCESS_SHARED 宏,使得开发者能够让用户级进程在不同进程的线程之间共享相互排斥锁。

  • 因为 NPTL 是 POSIX 兼容的,因此它对信号的处理是依照每进程的原则进行的。getpid() 会为全部的线程返回同样的进程 ID。比如。假设发送了 SIGSTOP 信号。那么整个进程都会停止;使用 LinuxThreads,仅仅有接收到这个信号的线程才会停止。这样能够在基于 NPTL 的应用程序上更好地利用调试器,比如 GDB。

  • 因为在 NPTL 中全部线程都具有一个父进程。因此对父进程汇报的资源使用情况(比如 CPU 和内存百分比)都是对整个进程进行统计的,而不是对一个线程进行统计的。

  • NPTL 线程库所引入的一个实现特性是对 ABI(应用程序二进制接口)的支持。

    这帮助实现了与 LinuxThreads 的向后兼容性。这个特性是通过使用 LD_ASSUME_KERNEL 实现的。以下就来介绍这个特性。

LD_ASSUME_KERNEL 环境变量

正如上面介绍的一样。ABI 的引入使得能够同一时候支持 NPTL 和 LinuxThreads 模型。

基本上来说,这是通过 ld (一个动态链接器/载入器)来进行处理的,它会决定动态链接到哪个执行时线程库上。

举例来说,以下是 WebSphere? Application Server 对这个变量所使用的一些通用设置;您能够依据自己的须要进行适当的设置:

  • LD_ASSUME_KERNEL=2.4.19:这会覆盖 NPTL 的实现。这样的实现通常都表示使用标准的 LinuxThreads 模型,并启用浮动堆栈的特性。
  • LD_ASSUME_KERNEL=2.2.5:这会覆盖 NPTL 的实现。这样的实现通常都表示使用 LinuxThreads 模型,同一时候使用固定堆栈大小。

我们能够使用以下的命令来设置这个变量:

export LD_ASSUME_KERNEL=2.4.19

注意,对于不论什么 LD_ASSUME_KERNEL 设置的支持都取决于眼下所支持的线程库的 ABI 版本号。比如。假设线程库并不支持 2.2.5 版本号的 ABI,那么用户就不能将 LD_ASSUME_KERNEL 设置为 2.2.5。

通常,NPTL 须要 2.4.20,而 LinuxThreads 则须要 2.4.1。

假设您正执行的是一个启用了 NPTL 的 Linux 发行版,可是应用程序却是基于 LinuxThreads 模型来设计的,那么全部这些设置通常都能够使用。

GNU_LIBPTHREAD_VERSION 宏

大部分现代 Linux 发行版都预装了 LinuxThreads 和 NPTL,因此它们提供了一种机制来在二者之间进行切换。要查看您的系统上正在使用的是哪个线程库。请执行以下的命令:

$ getconf GNU_LIBPTHREAD_VERSION

这会产生类似于以下的输出结果:

NPTL 0.34

或者:

linuxthreads-0.10

Linux 发行版所使用的线程模型、glibc 版本号和内核版本号

表 1 列出了一些流行的 Linux 发行版,以及它们所採用的线程实现的类型、glibc 库和内核版本号。

表 1. Linux 发行版及其线程实现
线程实现C 库发行版内核
LinuxThreads 0.7, 0.71 (for libc5)libc 5.xRed Hat 4.2 
LinuxThreads 0.7, 0.71 (for glibc 2)glibc 2.0.xRed Hat 5.x 
LinuxThreads 0.8glibc 2.1.1Red Hat 6.0 
LinuxThreads 0.8glibc 2.1.2Red Hat 6.1 and 6.2 
LinuxThreads 0.9 Red Hat 7.22.4.7
LinuxThreads 0.9glibc 2.2.4Red Hat 2.1 AS2.4.9
LinuxThreads 0.10glibc 2.2.93Red Hat 8.02.4.18
NPTL 0.6glibc 2.3Red Hat 9.02.4.20
NPTL 0.61glibc 2.3.2Red Hat 3.0 EL2.4.21
NPTL 2.3.4glibc 2.3.4Red Hat 4.02.6.9
LinuxThreads 0.9glibc 2.2SUSE Linux Enterprise Server 7.12.4.18
LinuxThreads 0.9glibc 2.2.5SUSE Linux Enterprise Server 82.4.21
LinuxThreads 0.9glibc 2.2.5United Linux2.4.21
NPTL 2.3.5glibc 2.3.3SUSE Linux Enterprise Server 92.6.5

注意,从 2.6.x 版本号的内核和 glibc 2.3.3 開始,NPTL 所採用的版本号号命名约定发生了变化:这个库如今是依据所使用的 glibc 的版本号进行编号的。

Java? 虚拟机(JVM)的支持可能会稍有不同。IBM 的 JVM 能够支持表 1 中 glibc 版本号高于 2.1 的大部分发行版。

结束语

LinuxThreads 的限制已经在 NPTL 以及 LinuxThreads 后期的一些版本号中得到了克服。比如。最新的 LinuxThreads 实现使用了线程注冊来定位线程本地数据;比如在 Intel? 处理器上,它就使用了 %fs 和 %gs 段寄存器来定位訪问线程本地数据所使用的虚拟地址。

虽然这个结果展示了 LinuxThreads 所採纳的一些改动的改进结果,可是它在更高负载和压力測试中,依旧存在非常多问题,由于它过分地依赖于一个管理线程,使用它来进行信号处理等操作。

您应该记住,在使用 LinuxThreads 构建库时,须要使用 -D_REENTRANT 编译时标志。这使得库线程是安全的。

最后,或许是最重要的事情,请记住 LinuxThreads 项目的创建者已经不再积极更新它了。他们觉得 NPTL 会代替 LinuxThreads。

LinuxThreads 的缺点并不意味着 NPTL 就没有错误。作为一个面向 SMP 的设计,NPTL 也有一些缺点。

我以前看到过在近期的 Red Hat 内核上出现过这种问题:一个简单线程在单处理器的机器上执行良好,但在 SMP 机器上却挂起了。我相信在 Linux 上还有很多其它工作要做才干使它具有更好的可伸缩性,从而满足高端应用程序的需求。


LinuxThreads 和 NPTL