(转)st(state-threads) coroutine和stack分析

时间:2022-12-28 19:04:18

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数据结构定义为:

  1. typedef struct _st_stack {
  2. _st_clist_t links;
  3. char *vaddr;                /* Base of stack's allocated memory */
  4. int  vaddr_size;            /* Size of stack's allocated memory */
  5. int  stk_size;              /* Size of usable portion of the stack */
  6. char *stk_bottom;           /* Lowest address of stack's usable portion */
  7. char *stk_top;              /* Highest address of stack's usable portion */
  8. void *sp;                   /* Stack pointer from C's point of view */
  9. } _st_stack_t;

实际上vaddr是栈的内存开始地址,其他几个地址下面分析。

栈的分配是在_st_stack_new函数,在st_thread_create函数调用,先计算stack的尺寸,然后分配栈。

  1. | REDZONE |          stack         |  extra  | REDZONE |
  2. +---------+------------------------+---------+---------+
  3. |    4k   |                        |   4k/0  |    4k   |
  4. +---------+------------------------+---------+---------+
  5. 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的栈,而是做了以下分配:

  1. +--------------------------------------------------------------+
  2. |                         stack                                |
  3. +--------------------------------------------------------------+
  4. bottom                                                         top

分配如下:

  1. +-----------------+-----------------+-------------+------------+
  2. | stack of thread |pad+align(128B+) |thread(336B) | keys(128B) |
  3. +-----------------+-----------------+-------------+------------+
  4. bottom            sp                trd           ptds         top
  5. (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。

参考我改过的代码:

  1. _st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinable, int stk_size)
  2. {
  3. // by winlin, expend macro MD_INIT_CONTEXT
  4. #if defined(__mips__)
  5. MD_SETJMP((trd)->context);
  6. trd->context[0].__jmpbuf[0].__pc = (__ptr_t) _st_thread_main;
  7. trd->context[0].__jmpbuf[0].__sp = stack->sp;
  8. #else
  9. int ret_setjmp = 0;
  10. if ((ret_setjmp = MD_SETJMP((trd)->context)) != 0) {
  11. _st_thread_main();
  12. }
  13. MD_GET_SP(trd) = (long) (stack->sp);
  14. #endif
  15. }

gdb调试,第一次setjmp时,返回值是0,调用堆栈是创建线程的堆栈,62行的代码是st_thread_t trd = st_thread_create(thread_func, NULL, 1, 0);:

  1. (gdb) f
  2. #0  st_thread_create (start=0x4073fb <thread_func>, arg=0x0, joinable=1, stk_size=65536) at sched.c:600
  3. 600     if ((ret_setjmp = MD_SETJMP((trd)->context)) != 0) {
  4. (gdb) bt
  5. #0  st_thread_create (start=0x4073fb <thread_func>, arg=0x0, joinable=1, stk_size=65536) at sched.c:600
  6. #1  0x00000000004074b5 in thread_test () at srs.c:62
  7. #2  0x00000000004081c3 in main (argc=1, argv=0x7fffffffe4b8) at srs.c:344
  8. (gdb) p ret_setjmp
  9. $36 = 0

从其他线程切换过来时,即longjmp过来时,返回值非0,调用堆栈是longjmp的堆栈,68行的代码是st_thread_join(trd, NULL);:

  1. (gdb) f
  2. #0  st_thread_create (start=0x4073fb <thread_func>, arg=0x6390b0, joinable=0, stk_size=6599392) at sched.c:601
  3. 601         _st_thread_main();
  4. (gdb) bt
  5. #0  st_thread_create (start=0x4073fb <thread_func>, arg=0x6390b0, joinable=0, stk_size=6599392) at sched.c:601
  6. #1  0x00000000004074f4 in thread_test () at srs.c:68
  7. #2  0x00000000004081c3 in main (argc=1, argv=0x7fffffffe4b8) at srs.c:344
  8. (gdb) p ret_setjmp
  9. $37 = 1

注意,虽然显示都是thread_test这个函数过来,实际上函数行数已经不一样了,gdb显示的stk_size也是破坏了的,因为这个时候的栈是用的st自己开辟的栈了。

进入到_st_thread_main中后,会调用用户指定的线程函数(这个函数里面会调用st函数setjmp,下次longjmp是到这个位置了);从线程函数返回后,会调用st_thread_exit清理线程,然后切换到其他函数,直到完成最后一个函数就返回了。

  1. void _st_thread_main(void)
  2. {
  3. _st_thread_t *trd = _ST_CURRENT_THREAD();
  4. /* Run thread main */
  5. trd->retval = (*trd->start)(trd->arg);
  6. /* All done, time to go away */
  7. st_thread_exit(trd->retval);
  8. }

这个就是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线程:

  1. void _st_vp_schedule(void)
  2. {
  3. _st_thread_t *trd;
  4. if (_ST_RUNQ.next != &_ST_RUNQ) {
  5. /* Pull thread off of the run queue */
  6. trd = _ST_THREAD_PTR(_ST_RUNQ.next);
  7. _ST_DEL_RUNQ(trd);
  8. } else {
  9. /* If there are no threads to run, switch to the idle thread */
  10. trd = _st_this_vp.idle_thread;
  11. }

idle线程是在st_init时创建,也就是说st_init会创建一个idle线程(使用st_thread_create),以及直接创建一个_ST_FL_PRIMORDIAL线程(直接calloc)。idle线程的代码:

  1. void *_st_idle_thread_start(void *arg)
  2. {
  3. _st_thread_t *me = _ST_CURRENT_THREAD();
  4. while (_st_active_count > 0) {
  5. /* Idle vp till I/O is ready or the smallest timeout expired */
  6. _ST_VP_IDLE();
  7. /* Check sleep queue for expired threads */
  8. _st_vp_check_clock();
  9. me->state = _ST_ST_RUNNABLE;
  10. _ST_SWITCH_CONTEXT(me);
  11. }
  12. /* No more threads */
  13. exit(0);
  14. /* NOTREACHED */
  15. return NULL;
  16. }

所有线程完成时就exit。

Thread初始线程

st的初始线程,或者叫做物理线程,primordial线程,是调用st_init的那个线程。一般而言,调用st的程序都是单线程,所以这个初始线程也就是那个系统的唯一的一个线程。

所有st的线程都是调用st_create_thread创建的,使用st自己开辟的stack;除了一种初始线程,没有重新设置stack,这个就是初始线程(物理线程)。

参考st_init的代码:

  1. /*
  2. * Initialize primordial thread
  3. */
  4. trd = (_st_thread_t *) calloc(1, sizeof(_st_thread_t) +
  5. (ST_KEYS_MAX * sizeof(void *)));
  6. if (!trd) {
  7. return -1;
  8. }
  9. trd->private_data = (void **) (trd + 1);
  10. trd->state = _ST_ST_RUNNING;
  11. trd->flags = _ST_FL_PRIMORDIAL;
  12. _ST_SET_CURRENT_THREAD(trd);
  13. _st_active_count++;

在分配trd对象时,分配了_st_thread_t和keys两个对象,可以参考前面对于stack的使用。keys用来做private_data,所以后面初始化private_data时是指向下一个thread。

创建后设置这个线程为_ST_FL_PRIMORDIAL,这个就是用来指明stack是否是st自己分配的:

  1. void st_thread_exit(void *retval)
  2. {
  3. if (!(trd->flags & _ST_FL_PRIMORDIAL)) {
  4. _st_stack_free(trd->stack);
  5. }
  6. }

如果是初始线程(物理线程),那么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