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
實際上vaddr是棧的內存開始地址,其他幾個地址下面分析。
棧的分配是在_st_stack_new函數,在st_thread_create函數調用,先計算stack的尺寸,然后分配棧。
[plain]?view plaincopy
上圖是棧分配后的結果,兩邊是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
分配如下:
[plain]?view plaincopy
也就是說:
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
gdb調試,第一次setjmp時,返回值是0,調用堆棧是創建線程的堆棧,62行的代碼是st_thread_t trd = st_thread_create(thread_func, NULL, 1, 0);:
[cpp]?view plaincopy
從其他線程切換過來時,即longjmp過來時,返回值非0,調用堆棧是longjmp的堆棧,68行的代碼是st_thread_join(trd, NULL);:
[cpp]?view plaincopy
注意,雖然顯示都是thread_test這個函數過來,實際上函數行數已經不一樣了,gdb顯示的stk_size也是破壞了的,因為這個時候的棧是用的st自己開辟的棧了。
進入到_st_thread_main中后,會調用用戶指定的線程函數(這個函數里面會調用st函數setjmp,下次longjmp是到這個位置了);從線程函數返回后,會調用st_thread_exit清理線程,然后切換到其他函數,直到完成最后一個函數就返回了。
[cpp]?view plaincopy
這個就是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
idle線程是在st_init時創建,也就是說st_init會創建一個idle線程(使用st_thread_create),以及直接創建一個_ST_FL_PRIMORDIAL線程(直接calloc)。idle線程的代碼:
[cpp]?view plaincopy
所有線程完成時就exit。
Thread初始線程
st的初始線程,或者叫做物理線程,primordial線程,是調用st_init的那個線程。一般而言,調用st的程序都是單線程,所以這個初始線程也就是那個系統的唯一的一個線程。
所有st的線程都是調用st_create_thread創建的,使用st自己開辟的stack;除了一種初始線程,沒有重新設置stack,這個就是初始線程(物理線程)。
參考st_init的代碼:
[cpp]?view plaincopy
在分配trd對象時,分配了_st_thread_t和keys兩個對象,可以參考前面對于stack的使用。keys用來做private_data,所以后面初始化private_data時是指向下一個thread。
創建后設置這個線程為_ST_FL_PRIMORDIAL,這個就是用來指明stack是否是st自己分配的:
[cpp]?view plaincopy
如果是初始線程(物理線程),那么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,就等所有線程退出。
總結
以上是生活随笔為你收集整理的st(state-threads) coroutine和stack分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 协程库st(state threads
- 下一篇: c++ 虚继承与继承的差异