博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
(转)st(state-threads) coroutine和stack分析
阅读量:5875 次
发布时间:2019-06-19

本文共 7326 字,大约阅读时间需要 24 分钟。

 
目录

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] 
 
  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] 
 
  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] 
 
  1. +--------------------------------------------------------------+  
  2. |                         stack                                |  
  3. +--------------------------------------------------------------+  
  4. bottom                                                         top  
分配如下:

 

 

[plain] 
 
  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] 
 
  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] 
 
  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] 
 
  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] 
 
  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] 
 
  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] 
 
  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] 
 
  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] 
 
  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

你可能感兴趣的文章
SCCM 2016 配置管理系列(Part8)
查看>>
zabbix监控部署
查看>>
struts中的xwork源码下载地址
查看>>
Android硬件抽象层(HAL)深入剖析(二)
查看>>
CDays–4 习题一至四及相关内容解析。
查看>>
L3.十一.匿名函数和map方法
查看>>
java面向对象高级分层实例_实体类
查看>>
android aapt 用法 -- ApkReader
查看>>
[翻译]用 Puppet 搭建易管理的服务器基础架构(3)
查看>>
Android -- AudioPlayer
查看>>
Python大数据依赖包安装
查看>>
Android View.onMeasure方法的理解
查看>>
Node.js 爬虫初探
查看>>
ABP理论学习之仓储
查看>>
NestJS 脑图
查看>>
我的友情链接
查看>>
Html body的滚动条禁止与启用
查看>>
Tengine新增nginx upstream模块的使用
查看>>
多媒体工具Mediainfo
查看>>
1-小程序
查看>>