1. 程式人生 > >pthread_create執行緒建立的過程剖析

pthread_create執行緒建立的過程剖析

概述

在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.\n"

);

    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!\n");
        exit (1);
    }
    for(i=0;i<3;i++)
        printf("This is the main process.\n");
    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來改變執行緒棧的大小。比如說以下程式碼片段:

//-------------------------------------------------------
// Name   : create_thd
// Usage  : Create a thread
// Return : 0, if OK
//          -1, if error (errno is set)
//-------------------------------------------------------
static int create_thd(
                    void       *thd_par,  // Thread parameters
                    size_t      stack_sz,
                    void       *(*entry)(void *),
                    pthread_t  *pThreadId // Thread identifier
                     )
{
pthread_attr_t      attr;
int                 rc = 0;
int                 err_sav;

  // Check the parameters
  if (!pThreadId)
  {
    fprintf(stderr, "NULL thread id\n");
    errno = EINVAL;
    return -1;
  }

  memset(&attr, 0, sizeof(attr));

  errno = pthread_attr_init(&attr);
  if (0 != errno)
  {
    err_sav = errno;
    fprintf(stderr, "pthread_attr_init() failed (errno = %d)\n", errno);
    errno = err_sav;
    return -1;
  }

  errno = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
  if (0 != errno)
  {
    err_sav = errno;
    fprintf(stderr, "pthread_attr_setscope() failed (errno = %d)\n", errno);
    errno = err_sav;
    rc = -1;
    goto err;
  }

  errno = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  if (0 != errno)
  {
    err_sav = errno;
    fprintf(stderr, "pthread_attr_setdetachstate() failed (errno = %d)\n", errno);
    errno = err_sav;
    rc = -1;
    goto err;
  }

  // Set the stack size
  errno = pthread_attr_setstacksize(&attr, stack_sz);
  if (0 != errno)
  {
    err_sav = errno;
    fprintf(stderr, "Error %d on pthread_attr_setstacksize()\n", errno);
    errno = err_sav;
    rc = -1;
    goto err;
  }

  // Thread creation
  errno = pthread_create(pThreadId,
                         &attr,
                         entry,
                         thd_par);
  if (0 != errno)
  {
    err_sav = errno;
    fprintf(stderr, "pthread_create() failed (errno = %m - %d)\n", errno);
    errno = err_sav;
    rc = -1;
    goto err;
  }

  goto ok;

err:

ok:

  // The following calls will alter errno
  err_sav = errno;

  errno = pthread_attr_destroy(&attr);
  if (0 != errno)
  {
    fprintf(stderr, "pthread_attr_destroy() failed (errno = %d)\n", errno);
    rc = -1;
  }

  errno = err_sav;

  return rc;
} // create_thd

符號版本的連結問題

     回到我們前面的示例程式碼中來,在裡面,我們在主程序裡直接呼叫pthread_create()函式。我們來看一下它的連結情況:

[[email protected] yeyj]# nm example | grep pthread
         U [email protected]@GLIBC_2.1
         U [email protected]@GLIBC_2.0

而上次在除錯Freeswitch時,發現配置的棧大小居然不生效,所有子執行緒全部繼承父執行緒的大小。這是怎麼回事呢?Freeswitch呼叫的是apr封裝後的介面,那我們看下apr的連結符號:

[[email protected] .libs]# nm libapr-1.a | grep pthread
         U pthread_rwlock_destroy
         U pthread_rwlock_init
         U pthread_rwlock_rdlock
         U pthread_rwlock_tryrdlock
         U pthread_rwlock_trywrlock
         U pthread_rwlock_unlock
         U pthread_rwlock_wrlock
         U pthread_mutex_destroy
         U pthread_mutex_init
         U pthread_mutex_lock
         U pthread_mutex_trylock
         U pthread_mutex_unlock
         U pthread_mutexattr_destroy
         U pthread_mutexattr_init
         U pthread_mutexattr_settype
         U pthread_cond_broadcast
         U pthread_cond_destroy
         U pthread_cond_init
         U pthread_cond_signal
         U pthread_cond_timedwait
         U pthread_cond_wait
00000080 d mutex_proc_pthread_methods
00000a10 t proc_mutex_proc_pthread_acquire
00000990 t proc_mutex_proc_pthread_cleanup
00000a50 t proc_mutex_proc_pthread_create
00000960 t proc_mutex_proc_pthread_release
         U pthread_mutex_destroy
         U pthread_mutex_init
         U pthread_mutex_lock
         U pthread_mutex_unlock
         U pthread_mutexattr_destroy
         U pthread_mutexattr_init
         U pthread_mutexattr_setprotocol
         U pthread_mutexattr_setpshared
         U pthread_mutexattr_setrobust_np
         U pthread_attr_destroy
         U pthread_attr_getdetachstate
         U pthread_attr_init
         U pthread_attr_setdetachstate
         U pthread_attr_setguardsize
         U pthread_attr_setstacksize
         U pthread_create
         U pthread_detach
         U pthread_exit
         U pthread_join
         U pthread_once
         U pthread_self
         U pthread_sigmask
         U pthread_getspecific
         U pthread_key_create
         U pthread_key_delete
         U pthread_setspecific


   和前面相比,好像符號後面少了[email protected]@GLIBC_2.1或者[email protected]@GLIBC_2.0。通過GDB跟蹤,發現最終呼叫的是[email protected]@GLIBC_2.0。弄出兩個版本來了。通過第三庫呼叫pthread庫,經常會出現這種情況。

我們看2.0的程式碼,開啟檔案…//nptl/pthread_create.c

int
__pthread_create_2_0 (newthread, attr, start_routine, arg)
     pthread_t *newthread;
     const pthread_attr_t *attr;
     void *(*start_routine) (void *);
     void *arg;
{
  /* The ATTR attribute is not really of type `pthread_attr_t *'.  It has
     the old size and access to the new members might crash the program.
     We convert the struct now.  */
  struct pthread_attr new_attr;

  if (attr != NULL)
    {
      struct pthread_attr *iattr = (struct pthread_attr *) attr;
      size_t ps = __getpagesize ();

      /* Copy values from the user-provided attributes.  */
      new_attr.schedparam = iattr->schedparam;
      new_attr.schedpolicy = iattr->schedpolicy;
      new_attr.flags = iattr->flags;

      /* Fill in default values for the fields not present in the old
     implementation.  */
      new_attr.guardsize = ps;
      new_attr.stackaddr = NULL;
      new_attr.stacksize = 0;

      new_attr.cpuset = NULL;

      /* We will pass this value on to the real implementation.  */
      attr = (pthread_attr_t *) &new_attr;
    }

  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;
[...]
   void *pSym;

  // Get the version GLIBC_2.1 of pthread_create() symbol
  pSym = dlvsym(RTLD_DEFAULT, "pthread_create", "GLIBC_2.1");
  if (NULL == pSym)
  {
    lxb_pthread_create = pthread_create;
  }
  else
  {
    lxb_pthread_create = (lxb_pcreate_t)pSym;
    if (pSym != (void *)pthread_create)
    {
      LXB_PRINTF("Unexpected version of pthread_create() symbol ==> Forced to GLIBC_2.1\n");
    }
  }

相關推薦

pthread_create執行建立過程剖析

概述 在Linux環境下,pthread庫提供的pthread_create()API函式,用於建立一個執行緒。執行緒建立失敗時,它可能會返回ENOMEM或EAGAIN。這篇文章主要討論執行緒建立過程中碰到的一些問題和解決方法。 建立執行緒 首先,本文用的例項程式碼exam

pthread執行建立過程(未完)

1.  當我們使用pthread_create來建立執行緒的時候, 實際上呼叫的是__pthread_create_2_1      versioned_symbol (libpthread, __pthread_create_2_1, pthread_create, GLIBC_2_1);  2.  而_

執行建立函式pthread_create的引數解析

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);   void *(*start_routine) (void

執行建立 pthread_create 中自定義引數注意事項

1. 函式原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,          void *(*start_routine) (void *), void *arg); 本文主要討論最後一個引數,同時傳遞多個的問題

Python多工(2.執行(建立執行的兩種方式))

Python中threading模組 可以總結出:    (1)當呼叫Thread的時候,不會建立執行緒 (2)呼叫Thread創建出來的例項物件的start方法的時候,才會建立執行緒以及讓這個執行緒開始執行      

Thread、執行建立、synchronized、執行生命週期

程序:程序指正在執行的程式,當一個程式進入記憶體執行,即變成一個程序,程序是處於執行過程中的程式,並且具有一定獨立功能。 執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。 jvm啟動後,必然有一個執行路徑(執行緒)從main方法開始的,一直執行到main方法結束

鎖和多執行:執行建立3種方式(一)

執行緒  鎖Synchronized  搞明白 執行緒 鎖和多執行緒系列 1.執行緒建立 執行緒建立常見的三種方式: 繼承Thread類 實現Runnable介面

Java執行建立的兩種方式

package test; /**  * 建立執行緒  *  */ public class Demo1 {      public static void main(String arg[]){

一、多執行[建立,interrupt,setDaemon,getPriority,isAlive等]

一、多執行緒簡介 在CPU上的執行緒,執行一個任務,巨集觀當然是同時執行的,但微觀裡的確實序列執行的,CPU通過執行緒中斷,讓某一個執行緒掛起來,然後切換到另一個執行緒,執行一會兒,再切換回來。但是執行緒切換來回會犧牲一定的效能,如果增加CPU那麼執行緒是可以達到並行的。

java之執行建立的兩種方式,六種狀態和匿名內部類建立子類或實現類物件

一.匿名內部類建立子類或實現類物件 new Test(){} 相當於建立了Test類的子類物件 並且沒有類名 建立介面實現類 new 介面名() {};介面實現類的物件 注意 : new 後邊是類或者介面名 大括號內是類或者介面中的方法 public

Java中執行建立的方式:繼承thread類與實現Runnable介面

Java中執行緒的建立有兩種方式: 1.  通過繼承Thread類,重寫Thread的run()方法,將執行緒執行的邏輯放在其中 2.  通過實現Runnable介面,例項化Thread類     在實際應用中,我們經常用到多執行緒,如車站的售票系統,車站的

Java多執行建立的三種方式與對比

一、繼承Thread類建立執行緒類 1、定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體代表了執行緒需要完成的任務,即執行緒的執行體。 2、建立Thread子類的例項,即建立執行緒物件。 3、呼叫執行緒物件的start()方法來啟動該執行緒

在MDI子窗體中開執行建立新彈出窗體程式會被掛起無法繼續下去

public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); String[] st = sc.nextLine().split("

Java:多執行 - 建立方法

多執行緒的理解 可以理解成程序中獨立執行的子任務,比如QQ.exe執行時的視訊聊天執行緒,下載檔案執行緒,傳送表情執行緒等,這些不同的任務或功能可以“同時”執行。實際上,CPU在這些執行緒之間不斷的切換,這樣做可以最大限度的利用CPU的空閒時間。 Java多執行緒的建立和使用 j

Java多執行-----建立執行的三種方式

   1.繼承Thread類建立執行緒 定義Thread類的子類,並重寫該類的run()方法,該方法的方法體就是執行緒需要完成的任務,run()方法也稱為執行緒執行體 建立Thread子類的例項,也就是建立了執行緒物件 啟動執行緒,即呼叫執行緒的start()方法

java建立執行&建立程序

概述 併發和並行是即相似又有區別: 並行:指兩個或多個事件在同一時刻發生; 併發:指兩個或多個事件在同一時間段內發生。 程序是指一個記憶體中執行中的應用程式。每個程序都有自己獨立的一塊記憶體空間,一個應用程式可以同時啟動多個程序。比如在Windows系統中,一個執行的abc.exe就是一個程序。 那麼我們

Java基礎加強之多執行篇 - 執行建立與終止、互斥、通訊、本地變數

執行緒建立與終止 執行緒建立 Thread類與 Runnable 介面的關係 public interface Runnable { public abstract void run(); } public class Thread implements Run

50、多執行建立的三種方式之實現Runnable介面

實現Runnable介面建立執行緒 使用Runnable建立執行緒步驟: package com.sutaoyu.Thread; //1.自定義一個類實現java.lang包下的Runnable介面 class MyRunnable implements Runnable{ /

執行和程序的關係 ----執行建立有幾種方式

程序是一個應用程式在處理機上的一次執行過程,他是一個動態的概念 ,執行緒是程序的一部分是程式執行的最小單元 一個程序中有多個執行緒 個人理解 :             

【JAVA】執行建立和匿名內部類

前言 看多執行緒時,發現一些匿名內部類的東西,然後就來總結一下。   1.繼承Thread類 在類上實現匿名內部類 public class Demo1 { public static void main(String[] args) { Thread t = new T