pthread_create源碼分析
下面來看glibc中pthread_create函數的源碼,分為兩部分來看。
__pthread_create_2_1第一部分
nptl/pthread_create.c
int __pthread_create_2_1 (newthread, attr, start_routine, arg)pthread_t
*newthread;const pthread_attr_t
*attr;
void *(
*start_routine) (
void *);
void *arg;
{STACK_VARIABLES;const struct pthread_attr
*iattr
= (struct pthread_attr
*) attr;
if (iattr
== NULL)iattr
= &default_attr;struct pthread
*pd
= NULL;ALLOCATE_STACK (iattr,
&pd);pd
->header.self = pd;pd
->header.tcb
= pd;pd
->start_routine
= start_routine;pd
->arg
= arg;
傳入的參數newthread為即將創建的新線程的pthread結構指針,attr為用戶指定的線程創建屬性,start_routine為新線程執行的函數指針,arg為新線程函數的參數地址。
STACK_VARIABLES宏定義了新的堆棧指針stackaddr,stackaddr指向即將分配的線程棧的有效棧頂(不含保護區)。
# define STACK_VARIABLES void *stackaddr = NULL
如果用戶未指定線程創建的屬性attr,則使用默認的屬性值default_attr,其定義如下。
static const struct pthread_attr default_attr ={.guardsize =
1,};
默認的屬性只定義了棧保護區guardsize的大小,該保護區通常用來檢測棧的數據是否到達保護區,即是否下溢。
#
define ALLOCATE_STACK(
attr, pd) allocate_stack (
attr, pd, &stackaddr)
ALLOCATE_STACK宏用來分配線程棧,并在棧底創建pthread結構并初始化。
然后設置執行線程函數的地址start_routine和參數地址arg。
下面重點來看allocate_stack函數的源碼。
__pthread_create_2_1->allocate_stack 第一部分
glibc nptl/allocatestack.c
static int allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
ALLOCATE_STACK_PARMS)
{struct pthread *pd;size_t size;size_t pagesize_m1 = __getpagesize () -
1;void *stacktop;size = attr->stacksize ?: __default_stacksize;
if (__builtin_expect (attr->flags &
ATTR_FLAG_STACKADDR,
0)){uintptr_t adj;
if (attr->stacksize !=
0&& attr->stacksize < (__static_tls_size +
MINIMAL_REST_STACK))return
EINVAL;adj = ((uintptr_t) attr->stackaddr -
TLS_TCB_SIZE)& __static_tls_align_m1;pd = (struct pthread *) ((uintptr_t) attr->stackaddr-
TLS_TCB_SIZE - adj);memset (pd, '\
0', sizeof (struct pthread));pd->specific[
0] = pd->specific_1stblock;pd->stackblock = (char *) attr->stackaddr - size;pd->stackblock_size = size;pd->user_stack = true;pd->header.multiple_threads =
1;pd->pid =
THREAD_GETMEM (
THREAD_SELF, pid);pd->setxid_futex = -
1;_dl_allocate_tls (pd);list_add (&pd->list, &__stack_user);}
首先,如果屬性attr中沒有設置堆棧大小stacksize,則使用默認值__default_stacksize,在不同的cpu體系結構中的默認值不一樣。__default_stacksize變量在__pthread_initialize_minimal_internal函數中根據系統的限制值計算得出。而__pthread_initialize_minimal_internal會在main函數前調用,因此在main函數前就為linux線程做了初始化工作,具體可參考《__pthread_initialize_minimal_internal源碼分析》。
ATTR_FLAG_STACKADDR標志位置位表示由用戶指定棧的地址空間。如果用戶指定了堆棧大小,就檢查該堆棧大小是否小于__static_tls_size + MINIMAL_REST_STACK。MINIMAL_REST_STACK在x64系統中的默認值為2048。__static_tls_size由__pthread_initialize_minimal_internal的_dl_get_tls_static_info函數賦值并對齊,其默認值為 GL(dl_tls_static_size),而GL(dl_tls_static_size)也是在__pthread_initialize_minimal_internal中初始化,如果可執行文件中并沒有定義tls段,則該變量的默認值為初始化的2048個字節加上pthread結構的大小(參考init_static_tls函數)。
第一個if循環表示由用戶指定棧的最高地址stackaddr(下面假設棧由高地址向低地址拓展),TLS_TCB_SIZE宏表示pthread結構的大小。
# define TLS_TCB_SIZE sizeof (struct pthread)
接下來計算即將在棧上初始化的pthread結構按照__static_tls_align_m1對齊后,需要扣除多少地址adj。
然后再計算在棧上分配的pthread結構的地址pd,并通過memset函數將其清0,可以看出pthread結構在新線程的棧底。
specific_1stblock和specific在pthread結構中的定義如下,
struct pthread_key_data{uintptr_t seq;
void *data;} specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
PTHREAD_KEY_2NDLEVEL_SIZE宏的默認值為32。specific相當于二維數組,specific_1stblock為第一組的數組,當第一次超過PTHREAD_KEY_2NDLEVEL_SIZE時,就要分配新的pthread_key_data數組,并將specific[1]指向新分配的數組,以此類推。
再往下繼續設置stackblock記錄線程棧的低端地址也即起始地址,設置stackblock_size記錄線程棧的大小。
接下來的user_stack成員變量標識這是一個根據用戶提供的線程棧。
THREAD_SELF返回調用進程的pthread結構,再通過THREAD_GETMEM宏獲取其中的pid值,因此誰調用了pthread_create函數,就初始化為誰的pid,也即當前進程的pid。
接下來設置setxid_futex防止setxid函數的調用,這里和同步機制有關,回頭碰到了再研究。
再往下通過_dl_allocate_tls函數在pthread結構中分配dtv并初始化。
然后將剛剛初始化的pthread結構添加到全局的__stack_user鏈表中。
__pthread_create_2_1->allocate_stack->_dl_allocate_tls
elf/dl-tls.c
void *internal_function _dl_allocate_tls (
void *mem)
{
return _dl_allocate_tls_init (allocate_dtv (mem));
}
static void *internal_function allocate_dtv (
void *result)
{dtv_t *dtv;size_t dtv_length;dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS;dtv =
calloc (dtv_length +
2,
sizeof (dtv_t));dtv[
0].counter = dtv_length;INSTALL_DTV (result, dtv);
return result;
}
首先計算即將分配的dtv的個數dtv_length,其中dl_tls_max_dtv_idx的值在__pthread_initialize_minimal_internal函數中被初始化為1,DTV_SURPLUS是額外需要分配的內存。
dtv結構的定義如下,
typedef union dtv
{size_t counter;
struct{
void *val;
bool is_static;} pointer;
} dtv_t;
接下來通過calloc分配dtv數組內存,返回內存的起始指針dtv。注意這里多分配了兩個dtv,其中dtv數組的第一個項用來記錄dtv數組的有效大小dtv_length,因此dtv是一個union結構。
INSTALL_DTV將剛剛分配的dtv數組設置到pthread結構中。
# define INSTALL_DTV(descr, dtvp) ((tcbhead_t *) (descr))->dtv = (dtvp) + 1
注意INSTALL_DTV宏是將dtv數組的第二個元素設置到pthread結構的dtv成員變量中。
__pthread_create_2_1->allocate_stack->_dl_allocate_tls->_dl_allocate_tls_init
elf/dl-tls.c
void * internal_function _dl_allocate_tls_init (
void *result)
{dtv_t *dtv = GET_DTV (result);
struct dtv_slotinfo_list *listp;size_t total =
0;size_t maxgen =
0;listp = GL(dl_tls_dtv_slotinfo_list);
while (
1){size_t cnt;
for (cnt = total ==
0 ?
1 :
0; cnt < listp->len; ++cnt){
struct link_map *
map;
void *dest;
if (total + cnt > GL(dl_tls_max_dtv_idx))
break;
map = listp->slotinfo[cnt].
map;maxgen = MAX (maxgen, listp->slotinfo[cnt].gen);
if (
map->l_tls_offset == NO_TLS_OFFSET){dtv[
map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;dtv[
map->l_tls_modid].pointer.is_static =
false;
continue;}dest = (
char *) result -
map->l_tls_offset;dtv[
map->l_tls_modid].pointer.val = dest;dtv[
map->l_tls_modid].pointer.is_static =
true;
memset (__mempcpy (dest,
map->l_tls_initimage,
map->l_tls_initimage_size),
'\0',
map->l_tls_blocksize -
map->l_tls_initimage_size);}total += cnt;
if (total >= GL(dl_tls_max_dtv_idx))
break;listp = listp->next;}dtv[
0].counter = maxgen;
return result;
}
傳入的參數result為pthread結構的地址。
GET_DTV宏和INSTALL_DTV宏相反,用來獲取pthread結構中的dtv結構。
# define GET_DTV(descr) (((tcbhead_t *) (descr))->dtv)
該dtv結構在前面分析的allocate_dtv函數中分配并初始化。
dl_tls_dtv_slotinfo_list為static_slotinfo中的si成員變量,類型為dtv_slotinfo_list,定義如下
EXTERN
struct dtv_slotinfo_list{size_t
len;
struct dtv_slotinfo_list *next;
struct dtv_slotinfo{size_t gen;
struct link_map *
map;} slotinfo
[0];} *_dl_tls_dtv_slotinfo_list;
該結構中的len變量記錄了slotinfo中數組的長度,根據《__pthread_initialize_minimal_internal源碼分析》中的init_slotinfo函數,dl_tls_dtv_slotinfo_list結構中的slotinfo數組的第二項也即slotinfo[1]的ink_map變量存儲了程序初始化時的static_map。
接下來遍歷dl_tls_dtv_slotinfo_list中的每個dtv_slotinfo_list結構的每個slotinfo,如果對應的link_map中的l_tls_offset為NO_TLS_OFFSET,則清空pthread結構中對應位置上dtv結構,否則將tls段的數據也即l_tls_initimage拷貝到對應的dtv結構中。
__pthread_create_2_1->allocate_stack 第二部分
glibc nptl/allocatestack.c
static int allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,ALLOCATE_STACK_PARMS)
{
...if (__builtin_expect (attr->flags & ATTR_FLAG_STACKADDR,
0)){
...}
else{size_t guardsize;size_t reqsize;void *mem;const int prot = (PROT_READ | PROT_WRITE| ((GL(dl_stack_flags) & PF_X) ? PROT_EXEC :
0));size &= ~__static_tls_align_m1;guardsize = (attr->guardsize + pagesize_m1) & ~pagesize_m1;
if (__builtin_expect (size < ((guardsize + __static_tls_size+ MINIMAL_REST_STACK + pagesize_m1)& ~pagesize_m1),
0))
return EINVAL;reqsize = size;pd = get_cached_stack (&size, &mem);
if (pd ==
NULL){mem = mmap (
NULL, size, prot,MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -
1,
0);pd = (struct pthread *) ((char *) mem + size) -
1;pd->stackblock = mem;pd->stackblock_size = size;pd->specific[
0] = pd->specific_1stblock;pd->header.multiple_threads =
1;pd->setxid_futex = -
1;pd->pid = THREAD_GETMEM (THREAD_SELF, pid);_dl_allocate_tls (pd);stack_list_add (&pd->list, &stack_used);
if (__builtin_expect ((GL(dl_stack_flags) & PF_X) !=
0&& (prot & PROT_EXEC) ==
0,
0)){change_stack_perm (pd);}}
第二種情況是用戶沒有提供新線程的棧空間。
此時首先將size對齊,static_tls_align_m1在__pthread_initialize_minimal_internal中的_dl_get_tls_static_info函數被初始化,默認為__alignof (struct pthread)。
再往下將guardsize按頁pagesize_m1對齊,前面看到傳入的參數屬性attr中的guardsize成員變量默認為1,guardsize為一個頁面。
再往下檢查即將分配的棧的大小size是否充足。
get_cached_stack從stack_cache緩存鏈表中獲得空閑的棧和棧的大小,分別保存在mem和size中。
如果從緩存中沒有找到合適的棧,就通過mmap函數在堆上分配size大小內存空間,然后在棧底初始化一個pthread結構,重點是設置了棧的起始地址(最低地址)和大小到stackblock和stackblock_size成員變量中,并且將pthread結構的pid設置為調用進程的pid。
_dl_allocate_tls函數分配并初始化pthread結構的dtv數組,該函數在前面已經分析過了。
再往下如果有別的進程修改了dl_stack_flags變量,即將原來對應的PF_X的值由0修改為了1,就要修改內存的屬性。change_stack_perm函數內部通過mprotect系統調用在棧對應的內存空間增加PF_X屬性,表示對應的內存段可執行。
__pthread_create_2_1->allocate_stack->get_cached_stack
glibc nptl/allocatestack.c
static struct pthread * get_cached_stack (size_t *sizep, void **memp)
{size_t size = *sizep;struct pthread *
result = NULL;list_t *entry;list_for_each (entry, &stack_cache){struct pthread *curr;curr = list_entry (entry, struct pthread,
list);
if (FREE_P (curr) && curr->stackblock_size >= size){
if (curr->stackblock_size == size){
result = curr;break;}
if (
result == NULL||
result->stackblock_size > curr->stackblock_size)
result = curr;}}
if (__builtin_expect (
result == NULL,
0)|| __builtin_expect (
result->stackblock_size >
4 * size,
0))
return NULL;
result->setxid_futex = -
1;stack_list_del (&
result->
list);stack_list_add (&
result->
list, &stack_used);stack_cache_actsize -=
result->stackblock_size;*sizep =
result->stackblock_size;*memp =
result->stackblock;
result->cancelhandling =
0;
result->cleanup = NULL;
result->nextevent = NULL;dtv_t *dtv = GET_DTV (
result);
for (size_t cnt =
0; cnt < dtv[-
1].counter; ++cnt)
if (! dtv[
1 + cnt].pointer.is_static&& dtv[
1 + cnt].pointer.val != TLS_DTV_UNALLOCATED)free (dtv[
1 + cnt].pointer.val);memset (dtv, '\
0', (dtv[-
1].counter +
1) * sizeof (dtv_t));_dl_allocate_tls_init (
result);
return result;
}
stack_cache被初始化為一個鏈表頭,即其next變量指向自身。
static LIST_HEAD (stack_cache);
list_for_each宏遍歷該鏈表,再利用list_entry宏根據entry指針獲得pthread的結構指針。pthread結構中的list結構變量組成鏈表stack_cache,因此獲得list地址entry之后,將其減去list變量在pthread結構中的偏移,就獲得了pthread結構的起始地址。
FREE_P宏檢查該pthread結構是否在使用中,其實是檢查pthread結構的tid成員變量是否小于等于0。
如果有空閑的pthread結構,并且其棧的大小stackblock_size等于即將建立的棧的大小size,就直接使用該pthread結構,否則就選擇所有棧大于即將分配的棧的大小size中最小的一個空閑的pthread結構。
再往下如果沒有空閑的pthread結構,或者空閑的pthread結構大小大于四倍的需求大小size,就返回null。
如果從鏈表stack_cache中找到了合適的pthread結構,接下來就重新初始化該結構,將其從鏈表stack_cache中刪除,加入stack_used鏈表中,表示該pthread結構已經在使用中了。
stack_cache_actsize記錄了stack_cache鏈表中所有空閑pthread結構的stackblock_size大小的和,因此將其減去即將使用的pthread結構的stackblock_size大小。
然后設置傳入的參數sizep和mem,分別表示新的棧的大小和起始地址,用于返回。
再往下清空pthread結構的dtv變量,注意dtv數組的第一個項是用來記錄dtv數組長度的,因此從第二個項開始釋放內存。然后遍歷每個項,通過free函數將非靜態并且已經分配內存的dtv_t結構釋放,最后通過memset函數清空該數組,注意這里講counter加1是因為allocate_dtv函數中通過calloc分配dtv內存時多分配了一塊內存。
最后通過_dl_allocate_tls_init初始化該結構,該函數在前面已經分析過了,然后返回剛剛從stack_cache中找到的pthread結構。
__pthread_create_2_1->allocate_stack 第三部分
glibc nptl/allocatestack.c
static int
allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,ALLOCATE_STACK_PARMS)
{
...if (__builtin_expect (attr->flags & ATTR_FLAG_STACKADDR,
0)){
...}
else{
...pd = get_cached_stack (&size, &mem);
if (pd ==
NULL){
...}
if (__builtin_expect (guardsize > pd->guardsize,
0)){char *guard = mem;
if (mprotect (guard, guardsize, PROT_NONE) !=
0){
...}pd->guardsize = guardsize;}
else if (__builtin_expect (pd->guardsize - guardsize > size - reqsize,
0)){
if (mprotect ((char *) mem + guardsize, pd->guardsize - guardsize,prot) !=
0)goto mprot_error;pd->guardsize = guardsize;}pd->reported_guardsize = guardsize;}*pdp = pd;stacktop = ((char *) (pd +
1) - __static_tls_size);*stack = stacktop;
return 0;
}
執行到這里,已經在棧上分配了內存并在棧底初始化了pthread結構。接下來調整棧上的保護區即guardsize的大小。
第一種情況是用戶需要的保護區大小guardsize大于當前pthread結構中guardsize的大小,如果pthread是剛剛在堆上通過mmap分配的,則此時pthread結構的guardsize成員變量為0,因此條件成立,此時通過mprotect函數將棧頂向上guardsize大小的內存屬性設置為PROT_NONE,因此當有數據訪問這段內存時,便會拋出異常,起到了保護線程棧的效果。
第二種情況是從緩存中找到的空閑的棧空間的保護區的大小大于需要的保護區的大小,此時如果空閑的棧沒有足夠的空間,就要通過mprotect函數刪除多余的保護區大小。
最后將pthread結構的地址存入pdp指針中并返回,并設置棧的有效棧頂的地址stacktop,即棧底減去一個pthread結構的大小再減去__static_tls_size的大小,從前面的分析可知,此時有效的棧大小就為2048個字節,即_dl_tls_static_size的默認值。
最后將該地址存入傳入的參數stack中并返回。
__pthread_create_2_1第二部分
nptl/pthread_create.c
struct pthread
*self = THREAD_SELF;pd
->flags
= ((iattr
->flags
& ~(ATTR_FLAG_SCHED_SET
| ATTR_FLAG_POLICY_SET))
| (
self->flags
& (ATTR_FLAG_SCHED_SET
| ATTR_FLAG_POLICY_SET)));pd
->joinid
= iattr
->flags
& ATTR_FLAG_DETACHSTATE
? pd :
NULL;pd
->eventbuf
= self->eventbuf;pd
->schedpolicy
= self->schedpolicy;pd
->schedparam
= self->schedparam;
if (attr
!= NULL&& __builtin_expect ((iattr
->flags
& ATTR_FLAG_NOTINHERITSCHED)
!= 0,
0)
&& (iattr
->flags
& (ATTR_FLAG_SCHED_SET
| ATTR_FLAG_POLICY_SET))
!= 0){INTERNAL_SYSCALL_DECL (scerr);
if (iattr
->flags
& ATTR_FLAG_POLICY_SET)pd
->schedpolicy
= iattr
->schedpolicy;
else if ((pd
->flags
& ATTR_FLAG_POLICY_SET)
== 0){pd
->schedpolicy
= INTERNAL_SYSCALL (sched_getscheduler, scerr,
1,
0);pd
->flags
|= ATTR_FLAG_POLICY_SET;}
if (iattr
->flags
& ATTR_FLAG_SCHED_SET)memcpy (
&pd
->schedparam,
&iattr
->schedparam,sizeof (struct sched_param));
else if ((pd
->flags
& ATTR_FLAG_SCHED_SET)
== 0){INTERNAL_SYSCALL (sched_getparam, scerr,
2,
0,
&pd
->schedparam);pd
->flags
|= ATTR_FLAG_SCHED_SET;}int minprio
= INTERNAL_SYSCALL (sched_get_priority_min, scerr,
1,iattr
->schedpolicy);int maxprio
= INTERNAL_SYSCALL (sched_get_priority_max, scerr,
1,iattr
->schedpolicy);
if (pd
->schedparam
.sched_priority
< minprio
|| pd
->schedparam
.sched_priority
> maxprio){
...return EINVAL;}}
*newthread
= (pthread_t) pd;
return create_thread (pd, iattr, STACK_VARIABLES_ARGS);
}
首先通過THREAD_SELF獲取當前進程的pthread結構。
接下來當前pthread結構的ATTR_FLAG_SCHED_SET和ATTR_FLAG_POLICY_SET值設置flag。
然后設置pthread的各個成員變量,其中schedpolicy表示線程的調度策略,schedparam表示線程調度的優先級,首先使用調用進程的schedpolicy和schedparam進行賦值。
如果用戶指定了ATTR_FLAG_NOTINHERITSCHED標志,則表示新的線程不使用調用進程的調度策略,并且用戶指定了ATTR_FLAG_SCHED_SET和ATTR_FLAG_POLICY_SET其中一個,此時就要重新設置新線程的調度策略。
如果用戶指定了ATTR_FLAG_POLICY_SET標志位,則直接使用用戶指定的調度策略schedpolicy,否則,如果調用進程的ATTR_FLAG_POLICY_SET標志位被置位,則新線程通過sched_getscheduler系統調用獲得當前進程的調度策略。
類似的,如果用戶指定了ATTR_FLAG_SCHED_SET標志位,則直接使用用戶指定的調度優先級schedparam,否則,如果調用進程的ATTR_FLAG_SCHED_SET標志位被置位,則新線程通過sched_getparam系統調用獲得當前進程的調度優先級。
再往下檢查新線程的調度優先級是否在合理的范圍內,分別通過sched_get_priority_min和sched_get_priority_max函數獲得優先級的最小值和最大值。
最后將前面創建的pthread結構賦值給傳入的參數newthread,最后調用create_thread繼續創建線程。create_thread函數留在下一章繼續分析。
__pthread_create_2_1->sched_getscheduler
linux kernel/sched/core.c
SYSCALL_DEFINE1(sched_getscheduler, pid_t, pid)
{struct task_struct
*p;int retval
= -ESRCH;p
= find_process_by_pid(pid);
if (p) {retval
= p
->policy
| (p
->sched_reset_on_fork
? SCHED_RESET_ON_FORK :
0);}
return retval;
}
傳入的參數pid為0,因此find_process_by_pid函數會簡單返回當前進程的task_struct結構,然后獲得當前進程的調度策略policy并返回。
__pthread_create_2_1->sched_get_priority_min
linux kernel/sched/core.c
SYSCALL_DEFINE1(sched_get_priority_min,
int, policy)
{
int ret = -EINVAL;
switch (policy) {
case SCHED_FIFO:
case SCHED_RR:ret =
1;
break;
case SCHED_DEADLINE:
case SCHED_NORMAL:
case SCHED_BATCH:
case SCHED_IDLE:ret =
0;}
return ret;
}
sched_get_priority_min系統調用返回調度優先級的最小值,當調度策略為SCHED_FIFO或SCHED_RR時,默認的調度優先級最小值為1。
__pthread_create_2_1->sched_get_priority_max
linux kernel/sched/core.c
SYSCALL_DEFINE1(sched_get_priority_max,
int, policy)
{
int ret = -EINVAL;
switch (policy) {
case SCHED_FIFO:
case SCHED_RR:ret = MAX_USER_RT_PRIO-
1;
break;
case SCHED_DEADLINE:
case SCHED_NORMAL:
case SCHED_BATCH:
case SCHED_IDLE:ret =
0;
break;}
return ret;
}
MAX_USER_RT_PRIO的默認值為100,因此當調度策略為SCHED_FIFO或SCHED_RR時,調度優先級的最大值為99。
總結
以上是生活随笔為你收集整理的pthread_create源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。