zoukankan      html  css  js  c++  java
  • OpenResty Lua钩子调用完整流程

    前面一篇文章介绍了Openresty Lua协程调度机制,主要关心的是核心调度函数run_thread()内部发生的事情,而对于外部的事情我们并没有涉及。本篇作为其姊妹篇,准备补上剩余的部分。本篇将通过一个例子,完整介绍OpenResty中Lua钩子的调用流程,包括初始化阶段的工作、新连接进来时如何进入钩子、I/O等待时如何出去、事件触发时如何恢复、钩子正常执行结束时的操作、钩子内出错的情况。本文同样是基于stream-lua模块的代码。

    本博客已经迁移至CatBro's Blog,那里是我自己搭建的个人博客,页面效果比这边更好,支持站内搜索,评论回复还支持邮件提醒,欢迎关注。这边只会在有时间的时候不定期搬运一下。

    本篇文章链接

    整体流程

    我们以ssl_certificate_by_lua*钩子为例来进行介绍,一来是因为它还涉及SSL握手,流程上更长一点。二来是因为在其上下文中是YIELDABLE的,支持的ngx接口比较完整。

    我们将以下面两个配置为例来展开介绍。例子非常简单,第一个是正常的结束情况在ssl_certificate_by_lua_block里面调用了ngx.sleep()。第二个是出错中止的情况,多了一个ngx.exit(ngx.ERROR)

    server {
        listen 443 ssl;
        ssl_certificate_by_lua_block {
            ngx.sleep(0.1)
        }
        ssl_certificate test.pem;
        ssl_certificate_key test.key;
    }
    
    server {
        listen 443 ssl;
        ssl_certificate_by_lua_block {
            ngx.sleep(0.1)
            ngx.exit(ngx.ERROR)
        }
        ssl_certificate test.pem;
        ssl_certificate_key test.key;
    }
    

    首先,分别来看一下初始化阶段和连接阶段的整体流程。后面章节会结合实际代码,来详细介绍每种情况下是如何处理的。

    初始化阶段

    openresty-lua-hook-flow-init-phase

    初始化阶段的流程比较简单:配置解析阶段会读取配置文件中的代码块进行解析保存,然后创建Lua代码的key,这个key是用于后面将代码cache到注册表的。配置合并阶段,主要是合并配置项,然后设置cert_cb回调。配置后处理阶段,主要工作是初始化Lua VM,包括创建注册表项、创建全局表项ngx、替换coroutine接口。

    连接阶段

    openresty-lua-hook-flow-connect-phase

    连接阶段因为涉及新连接进入钩子、I/O等待时出去、事件触发时恢复、钩子正常执行结束(YIELD之后)、钩子内出错(YIELD之后)等各种情况,相对比较复杂。图中用不同颜色分别表示这几种不同的情况,每种颜色又用数字标识了其流程顺序。读者可以结合这个图,阅读后续每个阶段的代码,应该能够帮助您更好地理解。

    另外提一下,本文没有涉及Lua代码执行过程没有碰到YIELD就直接完成或者出错的情况。因为这种情况比较简单,整个流程都是一个同步的过程。执行完成或者出错之后,lua_resume()返回,后续的流程就跟图中I/O等待(棕色)的情况是一样的。

    初始化阶段

    配置项解析

    解析到ssl_certificate_by_lua_block时会调用ngx_stream_lua_ssl_cert_by_lua_block()进行解析,里面会进行配置文件的词法分析,将代码块中的代码都合并到一个buffer之后,插入到参数数组的后面。然后调用ngx_stream_lua_ssl_cert_by_lua()。(如果是by_lua_file的情况会直接调用ngx_stream_lua_ssl_cert_by_lua()

    char *
    ngx_stream_lua_ssl_cert_by_lua_block(ngx_conf_t *cf, ngx_command_t *cmd,
        void *conf)
    {
        char        *rv;
        ngx_conf_t   save;
    
        save = *cf;
        cf->handler = ngx_stream_lua_ssl_cert_by_lua;
        cf->handler_conf = conf;
    
        rv = ngx_stream_lua_conf_lua_block_parse(cf, cmd);
    
        *cf = save;
    
        return rv;
    }
    

    ngx_stream_lua_ssl_cert_by_lua()主要工作是设置lscf->srv.ssl_cert_src以及创建Lua代码的key。如果是by_lua_file的情况,key以字符串nhlf_开头,后边是对文件路径计算的摘要十六进制值;而by_lua_block的情况,key以字符串"ssl_certificate_by_lua"开头,后边是对整个Lua代码块计算的摘要十六进制值。

    lscf->srv.ssl_cert_src = value[1];
    
    p = ngx_palloc(cf->pool,
                   sizeof("ssl_certificate_by_lua") +
                   NGX_STREAM_LUA_INLINE_KEY_LEN);
    if (p == NULL) {
        return NGX_CONF_ERROR;
    }
    
    lscf->srv.ssl_cert_src_key = p;
    
    p = ngx_copy(p, "ssl_certificate_by_lua",
                 sizeof("ssl_certificate_by_lua") - 1);
    p = ngx_copy(p, NGX_STREAM_LUA_INLINE_TAG, NGX_STREAM_LUA_INLINE_TAG_LEN);
    p = ngx_stream_lua_digest_hex(p, value[1].data, value[1].len);
    *p = '';
    

    配置项合并

    在配置合并阶段,由ngx_stream_lua_merge_srv_conf()cert_cb回调函数ngx_stream_lua_ssl_cert_handler()设置到server的SSL_CTX上。

            /* 先进行配置合并 */
            if (conf->srv.ssl_cert_src.len == 0) {
                conf->srv.ssl_cert_src = prev->srv.ssl_cert_src;
                conf->srv.ssl_cert_src_key = prev->srv.ssl_cert_src_key;
                conf->srv.ssl_cert_handler = prev->srv.ssl_cert_handler;
            }
            /* 如果设置了该配置 */
            if (conf->srv.ssl_cert_src.len) {
                if (sscf->ssl.ctx == NULL) {
                    ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                                  "no ssl configured for the server");
    
                    return NGX_CONF_ERROR;
                }
    
    #   if OPENSSL_VERSION_NUMBER >= 0x1000205fL
                /* 设置cert_cb回调 */
                SSL_CTX_set_cert_cb(sscf->ssl.ctx, ngx_stream_lua_ssl_cert_handler, NULL);
    
    #   else
                /* ... */
    #   endif
            }
    

    配置后处理 postconfiguration

    在postconfig阶段,会调用ngx_stream_lua_init(),它里面最关键的任务就是初始化Lua VM。(其实还会调用init_by*钩子,不过不在我们今天的讨论范围内。)

    rc = ngx_stream_lua_init_vm(&lmcf->lua, NULL, cf->cycle, cf->pool,
                                lmcf, cf->log, NULL);
    

    我们来看下ngx_stream_lua_init_vm()里面的实现,它先会创建Lua VM实例,然后注册其cleanup handler,如果有第三方模块的preload_hooks会注册之,然后会加载resty.core模块,最后会注入代码对全局变量的写操作加一个警告日志。

        /* create new Lua VM instance */
        L = ngx_stream_lua_new_state(parent_vm, cycle, lmcf, log);
        if (L == NULL) {
            return NGX_ERROR;
        }
    
        /* register cleanup handler for Lua VM */
        cln->handler = ngx_stream_lua_cleanup_vm;
    
        state = ngx_alloc(sizeof(ngx_stream_lua_vm_state_t), log);
        if (state == NULL) {
            return NGX_ERROR;
        }
        state->vm = L;
        state->count = 1;
    
        cln->data = state;
    
        if (lmcf->vm_cleanup == NULL) {
            /* this assignment will happen only once,
             * and also only for the main Lua VM */
            lmcf->vm_cleanup = cln;
        }
    
    #ifdef OPENRESTY_LUAJIT
        /* load FFI library first since cdata needs it */
        luaopen_ffi(L);
    #endif
    
        if (lmcf->preload_hooks) {
            /* 注册第三方preload_hooks */
        }
    
        *new_vm = L;
    
        lua_getglobal(L, "require");
        lua_pushstring(L, "resty.core");
    
        rc = lua_pcall(L, 1, 1, 0);
        if (rc != 0) {
            return NGX_DECLINED;
        }
    
    #ifdef OPENRESTY_LUAJIT
        ngx_stream_lua_inject_global_write_guard(L, log);
    #endif
    
        return NGX_OK;
    

    关键函数是创建Lua VM实例的ngx_stream_lua_new_state(),我们来一睹其芳容:

    /* 创建vm state*/
    L = luaL_newstate();
    /* 打开标准库 */
    luaL_openlibs(L);
    /* 获取package表 */
    lua_getglobal(L, "package");
    
    /* 设置package.path和package.cpath */
    
    lua_pop(L, 1); /* remove the "package" table */
    
    /* 初始化registry */
    ngx_stream_lua_init_registry(L, log);
    /* 初始化globals */
    ngx_stream_lua_init_globals(L, cycle, lmcf, log);
    
    return L;
    

    重点是最后的两个函数,它们分别初始化registryglobals。这个两个函数都不算太长,让我们来完整看下它们做了些什么。

    ngx_stream_lua_init_registry()创建了几个注册表项,分别用于存放协程、Lua的请求ctx、socket连接池、Lua预编译正则表达式对象cache及Lua代码cache。

    ngx_stream_lua_init_registry(lua_State *L, ngx_log_t *log)
    {
        ngx_log_debug0(NGX_LOG_DEBUG_STREAM, log, 0,
                       "lua initializing lua registry");
    
        /* {{{ register a table to anchor lua coroutines reliably:
         * {([int]ref) = [cort]} */
        lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                              coroutines_key));
        lua_createtable(L, 0, 32 /* nrec */);
        lua_rawset(L, LUA_REGISTRYINDEX);
        /* }}} */
    
        /* create the registry entry for the Lua request ctx data table */
        lua_pushliteral(L, ngx_stream_lua_ctx_tables_key);
        lua_createtable(L, 0, 32 /* nrec */);
        lua_rawset(L, LUA_REGISTRYINDEX);
    
        /* create the registry entry for the Lua socket connection pool table */
        lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                              socket_pool_key));
        lua_createtable(L, 0, 8 /* nrec */);
        lua_rawset(L, LUA_REGISTRYINDEX);
    
    #if (NGX_PCRE)
        /* create the registry entry for the Lua precompiled regex object cache */
        lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                              regex_cache_key));
        lua_createtable(L, 0, 16 /* nrec */);
        lua_rawset(L, LUA_REGISTRYINDEX);
    #endif
    
        /* {{{ register table to cache user code:
         * { [(string)cache_key] = <code closure> } */
        lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                              code_cache_key));
        lua_createtable(L, 0, 8 /* nrec */);
        lua_rawset(L, LUA_REGISTRYINDEX);
        /* }}} */
    }
    

    ngx_stream_lua_init_globals()则是创建了ngx表,接着把相关Lua Ngx API全部注册到全局表上了,其中就包括我们前面例子中的ngx.sleep()ngx.exit()。然后把ngx表分别设为全局表项,同时也设到package.loaded.ngx了。注意,原生的coroutine接口也在这里被替换了。

    static void
    ngx_stream_lua_inject_ngx_api(lua_State *L, ngx_stream_lua_main_conf_t *lmcf,
        ngx_log_t *log)
    {
        lua_createtable(L, 0 /* narr */, 113 /* nrec */);    /* ngx.* */
    
        lua_pushcfunction(L, ngx_stream_lua_get_raw_phase_context);
        lua_setfield(L, -2, "_phase_ctx");
    
    
        ngx_stream_lua_inject_core_consts(L);
    
        ngx_stream_lua_inject_log_api(L);
        ngx_stream_lua_inject_output_api(L);
        ngx_stream_lua_inject_string_api(L);
        ngx_stream_lua_inject_control_api(log, L);
    
    
        ngx_stream_lua_inject_sleep_api(L);
        ngx_stream_lua_inject_phase_api(L);
    
        ngx_stream_lua_inject_req_api(log, L);
    
    
        ngx_stream_lua_inject_shdict_api(lmcf, L);
        ngx_stream_lua_inject_socket_tcp_api(log, L);
        ngx_stream_lua_inject_socket_udp_api(log, L);
        ngx_stream_lua_inject_uthread_api(log, L);
        ngx_stream_lua_inject_timer_api(L);
        ngx_stream_lua_inject_config_api(L);
    
        lua_getglobal(L, "package"); /* ngx package */
        lua_getfield(L, -1, "loaded"); /* ngx package loaded */
        lua_pushvalue(L, -3); /* ngx package loaded ngx */
        lua_setfield(L, -2, "ngx"); /* ngx package loaded */
        lua_pop(L, 2);
    
        lua_setglobal(L, "ngx");
    
        ngx_stream_lua_inject_coroutine_api(log, L);
    }
    

    小结

    初始化阶段的主要工作就是这些,简单小结一下,配置项解析阶段完成了Lua代码key的创建,配置项合并阶段完成了Lua钩子回调的设置,postconfig阶段完成了Lua虚拟机的初始化,其中包括registry和globals的初始化。当master进程fork出worker子进程之后,每个worker都将有一个自己的Lua VM实例。

    进入Lua钩子

    接下来,我们来看连接发起阶段。当监听的socket接收到连接请求之后,会调用accept建立连接,因为是stream子系统调用到ngx_stream_init_connection,又因为是ssl server会先走到ngx_stream_ssl_handler,里面调用ngx_ssl_create_connection创建连接(SSL_new(ssl->ctx)),最终会调用SSL_do_handshake进入SSL状态机。

    for ( ;; ) {
        ngx_process_events_and_timers(cycle)
        +-- ngx_epoll_process_events()
            |-- epoll_wait()
            +-- ngx_event_accept()
                |-- accept4()
                |-- ngx_get_connection()
                +-- ngx_stream_init_connection()
                    +-- ngx_stream_session_handler()
                        |-- s = ngx_pcalloc(c->pool, sizeof(ngx_stream_session_t))
                        +-- ngx_stream_core_run_phases()
                            +-- ngx_stream_core_generic_phase()
                                +-- ngx_stream_ssl_handler()
                                    +-- ngx_stream_ssl_init_connection()
                                        |-- ngx_ssl_create_connection()
                                        |   +-- SSL_new(ssl->ctx)
                                        +-- ngx_ssl_handshake()
                                            |-- SSL_do_handshake()
                                            |-- sslerr = SSL_get_error();
                           
    }
    

    SSL状态机的部分不是我们今天的重点,这里暂且略过。

    ossl_statem_accept()
    +-- state_machine()
        +-- read_state_machine()
            +-- ossl_statem_server_post_process_message()
    

    状态机最终会调用到tls_post_process_client_hello()里的cert_cb。这个回调我们已经在配置初始化阶段设置了,在创建SSL连接的时候又会拷贝到SSL结构体里。

    tls_post_process_client_hello()
    +-- s->cert->cert_cb(); /* 即ngx_stream_lua_ssl_cert_handler */
        |   /* 即ngx_stream_lua_ssl_cert_handler_inline */
        +-- lscf->srv.ssl_cert_handler(r, lscf, L); 
            |-- ngx_stream_lua_cache_loadbuffer()
            +-- ngx_stream_lua_ssl_cert_by_chunk()
                |-- ngx_stream_lua_create_ctx()
                |-- lua_xmove(L, co, 1);    /* 将代码闭包从L移到co上 */
                |-- ngx_stream_lua_new_thread()
                +-- ngx_stream_lua_run_thread() 
                    |-- lua_resume()
    
    

    ngx_stream_lua_ssl_cert_handler中会做一些初始化工作,如创建fake连接、fake会话、fake请求(因为还在SSL握手阶段,还没有真实的前端请求),设置默认的返回码。

    fc = ngx_stream_lua_create_fake_connection(NULL);
    fs = ngx_stream_lua_create_fake_session(fc);
    r = ngx_stream_lua_create_fake_request(fs);
    cctx->exit_code = 1;  /* successful by default */
    cctx->connection = c;
    cctx->request = r;
    cctx->entered_cert_handler = 1;
    cctx->done = 0;
    SSL_set_ex_data(c->ssl->connection, ngx_stream_lua_ssl_ctx_index, 
                    cctx)
    
    

    然后因为是用配置指令是xxx_by_lua_block所以调用ngx_stream_lua_ssl_cert_handler_inline,它里面会加载Lua代码。如果是第一次加载会把代码块加载为一个Lua函数闭包工厂,然后保存闭包工厂到虚拟机的注册表上并生成一个闭包到栈顶;后续会直接从虚拟机注册表上查找并生成闭包到栈顶。

    ngx_int_t
    ngx_stream_lua_ssl_cert_handler_inline(ngx_stream_lua_request_t *r,
        ngx_stream_lua_srv_conf_t *lscf, lua_State *L)
    {
            rc = ngx_stream_lua_cache_loadbuffer(r->connection->log, L,
                                             lscf->srv.ssl_cert_src.data,
                                             lscf->srv.ssl_cert_src.len,
                                             lscf->srv.ssl_cert_src_key,
                                             "=ssl_certificate_by_lua");
            return ngx_stream_lua_ssl_cert_by_chunk(L, r);
    }
    

    接下来就是进入by_chunk()准备执行Lua代码了,这里首先创建模块ctx,接着在虚拟机上创建一个入口线程,并把代码闭包从虚拟机栈上移到新线程的栈上,还在fake请求上挂了一个cleanup。然后就是调用run_thread()进入协程调度循环了。里面的事情我们已经在上一篇中讲到了,lua_resume()开始执行我们的Lua代码。

    ctx = ngx_stream_lua_create_ctx(r->session);
    ctx->entered_content_phase = 1;
    /* 创建入口线程 */
    co = ngx_stream_lua_new_thread(r, L, &co_ref);
    /* 将代码闭包移到入口线程中 */
    lua_xmove(L, co, 1);
    /* 设置闭包的环境表为新协程的全局表 */
    ngx_stream_lua_get_globals_table(co);
    lua_setfenv(co, -2);
    /* 把nginx请求保存到协程全局表中 */
    ngx_stream_lua_set_req(co, r);
    
    /* 注册请求的cleanup hooks */
    if (ctx->cleanup == NULL) {
        cln = ngx_stream_lua_cleanup_add(r, 0);
        if (cln == NULL) {
            rc = NGX_ERROR;
            ngx_stream_lua_finalize_request(r, rc);
            return rc;
        }
    
        cln->handler = ngx_stream_lua_request_cleanup_handler;
        cln->data = ctx;
        ctx->cleanup = &cln->handler;
    }
    
    ctx->context = NGX_STREAM_LUA_CONTEXT_SSL_CERT;
    rc = ngx_stream_lua_run_thread(L, r, ctx, 0);
    

    I/O等待挂起

    我们在初始化阶段已经将Lua Ngx API设置到全局表中了,所以ngx.sleep()会调用到对应的C函数ngx_stream_lua_ngx_sleep(),里面主要是设置了一个定时器,其事件的handler是ngx_stream_lua_sleep_handler()。挂完定时器,就直接lua_yield()了。

        coctx->sleep.handler = ngx_stream_lua_sleep_handler;
        coctx->sleep.data = coctx;
        coctx->sleep.log = r->connection->log;
    
        ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
        return lua_yield(L, 0);
    

    回到我们的主线程run_thread()之后,因为是I/O等待就直接返回NGX_AGAIN

    rv = lua_resume(orig_coctx->co, nrets);
    switch (rv) {
        case LUA_YIELD:
            switch (ctx->co_op) {
                case NGX_STREAM_LUA_USER_CORO_NOP:
                    ctx->cur_co_ctx = NULL;
                    return NGX_AGAIN;
            }
    }
    

    这样又回到了我们的by_chunk()函数,因为返回值是NGX_AGAIN所以会检查先队列里面有没有posted的协程,如果有的话会去恢复协程的执行,在我们这个例子是没有的,不过它的返回值rc改成了NGX_DONE,所以ngx_stream_lua_finalize_request(r, rc);里啥也没干就返回了。

        rc = ngx_stream_lua_run_thread(L, r, ctx, 0);
    
        if (rc == NGX_ERROR || rc >= NGX_OK) {
            /* do nothing */
    
        } else if (rc == NGX_AGAIN) {
            rc = ngx_stream_lua_content_run_posted_threads(L, r, ctx, 0);
    
        } else if (rc == NGX_DONE) {
            rc = ngx_stream_lua_content_run_posted_threads(L, r, ctx, 1);
    
        } else {
            rc = NGX_OK;
        }
    
        ngx_stream_lua_finalize_request(r, rc);
        return rc;
    
    

    这个NGX_DONE的返回值往回传递到ngx_stream_lua_ssl_cert_handler,在这里会对不同返回值做不同处理。如果是完成NGX_OK或出错NGX_ERROR的情况,就意味着钩子的工作已经结束了。我们目前的返回值是NGX_DONE,说明工作还没有结束,它在返回-1之前,挂了两个cleanup。其中_done()的那个是挂在fake连接的pool上的,而_aborted()那个是是挂在前端连接上的。所以_done()函数上在钩子工作结束之后调用的,而_aborted()是在前端连接终止的时候调用。

     rc = lscf->srv.ssl_cert_handler(r, lscf, L);
    /* 已经处理完毕或者出错的情况 */
    if (rc >= NGX_OK || rc == NGX_ERROR) {
        cctx->done = 1;
        ...;
        return cctx->exit_code;
    }
    /* rc == NGX_DONE */
    
    cln = ngx_pool_cleanup_add(fc->pool, 0);
    
    cln->handler = ngx_stream_lua_ssl_cert_done;
    cln->data = cctx;
    
    if (cctx->cleanup == NULL) {
        cln = ngx_pool_cleanup_add(c->pool, 0);
    
        cln->data = cctx;
        cctx->cleanup = &cln->handler;
    }
    
    *cctx->cleanup = ngx_stream_lua_ssl_cert_aborted;
    
    return -1;
    

    这样就回到了OpenSSL的领地,我们看看出去的流程是怎么样的。因为上层的返回值是-1,这里设置状态为SSL_X509_LOOKUP然后返回WORK_MORE_B

    int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
    if (rv < 0) {
        s->rwstate = SSL_X509_LOOKUP;
        return WORK_MORE_B;
    }
    

    这个返回值传递到read_state_machine,变成了返回SUB_STATE_ERROR

    case READ_STATE_POST_PROCESS:
        st->read_state_work = post_process_message(s, st->read_state_work);
        switch (st->read_state_work) {
        case WORK_ERROR:
            check_fatal(s, SSL_F_READ_STATE_MACHINE);
            /* Fall through */
        case WORK_MORE_A:
        case WORK_MORE_B:
        case WORK_MORE_C:
            return SUB_STATE_ERROR;
    

    传递到state_machine,变成了返回-1。最终ossl_statem_acceptSSL_do_handshake()都返回这个值。

            if (st->state == MSG_FLOW_READING) {
                ssret = read_state_machine(s);
                if (ssret == SUB_STATE_FINISHED) {
                    st->state = MSG_FLOW_WRITING;
                    init_write_state_machine(s);
                } else {
                    /* NBIO or error */
                    goto end;
                }
    

    看看回到nginx之后做了什么,因为返回值是-1,所以会先去获取错误类型,因为之前在cert_cb()返回以后已经设置了s->rwstate = SSL_X509_LOOKUP;所以会返回SSL_ERROR_WANT_X509_LOOKUP,这里将读写事件的回调设置为ssl握手的回调以便下次恢复。

    n = SSL_do_handshake(c->ssl->connection);
    /* ... */
    sslerr = SSL_get_error(c->ssl->connection, n);
    /* ... */
    if (sslerr == SSL_ERROR_WANT_X509_LOOKUP)
    {
        c->read->handler = ngx_ssl_handshake_handler;
        c->write->handler = ngx_ssl_handshake_handler;
    
        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
            return NGX_ERROR;
        }
    
        if (ngx_handle_write_event(c->write, 0) != NGX_OK) {
            return NGX_ERROR;
        }
    
        return NGX_AGAIN;
    }
    
    

    然后NGX_AGAIN的返回值一直往上传递,直到ngx_stream_core_generic_phase变为NGX_OK。然后本次的事件处理就算结束了。

    事件触发时恢复

    ngx_process_events_and_timers
    |-- ngx_event_expire_timers
        |-- ngx_stream_lua_sleep_handler
            |-- ngx_stream_lua_sleep_resume
                |-- ngx_stream_lua_run_thread
    
    

    等到定时器超时的时候,会执行我们之前设置的ngx_stream_lua_sleep_handler,里面会设置当前协程上下文,然后调用ngx_stream_lua_sleep_resume()

    coctx = ev->data;
    ctx->cur_co_ctx = coctx;
    if (ctx->entered_content_phase) {
        (void) ngx_stream_lua_sleep_resume(r);
    }
    
    

    ngx_stream_lua_sleep_resume里调用ngx_stream_lua_run_thread恢复协程的执行。这样就又回到了我们的Lua代码里。

    Lua钩子正常执行结束

    接下来Lua代码执行完毕,lua_resume()返回,因为是协程正常结束,且没有其他在posted队列里的协程了,所以run_thread()直接返回NGX_OK。因此在ngx_stream_lua_finalize_request里就会实际清除fake请求。

    rc = ngx_stream_lua_run_thread(vm, r, ctx, 0);
    
    if (rc == NGX_AGAIN) {
        return ngx_stream_lua_run_posted_threads(c, vm, r, ctx, nreqs);
    }
    
    if (rc == NGX_DONE) {
        ngx_stream_lua_finalize_request(r, NGX_DONE);
        return ngx_stream_lua_run_posted_threads(c, vm, r, ctx, nreqs);
    }
    
    if (ctx->entered_content_phase) {
        ngx_stream_lua_finalize_request(r, rc);
        return NGX_DONE;
    }
    
    return rc;
    
    

    里面会调用到之前设置的cleanup函数,清理fake请求的时候调用ngx_stream_lua_request_cleanup_handler清理Lua线程。

    cln = r->cleanup;
    r->cleanup = NULL;
    while (cln) {
        if (cln->handler) {
            cln->handler(cln->data);
        }
    
        cln = cln->next;
    }
    
    r->connection->destroyed = 1;
    

    清理fake连接的时候调用ngx_stream_lua_ssl_cert_done。我们来看看ngx_stream_lua_ssl_cert_done里面做了什么。主要是设置了完成标志,然后把前端连接的写事件加入了ngx_posted_events队列里。

    cctx->done = 1;
    ngx_post_event(c->write, &ngx_posted_events);
    

    定时器超时事件完成之后返回到外层,处理后续的ngx_posted_events队列事件。

    (void) ngx_process_events(cycle, timer, flags);
    
    delta = ngx_current_msec - delta;
    
    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);
    
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }
    
    if (delta) {
        ngx_event_expire_timers();
    }
    
    ngx_event_process_posted(cycle, &ngx_posted_events);
    

    因为前端连接的写事件已经设置成ngx_ssl_handshake_handler,所以会再次调用到ngx_ssl_handshake-SSL_do_handshake,这样就再次进入了SSL状态机,又会来到ngx_stream_lua_ssl_cert_handler中。因为是第二次进入了,且已经设置了cctx->done,所以就直接返回离开码了,其中cctx->exit_code就是ngx.exit()时的参数,cctx->exit_code初始化时的默认值时0,但是注意到前面第一次进入ngx_stream_lua_ssl_cert_handler的时候已经将默认值设为1了。

    if (cctx && cctx->entered_cert_handler) {
        /* not the first time */
    
        if (cctx->done) {
            ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
                           "stream lua_certificate_by_lua:"
                           " cert cb exit code: %d",
                           cctx->exit_code);
    
            dd("lua ssl cert done, finally");
            return cctx->exit_code;
        }
    
        return -1;
    }
    
    

    接下来,回到了tls_post_process_client_hello()继续后面的握手流程了。

    Lua钩子内出错的情况

    出错的流程跟正常结束类似,只不过返回值不一样。ngx.exit()的实现如下

    ngx.exit = function (rc)
        local err = get_string_buf(ERR_BUF_SIZE)
        local errlen = get_size_ptr()
        local r = get_request()
        if r == nil then
            error("no request found")
        end
        errlen[0] = ERR_BUF_SIZE
        rc = ngx_lua_ffi_exit(r, rc, err, errlen)
        if rc == 0 then
            -- print("yielding...")
            return co_yield()
        end
        if rc == FFI_DONE then
            return
        end
        error(ffi_string(err, errlen[0]), 2)
    end
    

    里面会调用ffi函数ngx_stream_lua_ffi_exit(),在其中设置ctx->exit_code,然后返回NGX_OK

    if (ctx->context & (NGX_STREAM_LUA_CONTEXT_SSL_CERT
                        | NGX_STREAM_LUA_CONTEXT_SSL_CLIENT_HELLO ))
    {
        ctx->exit_code = status;
        ctx->exited = 1;
        return NGX_OK;
    }
    

    回到ngx.exit()函数之后,就调用原生的coroutine.yield(),回到我们的主线程run_thread()之后,因为设置了ctx->exited会调用ngx_stream_lua_handle_exit返回

    rv = lua_resume(orig_coctx->co, nrets);
    switch (rv) {
        case LUA_YIELD:
            if (ctx->exited) {
                return ngx_stream_lua_handle_exit(L, r, ctx);
            }
    }
    

    ngx_stream_lua_handle_exit()里面调用ngx_stream_lua_request_cleanup清理线程。

    ctx->cur_co_ctx->co_status = NGX_STREAM_LUA_CO_DEAD;
    ngx_stream_lua_request_cleanup(ctx, 0);
    return ctx->exit_code;
    
    

    然后返回到sleep_resume,此时rcctx->exit_code,即ngx.ERROR,接下来跟正常结束时一样也是结束我们的请求

    rc = ngx_stream_lua_run_thread(L, r, ctx, 0);
    ...;
    if (ctx->entered_content_phase) {
        ngx_stream_lua_finalize_request(r, rc);
        return NGX_DONE;
    }
    return rc;
    
    

    因为是fake请求,ngx_stream_lua_finalize_request调用ngx_stream_lua_finalize_fake_request,里面将cctx->exit_code设为0。

    if (rc == NGX_ERROR || rc >= NGX_STREAM_BAD_REQUEST) {
        if (r->connection->ssl) {
            ssl_conn = r->connection->ssl->connection;
            if (ssl_conn) {
                c = ngx_ssl_get_connection(ssl_conn);
                if (c && c->ssl) {
                    cctx = ngx_stream_lua_ssl_get_ctx(c->ssl->connection);
                    if (cctx != NULL) {
                        cctx->exit_code = 0;
                    }
                }
            }
        }
        ngx_stream_lua_close_fake_request(r);
        return;
    }
    

    在清理fake请求的时候调用ngx_stream_lua_request_cleanup_handler清理Lua线程。在清理fake连接的时候会触发ngx_stream_lua_ssl_cert_done,跟正常完成时一样,也是设置完成标志,然后把前端连接的写事件加入了ngx_posted_events队列里。

    cctx->done = 1;
    ngx_post_event(c->write, &ngx_posted_events);
    

    到此定时器的事件就结束了,开始处理后续的posted队列事件。同样地,也会再次调用ngx_ssl_handshake_handler最终调到到ngx_stream_lua_ssl_cert_handler中。因为是第二次进入了,且已经设置了cctx->done,所以就直接返回离开码了,而本次因为是出错cctx->exit_code的值是0.

    返回到OpenSSL之后,一路往上传递错误码。。。

    int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
    if (rv == 0) {
        goto err;
    }
    err:
    return WORK_ERROR;
    

    最终,SSL_do_handshake返回错误值,结束SSL握手。

    n = SSL_do_handshake(c->ssl->connection);
    sslerr = SSL_get_error(c->ssl->connection, n);
    return NGX_ERROR;
    

    总结

    我们本篇是以一个定时器为例子,对于socket I/O等待其实也是类似的流程。只不过触发事件由定时器超时变成了相应的fd的读写事件,协程的恢复由定时器时的直接恢复变成了完成本次I/O任务(或者出错)之后恢复协程。

  • 相关阅读:
    石子合并问题(直线版)
    Python_07-常用函数
    Python_06-函数与模块
    C++中的头文件和源文件
    sell 项目 商品表 设计 及 创建
    SpringBoot集成Mybatis
    SpringBoot集成jdbcTemplate/JPA
    SpringBoot使用JSP渲染页面
    SpringBoot引入freemaker前端模板
    使用SpringBoot创建Web项目
  • 原文地址:https://www.cnblogs.com/logchen/p/15149951.html
Copyright © 2011-2022 走看看