本文分析在Linux应用程序中错误使用pthread_mutex锁时会概率性触发SIG_ABRT信号而导致程序崩溃(库打印输出 :Assertion `mutex->__data.__owner == 0' failed)的原因。
程序环境如下:(1)Glibc-2.15 (2)Linux-4.1.12 (3)树莓派1b
首先给出出错的示例程序:
#include
#include
#include "pthread.h"
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void * process(void * arg)
{
fprintf(stderr, "Starting process %s\n", (char *) arg);
while (1) {
/* 加锁等待某些资源 */
pthread_mutex_lock(&lock);
fprintf(stderr, "Process %s lock mutex\n", (char *) arg);
/* 加锁成功表示资源就绪 */
usleep(1000);
/* do something */
}
return NULL;
}
int main(void)
{
pthread_t th_a, th_b;
int ret = 0;
ret = pthread_create(&th_a, NULL, process, "a");
if (ret != 0) fprintf(stderr, "create a failed %d\n", ret);
ret = pthread_create(&th_b, NULL, process, "b");
if (ret != 0) fprintf(stderr, "create b failed %d\n", ret);
while (1) {
/* 等待并检测某些资源就绪 */
/* something */
/* 解锁告知线程资源就绪 */
pthread_mutex_unlock(&lock);
fprintf(stderr, "Main Process unlock mutex\n");
}
return 0;
}
本示例程序中,main函数首先创建两个线程,然后主线程等待某些资源就绪(伪代码,程序中未体现),待就绪后解锁mutex lock以告知子线程可以执行相应的处理(在解锁后打印输出解锁成功),不断循环;创建出的两个线程均调用process函数,该函数会尝试加锁mutex lock,加锁成功则表示资源就绪可以处理(打印输出加锁成功),否则在锁上等待,亦往复循环。本程序中对mutex锁的用法特殊,并不对临界资源进行保护,而是作为线程间”生产---消费“同步功能的一个简化示例,加锁以等待资源就绪,解锁以通知资源就绪,加锁和解锁的操作分别在不同的线程中执行。
运行该程序后不到10s时间程序就会出错退出,并且触发SIG_ABRT信号,终端打印输出如下:
......
Main Process unlock mutex程序在Glibc库中的pthread_mutex_lock.c的第62行__pthread_mutex_unlock()函数中出错,程序ABRT退出。
下面先来分析对应的源码,首先是加锁流程:
加锁函数源码:
int
__pthread_mutex_lock (mutex)
pthread_mutex_t *mutex;
{
assert (sizeof (mutex->__size) >= sizeof (mutex->__data));
unsigned int type = PTHREAD_MUTEX_TYPE (mutex);
if (__builtin_expect (type & ~PTHREAD_MUTEX_KIND_MASK_NP, 0))
return __pthread_mutex_lock_full (mutex);
pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP)
== PTHREAD_MUTEX_TIMED_NP) //1---判断锁类型
{
simple:
/* Normal mutex. */
LLL_MUTEX_LOCK (mutex); //2---加锁(原子操作)
assert (mutex->__data.__owner == 0); //3---Owner判断
}
...
/* Record the ownership. */
mutex->__data.__owner = id; //4---Owner赋值
#ifndef NO_INCR
++mutex->__data.__nusers;
#endif
return 0;
}
加锁函数的主要4步操作已经列出,首先会判断锁的类型,这里仅对PTHREAD_MUTEX_TIMED_NP类型的锁做出分析,该该类型的锁为默认的锁类型,当一个线程加锁后其余请求锁的线程会排入一个等待队列,并在锁解锁后按优先级获得锁。然后程序调用LLT_MUTEX_LOCK()宏执行底层加锁动作,这个加锁流程是原子的且不同的架构实现并不相同,然后会判断是否已经有线程获取了该锁(因为PTHREAD_MUTEX_TIMED_NP类型的锁是不允许嵌套加锁的),若已经有线程获取了锁则出错退出(示例程序中就是在此出错的),在函数的最后会把当前获得锁的线程号赋给__owner字段(线程与锁绑定)就结束了,此时当前线程进入临界区,其他对锁请求的线程将阻塞。下面来看一下解锁流程:
解锁函数源码:
int
internal_function attribute_hidden
__pthread_mutex_unlock_usercnt (mutex, decr)
pthread_mutex_t *mutex;
int decr;
{
int type = PTHREAD_MUTEX_TYPE (mutex);
if (__builtin_expect (type & ~PTHREAD_MUTEX_KIND_MASK_NP, 0))
return __pthread_mutex_unlock_full (mutex, decr);
if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP) //1---判断锁类型
== PTHREAD_MUTEX_TIMED_NP)
{
/* Always reset the owner field. */
normal:
mutex->__data.__owner = 0; //2---Owner解除
if (decr)
/* One less user. */
--mutex->__data.__nusers;
/* Unlock. */
lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex)); //3---原子解锁
return 0;
}
...
}
解锁函数的3步主要操作如上,首先依旧是判断锁类型,然后解除锁和线程的绑定关系,最后就调用lll_unlock()函数原子的解锁,此时若有加锁线程需要获取锁,相应线程会从
LLT_MUTEX_LOCK()函数返回继续执行。
以上就是调用pthread mutex函数加解锁函数的主要流程,其中需要关注的一点就是这两个函数的执行并不是原子的,是可能存在上下文切换动作的。在通常的用法中,我们加锁操作一般都是为了保护临界资源不被重入改写,一半都是严格按照“加锁-->写入/读取临界资源-->解锁”的流程执行(由加锁的线程负责解锁),而从前文中分析的__pthread_mutex_lock()和__pthread_mutex_unlock_usercnt()函数中也可以看到,只有在原子加锁期间才会改变这__owner值(该值也可认为是临界资源的一部分而被保护起来了),因此是不可能出现加锁已经加锁的线程的,所以也不会调用assert()函数而退出程序的。
但是本程序中对锁的用法显然并不这么“一般”,而是作为一种线程间的同步功能使用。其中主进程中不停的解锁,即是线程A和B没有加锁也同样如此,而线程A和B会竞争的每隔一定时间去加锁,那么就有可能出现如下图中所示的一种情况:1、
该图中主进程待资源就绪后正在解锁一个未被加锁的mutex_lock时发成了线程切换,线程A打断解锁流程完成了一整个加锁的流程,随后线程又且换回了主进程继续执行真正的解锁操作,这样线程A所加的锁就被莫名其妙的解掉了(关键的一点),此时若线程B在等待该锁,则会进入到加锁流程,从而在加锁成功后崩溃在这个__owner判断上。其实该程序出错的主要原因即是解了并未加锁的mutex_lock,如若主进程解得锁是已经上了锁的,则线程A是没有机会加锁的,主进程会原子的完成整个mutex_unlock动作。
另外,其实可以适当的调整程序再来看一下另外一种可能的情形(两个执行流),同样是“线程间同步”用法:2、
这种情况就是在资源就绪较慢且资源处理较快的情况容易出现崩溃,同样是概率性出现的。最后来看第三种可能的情况:3、
这种情况崩溃出现在线程A加锁的过程中被主进程解锁,然后线程A或其他线程又一次加锁的时候。其实不论上述哪一种同步的情况,其出错的原因有两点:(1)解了未被上锁的锁;(2)A线程加的锁由其他线程去解,进一步分析就是没有严格按照“加锁-->解锁”的流程使用mutex锁。
最后对于以上这种“线程间同步”的使用方法可以使用条件变量或者是信号量实现而不要使用mutex锁,mutex锁一般被用在保护线程间临界资源的情况下。
总结:
1、不要去解锁一个未被加锁的mutex锁;
2、不要一个线程中加锁而在另一个线程中解锁;
3、使用mutex锁用于保护临界资源,严格按照“加锁-->写入/读取临界资源-->解锁”的流程执行,对于线程间同步的需求使用条件变量或信号量实现。