zoukankan      html  css  js  c++  java
  • (转)st(state-threads) coroutine和stack分析

     目录(?)[-]

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

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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的尺寸,然后分配栈。

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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的栈,而是做了以下分配:

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. +--------------------------------------------------------------+  
    2. |                         stack                                |  
    3. +--------------------------------------------------------------+  
    4. bottom                                                         top  

    分配如下:
    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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。

    参考我改过的代码:

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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);:
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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);:
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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清理线程,然后切换到其他函数,直到完成最后一个函数就返回了。

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. void _st_thread_main(void)  
    2. {  
    3.     _st_thread_t *trd = _ST_CURRENT_THREAD();  
    4.       
    5.     /* Run thread main */  
    6.     trd->retval = (*trd->start)(trd->arg);  
    7.       
    8.     /* All done, time to go away */  
    9.     st_thread_exit(trd->retval);  
    10. }  

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

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. void _st_vp_schedule(void)  
    2. {  
    3.     _st_thread_t *trd;  
    4.       
    5.     if (_ST_RUNQ.next != &_ST_RUNQ) {  
    6.         /* Pull thread off of the run queue */  
    7.         trd = _ST_THREAD_PTR(_ST_RUNQ.next);  
    8.         _ST_DEL_RUNQ(trd);  
    9.     } else {  
    10.         /* If there are no threads to run, switch to the idle thread */  
    11.         trd = _st_this_vp.idle_thread;  
    12.     }  

    idle线程是在st_init时创建,也就是说st_init会创建一个idle线程(使用st_thread_create),以及直接创建一个_ST_FL_PRIMORDIAL线程(直接calloc)。idle线程的代码:
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. void *_st_idle_thread_start(void *arg)  
    2. {  
    3.     _st_thread_t *me = _ST_CURRENT_THREAD();  
    4.       
    5.     while (_st_active_count > 0) {  
    6.         /* Idle vp till I/O is ready or the smallest timeout expired */  
    7.         _ST_VP_IDLE();  
    8.           
    9.         /* Check sleep queue for expired threads */  
    10.         _st_vp_check_clock();  
    11.           
    12.         me->state = _ST_ST_RUNNABLE;  
    13.         _ST_SWITCH_CONTEXT(me);  
    14.     }  
    15.       
    16.     /* No more threads */  
    17.     exit(0);  
    18.       
    19.     /* NOTREACHED */  
    20.     return NULL;  
    21. }  

    所有线程完成时就exit。

    Thread初始线程

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

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

    参考st_init的代码:

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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自己分配的:

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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

  • 相关阅读:
    使用Connector/C++(VS2015)连接MySQL的完整例子
    一个表里有多个字段需要同时使用字典表进行关联显示,如何写sql查询语句
    Delphi连接MySql(待测试验证,使用mysql.pas未通过)
    MySQL5.5.51启用网络远程连接
    delphi做的程序如何连接SQL数据库
    定时删除所有文件夹下的_desktop.ini文件
    Delphi中打开网页连接的几种方法
    SQL增删改查
    ADOConnection断线重连
    TDBGridEh 标头排序
  • 原文地址:https://www.cnblogs.com/lihaiping/p/4755872.html
Copyright © 2011-2022 走看看