st(state-threads) https://github.com/winlinvip/state-threads
以及基于st的RTMP/HLS服务器:https://github.com/winlinvip/simple-rtmp-server
st是实现了coroutine的一套机制,即用户态线程,或者叫做协程。将epoll(async,nonblocking socket)的非阻塞变成协程的方式,将所有状态空间都放到stack中,避免异步的大循环和状态空间的判断。
关于st的详细介绍,参考翻译:http://blog.csdn.net/win_lin/article/details/8242653
我将st进行了简化,去掉了其他系统,只考虑linux系统,以及i386/x86_64/arm/mips四种cpu系列,参考:https://github.com/winlinvip/simple-rtmp-server/tree/master/trunk/research/st
本文介绍了coroutine的创建和stack的管理。
STACK分配
Stack数据结构定义为:
- typedef struct _st_stack {
- _st_clist_t links;
- char *vaddr; /* Base of stack's allocated memory */
- int vaddr_size; /* Size of stack's allocated memory */
- int stk_size; /* Size of usable portion of the stack */
- char *stk_bottom; /* Lowest address of stack's usable portion */
- char *stk_top; /* Highest address of stack's usable portion */
- void *sp; /* Stack pointer from C's point of view */
- } _st_stack_t;
实际上vaddr是栈的内存开始地址,其他几个地址下面分析。
栈的分配是在_st_stack_new函数,在st_thread_create函数调用,先计算stack的尺寸,然后分配栈。
- | REDZONE | stack | extra | REDZONE |
- +---------+------------------------+---------+---------+
- | 4k | | 4k/0 | 4k |
- +---------+------------------------+---------+---------+
- vaddr bottom top
上图是栈分配后的结果,两边是REDZONE使用mprotect保护不被访问(在DEBUG开启后),extra是一个额外的内存块,st_randomize_stacks开启后会调整bottom和top,就是随机的向右边移动一点。
总之,最后使用的,对外提供的接口就是bottom和top,st_thread_create函数会初始化sp。stack对外提供的服务就是[bottom, top]这个内存区域。
THREAD初始化栈
开辟Stack后,st会对stack初始化和分配,这个stack并非直接就是thread的栈,而是做了以下分配:
- +--------------------------------------------------------------+
- | stack |
- +--------------------------------------------------------------+
- bottom top
分配如下:
- +-----------------+-----------------+-------------+------------+
- | stack of thread |pad+align(128B+) |thread(336B) | keys(128B) |
- +-----------------+-----------------+-------------+------------+
- bottom sp trd ptds top
- (context[0].__jmpbuf.sp) (private_data)
也就是说:
ptds:这个是thread的private_data,是12个指针(ST_KEYS_MAX指定),参考st_key_create()。
trd:thread结构本身也是在这个stack中分配的。
pad+align:在trd之后是对齐和pad(_ST_STACK_PAD_SIZE指定)。
sp:这个就是thread真正的stack了。
coroutine必须要自己分配stack,因为setjmp保存的只是sp的值,而没有全部copy栈,所以若使用系统的stack,各个thread之间longjmp时会导致栈混淆。参考:http://blog.csdn.net/win_lin/article/details/40948277
Thread启动和切换
st的thread如何进入到指定的入口呢?
其实在第一次setjmp时,是初始化thread,这时候返回值是0,初始化完后就返回到调用函数继续执行了。
调用函数会在其他地方调用longjmp到这个thread,这时候是从setjmp地方开始执行,返回值是非0,这时进入thread的主函数:_st_thread_main。
参考我改过的代码:
- _st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinable, int stk_size)
- {
- // by winlin, expend macro MD_INIT_CONTEXT
- #if defined(__mips__)
- MD_SETJMP((trd)->context);
- trd->context[0].__jmpbuf[0].__pc = (__ptr_t) _st_thread_main;
- trd->context[0].__jmpbuf[0].__sp = stack->sp;
- #else
- int ret_setjmp = 0;
- if ((ret_setjmp = MD_SETJMP((trd)->context)) != 0) {
- _st_thread_main();
- }
- MD_GET_SP(trd) = (long) (stack->sp);
- #endif
- }
gdb调试,第一次setjmp时,返回值是0,调用堆栈是创建线程的堆栈,62行的代码是st_thread_t trd = st_thread_create(thread_func, NULL, 1, 0);:
- (gdb) f
- #0 st_thread_create (start=0x4073fb <thread_func>, arg=0x0, joinable=1, stk_size=65536) at sched.c:600
- 600 if ((ret_setjmp = MD_SETJMP((trd)->context)) != 0) {
- (gdb) bt
- #0 st_thread_create (start=0x4073fb <thread_func>, arg=0x0, joinable=1, stk_size=65536) at sched.c:600
- #1 0x00000000004074b5 in thread_test () at srs.c:62
- #2 0x00000000004081c3 in main (argc=1, argv=0x7fffffffe4b8) at srs.c:344
- (gdb) p ret_setjmp
- $36 = 0
从其他线程切换过来时,即longjmp过来时,返回值非0,调用堆栈是longjmp的堆栈,68行的代码是st_thread_join(trd, NULL);:
- (gdb) f
- #0 st_thread_create (start=0x4073fb <thread_func>, arg=0x6390b0, joinable=0, stk_size=6599392) at sched.c:601
- 601 _st_thread_main();
- (gdb) bt
- #0 st_thread_create (start=0x4073fb <thread_func>, arg=0x6390b0, joinable=0, stk_size=6599392) at sched.c:601
- #1 0x00000000004074f4 in thread_test () at srs.c:68
- #2 0x00000000004081c3 in main (argc=1, argv=0x7fffffffe4b8) at srs.c:344
- (gdb) p ret_setjmp
- $37 = 1
注意,虽然显示都是thread_test这个函数过来,实际上函数行数已经不一样了,gdb显示的stk_size也是破坏了的,因为这个时候的栈是用的st自己开辟的栈了。
进入到_st_thread_main中后,会调用用户指定的线程函数(这个函数里面会调用st函数setjmp,下次longjmp是到这个位置了);从线程函数返回后,会调用st_thread_exit清理线程,然后切换到其他函数,直到完成最后一个函数就返回了。
- void _st_thread_main(void)
- {
- _st_thread_t *trd = _ST_CURRENT_THREAD();
- /* Run thread main */
- trd->retval = (*trd->start)(trd->arg);
- /* All done, time to go away */
- st_thread_exit(trd->retval);
- }
这个就是st的thread启动和调度的过程。
第一次创建线程和setjmp后,会设置sp,即设置stack。也就是说,这个函数的所有stack信息在longjmp之后都是未知的了,这就是所有st的thread结束后,必须longjmp到其他的线程,或者退出,不能直接return的原因(因为没法return了,顶级stack就是_st_thread_main)。
Thread退出
在st的thread中退出后,会切换到其他thread(st创建的线程stack是重新建立的,无法返回后继续执行)。
st创建的thread,结束后会调用st_thread_exit,参考_st_thread_main的定义,这个就是thread执行的主要流程。
st在初始化st_init时,会把当前的线程当作_ST_FL_PRIMORDIAL,也就是初始化线程,这个线程若调用exit,等待其他thread完成后,会直接exit。实际上是没有线程时会切换到idle线程:
- void _st_vp_schedule(void)
- {
- _st_thread_t *trd;
- if (_ST_RUNQ.next != &_ST_RUNQ) {
- /* Pull thread off of the run queue */
- trd = _ST_THREAD_PTR(_ST_RUNQ.next);
- _ST_DEL_RUNQ(trd);
- } else {
- /* If there are no threads to run, switch to the idle thread */
- trd = _st_this_vp.idle_thread;
- }
idle线程是在st_init时创建,也就是说st_init会创建一个idle线程(使用st_thread_create),以及直接创建一个_ST_FL_PRIMORDIAL线程(直接calloc)。idle线程的代码:
- void *_st_idle_thread_start(void *arg)
- {
- _st_thread_t *me = _ST_CURRENT_THREAD();
- while (_st_active_count > 0) {
- /* Idle vp till I/O is ready or the smallest timeout expired */
- _ST_VP_IDLE();
- /* Check sleep queue for expired threads */
- _st_vp_check_clock();
- me->state = _ST_ST_RUNNABLE;
- _ST_SWITCH_CONTEXT(me);
- }
- /* No more threads */
- exit(0);
- /* NOTREACHED */
- return NULL;
- }
所有线程完成时就exit。
Thread初始线程
st的初始线程,或者叫做物理线程,primordial线程,是调用st_init的那个线程。一般而言,调用st的程序都是单线程,所以这个初始线程也就是那个系统的唯一的一个线程。
所有st的线程都是调用st_create_thread创建的,使用st自己开辟的stack;除了一种初始线程,没有重新设置stack,这个就是初始线程(物理线程)。
参考st_init的代码:
- /*
- * Initialize primordial thread
- */
- trd = (_st_thread_t *) calloc(1, sizeof(_st_thread_t) +
- (ST_KEYS_MAX * sizeof(void *)));
- if (!trd) {
- return -1;
- }
- trd->private_data = (void **) (trd + 1);
- trd->state = _ST_ST_RUNNING;
- trd->flags = _ST_FL_PRIMORDIAL;
- _ST_SET_CURRENT_THREAD(trd);
- _st_active_count++;
在分配trd对象时,分配了_st_thread_t和keys两个对象,可以参考前面对于stack的使用。keys用来做private_data,所以后面初始化private_data时是指向下一个thread。
创建后设置这个线程为_ST_FL_PRIMORDIAL,这个就是用来指明stack是否是st自己分配的:
- void st_thread_exit(void *retval)
- {
- if (!(trd->flags & _ST_FL_PRIMORDIAL)) {
- _st_stack_free(trd->stack);
- }
- }
如果是初始线程(物理线程),那么stack是不释放的,这个stack是NULL。
在调度时,不管stack是否是自己创建的,对于调度都没有影响。stack如果是st自己创建的,只是在setjmp之后的context中修改sp的地址,这个时候longjmp会使用新的stack而已,对于longjmp的jmp_buf到底sp是自己创建的还是系统的,其实没有区别。
所以初始线程(物理线程)也是作为一个st的thread被调度,没有任何区别。
Thread生命周期
再整理下st整个线程的执行流程。
第一个阶段,st_init创建idle线程和创建priordial线程(初始线程,物理线程,_ST_FL_PRIMORDIAL),这时候_st_active_count是1,也就是初始线程(调用st_init,也是物理线程)在运行,idle线程不算一个active的线程,它主要是做切换和退出。
第二个阶段,可选的阶段,用户创建线程。调用st_thread_create时,会把_st_active_count递增,并且加入线程队列。譬如创建了一个线程;这时候st调度有两个线程,一个是初始线程,一个是刚刚创建的线程。
第三个阶段,初始线程切换,将控制权交给st。也就是初始线程,做完st_init和创建其他线程后,这个时候还没有任何的线程切换。初始线程(物理线程)需要将控制权切换给st,可以调用st_sleep循环和休眠,或者调用st_thread_exit(NULL)等待其他线程结束。假设这个阶段物理线程不进行切换,st将无法获取控制权,程序会直接返回。
这么设计其实很完善,如果物理线程不exit,那么st的idle线程也不退出(认为有个初始线程还在跑)。如果初始线程直接退出,那么idle线程不会拿到控制权。如果初始线程调用st_thread_exit(NULL),认为是物理线程也退出,那么idle会等所有线程完了再exit,相当于控制权交给st了。
或者说,可以在初始线程(物理线程)里面做各种的业务逻辑,譬如srs用初始线程更新各种数据,给api使用。或者可以直接创建线程后st_thread_exit,就等所有线程退出。
版权声明:本文为博主原创文章,未经博主允许不得转载。
出自:http://blog.csdn.net/win_lin/article/details/40978665