检测和避免 POSIX 线程内存泄漏的技巧
POSIX 线程(pthread)编程定义了一套标准的 C 编程语言类型、函数和常量 — 且 pthreads 提供了一种强大的线程管理工具。要充分使用 pthreads,您要避免常见错误。一个常见的错误就是忘记联接可接合的线程,从而导致内存泄漏并增加工作量。在该篇技巧型文章中,学习 POSIX 线程基础,了解如何识别和检测线程内存泄漏,并获得避免出现这种情况的可靠建议。
POSIX 线程简介
使用线程的主要原因是要提高程序性能。线程的创建和管理只需要较小的操作系统开销和较少的系统资源。一个进程内的所有线程共享相同的地址空间,使得线程间的通信更高效,且比进程间通信更易于实现。例如,如果一个线程在等待一个输入/输出系统调用完成,其他线程可以处理 CPU 密集型任务。通过线程,可以优先调度重要任务 — 甚至中断 — 低优先级任务。可将偶尔发生的任务放在定期调度的任务之间,创建调度灵活性。最后,pthreads 是在多 CPU 计算机上进行并行编程的理想之选。
而且使用 POSIX 线程或 pthreads 的主要原因更加简单:作为标准 C 语言线程编程接口的一部分,它们可高可移植的。
POSIX 线程编程有诸多优势,但是如果您不明确一些基本规则,就有可能编写一些难以调试的代码并造成内存泄漏。我们首先回顾一下 POSIX 线程,分为可接合线程 或分离线程。
可接合线程
如果您希望生成一个新的线程,且需要知道它是如何终止的,那么您需要一个可接合线程。对于可接合线程,系统分配专用存储器来存储线程终止状态。线程终止后状态得到更新。要获得线程终止状态,调用 pthread_join(pthread_t thread, void** value_ptr)
。
系统为每个线程分配底层存储,包括堆栈、线程 ID、线程终止状态等。这个底层存储将一直保留在进程空间(且不能回收),直至线程终止并为其他线程所联接。
分离线程
大多数时候,您只需创建一个线程,向其分配一些任务,然后继续处理其他事务。在这些情况下,您不关注线程是如何终止的,这时使用分离线程是一个很好的选择。
对于分离线程,在线程终止后系统自动回收其底层资源。
识别泄漏
如果您创建一个可接合的线程,但是忘记联接它,其资源或私有内存一直保存在进程空间中,从未进行回收再利用。一定要联接可接合的线程;否则,可能会引起严重的内存泄漏问题。
例如,Red Hat Enterprise Linux (RHEL4)上的一个线程需要一个 10MB 的堆栈,这意味着,如果不联接它,会有至少 10MB 的内存泄漏。假设您设计一个管理器-工作线程模式的程序来处理传入的请求。然后需要创建越来越多的工作线程来执行各个任务,最后终止这些线程。如果它们是可接合的线程,且您没有调用 pthread_join()
来联接它们,那么在线程终止后,每个产生的线程都将泄漏大量的内存(至少每堆栈 10MB)。随着创建并在未联接的情况下终止的工作线程越来越多,泄漏的内存量也持续增加。另外,进程将无法创建新的线程,因为无内存可供创建新线程使用。
清单 1 显示在忘记联接可接合线程时引发的严重内存泄漏。您还可以使用该代码检查可在一个进程空间中共存的线程体的最大量。
清单 1. 引发内存泄漏
#include<stdio.h> #include<pthread.h> void run() { pthread_exit(0); } int main () { pthread_t thread; int rc; long count = 0; while(1) { if(rc = pthread_create(&thread, 0, run, 0) ) { printf("ERROR, rc is %d, so far %ld threads created ", rc, count); perror("Fail:"); return -1; } count++; } return 0; }
清单 1 中调用了 pthread_create()
来创建一个含默认线程属性的新线程。默认情况下,新创建的线程是可接合的。它不断创建新的可接合线程,直至有故障发生。然后输出错误代码和故障原因。
使用以下命令在 Red Hat Enterprise Linux Server 5.4 上编译清单 1 中的代码时:[root@server ~]# cc -lpthread thread.c -o thread
, 您将获得清单 2 所示的结果。
清单 2. 内存泄漏结果
[root@server ~]# ./thread ERROR, rc is 12, so far 304 threads created Fail:: Cannot allocate memory
在代码创建了 304 个线程之后,它无法创建更多线程。错误代码是 12
,这表示无更多内存可用。
如清单 1 和清单 2 所示,虽然生成了可接合线程,但是却未将其联接,因此每个终止的可接合线程仍然占用进程空间,泄漏进程内存。
RHEL 上的一个 POSIX 线程拥有一个大小为 10MB 的私有堆栈。换言之,系统为每个 pthread 分配至少 10MB 的专用存储。在我们的示例中,304 个线程是在进程停止前创建的;这些线程占用 304*10MB 内存,合计约 3GB。一个进程的虚拟内存的大小是 4GB,其中四分之一的进程空间是为 Linux 内核预留的。这样一来,就有 3GB 的内存空间可用作用户空间。因此,3GB 内存由死线程消耗。这是很严重的内存泄漏。而且很容易理解它发生的速度为何如此之快。
要修复泄漏,您可以添加代码调用 pthread_join()
,该方法可联接每个可接合线程。
检测泄漏
如同其他内存泄漏中一样,进程启动时问题可能没那么明显。这里介绍一种无需访问源代码便可检测此类问题的方法:
- 计算进程中线程堆栈的数量。这包括正在运行的活动线程和已终止线程的数量。
- 计算进程中正在运行的活动线程的数量。
- 比较两者。如果现有线程堆栈的数量大于正在运行的活动线程的数量,且在程序运行时这两个数字的差量在不断增加,那么内存在泄漏。
这种内存泄漏很有可能是因未能联接可接合线程而造成的。
使用 pmap 计算线程堆栈数
在一个运行的进程中,线程堆栈的数量等于进程中线程体的数量。线程体包括运行的活动线程和可接合的死线程。
pmap
是一种用于汇报进程内存的 Linux 工具。结合使用以下命令来获取线程堆栈数:
[root@server ~]# pmap PID | grep 10240 | wc -l
(10240KB 是 Red Hat Enterprise Linux Server 5.4 上的默认堆栈大小。)
使用 /proc/PID/task 计算活动线程数
每次创建一个线程且该线程在运行时,会有一个条目填充到 /proc/PID/task 中。当线程终止时,不管该线程是可接合的还是分离的,都会将该条目从 /proc/PID/task 中删除。因此活动线程数可通过运行以下命令得出:
[root@server ~]# ls /proc/PID/task | wc -l
.
比较输出
检查 pmap PID | grep 10240 | wc -l
的输出并将其与 ls /proc/PID/task | wc -l
的输出进行比较。如果所有线程堆栈的数量大于活动线程的数量,且在程序运行时两者的差量在持续增长,您可以确定内存泄漏问题确实存在。
预防泄漏
在编程过程中应当联接可接合线程。如果您在程序中创建可接合的线程,切勿忘记调用pthread_join(pthread_t, void**)
来回收分配给线程的专用存储。否则,将引发严重的内存泄漏问题。
在编程后的测试阶段,您可以使用 pmap
和 /proc/PID/task
检测这种泄漏是否存在。如果确实存在,检查源代码,看是否联接了所有可接合线程。
就这些内容。只需少量预防工作即可为您省掉大量后续工作,避免令人头疼的内存泄漏问题。