• pthread_create线程创建的过程剖析


    http://blog.csdn.net/wangyin159/article/details/47082125

    在Linux环境下,pthread库提供的pthread_create()API函数,用于创建一个线程。线程创建失败时,它可能会返回ENOMEMEAGAIN。这篇文章主要讨论线程创建过程中碰到的一些问题和解决方法。

    创建线程

    首先,本文用的实例代码example.c:

    /* example.c*/
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <pthread.h>
    void thread(void)
    {
        int i;
        for(i=0;i<3;i++)
            printf("This is a pthread. ");

        sleep(30);
    }

    int main(int argc,char **argv)
    {
        pthread_t id;
        int i,ret;
        ret=pthread_create(&id,NULL,(void *) thread,NULL);
        if(ret!=0){
            printf ("Create pthread error! ");
            exit (1);
        }
        for(i=0;i<3;i++)
            printf("This is the main process. ");
        pthread_join(id,NULL);
        return 0;
    }

     

    编译,执行下面命令:

    # example.c -lpthread -o example -g

     

    用strace工具跟踪线程创建的过程:

    # strace ./example

     

    Strace工具输出:

    getrlimit(RLIMIT_STACK, {rlim_cur=10240*1024, rlim_max=RLIM_INFINITY}) = 0 
    uname({sys="Linux", node="yjye", ...})  = 0 
    mmap2(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0xb6d1c000
    brk(0)                                  = 0x90e0000
    brk(0x9101000)                          = 0x9101000
    mprotect(0xb6d1c000, 4096, PROT_NONE)  = 0 
    clone(child_stack=0xb771c494, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE
    _PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xb771cbd8, {entry_number:6, base_addr:0xb771cb70, limit:1048575, seg_32bi
    t:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}, child_tidptr=0xb771cbd8) = 17209
    fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0

     

    由上表中的输出可以看出创建线程过程中的调用步骤:

    • 通过系统调用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]
    Size:                132 kB
    Rss:                   4 kB
    Pss:                   4 kB
    Shared_Clean:          0 kB
    Shared_Dirty:          0 kB
    Private_Clean:         0 kB
    Private_Dirty:         4 kB
    Referenced:            4 kB
    Swap:                  0 kB
    KernelPageSize:        4 kB
    MMUPageSize:           4 kB
    b6d1c000-b6d1d000 ---p 00000000 00:00 0    #线程栈溢出监测区域
    Size:                  4 kB
    Rss:                   0 kB
    Pss:                   0 kB
    Shared_Clean:          0 kB
    Shared_Dirty:          0 kB
    Private_Clean:         0 kB
    Private_Dirty:         0 kB
    Referenced:            0 kB
    Swap:                  0 kB
    KernelPageSize:        4 kB
    MMUPageSize:           4 kB
    b6d1d000-b771e000 rw-p 00000000 00:00 0    #线程栈
    Size:              10244 kB
    Rss:                   8 kB
    Pss:                   8 kB
    Shared_Clean:          0 kB
    Shared_Dirty:          0 kB
    Private_Clean:         0 kB
    Private_Dirty:         8 kB
    Referenced:            8 kB
    Swap:                  0 kB
    KernelPageSize:        4 kB
    MMUPageSize:           4 kB

     

    从上面的映射文件的深蓝色部分中,我们看到,栈的空间总共为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,
                  MAP_PRIVATE | MAP_ANONYMOUS | ARCH_MAP_FLAGS, -1, 0);

          if (__builtin_expect (mem == MAP_FAILED, 0))
            {
    #ifdef ARCH_RETRY_MMAP
              mem = ARCH_RETRY_MMAP (size);
              if (__builtin_expect (mem == MAP_FAILED, 0))
    #endif
            return errno;
            }

     

    V2.8里的.../nptl/allocatestack.c:

    mem = mmap (NULL, size, prot,
                  MAP_PRIVATE | MAP_ANONYMOUS | ARCH_MAP_FLAGS, -1, 0);

          if (__builtin_expect (mem == MAP_FAILED, 0))
            {
    #ifdef ARCH_RETRY_MMAP
              mem = ARCH_RETRY_MMAP (size, prot);
              if (__builtin_expect (mem == MAP_FAILED, 0))
    #endif
                {
                  if (errno == ENOMEM)
                    errno = EAGAIN;

                  return errno;
                }
            }

     

    如上面的代码片段所示,在V2.5,简单地将mmap()调用结果返回给用户,而在V2.8里,如果mmap()返回ENOMEM,那么GLIBC会将错误码改成EAGAIN再返回。

     

    为什么pthread_create()会调用失败?

    随着运行中的线程数量的增大,pthread_create()失败的可能性也会增大。因为这会使分配给线程的内存空间(比如说线程栈)累积太多,导致mmap()系统调用失败。

    比如说,/proc/<pid>/smaps里有这样一个内存映射片段:

    [...]
    7eb3d000-7f33c000 rw-p 7eb3d000 00:00 0
    Size:               8188 kB
    Rss:                  12 kB
    Pss:                  12 kB
    Shared_Clean:          0 kB
    Shared_Dirty:          0 kB
    Private_Clean:         0 kB
    Private_Dirty:        12 kB
    Referenced:           12 kB
    Swap:                  0 kB
    7f8f5000-7f90a000 rw-p 7ffeb000 00:00 0          [stack]
    Size:                 84 kB
    Rss:                  16 kB
    Pss:                  16 kB
    Shared_Clean:          0 kB
    Shared_Dirty:          0 kB
    Private_Clean:         0 kB
    Private_Dirty:        16 kB
    Referenced:           16 kB
    Swap:                  0 kB

     

    可用的内存空间是最后一个内存段和[stack]标签之间的空间:0x7F8F5000 - 0x7F33C000 = 0x5B9000 = 6000640字节(也就是6MB)。按缺省配置,小于一个线程栈的空间(10MB)。这时再创建线程就要失败。

    解决方法

       通常情况下,缺省10M的线程栈空间显然是太大了,所以建议通过调用pthread_attr_setstacksize()API来改变线程栈的大小。比如说以下代码片段:

  • 相关阅读:
    Java实现旅行商问题
    Java实现旅行商问题
    Java实现旅行商问题
    Java实现旅行商问题
    递归执行顺序的探究
    递归执行顺序的探究
    递归执行顺序的探究
    配置我的Ubuntu Server记(包括桌面及VNC,SSH,NTP,NFS服务) good
    罗辑思维 137 藩镇割据:毒药还是良药?(要懂得自我收敛?不要什么事情都自己做?)
    云计算CTO工作的具体内容(挺详细)
  • 原文地址:https://www.cnblogs.com/feng9exe/p/5761457.html
Copyright © 2020-2023  润新知