多线程最初是由 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,坐标印度班加罗尔。
参考
本文主要翻译于下列中第一篇文章;为了方便理解,在原文的基础上进行了适当修改。
网友评论