本篇主要讲解pjlib关于线程的实现方式
转载请注明出处:http://blog.csdn.net/lhl_blog/article/details/44063229
系统环境:
1. Ubuntu14.04 TLS 内核3.13.0-45-generi
2. gcc 4.8.2
3. glibc 2.19
开始之前需要讲解两个概念:
1.线程栈
参考nono的csdn博文http://blog.csdn.net/dog250/article/details/7704898,其对于linux的进程和线程栈进行了较为详细的介绍.个人认为比较重要的就是
linux glibc所实现的线程栈为不能动态增长的,这就很值得注意了,如果线程中定义使用比较大的数据结构就会导致线程栈溢出,而线程栈是在进程的堆中分
配的内存,从而城门失火,殃及池鱼,这样一旦线程栈溢出就会间接破坏进程地址空间,从而导致很难调试的bug(可能导致内存段错误).因而,有效的管理线程
栈在多线程编程中就成为了比较重要的问题.也许,有的人会说,我在进行多线程编程时,对于线程栈也没有进行过多干预啊,也没有出现过什么问题啊? 这种认识
其实是存在问题的, 正所谓欠下的债早晚是要还的,是时候未到.
至于表面上没有出现问题,主要原因如下:
1.*NIX系统在创建线程时,如果没有指定线程栈的大小,系统会创建一个默认大小的栈(8M),我们一般不会使用如此'大'的自动变量,但是如果线程定义了大量
的自动变量,例如,定义了char array[8*1024*1024]的数组,不出意外程序会出现段错误,示例代码如下:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* threadfunc(void* args)
{
//char c = '1';
//printf("c = %c\n", c);
char *str1 = (char *)malloc(8*1024*1024);
char *str2 = (char *)malloc(8*1024*1024);
char array[8*1024*1024 - 1024*10] = {0};
}
int main(void)
{
pthread_t pid;
size_t stack_size;
pthread_attr_t attr;
int rc = pthread_create(&pid, NULL, threadfunc, NULL);
if(rc != 0) {
printf("create thread failed\n");
}
pthread_attr_init(&attr);
pthread_attr_getstacksize(&attr, &stack_size);
stack_size /= 1024 * 1024;
printf("default thread stacksize = %dMB\n", stack_size);
pthread_join(pid, NULL);
return 0;
}
2.线程TLS
线程TLS(thread local store),线程私有数据是存储和查询与某个线程相关的数据的一种机制.把这种数据称为线程私有数据或线程特定数据的原因为, 希望每
个线程可以独立的访问数据副本,而不用担心与其他线程的同步访问问题. 典型的应用为每个线程拥有自己的errno值, 各个线程之间的errno值互不影响.这就像
身份证定义空间好比系统的进程空间,每个独立的人好比一个单独的线程,每个线程都有一个身份证号,这个身份证号就是我们的TLS,而且我们之间的身份证号
不会相互影响,当然这个比喻有些不恰当,理论上身份证号是一成不变的.
3.pjlib线程实现
3.1 安全的线程栈保护机制
struct pj_thread_t
{
char obj_name[PJ_MAX_OBJ_NAME];
pthread_t thread;//linux 线程id
pj_thread_proc *proc;//线程处理函数
void *arg;//线程处理函数参数
pj_uint32_t signature1;//标签1,具体作用未知
pj_uint32_t signature2;//标签2
pj_mutex_t *suspended_mutex;//模拟线程挂起动作
//如果启用了线程栈相关的检查处理,则定义下面的成员变量:
#if defined(PJ_OS_HAS_CHECK_STACK) && PJ_OS_HAS_CHECK_STACK!=0
pj_uint32_t stk_size;//线程栈大小j
pj_uint32_t stk_max_usage;//已经使用的线程栈大小
char *stk_start;//线程栈的起始地址
const char *caller_file;//用于记录调用线程栈检测函数的当前位置
int caller_line;
#endif
};
pjlib线程描述符明确定义了线程栈相关的元素, 通过这些元素, pjlib线程可以基本实现安全的线程栈使用.下面介绍pjlib线程栈的使用机制的实现方式.
3.1.1 线程栈初始化
pjlib的pj_thread_create实现pjlib线程的创建,其中函数的stack_size为线程栈大小参数,下面的代码只保留pj_thread_create函数的参数和线程栈相关的内容:
/*
* pj_thread_create(...)
*/
PJ_DEF(pj_status_t) pj_thread_create( pj_pool_t *pool,
const char *thread_name,
pj_thread_proc *proc,
void *arg,
pj_size_t stack_size,
unsigned flags,
pj_thread_t **ptr_thread)
{
...
#if PJ_HAS_THREADS
PJ_CHECK_STACK();
/* Set default stack size */
if (stack_size == 0)
stack_size = PJ_THREAD_DEFAULT_STACK_SIZE;
#if defined(PJ_OS_HAS_CHECK_STACK) && PJ_OS_HAS_CHECK_STACK!=0
rec->stk_size = stack_size;
rec->stk_max_usage = 0;
#endif
//第一种方式:
#if defined(PJ_THREAD_SET_STACK_SIZE) && PJ_THREAD_SET_STACK_SIZE!=0
/* Set thread's stack size */
rc = pthread_attr_setstacksize(&thread_attr, stack_size);
if (rc != 0)
return PJ_RETURN_OS_ERROR(rc);
#endif
/* PJ_THREAD_SET_STACK_SIZE */
//第二种方式:
#if defined(PJ_THREAD_ALLOCATE_STACK) && PJ_THREAD_ALLOCATE_STACK!=0
/* Allocate memory for the stack */
stack_addr = pj_pool_alloc(pool, stack_size);
PJ_ASSERT_RETURN(stack_addr, PJ_ENOMEM);
rc = pthread_attr_setstackaddr(&thread_attr, stack_addr);
if (rc != 0)
return PJ_RETURN_OS_ERROR(rc);
#endif
/* PJ_THREAD_ALLOCATE_STACK */
...
/* Create the thread. */
rec->proc = proc;
rec->arg = arg;
rc = pthread_create( &rec->thread, &thread_attr, &thread_main, rec);
if (rc != 0) {
return PJ_RETURN_OS_ERROR(rc);
}
...
}
static void *thread_main(void *param)
{
pj_thread_t *rec = (pj_thread_t*)param;
void *result;
pj_status_t rc;
#if defined(PJ_OS_HAS_CHECK_STACK) && PJ_OS_HAS_CHECK_STACK!=0
rec->stk_start = (char*)&rec;
#endif
....
}
pjlib支持两种线程栈创建方式,1.使用系统默认线程栈大小直接创建. 2.动态申请堆内存,然后将堆内存地址作为线程栈的首地址.创建完线程栈之后,pjlib在
thread_main中对线程栈的起始地址进行了初始化.
3.1.2 线程栈保护机制
pjlib线程通过在每个线程函数中调用PJ_CHECK_STACK()宏实现线程栈的保护机制,线程函数执行执行之前首先通过PJ_CHECK_STACK()监测当前线程栈状态,如果
线程栈空间的使用率达到规定的警界值就会立即触发assert断言提示线程STACK OVERFLOW!,并退出.反之,PJ_CHECK_STACK()会重新更新当前线程栈的使用率.下
面为PJ_CHECK_STACK()(pj_thread_check_stack)的实现:
/*
* pj_thread_check_stack()
* Implementation for PJ_CHECK_STACK()
*/
PJ_DEF(void) pj_thread_check_stack(const char *file, int line)
{
char stk_ptr;
pj_uint32_t usage;
pj_thread_t *thread = pj_thread_this();
/*计算当前线程栈的使用率*/
usage = (&stk_ptr > thread->stk_start) ? &stk_ptr - thread->stk_start :
thread->stk_start - &stk_ptr;
/*如果线程栈的使用率超过警界值,立即触发断言*/
pj_assert("STACK OVERFLOW!! " && (usage <= thread->stk_size - 128));
/*重新统计线程栈使用率*/
if (usage > thread->stk_max_usage) {
thread->stk_max_usage = usage;
thread->caller_file = file;
thread->caller_line = line;
}
}
PJ_CHECK_STACK()一般在线程函数定义完局部变量时调用,这样就可以对stack overflow提前预警,从而减少大量的调试时间.
3.2 pjlib TLS
pjlib线程基于TLS机制实现本地线程描述符的存储,所有pjlib线程或外部线程(linux原生线程)都必须使用pj_thread_register将自己的描述符存储到TLS中,
当调用pjlib库函数时,库函数一般都会检测本地线程是否注册过,即是否调用过pthread_setspecific()注册线程TLS,否则pjlib库函数不能被正常使用,只有
这样才能光明正大的使用pjlib函数.
pjlib这样做的理由可能为:
1.每个pjlib线程都有一个线程描述符pj_thread_t,每个描述符中保存的内容各不相同,我们可以通过线程TLS将不同线程的描述符实现统一管理,就像errno.
2.方便线程调试,通过TLS可以方便的获取本地线程相关的所有信息,我们通过打印或者在线调试都可以方便的跟踪当前调用函数的线程状态.
3. ... ...
3.3 pjlib线程调度策略
pjlib线程封装了linux系统提供的几种线程调度策略,SCHED_FIFO,SCHED_RR,SCHED_OTHER,没有特别的设置.