概述
在Linux环境下,pthread库提供的pthread_create()API函数,用于创建一个线程。线程创建失败时,它可能会返回ENOMEM或EAGAIN。这篇文章主要讨论线程创建过程中碰到的一些问题和解决方法。
创建线程
首先,本文用的实例代码example.c:
/* example.c*/ sleep(30); int main(int argc,char **argv) |
编译,执行下面命令:
# example.c -lpthread -o example -g |
用strace工具跟踪线程创建的过程:
# strace ./example |
Strace工具输出:
getrlimit(RLIMIT_STACK, |
由上表中的输出可以看出创建线程过程中的调用步骤:
- 通过系统调用getrlimit()
获取线程栈的大小(参数中的RLIMIT_STACK),在我的环境里(CentOS6),缺省值是10M。 - 调用mmap2()分配内存,大小为10489856字节,合10244K,比栈空间大了4K。返回0xb6d1c000。
- 调用mprotect(),设置个内存页的保护区(大小为4K),页面起始地址为0xb6d1c000。这个页面用于监测栈溢出,如果对这片内存有读写操作,那么将会触发一个SIGSEGV信号。下面布局图中的红色区域既是。
- 调用clone()创建线程。调用的第一个参数是一个地址:栈底的地址(这里具体为0xb771c494)。栈空间的内存使用,是从高位内存开始的。
从/proc/<pid>/smaps文件里,我们可以清楚地看到栈内存的映射情况:
090e0000-09101000 rw-p 00000000 00:00 0 [heap] |
从上面的映射文件的深蓝色部分中,我们看到,栈的空间总共为10244Kb,内存段是从b6d1d000到b771e000。从strace的输出中,我们看到栈底的地址为0xb771c494,那么,从0xb771c494到b771e000这段内存是做什么用的呢?它就是线程的TCB(thread's
control block)和TLS区域( thread's local storage)。具体的线程内存空间布局如下:
GLIBC2.5与2.8
研究GLIBC2.5和2.8里的pthread_create()相关代码,会发现在mmap()调用失败并返回ENOMEM时,作了点变动,新版里替换了错误码。
V2.5相关代码.../nptl/allocatestack.c:
mem = mmap (NULL, size, prot, if (__builtin_expect (mem == MAP_FAILED, 0)) |
V2.8里的.../nptl/allocatestack.c:
mem = mmap (NULL, size, prot, if (__builtin_expect (mem == MAP_FAILED, 0)) return errno; |
如上面的代码片段所示,在V2.5,简单地将mmap()调用结果返回给用户,而在V2.8里,如果mmap()返回ENOMEM,那么GLIBC会将错误码改成EAGAIN再返回。
为什么pthread_create()会调用失败?
随着运行中的线程数量的增大,pthread_create()失败的可能性也会增大。因为这会使分配给线程的内存空间(比如说线程栈)累积太多,导致mmap()系统调用失败。
比如说,/proc/<pid>/smaps里有这样一个内存映射片段:
[...] |
可用的内存空间是最后一个内存段和[stack]标签之间的空间:0x7F8F5000
- 0x7F33C000 = 0x5B9000 = 6000640字节(也就是6MB)。按缺省配置,小于一个线程栈的空间(10MB)。这时再创建线程就要失败。
解决方法
通常情况下,缺省10M的线程栈空间显然是太大了,所以建议通过调用pthread_attr_setstacksize()API来改变线程栈的大小。比如说以下代码片段:
//------------------------------------------------------- // Check the parameters memset(&attr, 0, sizeof(attr)); errno = pthread_attr_init(&attr); errno = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); errno = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // Set the stack size goto ok; err: ok: // The following calls will alter errno errno = pthread_attr_destroy(&attr); errno = err_sav; return rc; |
符号版本的链接问题
回到我们前面的示例代码中来,在里面,我们在主进程里直接调用pthread_create()函数。我们来看一下它的链接情况:
[root@yjye yeyj]# nm example | grep pthread |
而上次在调试Freeswitch时,发现配置的栈大小居然不生效,所有子线程全部继承父线程的大小。这是怎么回事呢?Freeswitch调用的是apr封装后的接口,那我们看下apr的链接符号:
[root@yjye .libs]# nm libapr-1.a | grep pthread |
和前面相比,好像符号后面少了e@@GLIBC_2.1或者e@@GLIBC_2.0。通过GDB跟踪,发现最终调用的是pthread_join@@GLIBC_2.0。弄出两个版本来了。通过第三库调用pthread库,经常会出现这种情况。
我们看2.0的代码,打开文件…//nptl/pthread_create.c:
int if (attr != NULL) /* Copy values from the user-provided attributes. */ /* Fill in default values for the fields not present in the old /* We will pass this value on to the real implementation. */ return __pthread_create_2_1 (newthread, attr, start_routine, arg); |
很明显,如果链接到老版本,那么设置栈大小的属性完全被忽略掉了。
怎么解决这个问题呢?强制指定链接的符号,让它调用GLIBC_2.1。感谢Linux提供的系统调用,dlvsym()正好可以解决这个问题:
#include <dlfcn.h> typedef int (*lxb_pcreate_t)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg); static lxb_pcreate_t lxb_pthread_create; // Get the version GLIBC_2.1 of pthread_create() symbol |