美文网首页
【译】Linux 线程模型比较:LinuxThreads 和 N

【译】Linux 线程模型比较:LinuxThreads 和 N

作者: suesai | 来源:发表于2018-10-16 22:10 被阅读0次

多线程最初是由 LinuxThreads 这个工程带入到 Linux 的,但是 LinuxThreads 并不符合 POSIX 在线程方面的标准。之后的原生 POSIX 线程库(Native POSIX Thread Library,NPTL)比 LinuxThreads 更符合标准,且克服了后者的许多缺陷。下文将阐述这两个 Linux 线程模型的区别。本文面向于想要把应用程序从 LinuxThreads 移植到 NPTL,或者希望理解它们之间的不同的人群。

当初 Linux 被开发出来的时候,其内核并不真正地支持多线程。由于 clone 系统调用创建了调用进程的副本,而且可以和父进程共享地址空间;通过 clone,LinuxThreads 完全在用户空间模拟了线程。然而,这种方法有很多的缺陷,并没有符合 POSIX 的要求,特别是在信号处理,调度和进程间同步原语等方面。

为了改进 LinuxThreads,很明显需要内核的支持,而且需要重写线程库。意识到这点后,有两个工程被发起,一个是 IBM 的 NGPT(Next-Generation POSIX Threads),另一个是 Red Hat 的 NPTL。IBM 在 2003 年的时候放弃了 NGPT,于是在改进 LinuxThreads 的路上只剩下了 NPTL。

尽管相较于 LinuxThreads,应该毫无疑问地选择 NPTL,但是如果你在维护一个运行在很古老的 Linux 发行版上的应用,而且打算升级到最新的 Linux 上,那么在移植过程中 NPTL 是个非常重要的环节。所以了解这两个线程模型之间的不同,将使得应用既可以运行于旧版本的 Linux 上,也可以运行于新版本的 Linux 上。

LinuxThreads 设计细节

线程将程序分割成了一个或者多个并发运行的任务。线程与进程之间的不同在于:线程共享了进程的状态信息和也共享了内存等资源。同一个进程中的线程上下文切换要比进程上下文切换更快。这些优点促使了 LinuxThreads 的诞生。

LinuxThreads 的最初设计理念认为:在一个内核线程处理一个对应的用户线程的情况下,相关进程的上下文切换已经足够快了。于是,据此理念设计出了一对一的线程模型。

以下是 LinuxThreads 设计细节上的关键点:

  • 其中的一个显著特点是管理线程(manager thread)【译者注:该线程由 LinuxThreads 创建,作用是创建和终止其他线程,该线程大多数时间都处于休眠状态,一旦管理线程被意外地杀掉,则将发生不可预知的错误】。它达到了这些要求:

    • 关键信号能够杀死所有线程。当进程收到了关键信号(如 SIGKILL 等),管理线程将使用相同的信号杀死其他线程。【译者注:进程中的任意一个线程接受到了这些关键信号,所有的线程都会被杀掉;甚至是其中的一个线程发生了除零错误,也同样如此】
    • 线程结束后,作为栈的内存必须被释放;线程不能自己释放自己的栈。
    • 已终止的线程必须能够被等待(wait),从而不进入僵死状态。
    • 所有线程的 thread_local 数据必须被迭代地释放;该释放动作由管理线程完成。
    • 如果主线程调用了 pthread_exit 【译者注:LinuxThreads 和 NPTL 都采用了 POSIX 接口标准】,进程并没有终止,而主线程却进入了休眠状态,当其他的线程都被杀掉后,由管理线程唤醒主线程。
  • 使用了位于栈之下且紧靠着栈的内存来维护 thread_local 数据。

  • 通过信号实现同步。例如,线程被阻塞,直到由信号唤醒。

  • 通过 clone 来实现线程,因此每个线程都是一个具有独立 PID 的进程。

  • 若进程接受到了异步信号,则管理线程将会把该信号递送给相应的线程;若该线程正阻塞该信号,则该信号会保持未决状态而不是将该信号转发给其他的线程。【译者注:由于 LinuxThreads 中的线程在内核中都是进程,所以每个信号都有自己确定的目的地】

  • 线程之间的调度是由内核调度器完成的。

LinuxThreads 及其局限性

在通常情况下,LinuxThreads 都能够正常工作;但是,当有大量的应用同时工作时,就会面临性能、可扩展性、可用性等问题。让我们具体地看一下 LinuxThreads 的局限性:

  • 使用了管理线程来创建和协调进程中的每一个线程。这增加了创建和销毁线程的成本。

  • 由于设计是围绕着管理线程开展的,而管理线程导致了大量的上下文切换,这影响了可扩展性和性能。

  • 由于管理线程只能在一个 CPU 上运行,所以在 SMP 或者 NUMA 系统上,任何的同步操作都会导致可扩展性问题。

  • 由于 LinuxThreads 独特的线程管理方式,以及每个线程都有唯一的 PID,所以 LinuxThreads 和其他的 POSIX 线程库是不兼容的。

  • 由信号来实现同步原语。这会影响相应时间,而且也无法向进程发送信号。所以无法满足 POSIX 标准来处理信号。

  • 信号是面向线程,而不是面向进程的。例如通过 kill 发送的信号是传送给独立的线程,而不是传送给整个进程;若当前的线程正阻塞该信号,则信号保留在线程的信号队列里,直到线程开放接受该信号。这不符合 POSIX 标准。

  • 线程的用户 ID 和组 ID 可能不通用。例如,setuid/setgid 可能会导致这种情况的发生。

  • 在旧版本的 LinuxThreads 中,如果一个线程发生了 core dump,那么 core 中不会包含其他线程的信息。

  • 一个进程中的所有线程都会在 /proc 中有入口。

  • 一个应用只能创建有限的线程。例如在 IA32 系统上,由于进程的限制数量是 4096 个,所以最多能创建 4096 个线程。

  • 若要计算 thread_local 的值,需要先获取栈的位置,所以获取这部分数据的速度并不快。用户也无法安心地指定栈的大小和位置,因为用户可能会把栈映射到用于其他目的的内存区域。按需增长(grow on demand,也叫做浮动栈,floating stack)的概念是在内核 2.4.10 引入的,在这个版本之前,LinuxThreads 使用固定大小的栈。

NPTL

NPTL 是 LinuxThreads 的替代者,而且其符合了 POSIX 的标准,在稳定性和性能方面都有了很大的提升。和 LinuxThreads 一样,NPTL 采用了一对一的线程模型。

Ulrich Drepper 和 Ingo Molnar 是 NPTL 的设计先驱。他们的整体设计目标如下:

  • 新的线程库应兼容 POSIX 标准。

  • 线程库的实现应能在多处理器的系统上良好运转。

  • 创建新线程的代价应很低。

  • NPTL 线程库应和 LinuxThreads 是二进制兼容的。注意到,可以使用 LD_ASSUME_KERNEL 来达到此目的。

  • 应能够利用 NUMA。

NPTL 的优点

相较于 LinuxThreads,NPTL 在很多方面都有了提升:

  • 不使用管理线程。管理线程需要发送关键信号给进程中的所有线程,而对于 NPTL,这些都交给了内核处理。当线程结束后,内核也释放了栈内存。内核甚至管理着所有线程的终止:在清理父线程之前,等待子线程,从而避免僵死状态。

  • 由于没有使用管理线程,NPTL 线程模型在 NUMA 和 SMP 系统上有着更好的可扩展性和同步机制。

  • 有了 NPTL 线程库再加上新的内核实现,线程就可以不再使用信号来实现同步了。NPTL 引入了一个叫做 futex 的机制来同步线程。futex 工作于共享存储区,因此提供了进程间的同步。事实上,NPTL 包含了一个叫做 PTHREAD_PROCESS_SHARED 的宏,该宏给开发者提供了一种方式,使得在不同进程中的线程可以通过 mutex 实现同步。

  • 可以处理面向进程的信号;同一进程中的线程调用 getpid 后返回相同的 PID。例如,若发送了 SIGSTOP,则整个进程都会停止;而在 LinuxThreads 中,只有接受该信号的线程才会停止。这让类似 GDB 的调试器更加好用。

  • 报告给父进程的资源使用状况(例如 CPU 和内存)包含了整个进程,而不是某个线程。

  • NPTL 线程库的一种重要特性是提供了 ABI (Application Binary Interface)的支持。这使得 NPTL 向后兼容于 LinuxThreads。下文将介绍通过 LD_ASSUME_KERNEL 来实现兼容性。

环境变量 LD_ASSUME_KERNEL

由于有了 ABI,就可以写出既支持 NPTL 又支持 LinuxThreads 的代码。从根本上讲,这是 ld(动态加载器、链接器)的功劳,ld 决定了该链接哪个线程库。

以 WebSphere® Application Server 为例,下面是这个环境变量的常见配置:

  • LD_ASSUME_KERNEL=2.4.19:这通常指向了开启浮动栈的 LinuxThreads。

  • LD_ASSUME_KERNEL=2.2.5:这通常指向了固定栈的 LinuxThreads。

使用下面这个命令来设置环境变量:

export LD_ASSUME_KERNEL=2.4.19

应注意到,对 LD_ASSUME_KERNEL 的设置也应顾及到线程库当前的 ABI 版本,例如,如果当前的线程库版本不支持 ABI 2.2.5,那么用户就不能把 LD_ASSUME_KERNEL 设置为 2.2.5。通常,要使用 NPTL,就设置为 2.4.20;要使用 LinuxThreads,就设置为 2.4.1。

如果你的应用程序运行于支持 NPTL 的 Linux 发行版上,但是却是按照 LinuxThreads 来设计的,那么 LD_ASSUME_KERNEL 可以发挥作用了。

宏 GNU_LIBPTHREAD_VERSION

在现代的既有 NPTL 又有 LinuxThreads 的 Linux 发行版中,可以通过下面的命令来查看当前使用的是哪个线程库:

$ getconf GNU_LIBPTHREAD_VERSION

输出的结果如下:

NPTL 0.34

或者:

linuxthreads-0.10

线程模型、glibc 版本、内核版本、Linux 发行版

下表展示了线程模型、glibc 版本、内核版本与 Linux 发行版的对照关系

线程模型 C 库 发行版 内核
LinuxThreads 0.7, 0.71 (for libc5) libc 5.x Red Hat 4.2
LinuxThreads 0.7, 0.71 (for glibc 2) glibc 2.0.x Red Hat 5.x
LinuxThreads 0.8 glibc 2.1.1 Red Hat 6.0
LinuxThreads 0.8 glibc 2.1.2 Red Hat 6.1 and 6.2
LinuxThreads 0.9 Red Hat 7.2 2.4.7
LinuxThreads 0.9 glibc 2.2.4 Red Hat 2.1 AS 2.4.9
LinuxThreads 0.10 glibc 2.2.93 Red Hat 8.0 2.4.18
NPTL 0.6 glibc 2.3 Red Hat 9.0 2.4.20
NPTL 0.61 glibc 2.3.2 Red Hat 3.0 EL 2.4.21
NPTL 2.3.4 glibc 2.3.4 Red Hat 4.0 2.6.9
LinuxThreads 0.9 glibc 2.2 SUSE Linux Enterprise Server 7.1 2.4.18
LinuxThreads 0.9 glibc 2.2.5 SUSE Linux Enterprise Server 8 2.4.21
LinuxThreads 0.9 glibc 2.2.5 United Linux 2.4.21
NPTL 2.3.5 glibc 2.3.3 SUSE Linux Enterprise Server 9 2.6.5

注意到,从内核 2.6.x 和 glibc 2.3.3 开始,NPTL 版本号的命名规则发生了变化:NPTL 的版本号和 glibc 的版本号一致了起来。

Java™ 虚拟机(JVM)的支持有所不同。IBM 移植版的 JVM 支持上表中所有 glibc 版本大于 2.1 的发行版。

结语

NPTL 克服了 LinuxThreads 的缺陷。最新的 LinuxThreads 使用了寄存器来定位 thread_local 数据,例如在 Intel® 处理器上,使用了 %fs 和 %gs 段寄存器来定位虚拟地址中的 thread_local 数据。虽然这给 LinuxThreads 在一定程度带来了性能提升,但是由于过于依赖管理线程和信号处理等占了更高的比重,在高负荷或者压力测试下,问题依然存在。

记住,如果要使用 LinuxThreads 构造库,记得在编译时加上 -D_REENTRANT,这会使你的库线程安全。

最后,LinuxThreads 工程的发起者已经不再勤快地更新了,所以最好采用 NPTL。

LinuxThreads 的缺点并不能映衬 NPTL 是完美的。NPTL 也是有缺陷的,我就遇到过一个问题,在 Red Hat 上,一个简单的多线程应用在单核机器上正常运行,却在 SMP 机器上挂了。我相信在Linux上还有很多工作要做,以真正使 NPTL 来满足更高端的应用程序。

资源

学习

  • "The Native POSIX Thread Library for Linux" (PDF),作者 Ulrich Drepper and Ingo Molnar,文章描述了 NPTL 的设计理念和目标,同时也包括了 LinuxThreads 的缺陷和 NPTL 的优点。

  • "LinuxThreads FAQ" 涵盖了关于 LinuxThreads 和 NPTL 经常被提问的问题。文章对于学习旧版本的 LinuxThreads 很有帮助。

  • "Explaining LD_ASSUME_KERNEL",作者 Ulrich Drepper,文章描述了这个环境变量和细节。

  • "Native POSIX Threading Library (NPTL) support",从 WebSphere 的角度描述了这两个线程模型的不同。

  • "Diagnosis documentation for IBM ports of the JVM" ,文章描述了当 Java 应用在 Linux 上遇到问题时应该如何收集诊断信息。

  • "developerWorks Linux zone" ,对于 Linux 开发者来说,这里有很多资源。

  • "developerWorks technical events and Webcasts"。

软件和规格文档

  • "LinuxThreads README",LinuxThreads 的详细描述文档。

  • "Order the SEK for Linux",这是 DVD 套件,包含了 IBM 最新的 Linux 试用软件,DB2®,
    Lotus®,Rational®,Tivoli® 和 WebSphere®。

  • "IBM trial software",这个可以直接从 developerWorks 上下载,用这些软件来构造在 Linux 上工程。

论坛

  • "developerWorks blogs" 和 "developerWorks community"。

关于作者

Vikram Shukla,拥有超过六年的面向对象语言的开发和设计经验,目前就职于 IBM 的 Java Technology Center,坐标印度班加罗尔。


参考

本文主要翻译于下列中第一篇文章;为了方便理解,在原文的基础上进行了适当修改。

  1. Linux threading models compared: LinuxThreads and NPTL
  2. LinuxThreads Frequently Asked Questions (with answers)
  3. pthreads

相关文章

网友评论

      本文标题:【译】Linux 线程模型比较:LinuxThreads 和 N

      本文链接:https://www.haomeiwen.com/subject/wqqoaftx.html