作者:李樂
本文基於Swoole-4.3.2和PHP-7.1.0版本
Swoole協程簡介
Swoole4為PHP語言提供了強大的CSP協程程式設計模式,使用者可以通過go函式建立一個協程,以達到併發執行的效果,如下面程式碼所示:
<?php
//Co::sleep()是Swoole提供的API,並不會阻塞當前程式,只會阻塞協程觸發協程切換。
go(function (){
Co::sleep(1);
echo "a";
});
go(function (){
Co::sleep(2);
echo "b";
});
echo "c";
//輸出結果:cab
//程式總執行時間2秒
其實在Swoole4之前就實現了多協程程式設計模式,在協程建立、切換以及結束的時候,相應的操作php棧即可(建立、切換以及回收php棧)。
此時的協程實現無法完美的支援php語法,其根本原因在於沒有儲存c棧資訊。(vm內部或者某些擴充套件提供的API是通過c函式實現的,呼叫這些函式時如果發生協程切換,c棧該如何處理?)
Swoole4新增了c棧的管理,在協程建立、切換以及結束的同時會伴隨著c棧的建立、切換以及回收。
Swoole4協程實現方案如下圖所示:
其中:
- API層是提供給使用者使用的協程相關函式,比如go()函式用於建立協程;Co::yield()使得當前協程讓出CPU;Co::resume()可恢復某個協程執行;
- Swoole4協程需要同時管理c棧與php棧,Coroutine用於管理c棧,PHPCoroutine用於管理php棧;其中Coroutine(),yield(),resume()實現了c棧的建立以及換入換出;create_func(),on_yield(),on_resume()實現了php棧的建立以及換入換出;
- Swoole4在管理c棧時,用到了 boost.context庫,make_fcontext()和jump_fcontext()函式均使用匯編語言編寫,實現了c棧上下文的建立以及切換;
- Swoole4對boost.context進行了簡單封裝,即Context層,Context(),SwapIn()以及SwapOut()
對應c棧的建立以及換入換出。
深入理解C棧
函式是對程式碼的封裝,對外暴露的只是一組指定的引數和一個可選的返回值;假設函式P呼叫函式Q,Q執行後返回函式P,實現該函式呼叫需要考慮以下三點:
- 指令跳轉:進入函式Q的時候,程式計數器必須被設定為Q的程式碼的起始地址;在返回時,程式計數器需要設定為P中呼叫Q後面那條指令的地址;
- 資料傳遞:P能夠向Q提供一個或多個引數,Q能夠向P返回一個值;
- 記憶體分配與釋放:Q開始執行時,可能需要為區域性變數分配記憶體空間,而在返回前,又需要釋放這些記憶體空間;
大多數語言的函式呼叫都採用了棧結構實現,函式的呼叫與返回即對應的是一系列的入棧與出棧操作,我們通常稱之為函式棧幀(stack frame)。示意圖如下:
上面提到的程式計數器即暫存器%rip,另外還有兩個暫存器需要重點關注:%rbp指向棧幀底部,%rsp指向棧幀頂部。
下面將通過具體的程式碼事例,為讀者講解函式棧幀。c程式碼與彙編程式碼如下:
int add(int x, int y)
{
int a, b;
a = 10;
b = 5;
return x+y;
}
int main()
{
int sum = add(1,2);
}
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl $2, %esi
movl $1, %edi
call add
movl %eax, -4(%rbp)
leave
ret
add:
pushq %rbp
movq %rsp, %rbp
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl $10, -4(%rbp)
movl $5, -8(%rbp)
movl -24(%rbp), %eax
movl -20(%rbp), %edx
addl %edx, %eax
popq %rbp
ret
分析彙編程式碼:
- main函式與add函式入口,首先將暫存器%rbp壓入棧中用於儲存其值,其次移動%rbp指向當前棧頂部(此時%rbp,%rsp都指向棧頂,開始新的函式棧幀);
- main函式"subq $16, %rsp",是為main函式棧幀預留16個位元組的記憶體空間;
- 呼叫add函式時,第一個引數和第二個引數分別儲存在暫存器%edi和%esi,返回值儲存在暫存器%eax;
- call指令用於函式呼叫,實現了兩個功能:暫存器%rip壓入棧中,跳轉到新的程式碼位置;
- ret指令用於函式返回,彈出棧頂內容到暫存器%rip,依次實現程式碼跳轉;
- leave指令等同於兩條指令:movq %rsp,%rbp和popq %rbp,用於釋放main函式棧幀,恢復前一個函式棧幀;
- 注意add函式棧幀,並沒有為其分配空間,暫存器%rsp和%rbp都指向棧幀底部;根本因為是add函式沒有呼叫其他函式。
- 該程式的棧結構示意圖如下:
問題:觀察上面的彙編程式碼,輸入引數分別使用的是暫存器%edi和%esi,返回值使用的是暫存器%eax,輸入輸出引數不應該儲存在棧上嗎?暫存器比記憶體訪問要快的多,現代處理器暫存器數目也比較多,因此傾向於將引數優先儲存在暫存器。比如%rdi, %rsi, %rdx, %rcx, %r8d, %r9d 六個暫存器用於儲存函式呼叫時的前6個引數,那麼當輸入引數數目超過6個時,如何處理?這些輸入引數只能儲存在棧上了。
(%rdi等表示64位暫存器,%edi等表示32位暫存器)
//add函式需要9個引數
add(1,2,3,4,5,6,7,8,9);
//引數7,8,9儲存在棧上
movl $9, 16(%rsp)
movl $8, 8(%rsp)
movl $7, (%rsp)
movl $6, %r9d
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $1, %edi
Swoole C棧管理
通過學習c棧基本知識,我們知道最主要有三個暫存器:%rip程式計數器指向下一條需要執行的指令,%rbp指向函式棧幀底部,%rsp指向函式棧幀頂部。這三個暫存器可以確定一個c棧執行上下文,c棧的管理其實就是這些暫存器的管理。
第一節我們提到Swoole在管理c棧時,用到了 boost.context庫,其中make_fcontext()和jump_fcontext()函式均使用匯編語言編寫,實現了c棧執行上下文的建立以及切換;函宣告命如下:
fcontext_t make_fcontext(void *sp, size_t size, void (*fn)(intptr_t));
intptr_t jump_fcontext(fcontext_t *ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false);
make_fcontext函式用於建立一個執行上下文,其中引數sp指向記憶體最高地址處(在堆中分配一塊記憶體作為該執行上下文的c棧),引數size為棧大小,引數fn是一個函式指標,指向該執行上下文的入口函式;程式碼主要邏輯如下:
/*%rdi表示第一個引數sp,指向棧頂*/
movq %rdi, %rax
//保證%rax指向的地址按照16位元組對齊
andq $-16, %rax
//將%rax向低地址處偏移0x48位元組
leaq -0x48(%rax), %rax
/* %rdx表示第三個引數fn,儲存在%rax偏移0x38位置處 */
movq %rdx, 0x38(%rax)
stmxcsr (%rax)
fnstcw 0x4(%rax)
leaq finish(%rip), %rcx
movq %rcx, 0x40(%rax)
//返回值儲存在%rax暫存器
ret
make_fcontext函式建立的執行上下文示意圖如下(可以看到預留了若干位元組用於儲存上下文資訊):
Swoole協程實現的Context層封裝了上下文的建立,建立上下文函式實現如下:
Context::Context(size_t stack_size, coroutine_func_t fn, void* private_data) :
fn_(fn), stack_size_(stack_size), private_data_(private_data)
{
stack_ = (char*) sw_malloc(stack_size_);
void* sp = (void*) ((char*) stack_ + stack_size_);
ctx_ = make_fcontext(sp, stack_size_, (void (*)(intptr_t))&context_func);
}
可以看到c棧執行上下文是通過sw_malloc函式在堆上分配的一塊記憶體,預設大小為2M位元組;引數sp指向的是記憶體最高地址處;執行上下文的入口函式為Context::context_func()。
jump_fcontext函式用於切換c棧上下文:1)函式會將當前上下文(暫存器)儲存在當前棧頂(push),同時將%rsp暫存器內容儲存在ofc地址;2)函式從nfc地址處恢復%rsp暫存器內容,同時從棧頂恢復上下文資訊(pop)。程式碼主要邏輯如下:
//-------------------儲存當前c棧上下文-------------------
pushq %rbp /* save RBP */
pushq %rbx /* save RBX */
pushq %r15 /* save R15 */
pushq %r14 /* save R14 */
pushq %r13 /* save R13 */
pushq %r12 /* save R12 */
leaq -0x8(%rsp), %rsp
stmxcsr (%rsp)
fnstcw 0x4(%rsp)
//%rdi表示第一個引數,即ofc,儲存%rsp到ofc地址處
movq %rsp, (%rdi)
//-------------------從nfc中恢復上下文-------------------
//%rsi表示第二個引數,即nfc,從nfc地址處恢復%rsp
movq %rsi, %rsp
ldmxcsr (%rsp)
fldcw 0x4(%rsp)
leaq 0x8(%rsp), %rsp
popq %r12 /* restrore R12 */
popq %r13 /* restrore R13 */
popq %r14 /* restrore R14 */
popq %r15 /* restrore R15 */
popq %rbx /* restrore RBX */
popq %rbp /* restrore RBP */
//這裡彈出的其實是之前儲存的%rip
popq %r8
//%rdx表示第三個引數,%rax用於儲存函式返回值;
movq %rdx, %rax
//%rdi用於儲存第一個引數
movq %rdx, %rdi
//跳轉到%r8指向的地址
jmp *%r8
觀察jump_fcontext函式的彙編程式碼,可以看到儲存上下文與恢復上下文的程式碼基本是對稱的。恢復上下文時"popq %r8"用於彈出上一次儲存的程式計數器%rip的內容,然而並沒有看到儲存暫存器%rip的程式碼。這是因為呼叫jump_fcontext函式時,底層call指令已經將%rip入棧了。
Swoole協程實現的Context層封裝了上下文的換入換出,可以在上下文swap_ctx_和ctx_之間隨時換入換出,程式碼實現如下:
bool Context::SwapIn()
{
jump_fcontext(&swap_ctx_, ctx_, (intptr_t) this, true);
return true;
}
bool Context::SwapOut()
{
jump_fcontext(&ctx_, swap_ctx_, (intptr_t) this, true);
return true;
}
上下文示意圖如下所示:
Swoole PHP棧管理
php程式碼在執行時,同樣存在函式棧幀的分配與回收。php將此抽象為兩個結構,php棧zend_vm_stack,與執行資料(函式棧幀)zend_execute_data。
php棧結構與c棧結構基本類似,定義如下:
struct _zend_vm_stack {
zval *top;
zval *end;
zend_vm_stack prev;
};
其中top欄位指向棧頂位置,end欄位指向棧底位置;prev指向上一個棧,形成連結串列,當棧空間不夠時,可以進行擴容。php虛擬機器申請棧空間時預設大小為256K,Swoole建立棧空間時預設大小為8K。
執行資料結構體,我們需要重點關注這幾個欄位:當前函式編譯後的指令集(opline指向指令集陣列中的某一個元素,虛擬機器只需要遍歷該陣列並執行所有指令即可),函式返回值,以及呼叫該函式的執行資料;結構定義如下:
struct _zend_execute_data {
//當前執行指令
const zend_op *opline;
zend_execute_data *call;
//函式返回值
zval *return_value;
zend_function *func;
zval This; /* this + call_info + num_args */
//呼叫當前函式的棧幀
zend_execute_data *prev_execute_data;
//符號表
zend_array *symbol_table;
#if ZEND_EX_USE_RUN_TIME_CACHE
void **run_time_cache;
#endif
#if ZEND_EX_USE_LITERALS
//常量陣列
zval *literals;
#endif
};
php棧初始化函式為zend_vm_stack_init;當執行使用者函式呼叫時,虛擬機器通過函式zend_vm_stack_push_call_frame在php棧上分配新的執行資料,並執行該函式程式碼;函式執行完成後,釋放該執行資料。程式碼邏輯如下:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
//分配新的執行資料
execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,
(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));
//設定prev
execute_data->prev_execute_data = EG(current_execute_data);
//初始化當前執行資料,op_array即為當前函式編譯得到的指令集
i_init_execute_data(execute_data, op_array, return_value);
//執行函式程式碼
zend_execute_ex(execute_data);
//釋放執行資料
zend_vm_stack_free_call_frame(execute_data);
}
php棧幀結構示意圖如下:
Swoole協程實現,需要自己管理php棧,在發生協程建立以及切換時,對應的建立新的php棧,切換php棧,同時儲存和恢復php棧上下文資訊。這裡涉及到一個很重要的結構體php_coro_task:
struct php_coro_task
{
zval *vm_stack_top;
zval *vm_stack_end;
zend_vm_stack vm_stack;
zend_execute_data *execute_data;
};
這裡列出了php_coro_task結構體的若干關鍵欄位,這些欄位用於儲存和恢復php上下文資訊。
協程建立時,底層通過函式PHPCoroutine::create_func實現了php棧的建立:
void PHPCoroutine::create_func(void *arg)
{
//建立並初始化php棧
vm_stack_init();
call = (zend_execute_data *) (EG(vm_stack_top));
//為結構php_coro_task分配空間
task = (php_coro_task *) EG(vm_stack_top);
EG(vm_stack_top) = (zval *) ((char *) call + PHP_CORO_TASK_SLOT * sizeof(zval));
//建立新的執行資料結構
call = zend_vm_stack_push_call_frame(
ZEND_CALL_TOP_FUNCTION | ZEND_CALL_ALLOCATED,
func, argc, fci_cache.called_scope, fci_cache.object
);
}
從程式碼中可以看到結構php_coro_task是直接儲存在php棧的底部。
當通過yield函式讓出CPU時,底層會呼叫函式PHPCoroutine::on_yield切換php棧:
void PHPCoroutine::on_yield(void *arg)
{
php_coro_task *task = (php_coro_task *) arg;
php_coro_task *origin_task = get_origin_task(task);
//儲存當前php棧上下文資訊到php_coro_task結構
save_task(task);
//從php_coro_task結構中恢復php棧上下文資訊
restore_task(origin_task);
}
Swoole協程實現
前面我們簡單介紹了Swoole協程的實現方案,以及Swoole對c棧與php棧的管理,接下來將結合前面的知識,系統性的介紹Swoole協程的實現原理。
swoole協程資料模型
話不多說,先看一張圖:
- 每個協程都需要管理自己的c棧與php棧;
- Context封裝了c棧的管理操作;ctx_欄位儲存的是暫存器%rsp的內容(指向c棧棧頂位置);swap_ctx_欄位儲存的是將被換出的協程暫存器%rsp內容(即,將被換出的協程的c棧棧頂位置);SwapIn()對應協程換入操作;SwapOut()對應協程換出操作;
- 參考jump_fcontext實現,協程在換出時,會將暫存器%rip,%rbp等暫存在c棧棧頂;協程在換入時,相應的會從棧頂恢復這些暫存器的內容;
- Coroutine管理著協程所有內容;cid欄位表示當前協程的ID;task欄位指向當前協程的php_coro_task結構,該結構中儲存的是當前協程的php棧資訊(vm_stack_top,execute_data等);ctx欄位指向的是當前協程的Context物件;origin欄位指向的是另一個協程Coroutine物件;yield()和resume()對應的是協程的換出換入操作;
- 注意到php_coro_task結構的co欄位指向其對應的協程物件Coroutine;
- Coroutine還有一些靜態屬性,靜態屬性的屬於類屬性,所有協程共享的;last_cid欄位儲存的是當前最大的協程ID,建立協程時可用於生成協程ID;current欄位指向的是當前正在執行的協程Coroutine物件;on_yield和on_resume是兩個函式指標,用於實現php棧的切換操作,實際指向的是方法PHPCoroutine::on_yield和PHPCoroutine::on_resume;
swoole協程實現
協程建立
Swoole建立協程可以使用go()函式,底層實現對應的是PHP_FUNCTION(swoole_coroutine_create),其函式實現如下:
PHP_FUNCTION(swoole_coroutine_create)
{
……
long cid = PHPCoroutine::create(&fci_cache, fci.param_count, fci.params);
}
long PHPCoroutine::create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv)
{
……
save_task(get_task());
return Coroutine::create(create_func, (void*) &php_coro_args);
}
class Coroutine
{
public:
static inline long create(coroutine_func_t fn, void* args = nullptr)
{
return (new Coroutine(fn, args))->run();
}
}
- 注意Coroutine::create函式第一個引數偉create_func,該函式後續用於建立php棧,並開始協程程式碼的執行;
- 可以看到PHPCoroutine::create在呼叫Coroutine::create建立建立協程之前,儲存了當前php棧資訊到php_coro_task結構中。
- 注意主程式的php棧是虛擬機器建立的,結構與上面畫的協程php棧不同,主程式的php_coro_task結構並沒有儲存在php棧上,而是一個靜態變數PHPCoroutine::main_task,從get_task方法可以看出,主程式中get_current_task()返回的是null,因此最後獲得的php_coro_task結構是PHPCoroutine::main_task。
class PHPCoroutine
{
public:
static inline php_coro_task* get_task()
{
php_coro_task *task = (php_coro_task *) Coroutine::get_current_task();
return task ? task : &main_task;
}
}
- 在Coroutine的建構函式中完成了協程物件Coroutine的建立與初始化,以及Context物件的建立與初始化(建立了c棧);run()函式執行了協程的換入,從而開始協程的執行;
//全域性協程map
std::unordered_map<long, Coroutine*> Coroutine::coroutines;
class Coroutine
{
protected:
Coroutine(coroutine_func_t fn, void *private_data) :
ctx(stack_size, fn, private_data)
{
cid = ++last_cid;
coroutines[cid] = this;
}
inline long run()
{
long cid = this->cid;
origin = current;
current = this;
ctx.SwapIn();
if (ctx.end)
{
close();
}
return cid;
}
}
- 可以看到建立協程物件Coroutine時,通過last_cid來計算當前協程的ID,同時將該協程物件加入到全域性map中;程式碼ctx(stack_size, fn, private_data)建立並初始化了Context物件;
- run()函式將該協程換入執行時,賦值origin為當前協程(主程式中current為null),同時設定current為當前協程物件Coroutine;呼叫SwapIn()函式完成協程的換入執行;最後如果協程執行完畢,則關閉並釋放該協程物件Coroutine;
- 初始化Context物件時,可以看到其建構函式Context::Context(size_t stack_size, coroutine_func_t fn, void* private_data),其中引數fn為協程入口函式(PHPCoroutine::create_func),可以看到其賦值給ontext物件的欄位fn_,但是在建立c棧上下文時,其傳入的入口函式為context_func;
Context::Context(size_t stack_size, coroutine_func_t fn, void* private_data) :
fn_(fn), stack_size_(stack_size), private_data_(private_data)
{
……
ctx_ = make_fcontext(sp, stack_size_, (void (*)(intptr_t))&context_func);
}
- 函式context_func內部其實呼叫的就是方法PHPCoroutine::create_func;當協程執行結束時,會標記end欄位為true,同時將該協程換出;
void Context::context_func(void *arg)
{
Context *_this = (Context *) arg;
_this->fn_(_this->private_data_);
_this->end = true;
_this->SwapOut();
}
問題:引數arg為什麼是Context物件呢,是如何傳遞的呢?這就涉及到jump_fcontext彙編實現,以及jump_fcontext的呼叫了
jump_fcontext(&swap_ctx_, ctx_, (intptr_t) this, true);
jump_fcontext:
movq %rdx, %rdi
呼叫jump_fcontext函式時,第三個引數傳遞的是this,即當前Context物件;而函式jump_fcontext彙編實現時,將第三個引數的內容拷貝到%rdi暫存器中,當協程換入執行函式context_func時,暫存器%rdi儲存的就是第一個引數,即Context物件。
- 方法PHPCoroutine::create_func就是建立並初始化php棧,執行協程程式碼;這裡不做過多介紹。
問題:Coroutine的靜態屬性on_yield和on_resume時什麼時候賦值的?
在Swoole模組初始化時,會呼叫函式swoole_coroutine_util_init(該函式同時宣告瞭"Co"等短名稱),該函式進一步的呼叫PHPCoroutine::init()方法,該方法完成了靜態屬性的賦值操作。
void PHPCoroutine::init()
{
Coroutine::set_on_yield(on_yield);
Coroutine::set_on_resume(on_resume);
Coroutine::set_on_close(on_close);
}
協程切換
使用者可以通過Co::yield()和Co::resume()實現協程的讓出和恢復,
Co::yield()的底層實現函式為PHP_METHOD(swoole_coroutine_util, yield),Co::resume()的底層實現函式為PHP_METHOD(swoole_coroutine_util, resume)。本節將為讀者講述協程切換的實現原理。
static unordered_map<int, Coroutine *> user_yield_coros;
static PHP_METHOD(swoole_coroutine_util, yield)
{
Coroutine* co = Coroutine::get_current_safe();
user_yield_coros[co->get_cid()] = co;
co->yield();
RETURN_TRUE;
}
static PHP_METHOD(swoole_coroutine_util, resume)
{
……
auto coroutine_iterator = user_yield_coros.find(cid);
if (coroutine_iterator == user_yield_coros.end())
{
swoole_php_fatal_error(E_WARNING, "you can not resume the coroutine which is in IO operation");
RETURN_FALSE;
}
user_yield_coros.erase(cid);
co->resume();
}
- 呼叫Co::resume()恢復某個協程之前,該協程必然已經呼叫Co::yield()讓出CPU;因此在Co::yield()時,會將該協程物件新增到全域性map中;Co::resume()時做相應校驗,如果校驗通過則恢復協程,並從map種刪除該協程物件;
- co->yield()實現了協程的讓出操作;1)設定協程狀態為SW_CORO_WAITING;2)回撥on_yield方法,即PHPCoroutine::on_yield,儲存當前協程(task代表協程)的php棧上下文,恢復另一個協程的php棧上下文(origin代表另一個協程物件);3)設定當前協程物件為origin;4)換出該協程;
void Coroutine::yield()
{
state = SW_CORO_WAITING;
if (on_yield)
{
on_yield(task);
}
current = origin;
ctx.SwapOut();
}
- co->resume()實現了協程的恢復操作:1)設定協程狀態為SW_CORO_RUNNING;2)回撥on_resume方法,即PHPCoroutine::on_resume,儲存當前協程(current協程)的php棧上下文,恢復另一個協程(task代表協程)的php棧上下文;3)設定origin為當前協程物件,current為即將要換入的協程物件;4)換入協程;
void Coroutine::resume()
{
state = SW_CORO_RUNNING;
if (on_resume)
{
on_resume(task);
}
origin = current;
current = this;
ctx.SwapIn();
if (ctx.end)
{
close();
}
}
- Swoole協程有四種狀態:初始化,執行中,等待執行,執行結束;定義如下:
typedef enum
{
SW_CORO_INIT = 0,
SW_CORO_WAITING,
SW_CORO_RUNNING,
SW_CORO_END,
} sw_coro_state;
- 協程之間可以通過Coroutine物件的origin欄位形成一個類似連結串列的結構;Co::yield()換出當前協程時,會換入origin協程;在A協程種呼叫Co::resume()恢復B協程時,會換出A協程,換入B協程,同時標記A協程為B的origin協程;
協程切換過程比較簡單,這裡不做過多詳述。
協程排程
當我們呼叫Co::sleep()讓協程休眠時,會換出當前協程;或者呼叫CoroutineSocket->recv()從socket接收資料,但socket資料還沒有準備好時,會阻塞當前協程,從而使得協程換出。那麼問題來了,什麼時候再換入執行這個協程呢?
socket讀寫實現
Swoole的socket讀寫使用的成熟的IO多路複用模型:epoll/kqueue/select/poll等,並且將其封裝在結構體_swReactor中,其定義如下:
struct _swReactor
{
//超時時間
int32_t timeout_msec;
//fd的讀寫事件處理函式
swReactor_handle handle[SW_MAX_FDTYPE];
swReactor_handle write_handle[SW_MAX_FDTYPE];
swReactor_handle error_handle[SW_MAX_FDTYPE];
//fd事件的註冊修改刪除以及wait
//函式指標,(以epoll為例)指向的是epoll_ctl、epoll_wait
int (*add)(swReactor *, int fd, int fdtype);
int (*set)(swReactor *, int fd, int fdtype);
int (*del)(swReactor *, int fd);
int (*wait)(swReactor *, struct timeval *);
void (*free)(swReactor *);
//超時回撥函式,結束、開始回撥函式
void (*onTimeout)(swReactor *);
void (*onFinish)(swReactor *);
void (*onBegin)(swReactor *);
}
在呼叫函式PHPCoroutine::create建立協程時,會校驗是否已經初始化_swReactor物件,如果沒有則會呼叫php_swoole_reactor_init函式建立並初始化main_reactor物件;
void php_swoole_reactor_init()
{
if (SwooleG.main_reactor == NULL)
{
SwooleG.main_reactor = (swReactor *) sw_malloc(sizeof(swReactor));
if (swReactor_create(SwooleG.main_reactor, SW_REACTOR_MAXEVENTS) < 0)
{
}
……
php_swoole_register_shutdown_function_prepend("swoole_event_wait");
}
}
我們以epoll為例,main_reactor各回撥函式如下:
reactor->onFinish = swReactor_onFinish;
reactor->onTimeout = swReactor_onTimeout;
reactor->add = swReactorEpoll_add;
reactor->set = swReactorEpoll_set;
reactor->del = swReactorEpoll_del;
reactor->wait = swReactorEpoll_wait;
reactor->free = swReactorEpoll_free;
注意:這裡註冊了一個函式swoole_event_wait,在生命週期register_shutdown階段會執行該函式,開始Swoole的事件迴圈,阻擋了php生命週期的結束。
類Socket封裝了socket讀寫相關的所有操作以及資料結構,其定義如下:
class Socket
{
public:
swConnection *socket = nullptr;
//讀寫函式
ssize_t recv(void *__buf, size_t __n);
ssize_t send(const void *__buf, size_t __n);
……
private:
swReactor *reactor = nullptr;
Coroutine *read_co = nullptr;
Coroutine *write_co = nullptr;
//連線超時時間,接收資料、傳送資料超時時間
double connect_timeout = default_connect_timeout;
double read_timeout = default_read_timeout;
double write_timeout = default_write_timeout;
}
- socket欄位型別為swConnection,代表傳輸層連線;
- reactor欄位指向結構體swReactor物件,用於fd事件的註冊、修改、刪除以及wait;
- 當呼叫recv()函式接收資料,阻塞了該協程時,read_co欄位指向該協程物件Coroutine;
- 當呼叫send()函式接收資料,阻塞了該協程時,write_co欄位指向該協程物件Coroutine;
- 類Socket初始化函式為Socket::init_sock:
void Socket::init_sock(int _fd)
{
reactor = SwooleG.main_reactor;
//設定協程型別fd(SW_FD_CORO_SOCKET)的讀寫事件處理函式
if (!swReactor_handle_isset(reactor, SW_FD_CORO_SOCKET))
{
reactor->setHandle(reactor, SW_FD_CORO_SOCKET | SW_EVENT_READ, readable_event_callback);
reactor->setHandle(reactor, SW_FD_CORO_SOCKET | SW_EVENT_WRITE, writable_event_callback);
reactor->setHandle(reactor, SW_FD_CORO_SOCKET | SW_EVENT_ERROR, error_event_callback);
}
}
當我們呼叫CoroutineSocket->recv接收資料時,底層實現如下:
Socket::timeout_setter ts(sock->socket, timeout, SW_TIMEOUT_READ);
ssize_t bytes = all ? sock->socket->recv_all(ZSTR_VAL(buf), length) : sock->socket->recv(ZSTR_VAL(buf), length);
類timeout_setter會設定socket的接收資料超時時間read_timeout為timeout。
函式socket->recv_all會迴圈讀取資料,直到讀取到指定長度的資料,或者底層返回等待標識阻塞當前協程:
ssize_t Socket::recv_all(void *__buf, size_t __n)
{
timer_controller timer(&read_timer, read_timeout, this, timer_callback);
while (true)
{
do {
retval = swConnection_recv(socket, (char *) __buf + total_bytes, __n - total_bytes, 0);
} while (retval < 0 && swConnection_error(errno) == SW_WAIT && timer.start() && wait_event(SW_EVENT_READ));
if (unlikely(retval <= 0))
{
break;
}
total_bytes += retval;
if ((size_t) total_bytes == __n)
{
break;
}
}
}
- 函式首先建立timer_controller物件,設定其超時時間為read_timeout,以及超時回撥函式為timer_callback;
- while (true)死迴圈讀取fd資料,當讀取資料量等於__n時,讀取操作結束,break該迴圈;如果讀取操作swConnection_recv返回值小於0,並且錯誤標識為SW_WAIT,說明需要等待資料到來,此時阻塞當前協程等待資料到來(函式wait_event會換出當前協程),阻塞超時時間為read_timeout(函式timer.start()用於設定超時時間)。
class timer_controller
{
public:
bool start()
{
if (timeout > 0)
{
*timer_pp = swTimer_add(&SwooleG.timer, (long) (timeout * 1000), 0, data, callback);
}
}
}
- 函式swTimer_add用於新增一個定時器;Swoole底層定時任務是通過最小堆實現的,堆頂元素的超時時間最近;結構體_swTimer維護著Swoole內部所有的定時任務:
struct _swTimer
{
swHeap *heap; //最小堆
swHashMap *map; //map,定時器ID作為key
//最早的定時任務觸發時間
long _next_msec;
//函式指標,指向swReactorTimer_set
int (*set)(swTimer *timer, long exec_msec);
//函式指標,指向swReactorTimer_free
void (*free)(swTimer *timer);
};
- 當呼叫swTimer_add向_swTimer結構中新增定時任務時,需要更新_swTimer中最早的定時任務觸發時間_next_msec,同時更新main_reactor物件的超時時間:
if (timer->_next_msec < 0 || timer->_next_msec > _msec)
{
timer->set(timer, _msec);
timer->_next_msec = _msec;
}
static int swReactorTimer_set(swTimer *timer, long exec_msec)
{
SwooleG.main_reactor->timeout_msec = exec_msec;
return SW_OK;
}
- 函式wait_event負責將當前協程換出,直到註冊的事件發生
bool Socket::wait_event(const enum swEvent_type event, const void **__buf, size_t __n)
{
if (unlikely(!add_event(event)))
{
return false;
}
if (likely(event == SW_EVENT_READ))
{
read_co = co;
read_co->yield();
read_co = nullptr;
}
else // if (event == SW_EVENT_WRITE)
{
write_co = co;
write_co->yield();
write_co = nullptr;
}
}
- 函式add_event用於新增事件,底層呼叫reactor->add新增fd的監聽事件;
- read_co = co或者write_co = co,用於記錄當前哪個協程阻塞在該socket物件上,當該socket物件的讀寫事件被觸發時,可以恢復該協程執行;
- 函式yield()將該協程換出;
上面提到,建立協程時,註冊了一個函式swoole_event_wait,在生命週期register_shutdown階段會執行該函式,開始Swoole的事件迴圈,阻擋了php生命週期的結束。函式swoole_event_wait底層就是呼叫main_reactor->wait等待fd讀寫事件的產生;我們以epoll為例講述事件迴圈的邏輯:
static int swReactorEpoll_wait(swReactor *reactor, struct timeval *timeo)
{
while (reactor->running > 0)
{
n = epoll_wait(epoll_fd, events, max_event_num, swReactor_get_timeout_msec(reactor));
if (n == 0)
{
if (reactor->onTimeout != NULL)
{
reactor->onTimeout(reactor);
}
SW_REACTOR_CONTINUE;
}
for (i = 0; i < n; i++)
{
if ((events[i].events & EPOLLIN) && !event.socket->removed)
{
handle = swReactor_getHandle(reactor, SW_EVENT_READ, event.type);
ret = handle(reactor, &event);
}
if ((events[i].events & EPOLLOUT) && !event.socket->removed)
{
handle = swReactor_getHandle(reactor, SW_EVENT_WRITE, event.type);
ret = handle(reactor, &event);
}
}
}
}
- swReactorEpoll_wait是對函式epoll_wait的封裝;當有讀寫事件發生時,執行相應的handle,根據上面的講解我們知道讀寫事件的handle分別為readable_event_callback和writable_event_callback;
int Socket::readable_event_callback(swReactor *reactor, swEvent *event)
{
Socket *socket = (Socket *) event->socket->object;
socket->read_co->resume();
}
- 可以看到函式readable_event_callback只是簡單的恢復read_co協程即可;
- 當epoll_wait發生超時,最終呼叫的是函式swReactor_onTimeout,該函式會從Swoole維護的一系列定時任務swTimer中查詢已經超時的定時任務,同時執行其callback回撥;
while ((tmp = swHeap_top(timer->heap)))
{
tnode = tmp->data;
if (tnode->exec_msec > now_msec || tnode->round == timer->round)
{
break;
}
timer->_current_id = tnode->id;
if (!tnode->remove)
{
tnode->callback(timer, tnode);
}
……
}
//該定時任務沒有超時,需要更新需要更新_swTimer中最早的定時任務觸發時間_next_msec
long next_msec = tnode->exec_msec - now_msec;
if (next_msec <= 0)
{
next_msec = 1;
}
//同時更新main_reactor物件的超時時間,實現函式為swReactorTimer_set
timer->set(timer, next_msec);
- 該callback回撥函式即為上面設定的timer_callback:
void Socket::timer_callback(swTimer *timer, swTimer_node *tnode)
{
Socket *socket = (Socket *) tnode->data;
socket->set_err(ETIMEDOUT);
if (likely(tnode == socket->read_timer))
{
socket->read_timer = nullptr;
socket->read_co->resume();
}
else if (tnode == socket->write_timer)
{
socket->write_timer = nullptr;
socket->write_co->resume();
}
}
- 同樣的,timer_callback函式只是簡單的恢復read_co或者write_co協程即可
sleep實現
Co::sleep()的實現函式為PHP_METHOD(swoole_coroutine_util, sleep),該函式通過呼叫Coroutine::sleep實現了協程休眠的功能:
int Coroutine::sleep(double sec)
{
Coroutine* co = Coroutine::get_current_safe();
if (swTimer_add(&SwooleG.timer, (long) (sec * 1000), 0, co, sleep_timeout) == NULL)
{
return -1;
}
co->yield();
return 0;
}
可以看到,與socket讀寫事件超時處理相同,sleep內部實現時通過swTimer_add新增定時任務,同時換出當前協程實現的。該定時任務會導致main_reactor物件的超時時間的改變,即修改了epoll_wait的超時時間。
sleep的超時處理函式為sleep_timeout,只需要換入該阻塞協程物件即可,實現如下:
static void sleep_timeout(swTimer *timer, swTimer_node *tnode)
{
((Coroutine *) tnode->data)->resume();
}