put,【Swoole源代码探讨】深层次了解Swoole协程保持-ope电竞投注-ope电竞app下载-ope电竞

暖心故事 245℃ 0

Swoole简介

Swoole:面向出产环境的 PHP 异步网络通讯引擎

使 PHP 开发人员能够编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 能够广泛应用于互联网、移动通讯、企业软件、云核算、网络游戏、物联网(IOT)、车联网、智能家居等范畴。 运用 PHP + Swoolw酒店e 作为网络通讯结构,能够使企业 IT 研制团队的功率大大提高,愈加专心于开发立异产品。

Swoole协程简介

笔者会继续同享架构规划,编程言语理论,面试题同享,互联网轶事等范畴优质文章,对这些感兴趣欢迎重视笔者,没有错。

Swoole4为PHP言语供给了强壮的CSP协程编程形式,用户能够经过go函数创立一个协程,以到达并发履行的作用,如下面代码所示:

//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,输入输出参数不应该保存在栈上吗?寄存器比内存拜访要快的多,现代处理器寄存器数目也比较多红旗l9,因而倾向于将参数优先保存在寄存器。比方%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_tput,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞 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)。代码首要逻辑如下:

//-------------------保put,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞存当时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"用于put,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞弹出上一次保存的程序计数器%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_tput,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞able;
#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_co火辣辣的情歌ro_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_ta嘴苦sk结构体的若干要害字段,这些字段用于保存和康复php上下文信息。

协程创立时,底层芍药花经过函数PHPCoroutine::create_func完结了php栈的创立:

void PHPCoroutine::create_func(void *arg)
{
//创立并初始化php栈
vm_sput,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞tack_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栈,并开端协程代码的履行;
  • 能够看到PHPCorou罄tine::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 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 ci陈鸿宇d = 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_sizeb12_(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 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()时,会将该协程目标增加到大局maput,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞p中;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_C货运资格证ORO_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_MAput,【Swoole源代码讨论】深层次了解Swoole协程坚持-ope电竞投注-ope电竞app下载-ope电竞XEVENTS) < 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朗逸轿车_t假性宫缩imeout = 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();
}
  • 能够看到函数readabcharlottele_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();
}

文章转载来自李乐,这篇对swoole的研讨文章写的很深化,欢迎阅览。

你的转发+重视便是对笔者同享最大的支撑,欢迎重视笔者,更多优质文章精彩贡献。

标签: 斗奶yl恩恩