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.youkuaiyun.com/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数据结构定义为:
实际上vaddr是栈的内存开始地址,其他几个地址下面分析。
栈的分配是在_st_stack_new函数,在st_thread_create函数调用,先计算stack的尺寸,然后分配栈。
上图是栈分配后的结果,两边是REDZONE使用mprotect保护不被访问(在DEBUG开启后),extra是一个额外的内存块,st_randomize_stacks开启后会调整bottom和top,就是随机的向右边移动一点。
总之,最后使用的,对外提供的接口就是bottom和top,st_thread_create函数会初始化sp。stack对外提供的服务就是[bottom, top]这个内存区域。
THREAD初始化栈
开辟Stack后,st会对stack初始化和分配,这个stack并非直接就是thread的栈,而是做了以下分配:
分配如下:
也就是说:
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.youkuaiyun.com/win_lin/article/details/40948277
Thread启动和切换
st的thread如何进入到指定的入口呢?
其实在第一次setjmp时,是初始化thread,这时候返回值是0,初始化完后就返回到调用函数继续执行了。
调用函数会在其他地方调用longjmp到这个thread,这时候是从setjmp地方开始执行,返回值是非0,这时进入thread的主函数:_st_thread_main。
参考我改过的代码:
gdb调试,第一次setjmp时,返回值是0,调用堆栈是创建线程的堆栈,62行的代码是st_thread_t trd = st_thread_create(thread_func, NULL, 1, 0);:
从其他线程切换过来时,即longjmp过来时,返回值非0,调用堆栈是longjmp的堆栈,68行的代码是st_thread_join(trd, NULL);:
注意,虽然显示都是thread_test这个函数过来,实际上函数行数已经不一样了,gdb显示的stk_size也是破坏了的,因为这个时候的栈是用的st自己开辟的栈了。
进入到_st_thread_main中后,会调用用户指定的线程函数(这个函数里面会调用st函数setjmp,下次longjmp是到这个位置了);从线程函数返回后,会调用st_thread_exit清理线程,然后切换到其他函数,直到完成最后一个函数就返回了。
这个就是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线程:
idle线程是在st_init时创建,也就是说st_init会创建一个idle线程(使用st_thread_create),以及直接创建一个_ST_FL_PRIMORDIAL线程(直接calloc)。idle线程的代码:
所有线程完成时就exit。
Thread初始线程
st的初始线程,或者叫做物理线程,primordial线程,是调用st_init的那个线程。一般而言,调用st的程序都是单线程,所以这个初始线程也就是那个系统的唯一的一个线程。
所有st的线程都是调用st_create_thread创建的,使用st自己开辟的stack;除了一种初始线程,没有重新设置stack,这个就是初始线程(物理线程)。
参考st_init的代码:
在分配trd对象时,分配了_st_thread_t和keys两个对象,可以参考前面对于stack的使用。keys用来做private_data,所以后面初始化private_data时是指向下一个thread。
创建后设置这个线程为_ST_FL_PRIMORDIAL,这个就是用来指明stack是否是st自己分配的:
如果是初始线程(物理线程),那么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,就等所有线程退出。