项目的性能测试告一段落,暂时松了一口气。但是也发现很多知识的盲点,也许这就是所谓的知道的越多,不知道的也越多。
现在所有的程序基本上都是用多线程来实现的,尤其是Unix/Linux server程序。我原本以为线程是直接在内核实现的,或者大部分代码应该在内核中。但是当我找pthread_create或者pthread字眼时,发现Linux内核中的代码根本搜索不到,于是用g++ -M命令找到了pthread的头文件,内秀的pthread才肯出来露面,原来是在glibc中。
glibc是GNU发布的libc库,提供了Linux系统中底层的API。在这里,把我阅读的glibc代码分享出来,更改一些注释,规划下格式,希望能够让大家看的时候更省心一些(实在不敢恭维glibc的格式,这是我看过的最丑的开源代码)。
POSIX中使用pthread_create创建线程,glibc中有一个nptl(Native POSIX Thread Library)版本的线程实现。主要阅读其中的pthread_create, pthread_join和pthread_exit这几个常用函数,了解线程是如何创建和退出的。理解了这几个再看其余的,加深对线程的理解,了解更多的线程特性。
这是代码中经常见到的一个变量类型,所以单独提前写出来
sysdeps/generic/stdint.h
typedef unsigned long int uintptr_t
sysdeps/nptl/internaltypes.h
struct pthread_attr
{
// 调度参数和优先级 NOTE: sched_param中定义的也就一个优先级的成员
struct sched_param schedparam;
int schedpolicy;
int flags; // 状态标识,比如detachstate, scope等
size_t guardsize; // 保护区大小
void *stackaddr; // stack内存地址
size_t stacksize;
cpu_set_t *cpuset; // 关系映射 Affinity map
size_t cpusetsize;
};
NOTE: struct sched_param
// 调度算法
#define SCHED_OTHER 0
#define SCHED_FIFO 1
#define SCHED_RR 2
struct sched_param
{
int __sched_priority;
};
// 访问cpu_set_t的函数
# define __CPUELT(cpu) ((cpu) / __NCPUBITS)
# define __CPUMASK(cpu) ((__cpu_mask) 1 << ((cpu) % __NCPUBITS))
typedef struct
{
__cpu_mask __bits[__CPU_SETSIZE / __NCPUBITS];
} cpu_set_t;
// sysdeps/nptl/pthread.h
struct _pthread_cleanup_buffer
{
void (*__routine) (void *);
void *__arg;
int __canceltype;
struct _pthread_cleanup_buffer *__prev;
};
// 存储取消处理器信息缓存的内部实现
struct pthread_unwind_buf
{
struct
{
__jmp_buf jmp_buf;
int mask_was_saved;
} cancel_jmp_buf[1];
union
{
void *pad[4]; // 公用版本实现的占位符
struct
{
// 指向前一个cleanup buffer
struct pthread_unwind_buf *prev;
// 向后兼容:前一个新风格清理处理器安装的时间老风格的清理处理器状态
struct _pthread_cleanup_buffer *cleanup;
int canceltype; // push 调用前取消(cancellation)的类型
} data;
} priv;
};
这个结构体是用来处理异常的,不了解这个结构体,也不怎么影响阅读整个代码,此处贴出来只是用来浏览。
#define _Unwind_Exception _Unwind_Control_Block
struct _Unwind_Control_Block
{
#ifdef _LIBC
/* For the benefit of code which assumes this is a scalar. All glibc ever does is clear it. */
_uw64 exception_class;
#else
char exception_class[8];
#endif
void (*exception_cleanup)(_Unwind_Reason_Code, _Unwind_Control_Block *);
/* Unwinder cache, private fields for the unwinder's use */
struct
{
_uw reserved1; /* Forced unwind stop fn, 0 if not forced */
_uw reserved2; /* Personality routine address */
_uw reserved3; /* Saved callsite address */
_uw reserved4; /* Forced unwind stop arg */
_uw reserved5;
} unwinder_cache;
/* Propagation barrier cache (valid after phase 1): */
struct
{
_uw sp;
_uw bitpattern[5];
}barrier_cache;
/* Cleanup cache (preserved over cleanup): */
struct
{
_uw bitpattern[4];
}cleanup_cache;
/* Pr cache (for pr's benefit): */
struct
{
_uw fnstart; /* function start address */
_Unwind_EHT_Header *ehtp; /* pointer to EHT entry header word */
_uw additional; /* additional data */
_uw reserved1;
}pr_cache;
long long int :0; /* Force alignment to 8-byte boundary */
};
nptl/descr.h
这里有很多线程中陌生的概念,或者平时工作时也极少用到的,先做入门,不考虑太复杂的场景。
众所周知,每个进程都有自己的用户空间,但是线程是没有的,所以应该是所有线程公用同一个进程空间。
一个线程要执行代码,必须要有自己的数据区域用来存放执行的函数中的临时变量,这个就是栈。每个线程的栈空间应该是隔离的,但是为了防止线程代码越界访问,超出了栈空间,比如递归程序,可以设置一块内存作为保护区域(将这块内存的权限设置为不可读写或执行),因此就有了保护区(guard)。
线程还有一个特性,就是可以存放每个线程特有的数据,就是TLS。存放TLS是一个数组指针,也是线程数据的一部分,这样才能做到快速访问当前线程的特有数据。
一个线程关联的主要数据和线程数据结构的描述:
- 存放线程信息本身数据的内存
- 线程环境栈
void *stackblock;
size_t stackblock_size;
size_t guardsize;
struct pthread_key_data specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
下面是glibc中对线程描述的结构体struct pthread.
struct pthread
{
union
{
#if !TLS_DTV_AT_TP
tcbhead_t header; /* TLS 使用的TCB,不包含线程 */
struct
{
/* 当进程产生了至少一个线程或一个单线程的进程取消本身时 启用multiple_threads。这样就允许在做一些compare_and_exchange 操作之前添加一些额外的代码来引入锁,也可以开启取消点(cancellation point)。 多个线程的概念和取消点的概念是分开的,因为没有必要为取消 点设计成多线程,就跟单线程进程取消本身一样。 因为开启多线程就允许在取消点和compare_and_exchange操作中 添加一些额外的代码,这样的话对于一个单线程、自取消(self-canceling) 的进程可能会有不必要的性能影响。但是这样也没问题,因为 仅当它要取消自己并且要结束的时候,一个单线程的进程会开启 异步取消 */
int multiple_threads;
int gscope_flag;
# ifndef __ASSUME_PRIVATE_FUTEX
int private_futex;
# endif
} header;
#endif
void *__padding[24];
};
list_t list; // `stack_used' 或 `__stack_user' 链表节点
pid_t tid; // 线程ID,也是线程描述符
pid_t pid; // 进程ID,线程组ID
// 进程当前持有的robust mutex
#ifdef __PTHREAD_MUTEX_HAVE_PREV
void *robust_prev;
struct robust_list_head robust_head;
/* The list above is strange. It is basically a double linked list but the pointer to the next/previous element of the list points in the middle of the object, the __next element. Whenever casting to __pthread_list_t we need to adjust the pointer first. */
#else
union
{
__pthread_slist_t robust_list;
struct robust_list_head robust_head;
};
#endif
struct _pthread_cleanup_buffer *cleanup; // cleanup缓存链表
struct pthread_unwind_buf *cleanup_jmp_buf; // unwind信息
int cancelhandling; // 判断处理取消的标识
int flags; // 标识. 包含从线程属性中复制的信息
// 这里分配一个块. 对大多数应用程序应该能够尽量避免动态分配内存
struct pthread_key_data
{
uintptr_t seq; // 序列号
void *data; // 数据指针
} specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
// 存放线程特有数据的二维数组
// 第1个元素就是specific_1stblock
struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
bool specific_used; // 标识符:是否使用特定(specific)数据(TLS)
bool report_events; // 是否要汇报事件
bool user_stack; // 是否用户提供栈
bool stopped_start; // 启动的时候线程是否应该是停止状态
// pthread_create执行的时候,parent的取消处理器。当需要取消的时候才用到
int parent_cancelhandling;
int lock; // 同步访问的锁
int setxid_futex; // setxid调用的同步锁
#if HP_TIMING_AVAIL
/* Offset of the CPU clock at start thread start time. */
hp_timing_t cpuclock_offset;
#endif
// 如果线程等待关联另一个线程ID, 就将那个线程的ID放在这里
// 如果一个线程状态是detached,这里就存放它自己.
struct pthread *joinid; // 如果joinid是线程本身,就说明这个线程是detached状态
void *result; // 线程函数执行的结果
// 新线程的调度参数
struct sched_param schedparam; // 只有一个成员: int __sched_priority
int schedpolicy;
// 执行函数的地址和参数
void *(*start_routine) (void *);
void *arg;
td_eventbuf_t eventbuf; // 调试状态
struct pthread *nextevent; // 下一个有pending事件的描述符,应该是用来调试的
#ifdef HAVE_FORCED_UNWIND
struct _Unwind_Exception exc; // 与平台相关的unwind信息
#endif
// 如果是非0,指栈上分配的区域和大小
void *stackblock;
size_t stackblock_size;
size_t guardsize; // 保护区域的大小
size_t reported_guardsize; // 用户指定的并且展示的保护区大小(就是通过接口获取保护区大小时,返回这个数字)
struct priority_protection_data *tpp; // 线程有限保护数据
/* Resolver state. */
struct __res_state res;
char end_padding[];
} __attribute ((aligned (TCB_ALIGNMENT)));
这里面有一些宏定义或者变量的值,可能不止一个,但是为了简单容易理解,只是贴出来一个来看。
线程中最重要的函数pthread_create,功能就是创建线程。它的任务可以分为以下几步:
1. 初始化线程属性
如果用户提供了线程属性,就使用用户的,否则复制默认线程属性。
glibc使用全局变量__default_pthread_attr保存线程默认属性。
2. 为线程分配栈空间: ALLOCATE_STACK
3. 启动线程,执行线程函数:create_thread
一般创建的线程不是立即启动的,而是最后调用create_thread才启动。Posix没有提供一个创建线程,然后在主动启动的接口,所以启动的接口直接在创建的时候调用了。
这里面当然还有一些其它的任务,比如线程调度参数,event report等,但是代码简单,而且与平时接触到的接口关系不大,因此暂时略过不提.
int
__pthread_create_2_1 (newthread, attr, start_routine, arg)
pthread_t *newthread;
const pthread_attr_t *attr;
void *(*start_routine) (void *);
void *arg;
{
# define STACK_VARIABLES void *stackaddr = NULL; size_t stacksize = 0
STACK_VARIABLES;
const struct pthread_attr *iattr = (struct pthread_attr *) attr;
struct pthread_attr default_attr;
bool free_cpuset = false;
/// 1 初始化线程属性
if (iattr == NULL) // 如果传入的属性参数是空,使用默认
{
// 复制默认线程属性
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
default_attr = __default_pthread_attr;
size_t cpusetsize = default_attr.cpusetsize;
if (cpusetsize > 0)
{
cpu_set_t *cpuset;
if (__glibc_likely (__libc_use_alloca (cpusetsize)))
cpuset = __alloca (cpusetsize);
else
{
cpuset = malloc (cpusetsize);
if (cpuset == NULL)
{
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
return ENOMEM;
}
free_cpuset = true;
}
memcpy (cpuset, default_attr.cpuset, cpusetsize);
default_attr.cpuset = cpuset;
}
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
iattr = &default_attr;
}
/// 2. 创建线程栈空间
struct pthread *pd = NULL;
int err = ALLOCATE_STACK (iattr, &pd); // 分配一个堆栈
int retval = 0;
if (__glibc_unlikely (err != 0))
{
/* 出错了.可能是属性的参数不正确或者不能分配内存.需要转换错误码*/
retval = err == ENOMEM ? EAGAIN : err;
goto out;
}
/* 初始化TCB. 都在'get_cached_stack'中被初始化为0. 如果使用'mmap'新分配 * 的stack, 这种方法可以避免多次初始化 */
#if TLS_TCB_AT_TP
/* Reference to the TCB itself. */
pd->header.self = pd;
/* Self-reference for TLS. */
pd->header.tcb = pd;
#endif
// 线程执行的回调函数和参数
pd->start_routine = start_routine;
pd->arg = arg;
// 复制线程属性标识
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)));
// 如果是detached状态,joined就设置为自己本身
pd->joinid = iattr->flags & ATTR_FLAG_DETACHSTATE ? pd : NULL;
// debug event从父线程继承
pd->eventbuf = self->eventbuf;
pd->schedpolicy = self->schedpolicy;
pd->schedparam = self->schedparam;
// 复制栈保护区
#ifdef THREAD_COPY_STACK_GUARD
THREAD_COPY_STACK_GUARD (pd);
#endif
// 复制指针保护值
#ifdef THREAD_COPY_POINTER_GUARD
THREAD_COPY_POINTER_GUARD (pd);
#endif
// 计算线程的调度参数
// NOTE: # define __builtin_expect(expr, val) (expr)
// 如果val是0,它的功能类似于unlikely; 如果val 是1, 功能就是likely
if (__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)
{
// 可能是线程想要改变ID并且等待刚创建就失败的线程
if (__builtin_expect (atomic_exchange_acq (&pd->setxid_futex, 0)
== -2, 0))
lll_futex_wake (&pd->setxid_futex, 1, LLL_PRIVATE);
__deallocate_stack (pd); // 释放申请的栈
retval = EINVAL;
goto out;
}
}
// 将描述符返回调用者
*newthread = (pthread_t) pd;
LIBC_PROBE (pthread_create, 4, newthread, attr, start_routine, arg);
/// 3. 启动线程
retval = create_thread (pd, iattr, STACK_VARIABLES_ARGS);
out:
if (__glibc_unlikely (free_cpuset))
free (default_attr.cpuset);
return retval;
}
线程栈可以由用户提供,也可以让glibc自己分配内存。
如果是用户提供了内存,由用户自己销毁,glibc会校验栈空间大小是否合适,但是不会为它设置保护区。
如果是glibc自己分配栈空间,优先从栈缓存中找出一个合适大小的栈(以前创建了线程又销毁了,这个内存会存起来),如果找不到就调用mmap分配一个。glibc自己分配的栈空间会设置保护区。如果是用的缓存中的栈,还会校验以前的保护区的位置和大小是否满足当前的要求,必要时重新设置。
一个栈,必须要能够容纳线程数据本身(sizeof(struct pthread)),保护区,TLS空间和一个最小的栈预留空间(大部分都是2048).
这里计算空间大小时,大部分都要按照页对齐。
struct pthread存放在栈空间的最后,不管栈是向上增长还是向下增长;
保护区是根据栈增长方向不同存放的位置也不同,都是放在增长方向的尾端。如果是向上增长,保护区就紧贴着struct pthread的内存;如果是向下增长,那就是栈的起始位置。不过还有一种奇葩的保护区存放方式,就是在栈中间劈开(真是毁三观),这种保护区存放的位置自然不用说,就是在栈中间。
创建线程栈的宏定义:ALLOCATE_STACK(源代码中有两个,这里为了简单就分析一个)
# define ALLOCATE_STACK(attr, pd) \
allocate_stack (attr, pd, &stackaddr, &stacksize)
allocate_stack的实现如下
// nptl/allocatestack.c
// 通过分配一个新的栈或者重用缓冲区中的栈来创建并返回一个可用的线程
// 参数attr不能是空指针,pdp也不能是空指针
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; // 暂时假定页大小是4096,其实大部分都是这样的
void *stacktop;
assert (powerof2 (pagesize_m1 + 1));
assert (TCB_ALIGNMENT >= STACK_ALIGN);
// 如果用户指定了栈大小,就用用户指定的,否则使用默认的
if (attr->stacksize != 0)
size = attr->stacksize;
else
{
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
size = __default_pthread_attr.stacksize;
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
}
// 获取栈的内存
if (__glibc_unlikely (attr->flags & ATTR_FLAG_STACKADDR))
{
// 如果用户给定了栈内存,直接用用户指定的
// 可以用pthread_attr_setstack设置栈内存
uintptr_t adj; // uintptr_t: unsigned long int
// 验证用户指定的栈内存大小是否充足
if (attr->stacksize != 0
&& attr->stacksize < (__static_tls_size + MINIMAL_REST_STACK))
return EINVAL;
// NOTE:__static_tls_size在代码中设置的值是2048(_dl_tls_static_size)
// # define MINIMAL_REST_STACK 2048 正好也是一页大小
// 调整栈大小,按照TLS块对齐
// 看下面的代码,attr->stackaddr应该是在分配的一块内存的最顶端
#if TLS_TCB_AT_TP
adj = ((uintptr_t) attr->stackaddr - TLS_TCB_SIZE) // TLS_TCB_SIZE:sizeof (struct pthread) (在x86_64系统上)
& __static_tls_align_m1; // __static_tls_align_m1 = STACK_ALIGN = __alignof__ (long double)
assert (size > adj + TLS_TCB_SIZE);
#elif TLS_DTV_AT_TP
adj = ((uintptr_t) attr->stackaddr - __static_tls_size)
& __static_tls_align_m1;
assert (size > adj);
#endif
// 用户提供的内存. 如果是用户自己提供的栈,不会分配保护页.
#if TLS_TCB_AT_TP
pd = (struct pthread *) ((uintptr_t) attr->stackaddr
- TLS_TCB_SIZE - adj);
#elif TLS_DTV_AT_TP
pd = (struct pthread *) (((uintptr_t) attr->stackaddr
- __static_tls_size - adj)
- TLS_PRE_TCB_SIZE);
#endif
// 清零
memset (pd, '\0', sizeof (struct pthread));
// 第一个TSD块包含在TCB中
pd->specific[0] = pd->specific_1stblock;
// 记录栈相关的值
pd->stackblock = (char *) attr->stackaddr - size; // size就是栈大小
pd->stackblock_size = size;
// 用户提供的栈. 不会放在栈缓存中或者释放它(除了TLS内存)
pd->user_stack = true;
// 最少是第二个线程(肯定是其它线程创建出来的线程)
pd->header.multiple_threads = 1;
#ifndef TLS_MULTIPLE_THREADS_IN_TCB
__pthread_multiple_threads = *__libc_multiple_threads_ptr = 1;
#endif
#ifndef __ASSUME_PRIVATE_FUTEX
// 线程知道什么时候支持私有的futex(简单的理解为轻量级的mutex)
pd->header.private_futex = THREAD_GETMEM (THREAD_SELF,
header.private_futex);
#endif
#ifdef NEED_DL_SYSINFO
// 从父线程中复制系统信息
THREAD_SYSINFO(pd) = THREAD_SELF_SYSINFO;
#endif
// 进程ID与调用者的进程ID相同(肯定是同一个进程)
pd->pid = THREAD_GETMEM (THREAD_SELF, pid);
// 完全结束克隆后(cloned)才允许setxid(xid是什么)
pd->setxid_futex = -1;
// 为线程分配DTV(Dynamic Thread Vector)
if (_dl_allocate_tls (TLS_TPADJ (pd)) == NULL)
{
assert (errno == ENOMEM);
return errno;
}
// 准备修改全局变量
lll_lock (stack_cache_lock, LLL_PRIVATE);
// 将栈添加到当前正在使用的栈链表中(感觉名字应该叫__stack_use)
list_add (&pd->list, &__stack_user);
lll_unlock (stack_cache_lock, LLL_PRIVATE);
}
else // 不是用户提供的栈, 需要glib分配内存
{
// 分配匿名内存. 可能使用缓存
size_t guardsize;
size_t reqsize;
void *mem;
const int prot = (PROT_READ | PROT_WRITE
| ((GL(dl_stack_flags) & PF_X) ? PROT_EXEC : 0));
#if COLORING_INCREMENT != 0
// 添加一页或多页用来做栈着色(着色到底是什么,做什么用的?)
// 16整数倍的或更大的页不需要再这样做. 否则可能不必要的未对齐(unnecessary misalignment)
if (size <= 16 * pagesize_m1)
size += pagesize_m1 + 1;
#endif
// 对齐栈大小
size &= ~__static_tls_align_m1;
assert (size != 0);
// 确保栈大小足够存放保护区, 还有线程描述符,就是trhead
// pagesize_m1 是pagesize - 1, 这个语句会将guardsize按照pagesize对齐
guardsize = (attr->guardsize + pagesize_m1) & ~pagesize_m1;
if (__builtin_expect (size < ((guardsize + __static_tls_size
+ MINIMAL_REST_STACK + pagesize_m1)
& ~pagesize_m1),
0))
// 这个if语句是判断size(即当前的栈大小)是否满足需求(保护区大小 + TLS大小 + 最小的栈尺寸)
return EINVAL;
// 首先尝试从缓存中获取栈内存
reqsize = size; // reqsize记录了最原始计算出来的期望的栈空间大小,size在get_cached_stack返回后可能会改变
pd = get_cached_stack (&size, &mem);
if (pd == NULL)
{
// 避免造成比调整后的分配栈大小更大范围的影响.
// 这种方法直接分配不会造成混淆的问题
#if MULTI_PAGE_ALIASING != 0
if ((size % MULTI_PAGE_ALIASING) == 0) // MULTI_PAGE_ALIASING默认定义是0
size += pagesize_m1 + 1; // 很奇怪,这里不是做对齐处理,而是非要多出来一点或一页
#endif
// 使用内存映射创建需要的栈空间
mem = mmap (NULL, size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (__glibc_unlikely (mem == MAP_FAILED))
return errno;
assert (mem != NULL);
// 这段代码做着色计算。 着色我理解为按照一定算法计算出一个随机数
// 线程栈空间中存放的struct pthread按照这个着色参数进行一定偏移,
// 提高一定的安全度,类似于TCP的SYN初始序列号的功能(以上都是我的猜想)
#if COLORING_INCREMENT != 0 // COLORING_INCREMENT 默认定义是0
// 自增长NCREATED (猜测是每创建一个新线程,这个值就增长固定的值)
// atomic_increment_val每次加1
// nptl_ncreated是在nptl/allocatestack.c中定义的一个静态变量
unsigned int ncreated = atomic_increment_val (&nptl_ncreated);
// 选择一个使用每个新线程固定自增长着色的偏移量(就是上一行自增长的NCREATED).
// 这个偏移量对pagesize取模.
// 即使着色会相对更高的对齐值更好, 但是这样做没有意义,
// mmap()接口并不允许我们指定任何返回对齐的内存块
size_t coloring = (ncreated * COLORING_INCREMENT) & pagesize_m1;
// 确保着色偏移量不会干扰TCB和静态TLS区的对齐, 就是在对coloring做对齐计算
// 这里为啥用unlikely? 我认为大部分情况应该都不是按照tls_align对齐的, 除非
// COLORING_INCREMENT 按照tls_align的倍数配置
if (__glibc_unlikely ((coloring & __static_tls_align_m1) != 0))
coloring = (((coloring + __static_tls_align_m1)
& ~(__static_tls_align_m1)) // 这两行对__static_tls_align对齐
& ~pagesize_m1); // 功能是 round_low(pagesize)
#else
// 没有特别指定,不做任何调整操作
# define coloring 0
#endif
// 线程的数据放到栈的尾部, 会把着色的那部分内存在顶端空出来
#if TLS_TCB_AT_TP
pd = (struct pthread *) ((char *) mem + size - coloring) - 1;
#elif TLS_DTV_AT_TP // 把着色和TLS的空间都在顶端空出来
pd = (struct pthread *) ((((uintptr_t) mem + size - coloring
- __static_tls_size)
& ~__static_tls_align_m1)
- TLS_PRE_TCB_SIZE); // TLS_PRE_TCB_SIZE: sizeof (struct pthread), 不同系统可能不同
#endif
// 设置栈相关的值
pd->stackblock = mem;
pd->stackblock_size = size;
// 分配第一个线程相关数据数组块
// 这个地址在进程描述符的整个生命空间都不会改变
pd->specific[0] = pd->specific_1stblock;
// 最少是第二个线程
pd->header.multiple_threads = 1;
#ifndef TLS_MULTIPLE_THREADS_IN_TCB
__pthread_multiple_threads = *__libc_multiple_threads_ptr = 1;
#endif
#ifndef __ASSUME_PRIVATE_FUTEX
// 线程知道什么时候支持私有的futex(简单的理解为轻量级的mutex)
pd->header.private_futex = THREAD_GETMEM (THREAD_SELF,
header.private_futex);
#endif
#ifdef NEED_DL_SYSINFO
// 从父线程中复制系统信息
THREAD_SYSINFO(pd) = THREAD_SELF_SYSINFO;
#endif
// 完全结束克隆后(cloned)才允许setxid(xid是什么)
pd->setxid_futex = -1;
// 进程号跟调用者相同
pd->pid = THREAD_GETMEM (THREAD_SELF, pid);
// 为线程分配DTV
if (_dl_allocate_tls (TLS_TPADJ (pd)) == NULL)
{
assert (errno == ENOMEM);
(void) munmap (mem, size);
return errno;
}
lll_lock (stack_cache_lock, LLL_PRIVATE);
stack_list_add (&pd->list, &stack_used);
lll_unlock (stack_cache_lock, LLL_PRIVATE);
// 在准备生成这个栈的时候,另一个线程可能是栈有可执行权限
// 也就是修改GL(dl_stack_flags)的值
// 这里只是检测这个发生的可能性
// GL(dl_stack_flags) : _dl_stack_flags 全局变量
// #define PF_X (1 << 0) /* Segment is executable */
// #define PROT_EXEC 0x4 /* Page can be executed. */
// const int prot = (PROT_READ | PROT_WRITE
// | ((GL(dl_stack_flags) & PF_X) ? PROT_EXEC : 0));
// 按照上面的prot赋值语句,这个可能性是很小的,除非修改全局变量_dl_stack_flags
if (__builtin_expect ((GL(dl_stack_flags) & PF_X) != 0
&& (prot & PROT_EXEC) == 0, 0))
{
int err = change_stack_perm (pd
#ifdef NEED_SEPARATE_REGISTER_STACK
, ~pagesize_m1
#endif
);
if (err != 0)
{
/* Free the stack memory we just allocated. */
(void) munmap (mem, size);
return err;
}
}
// NOTE:所有的栈和线程描述符都是用零填充的
// 就是说不用再用0初始化一些字段
// 对于'tid'字段更确定是0, 一个栈一旦不再使用就会设置成0
// 对于'guardsize' 字段,下一次还会再用不会清零
}
// 创建或者重新规划保护区的大小
// pd->guardsize中存放着原先栈空间中的保护区大小(如果是从缓存中捞出来的栈)
if (__glibc_unlikely (guardsize > pd->guardsize))
{
#ifdef NEED_SEPARATE_REGISTER_STACK // 大概从一般的时候开始
char *guard = mem + (((size - guardsize) / 2) & ~pagesize_m1);
#elif _STACK_GROWS_DOWN // 自顶向下增长的栈,保护区就在栈的末尾,即栈内存的起始处
char *guard = mem;
# elif _STACK_GROWS_UP // 自底向上增长的栈,保护区在线程描述符(pd)减去保护区大小(页对齐)
char *guard = (char *) (((uintptr_t) pd - guardsize) & ~pagesize_m1);
#endif
// mprotect是POSIX系统标准接口,设置内存访问权限
if (mprotect (guard, guardsize, PROT_NONE) != 0)
{
mprot_error:
lll_lock (stack_cache_lock, LLL_PRIVATE);
// 从列表中删除这个线程
stack_list_del (&pd->list);
lll_unlock (stack_cache_lock, LLL_PRIVATE);
// 删除分配的TLS块
_dl_deallocate_tls (TLS_TPADJ (pd), false);
// 不管缓冲区的size是否超过了限制,释放栈内存
// 如果这片内存造成一些异常就最好不要再用了.
// 忽略可能出现的错误, 就是出错也无能为力
(void) munmap (mem, size);
return errno;
}
pd->guardsize = guardsize;
}
else if (__builtin_expect (pd->guardsize - guardsize > size - reqsize,
0))
// size当前的值是栈缓存分配的内存大小或mmap分配的内存大小, reqsize是原先需求的栈大小
// 为什么是size - reqsize? 因为上一次的保护区范围已经超出了当前的栈空间
// 当前的栈空间大小其实是reqsize
{
// 旧的保护区太大
#ifdef NEED_SEPARATE_REGISTER_STACK
char *guard = mem + (((size - guardsize) / 2) & ~pagesize_m1);
char *oldguard = mem + (((size - pd->guardsize) / 2) & ~pagesize_m1);
if (oldguard < guard
&& mprotect (oldguard, guard - oldguard, prot) != 0)
goto mprot_error;
if (mprotect (guard + guardsize,
oldguard + pd->guardsize - guard - guardsize,
prot) != 0)
goto mprot_error;
#elif _STACK_GROWS_DOWN
if (mprotect ((char *) mem + guardsize, pd->guardsize - guardsize,
prot) != 0)
goto mprot_error;
#elif _STACK_GROWS_UP
if (mprotect ((char *) pd - pd->guardsize,
pd->guardsize - guardsize, prot) != 0)
goto mprot_error;
#endif
pd->guardsize = guardsize;
}
// pthread_getattr_np()调用需要传递请求的属性大小,不论guardsize使用的实际上是多大。
pd->reported_guardsize = guardsize;
}
// 初始化锁. 任何情况下都要做这个动作,因为创建失败的线程可能在加锁的状态被取消
pd->lock = LLL_LOCK_INITIALIZER;
// robust mutex链表必须被初始化, 内核中可能正好在清理前一个栈
pd->robust_head.futex_offset = (offsetof (pthread_mutex_t, __data.__lock)
- offsetof (pthread_mutex_t,
__data.__list.__next));
pd->robust_head.list_op_pending = NULL;
#ifdef __PTHREAD_MUTEX_HAVE_PREV
pd->robust_prev = &pd->robust_head;
#endif
pd->robust_head.list = &pd->robust_head;
// 将线程描述符放在栈的尾部
*pdp = pd;
#if TLS_TCB_AT_TP
// 栈是从TCB和静态TLS块之前开始的
stacktop = ((char *) (pd + 1) - __static_tls_size);
#elif TLS_DTV_AT_TP
stacktop = (char *) (pd - 1);
#endif
#ifdef NEED_SEPARATE_REGISTER_STACK
*stack = pd->stackblock;
*stacksize = stacktop - *stack;
#elif _STACK_GROWS_DOWN
*stack = stacktop;
#elif _STACK_GROWS_UP
*stack = pd->stackblock;
assert (*stack > 0);
#endif
return 0;
}
再次简单描述如下:
1. 确定栈大小
如果用户提供了栈大小,就用用户提供的,否则使用默认的。
2. 栈内存
3. 保护区
4. TLS
5. 着色
pthread_create前半部分只是为创建线程做准备,但是真正的创建线程是在create_thread,这里初始化创建线程参数,调用内核接口。
代码中对创建线程的一个参数clone_flag做了重要介绍,这里单独列出来.
CLONE_VM, CLONE_FS, CLONE_FILES
这几个标识符是按照POSIX的要求,共享地址空间和文件描述符
CLONE_SIGNAL
应用POSIX信号
#define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
CLONE_SETTLS
第六个克隆的参数决定了新线程的TLS区域
CLONE_PARENT_SETTID
内核将新创建的线程的ID写到克隆的第五个参数指定的位置上
与CLONE_CHILD_SETTID语义相同,但是在内核中消耗更大.
CLONE_CHILD_CLEARTID
内核清理调用sys_exit的线程的线程ID, 这个数据是在CLONE的第7个参数指定的位置上.
终止的信号设置为0,就是不需要发送信号
CLONE_SYSVSEM
共享sysvsem
// nptl/createthread.c
static int
create_thread (struct pthread *pd, const struct pthread_attr *attr,
STACK_VARIABLES_PARMS)
{
#if TLS_TCB_AT_TP
assert (pd->header.tcb != NULL);
#endif
int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
| 0);
if (__glibc_unlikely (THREAD_GETMEM (THREAD_SELF, report_events)))
{
// 这个report events到底是做什么的, 不得而知, 因此先跳过此段...
// 父线程支持report event, 检查是否需要TD_CREATE
const int _idx = __td_eventword (TD_CREATE);
const uint32_t _mask = __td_eventmask (TD_CREATE);
if ((_mask & (__nptl_threads_events.event_bits[_idx]
| pd->eventbuf.eventmask.event_bits[_idx])) != 0)
{
// 线程启动时一定是stopped状态
pd->stopped_start = true;
// 创建线程. 使用stopped状态创建线程,这样的话通知调试器时程序不会已经运行很多了
int res = do_clone (pd, attr, clone_flags, start_thread,
STACK_VARIABLES_ARGS, 1);
if (res == 0)
{
// 在新创建的线程数据结构中填充新线程的信息.
// 新线程不能做这个任务,因为在发送这个事件时不能确定它是否已经调度过了
pd->eventbuf.eventnum = TD_CREATE;
pd->eventbuf.eventdata = pd;
do
pd->nextevent = __nptl_last_event;
while (atomic_compare_and_exchange_bool_acq (&__nptl_last_event,
pd, pd->nextevent)
!= 0);
// 发送事件
__nptl_create_event ();
// 启动线程
lll_unlock (pd->lock, LLL_PRIVATE);
}
return res;
}
}
#ifdef NEED_DL_SYSINFO
assert (THREAD_SELF_SYSINFO == THREAD_SYSINFO (pd));
#endif
// 因为需要设置调度参数和亲属关系(就是与父线程的关系),所以需要判断新线程
// 是否需要启动或者停止
// 疑问:cpuset是做什么用的
bool stopped = false;
if (attr != NULL && (attr->cpuset != NULL
|| (attr->flags & ATTR_FLAG_NOTINHERITSCHED) != 0))
stopped = true;
pd->stopped_start = stopped;
pd->parent_cancelhandling = THREAD_GETMEM (THREAD_SELF, cancelhandling);
// do_clone会调用内核接口创建线程
int res = do_clone (pd, attr, clone_flags, start_thread,
STACK_VARIABLES_ARGS, stopped);
if (res == 0 && stopped)
lll_unlock (pd->lock, LLL_PRIVATE);
return res;
}
这个函数是由create_thread调用的,调用内核接口创建线程的接口
// nptl/createthread.c
static int
do_clone (struct pthread *pd, const struct pthread_attr *attr,
int clone_flags, int (*fct) (void *), STACK_VARIABLES_PARMS,
int stopped)
{
TLS_DEFINE_INIT_TP (tp, pd);
if (__glibc_unlikely (stopped != 0))
// 强制性加锁可以防止线程运行太多. 加锁后可以让线程停止继续执行直到通知它开始
lll_lock (pd->lock, LLL_PRIVATE);
// 新增一个线程. 不能在线程中处理, 因为可能这个线程已经存在但是在返回时还没有调度到.
// 如果失败, 临时放一个错误的值; 这样做也没关系, 因为没有一个合适的信号处理机制在这里中断下来
// 去关注线程计数是否正确
// 这样说感觉太生硬, 原文的意思就是这个全局变量的线程计数在创建线程前就做了加1操作, 就是有短暂
// 的时间是错误的, 因为线程毕竟还没有创建出来. 但是这样做没有关系, 也没有一种合适的机制去处理这
// 种异常
atomic_increment (&__nptl_nthreads);
// 调用内核接口
// # define ARCH_CLONE __clone // 这是内核特定的函数了
int rc = ARCH_CLONE (fct, STACK_VARIABLES_ARGS, clone_flags, // fct = start_thread
pd, &pd->tid, tp, &pd->tid);
if (__glibc_unlikely (rc == -1))
{
// 如果线程创建失败,线程计数会有短暂的错误值,在这里才将计数减去
atomic_decrement (&__nptl_nthreads);
// 可能会有线程想要改变ID并且等待这个创建失败的线程
if (__builtin_expect (atomic_exchange_acq (&pd->setxid_futex, 0)
== -2, 0))
lll_futex_wake (&pd->setxid_futex, 1, LLL_PRIVATE);
__deallocate_stack (pd);
return errno == ENOMEM ? EAGAIN : errno;
}
// 设置调度参数
if (__glibc_unlikely (stopped != 0))
{
INTERNAL_SYSCALL_DECL (err);
int res = 0;
// 设置affinity(亲密关系)
if (attr->cpuset != NULL)
{
res = INTERNAL_SYSCALL (sched_setaffinity, err, 3, pd->tid,
attr->cpusetsize, attr->cpuset);
if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (res, err)))
{
// 设置失败. 杀掉线程,首先发送取消信号
INTERNAL_SYSCALL_DECL (err2);
err_out:
(void) INTERNAL_SYSCALL (tgkill, err2, 3,
THREAD_GETMEM (THREAD_SELF, pid),
pd->tid, SIGCANCEL);
// NOTE: 这里没有释放线程栈,放到了取消线程中处理了
return (INTERNAL_SYSCALL_ERROR_P (res, err)
? INTERNAL_SYSCALL_ERRNO (res, err)
: 0);
}
}
// 设置调度参数
// ATTR_FLAG_NOTINHERITSCHED: 不知道这个标志的具体含义
// 从字面意思理解,就是不继承父线程的调度参数
if ((attr->flags & ATTR_FLAG_NOTINHERITSCHED) != 0)
{
res = INTERNAL_SYSCALL (sched_setscheduler, err, 3, pd->tid,
pd->schedpolicy, &pd->schedparam);
if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (res, err)))
goto err_out;
}
}
// 这里的时候已经确定确实多了一个线程了.
// 主线程中可能还没有这个标志. 全局变量已经不需要再次设置了(就是这个__nptl_nthreads)
THREAD_SETMEM (THREAD_SELF, header.multiple_threads, 1);
return 0;
}
// nptl/pthread_create.c
static int
start_thread (void *arg)
{
struct pthread *pd = (struct pthread *) arg;
#if HP_TIMING_AVAIL
// 记录线程启动时间
hp_timing_t now;
HP_TIMING_NOW (now);
THREAD_SETMEM (pd, cpuclock_offset, now);
#endif
// 初始化解析器状态指针
__resp = &pd->res;
// 初始化本地数据指针
__ctype_init ();
// 现在开始允许setxid
if (__glibc_unlikely (atomic_exchange_acq (&pd->setxid_futex, 0) == -2))
lll_futex_wake (&pd->setxid_futex, 1, LLL_PRIVATE);
#ifdef __NR_set_robust_list
# ifndef __ASSUME_SET_ROBUST_LIST
if (__set_robust_list_avail >= 0)
# endif
{
INTERNAL_SYSCALL_DECL (err);
// 这个函数永远不会失败(太强大了)
// 应该就是在一个链表中添加一个节点
INTERNAL_SYSCALL (set_robust_list, err, 2, &pd->robust_head,
sizeof (struct robust_list_head));
}
#endif
// 如果在创建线程的时候父线程正在执行取消的代码, 新线程就继承信号mask标识
// 重置取消信号mask标志位
if (__glibc_unlikely (pd->parent_cancelhandling & CANCELING_BITMASK))
{
INTERNAL_SYSCALL_DECL (err);
sigset_t mask;
__sigemptyset (&mask);
__sigaddset (&mask, SIGCANCEL);
(void) INTERNAL_SYSCALL (rt_sigprocmask, err, 4, SIG_UNBLOCK, &mask,
NULL, _NSIG / 8);
}
// 这是一个try/finally 代码块. 编译器没有这个功能就是用setjmp
struct pthread_unwind_buf unwind_buf;
unwind_buf.priv.data.prev = NULL;
unwind_buf.priv.data.cleanup = NULL;
// 这里是设置异常处理的代码,比如线程不是在函数中正常的return退出,而是pthread_exit中途
// 退出. 如果是中途退出的代码, setjmp返回的不是0,是longjmp设置的值(当然也可以设置为0,但
// 这样做没有意义)
int not_first_call;
not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);
if (__glibc_likely (! not_first_call)) // 第一次进入这个逻辑setjmp返回0
{
// 存储新清理处理器信息
THREAD_SETMEM (pd, cleanup_jmp_buf, &unwind_buf);
if (__glibc_unlikely (pd->stopped_start))
{
int oldtype = CANCEL_ASYNC ();
// 这里加锁又解锁,只是为了强制性同步一下
lll_lock (pd->lock, LLL_PRIVATE);
lll_unlock (pd->lock, LLL_PRIVATE);
CANCEL_RESET (oldtype);
}
// LIBC_PROBE, 按照代码中的宏定义,其实这句话什么也没有生成,不知道它的用意是什么
LIBC_PROBE (pthread_start, 3, (pthread_t) pd, pd->start_routine, pd->arg);
// 调用用户提供的函数
#ifdef CALL_THREAD_FCT
THREAD_SETMEM (pd, result, CALL_THREAD_FCT (pd));
#else
THREAD_SETMEM (pd, result, pd->start_routine (pd->arg));
#endif
}
// 一个线程结束后,就会执行下面的代码,清理一些资源
// 调用线程局部TLS变量的析构函数
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
// 这里只是调用所有的析构函数而已
// 根据glibc中的解释,这些析构函数都是C++编译器生成的TLS变量的析构函数
__call_tls_dtors ();
// 调用线程局部数据的析构函数
__nptl_deallocate_tsd ();
// 清理所有的线程局部变量的状态
// 调用这些函数
// arena_thread_freeres
// res_thread_freeres
// __rpc_thread_destroy
// strerror_thread_freeres
// 实现方式可以需要看GCC编译器中attribute section的说明
__libc_thread_freeres ();
// 判断是否是进程中最后一个线程. 不会通知调试器
if (__glibc_unlikely (atomic_decrement_and_test (&__nptl_nthreads)))
// 最后一个线程直接调用exit
exit (0);
// 汇报线程结束的事件
if (__glibc_unlikely (pd->report_events))
{
// 判断事件mask标志位中是否包含TD_DEATH
const int idx = __td_eventword (TD_DEATH);
const uint32_t mask = __td_eventmask (TD_DEATH);
if ((mask & (__nptl_threads_events.event_bits[idx]
| pd->eventbuf.eventmask.event_bits[idx])) != 0)
{
// 必须发送线程结束的信号. 将描述符添加到链表中
if (pd->nextevent == NULL)
{
pd->eventbuf.eventnum = TD_DEATH;
pd->eventbuf.eventdata = pd;
do
pd->nextevent = __nptl_last_event;
while (atomic_compare_and_exchange_bool_acq (&__nptl_last_event,
pd, pd->nextevent));
}
// 汇报事件
__nptl_death_event ();
}
}
// 线程已经退出. 在触发事件汇报断点前不能设置这个bit标记, 因此当断点汇报状态时,
// td_thr_get_info获取的状态是TD_THR_RUN而不是TD_THR_ZOMBIE
atomic_bit_set (&pd->cancelhandling, EXITING_BIT);
#ifndef __ASSUME_SET_ROBUST_LIST
// 如果线程加了什么robust的锁,就在这里处理掉
# ifdef __PTHREAD_MUTEX_HAVE_PREV
void *robust = pd->robust_head.list;
# else
__pthread_slist_t *robust = pd->robust_list.__next;
# endif
// 如果可以的话,让内核做这个通知.
// 这里做的事情就是确保不涉及PI mutex, 因为内核能更好的支持(文中说more recent)
if (__set_robust_list_avail < 0
&& __builtin_expect (robust != (void *) &pd->robust_head, 0))
{
do
{
struct __pthread_mutex_s *this = (struct __pthread_mutex_s *)
((char *) robust - offsetof (struct __pthread_mutex_s,
__list.__next));
robust = *((void ##) robust);
# ifdef __PTHREAD_MUTEX_HAVE_PREV
this->__list.__prev = NULL;
# endif
this->__list.__next = NULL;
atomic_or (&this->__lock, FUTEX_OWNER_DIED);
lll_futex_wake (this->__lock, 1, /* XYZ */ LLL_SHARED);
}
while (robust != (void *) &pd->robust_head);
}
#endif
// 将栈的内存标记为对内核可用. 除了TCB, 释放所有的空间
size_t pagesize_m1 = __getpagesize () - 1;
#ifdef _STACK_GROWS_DOWN
char *sp = CURRENT_STACK_FRAME;
size_t freesize = (sp - (char *) pd->stackblock) & ~pagesize_m1;
#else // 这里就是说只能定义栈向下增长? 之前的UP又有什么意义?!还是我下载的代码版本不正确
# error "to do"
#endif
assert (freesize < pd->stackblock_size);
if (freesize > PTHREAD_STACK_MIN)
__madvise (pd->stackblock, freesize - PTHREAD_STACK_MIN, MADV_DONTNEED);
// 如果线程是detached状态,就释放TCB
if (IS_DETACHED (pd))
__free_tcb (pd);
else if (__glibc_unlikely (pd->cancelhandling & SETXID_BITMASK))
{
// 其它的线程可能调用了setXid相关的函数, 并且期望得到回复.
// 这样的话, 就必须一致等待一直到这里做这样的处理
do
lll_futex_wait (&pd->setxid_futex, 0, LLL_PRIVATE);
while (pd->cancelhandling & SETXID_BITMASK);
// 重置setxid_futex, stack就可以再次使用
pd->setxid_futex = 0;
}
// 不能调用'_exit'. '_exit'会终止进程.
// 因为'clone'函数设置了参数CLONE_CHILD_CLEARTID标志位, 在进程真正的结束后,
// 内核中实现的'exit'会发送信号.
// TCB中的'tid'字段会设置成0
// 退出代码是0,以防所有线程调用'pthread_exit'退出
__exit_thread ();
return 0;
}
线程中调用这个函数时,即使没有return也会直接退出。
void
__pthread_exit (value)
void *value;
{
THREAD_SETMEM (THREAD_SELF, result, value);
__do_cancel ();
}
其中的__do_cancel是一个隐藏很深的函数
// nptl/pthreadP.h
// 当一个线程执行取消请求时调用
static inline void
__attribute ((noreturn, always_inline))
__do_cancel (void)
{
struct pthread *self = THREAD_SELF;
// 确保不会取消多次
THREAD_ATOMIC_BIT_SET (self, cancelhandling, EXITING_BIT);
// cleanup_jmp_buf这个变量的值是在start_thread中使用setjmp调用的
// 这里调用clean_up相关的函数,然后做longjmp
__pthread_unwind ((__pthread_unwind_buf_t *)
THREAD_GETMEM (self, cleanup_jmp_buf));
}
再来看__pthread_unwind。
// nptl/unwind.c
void
__cleanup_fct_attribute __attribute ((noreturn))
__pthread_unwind (__pthread_unwind_buf_t *buf)
{
// 这个buf是在start_thread中设置的
struct pthread_unwind_buf *ibuf = (struct pthread_unwind_buf *) buf;
struct pthread *self = THREAD_SELF;
#ifdef HAVE_FORCED_UNWIND
// 也不是一个可以捕获的异常, 因此不用提供关于异常类型的任何信息.
// 但是需要初始化这些异常的字段
THREAD_SETMEM (self, exc.exception_class, 0);
THREAD_SETMEM (self, exc.exception_cleanup, &unwind_cleanup);
_Unwind_ForcedUnwind (&self->exc, unwind_stop, ibuf);
#else
// 首先处理兼容的字段. 执行所有注册的函数. 但是不是按照顺序执行的
struct _pthread_cleanup_buffer *oldp = ibuf->priv.data.cleanup;
struct _pthread_cleanup_buffer *curp = THREAD_GETMEM (self, cleanup);
if (curp != oldp)
{
do
{
// 遍历链表,执行所有的handler
struct _pthread_cleanup_buffer *nextp = curp->__prev;
curp->__routine (curp->__arg);
curp = nextp;
}
while (curp != oldp);
THREAD_SETMEM (self, cleanup, curp);
}
// 调用longjmp,跳转到setjmp注册的地方
__libc_unwind_longjmp ((struct __jmp_buf_tag *) ibuf->cancel_jmp_buf, 1);
#endif
// 正常情况下都不会到这里
abort ();
}
创建TLS相关的接口是_dl_allocate_tls。TLS牵涉范围很广,阅读创建线程相关的代码时,遇到了很多陌生的概念,我也不能一次弄清楚这么多,因为太发散,后面再找资料,专门阅读下TLS相关的代码,这里先简单看下,有个印象。
这里的代码主要功能是为线程分配DTV,复制每个模块的TLS部分的全局变量,并清零BSS数据段。
// elf/dl-tls.c
void *
internal_function
_dl_allocate_tls (void *mem)
{
return _dl_allocate_tls_init (mem == NULL
? _dl_allocate_tls_storage ()
: allocate_dtv (mem));
// 这里的mem不是NULL,因此参数就是allocate_dtv(mem)的返回值
}
DTV: Dynamic Thread Vector
在x86_64系统上, dtv_t的定义如下
/* Type for the dtv. */
typedef union dtv
{
size_t counter;
struct
{
void *val;
bool is_static;
} pointer;
} dtv_t;
static void *
internal_function
allocate_dtv (void *result)
{
dtv_t *dtv;
size_t dtv_length;
// 分配的比实际需要的DTV要多一点。可以避免大部分需要扩展DTV的情况。
// #define DTV_SURPLUS (14)
dtv_length = GL(dl_tls_max_dtv_idx) + DTV_SURPLUS;
dtv = calloc (dtv_length + 2, sizeof (dtv_t));
if (dtv != NULL)
{
dtv[0].counter = dtv_length; // dtv的第一个元素只是用来计数
// 剩下的DTV都初始化为0
INSTALL_DTV (result, dtv); // 将dtv设置到struct pthread中
# define INSTALL_DTV(descr, dtvp) \
((tcbhead_t *) (descr))->dtv = (dtvp) + 1 // 只是设置成员变量
}
else
result = NULL;
return result;
}
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]; // 这里是结构体中最后一个元素,可以随意扩展
};
void *
internal_function
_dl_allocate_tls_init (void *result)
{
if (result == NULL)
return NULL;
dtv_t *dtv = GET_DTV (result);
struct dtv_slotinfo_list *listp;
size_t total = 0;
size_t maxgen = 0;
// 为所有当前已经加载使用TLS的模块准备DTV
// 动态加载的模块,设置为延迟分配
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;
// 检查使用的slot总数
if (total + cnt > GL(dl_tls_max_dtv_idx)) // dl_tls_max_dtv_idx不知道这个是啥,而且不用加锁访问全局变量感觉很奇怪
break; // 难道这个数字是初始化后固定死的?
map = listp->slotinfo[cnt].map;
if (map == NULL)
continue;
// 跟踪最大的generation值. 这可能不是generation的个数
assert (listp->slotinfo[cnt].gen <= GL(dl_tls_generation));
maxgen = MAX (maxgen, listp->slotinfo[cnt].gen);
if (map->l_tls_offset == NO_TLS_OFFSET // # define NO_TLS_OFFSET 0
|| map->l_tls_offset == FORCED_DYNAMIC_TLS_OFFSET) // FORCED_DYNAMIC_TLS_OFFSET 小于0的数字
{
// 对于动态加载的模块,只是存储这个值表示延迟分配
dtv[map->l_tls_modid].pointer.val = TLS_DTV_UNALLOCATED;
dtv[map->l_tls_modid].pointer.is_static = false;
continue;
}
assert (map->l_tls_modid == cnt);
assert (map->l_tls_blocksize >= map->l_tls_initimage_size);
#if TLS_TCB_AT_TP
assert ((size_t) map->l_tls_offset >= map->l_tls_blocksize);
dest = (char *) result - map->l_tls_offset; // 为啥要弄这个偏移量呢?
#elif TLS_DTV_AT_TP
dest = (char *) result + map->l_tls_offset;
#else
# error "Either TLS_TCB_AT_TP or TLS_DTV_AT_TP must be defined"
#endif
// 复制初始化区域并清除BSS部分数据
// BSS:Block Started by Symbol segment, 存放程序中未初始化的全局变量
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;
assert (listp != NULL); // 这个list没有结尾吗?
}
dtv[0].counter = maxgen; // DTV的第一个元素只是一个计数
return result;
}
// nptl/pthread_create.c
// 释放POSIX TLS
void
attribute_hidden
__nptl_deallocate_tsd (void)
{
struct pthread *self = THREAD_SELF;
// 可能没有分配过数据.
// 这样的情况会经常出现,所以使用一个标识记录一下
if (THREAD_GETMEM (self, specific_used))
{
size_t round;
size_t cnt;
round = 0;
do
{
size_t idx;
// 到目前为止已经没有非零数据了
THREAD_SETMEM (self, specific_used, false);
// TLS数组:
// struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE]
// struct pthread_key_data *specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
// 并且specific[0] = specific_1stblock
for (cnt = idx = 0; cnt < PTHREAD_KEY_1STLEVEL_SIZE; ++cnt)
{
struct pthread_key_data *level2;
level2 = THREAD_GETMEM_NC (self, specific, cnt);
if (level2 != NULL)
{
size_t inner;
for (inner = 0; inner < PTHREAD_KEY_2NDLEVEL_SIZE;
++inner, ++idx)
{
void *data = level2[inner].data;
if (data != NULL)
{
level2[inner].data = NULL;
// 确保这里的数据包含一个正确的key
// 如果这个key已经被释放了,并且是重新分配的,这个检测就会失败
// 这种情况下就由用户来确保内存的释放
// 这里的代码与pthread_key_create等函数相关,暂时不做理解,只是留个印象
if (level2[inner].seq
== __pthread_keys[idx].seq
&& __pthread_keys[idx].destr != NULL)
// 这个析构函数并不是一定要提供的,比如只是一个int数字的时候
__pthread_keys[idx].destr (data);
}
}
}
else
idx += PTHREAD_KEY_1STLEVEL_SIZE;
}
if (THREAD_GETMEM (self, specific_used) == 0)
goto just_free;
}
// PTHREAD_DESTRUCTOR_ITERATIONS 4
// 这里为啥做个循环?不能一次性清理掉数据?线程都退出了,为什么还要多次遍历释放
// 其实在goto just_free那里,就已经退出了,因为这个循环开始就设置了specific_used = false
while (__builtin_expect (++round < PTHREAD_DESTRUCTOR_ITERATIONS, 0));
// 清空第一个数据库,就是specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE],固定存放的这个数组
// 按说也不用做这个处理,因为这是线程退出,这个线程已经不会再用这个内存了
// 新线程再创建的时候,也会将这块内存清零
memset (&THREAD_SELF->specific_1stblock, '\0',
sizeof (self->specific_1stblock));
just_free:
// specific中除了第一个元素不用释放,其它的都要释放掉
for (cnt = 1; cnt < PTHREAD_KEY_1STLEVEL_SIZE; ++cnt)
{
struct pthread_key_data *level2;
level2 = THREAD_GETMEM_NC (self, specific, cnt);
if (level2 != NULL)
{
free (level2);
THREAD_SETMEM_NC (self, specific, cnt, NULL);
}
}
THREAD_SETMEM (self, specific_used, false); // 设置了一次又一次,怕它跑了?
}
}