从pthread中获得tid及pthread_mutex_unlock本身用户态互斥
一、pthread结构中获取tid
这个问题是由于很多时候我们都是通过gettid来获得一个线程的tid,其实这个是一个非常简单的系统调用,但是即使它非常简单,我们还是要执行进行系统调用而引入的寄存器保存/恢复等操作。但是,在C库的pthread库的实现过程中,我们可以看到,用户态是肯定保存了一个线程的tid的,如果我们能够通过用户态这个一定会存在的pthread结构来获得这个tid,就可以避免大量的系统调用,从而为系统的效率提供方便。
1、C库实现方法
__pthread_create_2_0--->>>__pthread_create_2_1--->>>create_thread
我们可以看到,在通过clone系统调用来创建子线程的时候,创建的参数中传递了CLONE_PARENT_SETTID的标志,也就是让内核在创建子进程的时候将新创建的子线程的线程id放置到指定的用户态地址中。
CLONE_PARENT_SETTID
The kernels writes the thread ID of the newly created thread
into the location pointed to by the fifth parameters to CLONE.
Note that it would be semantically equivalent to use
CLONE_CHILD_SETTID but it is be more expensive in the kernel.
if (ARCH_CLONE (fct, STACK_VARIABLES_ARGS, clone_flags,
pd, &pd->tid, TLS_VALUE, &pd->tid) == -1)
所以用户态是保存了一个线程自己的tid在自己的pthread结构中,所以我们就可以从这个位置获得线程的tid。
struct pthread
{
union
{
#if !TLS_DTV_AT_TP
/* This overlaps the TCB as used for TLS without threads (see tls.h). */
tcbhead_t header;
#else
struct
{
int multiple_threads;
int gscope_flag;
# ifndef __ASSUME_PRIVATE_FUTEX
int private_futex;
# endif
} header;
#endif
/* This extra padding has no special purpose, and this structure layout
is private and subject to change without affecting the official ABI.
We just have it here in case it might be convenient for some
implementation-specific instrumentation hack or suchlike. */
void *__padding[16];虽然这个结构是私有的,但是这里定义了一个强制的对其,也就是声明了一个union类型,这个类型中定义了一个最大的16字节指针,可以看到,这个是一个比较大的结构。我们可以认为它始终是这个结构中最大的一个成员,所以整个开头的这个结构认为是16个整数。当然,作者也说了,这个结构是私有的,并且以后可能还会改变,所以大家不要依赖,但是也可以供一些特殊的应用使用。看来作者当时也是很纠结的说。这就是“犹抱琵琶半遮面”。
};
/* This descriptor's link on the `stack_used' or `__stack_user' list. */
list_t list;
/* Thread ID - which is also a 'is this thread descriptor (and
therefore stack) used' flag. */
pid_t tid;这里就是我们说的那个tid结构,加上list_t中的两个指针,所以这个结构应该是pthread的基地址偏移 4 * sizeof(void*)个字节的位置处。
/* Process ID - thread group ID in kernel speak. */
pid_t pid;
2、pthread的基地址
还好,从代码中看到,当我们通过pthread_create创建一个线程的时候,C库无私的把这个结构给倒出来了。它就保存在pthread_create的第一个参数中,注意,不是返回值。所以我们就不用客气,直接使用这个就行了。有代码为证
int
__pthread_create_2_1 (newthread, attr, start_routine, arg)
pthread_t *newthread;
const pthread_attr_t *attr;
void *(*start_routine) (void *);
void *arg;
/* Pass the descriptor to the caller. */
*newthread = (pthread_t) pd;
3、这个tid内核何时填入
如果clone执行之后,子进程一致没有被调度到,那么这个值还有效吗?或者说,是不是这个值在pthread_create之后就直接可用呢?
我们看一下内核代码的实现:
do_fork--->>>copy_process
if (clone_flags & CLONE_PARENT_SETTID)
if (put_user(p->pid, parent_tidptr))
goto bad_fork_cleanup_delays_binfmt;
从这里可以看到,这里执行的代码还是在父进程的上下文中,它不依赖子进程的调度和执行,所以当pthread_create返回的时候,用户态pthread结构的tid已经是可用的了,不依赖新创建的线程是否被调度。
二、pthread_mutex_lock及pthread_mutex_unlock本身数据结构用户态互斥问题
1、问题的引出
因为futex机制是为了尽量避免进入内核,这样就需要在用户态实现用户态本身的互斥,那么它是怎么在用户态保证pthread_mutex结构本身的数据结构在多线程之间是如何共享的和互斥的呢?
我们看glibc-2.7\nptl\pthread_mutex_unlock.c本身的代码:
int
internal_function attribute_hidden
__pthread_mutex_unlock_usercnt (mutex, decr)
pthread_mutex_t *mutex;
int decr;
{
int newowner = 0;
switch (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex),
PTHREAD_MUTEX_TIMED_NP))
{
case PTHREAD_MUTEX_RECURSIVE_NP:
/* Recursive mutex. */
if (mutex->__data.__owner != THREAD_GETMEM (THREAD_SELF, tid))
return EPERM;
if (--mutex->__data.__count != 0)
/* We still hold the mutex. */
return 0;
goto normal;
case PTHREAD_MUTEX_ERRORCHECK_NP:
/* Error checking mutex. */
if (mutex->__data.__owner != THREAD_GETMEM (THREAD_SELF, tid)
|| ! lll_islocked (mutex->__data.__lock))
return EPERM;
/* FALLTHROUGH */
case PTHREAD_MUTEX_TIMED_NP:
case PTHREAD_MUTEX_ADAPTIVE_NP:
/* Always reset the owner field. */
normal:
mutex->__data.__owner = 0;
if (decr)
/* One less user. */
--mutex->__data.__nusers;这里没有使用任何机制就大摇大摆的对pthread结构本身的内容进行操作,而这个是一个典型的费多线程安全代码,它涉及到 内存读+ 寄存器递减+内存写 三个步骤(若干条指令),如果在这之间切换,一定会造成这个结构数值的不一致。
/* Unlock. */
lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex));
break;
2、原因分析
①pthread_mutex_unlock无需互斥的原因
对于pthread_mutex_unlock来说,由于这个mutex本身是一个互斥锁,我们在这里假设只能有一个线程能够通过pthread_mutex_lock执行到这里,所以在unlock执行这段代码的时候,它可以认为他是万里两天一棵苗,它就是可以大大咧咧的操作这个数据,不用担心被其它线程抢占,丫包场了。
②pthread_mutex_lock的原因
这样问题就被推给了pthread_mutex_lock,虽然它表示压力很大。glibc-2.7\nptl\pthread_mutex_lock.c
int
__pthread_mutex_lock (mutex)
pthread_mutex_t *mutex;
{
assert (sizeof (mutex->__size) >= sizeof (mutex->__data));
int oldval;
pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
int retval = 0;
switch (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex),
PTHREAD_MUTEX_TIMED_NP))
{
/* Recursive mutex. */
case PTHREAD_MUTEX_RECURSIVE_NP:
/* Check whether we already hold the mutex. */
if (mutex->__data.__owner == id)
{
/* Just bump the counter. */
if (__builtin_expect (mutex->__data.__count + 1 == 0, 0))
/* Overflow of the counter. */
return EAGAIN;
++mutex->__data.__count;
return 0;
}
/* We have to get the mutex. */
LLL_MUTEX_LOCK (mutex);可以看到,这个兄弟成为了数据保护的最后一道防线了。只要通过这个检测,线程就算安全了,可以认为是偷渡的最后一道门槛,过了这个,就到米国了。这就涉及到了那个经典的“用户态原子操作”了,这个问题在 ulrich dreppler的文章中有论述,这个依赖于体系结构,例如386下的cmpx本身是多CPU原子操作,所以可以实现原子性操作,而对于PowerPC和MIPS这类RISC机型,人家也有自己的玩法,就是lwarx和swarx这个姐妹花,从而可以完成用户态原子操作。
assert (mutex->__data.__owner == 0);
mutex->__data.__count = 1;
break;
/* Error checking mutex. */
case PTHREAD_MUTEX_ERRORCHECK_NP:
/* Check whether we already hold the mutex. */
if (__builtin_expect (mutex->__data.__owner == id, 0))
return EDEADLK;
/* FALLTHROUGH */
case PTHREAD_MUTEX_TIMED_NP:
simple:
/* Normal mutex. */
LLL_MUTEX_LOCK (mutex);
assert (mutex->__data.__owner == 0);
break;
③ 一些处理器用户态原子操作的实现
为了本着“撞死南墙”的精神和大家的偷窥欲望,我们再看看这个LLL_MUTEX_LOCK的不同实现
#define LLL_MUTEX_LOCK(mutex) \
lll_cond_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex))
看i386的实现
/* Special version of lll_lock which causes the unlock function to
always wakeup waiters. */
#define lll_cond_lock(futex, private) \
(void) \
({ int ignore1, ignore2, ignore3; \
__asm __volatile (LOCK_INSTR "cmpxchgl %1, %2\n\t" \当头就是这个苦B的cmpxchg,从而完成用户态原子性比较替换操作。
"jnz _L_cond_lock_%=\n\t" \
".subsection 1\n\t" \
".type _L_cond_lock_%=,@function\n" \
"_L_cond_lock_%=:\n" \
"1:\tleal %2, %%edx\n" \
"0:\tmovl %7, %%ecx\n" \
"2:\tcall __lll_lock_wait\n" \
"3:\tjmp 18f\n" \
"4:\t.size _L_cond_lock_%=, 4b-1b\n\t" \
".previous\n" \
LLL_STUB_UNWIND_INFO_4 \
"18:" \
: "=a" (ignore1), "=c" (ignore2), "=m" (futex), \
"=&d" (ignore3) \
: "0" (0), "1" (2), "m" (futex), "g" (private) \
: "memory"); \
})
powerPC也客串一下
#define __arch_compare_and_exchange_val_32_acq(mem, newval, oldval) \
({ \
__typeof (*(mem)) __tmp; \
__typeof (mem) __memp = (mem); \
__asm __volatile ( \
"1: lwarx %0,0,%1" MUTEX_HINT_ACQ "\n" \
" cmpw %0,%2\n" \
" bne 2f\n" \
" stwcx. %3,0,%1\n" \这两个难兄难弟,就是这么实现原子性操作滴。
" bne- 1b\n" \
"2: " __ARCH_ACQ_INSTR \
: "=&r" (__tmp) \
: "b" (__memp), "r" (oldval), "r" (newval) \
: "cr0", "memory"); \
__tmp; \
})