OpenSSL异步模式流程梳理

源码

来源于 OpenSSL Master Commit ID d550d2aae531c6fa2e10b1a30d2acdf373663889

总览

核心入口函数为 ssl_start_async_job,以 SSL_do_handshake 为入口举例分析,同时通过标注步骤【1~N】,来明确阅读的顺序。

  • 步骤【1】到步骤【18】为一个阶段
  • 步骤【19】到步骤【23】为一个阶段
  • 步骤【24】到步骤【34】之后为一个阶段
    为了减少阅读的干扰和提升阅读效率,不相关的代码省略,同时部分代码会在注释中展示

流程

SSL_do_handshake

int SSL_do_handshake(SSL *s)
{
    SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL(s);
    /* 步骤【24】
     * 再次进入
     */

    if ((sc->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL) {
        /* 步骤【1】
         * 检测是否开启了异步模式,并且没有正在运行的 job
         * 通过 ASYNC_get_current_job 可获取当前正在运行的 job
         * ASYNC_get_current_job 中通过 async_get_ctx 获取线程变量 ctx
         * ctx 中的字段 currjob 即为返回值
         */
        struct ssl_async_args args;
        memset(&args, 0, sizeof(args));
        args.s = s;

        /* 步骤【2】
         * 通过 ssl_start_async_job 启动一个异步 job
         * 关注一下 args 中赋值的唯一一个参数 s
         *
         * 步骤【25】
         * 通过 ssl_start_async_job 启动一个异步 job
         */
        ret = ssl_start_async_job(s, &args, ssl_do_handshake_intern);

        /* 步骤【18】
         * 至此通过 ssl_start_async_job 得到了与 else 中一样的返回值
         * 因为提到过 ssl_start_async_job 中最终也是执行的 sc->handshake_func,且执行完成
         *
         * 步骤【23】
         * 返回值为 -1,但此时 job,并没有完成
         * 函数的调用者需要有能力处理这种情况,并且在适当实际继续执行 job
         * 这就需要调用者去适配 OpenSSL 异步模式
         */
    } else {
        ret = sc->handshake_func(s);
    }
}

ssl_start_async_job

static int ssl_start_async_job(SSL *s, struct ssl_async_args *args,
                               int (*func) (void *))
{
    /* 步骤【3】
     * 启动一个 job
     * sc->job 在首次调用时为 NULL
     * args.s 为 s
     * func 为 ssl_do_handshake_intern
     *
     * 步骤【26】
     */
    switch (ASYNC_start_job(&sc->job, sc->waitctx, &ret, func, args,
                            sizeof(struct ssl_async_args))) {
    /* 步骤【16】
     * 返回值为 ASYNC_FINISH
     *
     * 步骤【21】
     * 返回值为 ASYNC_PAUSE
     */
    case ASYNC_ERR:
        sc->rwstate = SSL_NOTHING;
        ERR_raise(ERR_LIB_SSL, SSL_R_FAILED_TO_INIT_ASYNC);
        return -1;
    case ASYNC_PAUSE:
        /* 步骤【22】
         */
        sc->rwstate = SSL_ASYNC_PAUSED;
        return -1;
    case ASYNC_NO_JOBS:
        sc->rwstate = SSL_ASYNC_NO_JOBS;
        return -1;
    case ASYNC_FINISH:
        /* 步骤【17】
         */
        sc->job = NULL;
        return ret;
    default:
        sc->rwstate = SSL_NOTHING;
        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
        /* Shouldn't happen */
        return -1;
    }
}

ASYNC_start_job

int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *wctx, int *ret,
                    int (*func)(void *), void *args, size_t size)
{
    /* 步骤【4】
     * ctx 为线程变量,对线程而言全局可见,当前为第一次
     * 会创建 ctx,后续的 job 都能通过 async_get_ctx() 获取
     *
     * 步骤【27】
     * ctx 为线程变量,之前已经创建过,可以直接获取
     */
    ctx = async_get_ctx();
    if (ctx == NULL)
        ctx = async_ctx_new();
    if (ctx == NULL)
        return ASYNC_ERR;

    /* 步骤【5】
     * 首次调用 *job 应为 NULL
     *
     * 步骤【28】
     * job 有值
     */
    if (*job != NULL)
        ctx->currjob = *job;

    for (;;) {
        /* 步骤【6】
         * ctx 刚创建,ctx->currjob 为 NULL,跳过
         *
         * 步骤【14】
         * 此时 ctx->currjob 有值,且 ctx->currjob->status 为 ASYNC_JOB_STOPPING
         *
         * 步骤【29】
         * 此时 ctx->currjob 有值,且 ctx->currjob->status 为 ASYNC_JOB_PAUSED
         */
        if (ctx->currjob != NULL) {
            if (ctx->currjob->status == ASYNC_JOB_STOPPING) {
                /* 步骤【15】
                 * job 完成,返回 ASYNC_FINISH
                 * 此时,可以释放针对该 job 申请的各种资源
                 * 重置线程变量 ctx 上有关的值
                 */
                *ret = ctx->currjob->ret;
                ctx->currjob->waitctx = NULL;
                async_release_job(ctx->currjob);
                ctx->currjob = NULL;
                *job = NULL;
                return ASYNC_FINISH;
            }

            if (ctx->currjob->status == ASYNC_JOB_PAUSING) {
                /* 步骤【20】
                 * job 暂停,返回 ASYNC_PAUSE
                 * 此时,不要释放针对该 job 申请的各种资源
                 * 仅仅将 currjob 设置为 NULL
                 */
                *job = ctx->currjob;
                ctx->currjob->status = ASYNC_JOB_PAUSED;
                ctx->currjob = NULL;
                return ASYNC_PAUSE;
            }

            if (ctx->currjob->status == ASYNC_JOB_PAUSED) {
                /* 步骤【30】
                 * 将 ctx->currjob->fibrectx->fibre 作为目标上下文切换
                 * ctx->currjob->fibrectx->fibre 中保存的即为
                 * ASYNC_pause_job 中切换上下文时保存的信息
                 * 当前上下午的信息保存在 ctx->dispatcher->fibre 中
                 */
                if (!async_fibre_swapcontext(&ctx->dispatcher,
                        &ctx->currjob->fibrectx, 1)) {
                    ctx->currjob->libctx = OSSL_LIB_CTX_set0_default(libctx);
                    ERR_raise(ERR_LIB_ASYNC, ASYNC_R_FAILED_TO_SWAP_CONTEXT);
                    goto err;
                }

                /* 步骤【34】
                 * 上下文切换回此,由于 job 已经完成,status 为 ASYNC_JOB_STOPPING
                 * 接下来的流程与步骤【15】相同
                 */
                continue;
            }
        }

        /* 步骤【7】
         * 由于 job 尚未创建,通过 async_get_pool_job 从池子中获取一个 job
         * 由于 job 中有参数需要关注,所以还要看一下 async_get_pool_job 的实现
         * 具体的 async_get_pool_job 中调用 async_job_new 创建 job
         * 然后调用 async_fibre_makecontext 初始化 job->fibrectx
         * 使用 makecontext(&fibre->fibre, async_start_func, 0),其中的 fibre 即 job->fibrectx
         * makecontext 是 libc 提供的库,与之对应的是 swapcontext
         * makecontext 用于组装上下文信息,swapcontext 用于切换上下文信息
         * 后续有关 async_fibre_makecontext 和 async_fibre_swapcontext 其实就是
         * makecontext 与 swapcontext
         * 当通过 swapcontext 切换上下文,切换的目标为 job->fibrectx->fibre,就会调用 async_start_func
         */
        if ((ctx->currjob = async_get_pool_job()) == NULL)
            return ASYNC_NO_JOBS;

        /* 步骤【8】
         * 将 args,func(ssl_do_handshake_intern)等参数绑定至 ctx->currjob
         */
        if (args != NULL) {
            ctx->currjob->funcargs = OPENSSL_malloc(size);
            if (ctx->currjob->funcargs == NULL) {
                async_release_job(ctx->currjob);
                ctx->currjob = NULL;
                return ASYNC_ERR;
            }
            memcpy(ctx->currjob->funcargs, args, size);
        } else {
            ctx->currjob->funcargs = NULL;
        }
        ctx->currjob->func = func;
        ctx->currjob->waitctx = wctx;

        /* 步骤【9】
         * 至此,job 初始化完成
         * 其中有几个关键参数,再总结一下,即
         * func 为 ssl_do_handshake_intern
         * args 中的 args.s 为 ssl_start_async_job 传入的 s
         * 该 job 也同时被绑定到了线程变量 ctx 的 currjob 上
         * job->fibrectx->fibre 与 async_start_func 绑定
         */

        /* 步骤【10】
         * 这是关键,用于做上下文切换,即上文提到过的
         * swapcontext(&o->fibre, &n->fibre)
         * 第一个参数中的 o 与第二个参数中的 n,分别对应 &ctx->dispatcher 和 &ctx->currjob->fibrectx
         * 第二个参数是目标,而这个目标就是上文反复提到的 job->fibrectx->fibre,因此会调用 async_start_func
         * 第一个参数用来存储当前状态,即会记录此时的上下文信息
         * 当下一次将 ctx->dispatcher 作为 swapcontext 的第二个参数时,就能恢复到此处继续执行
         */
        if (!async_fibre_swapcontext(&ctx->dispatcher,
                &ctx->currjob->fibrectx, 1)) {
            ERR_raise(ERR_LIB_ASYNC, ASYNC_R_FAILED_TO_SWAP_CONTEXT);
            goto err;
        }

        /* 步骤【13】
         * 在 async_start_func 中完成 job->func 后执行 swapcontext 切换回此
         * 此时的 job->status 值为 ASYNC_JOB_STOPPING
         *
         * 步骤【19】
         * 在 ASYNC_pause_job 中,执行 swapcontext 切换回此
         * 此时的 job->status 值为 ASYNC_JOB_PAUSING
         */
    }
}

async_start_func

void async_start_func(void)
{
    async_ctx *ctx = async_get_ctx();

    while (1) {
        /* 步骤【11】
         * 此处所有参数在前文都重点提到过
         * func 为 ssl_do_handshake_intern,参数 args,查看 ssl_do_handshake_intern
         * 实际上,就是调用的 sc->handshake_func
         * 这等同于步骤【1】中未开启异步模式的情况,区别就是现在是启动了一个 job 执行
         */
        job = ctx->currjob;
        job->ret = job->func(job->funcargs);

        /* 步骤【12】
         * job->func 执行完成,即 job 结束
         * 进行上下文切换,将 ctx->dispatcher 作为 swapcontext 的第二个参数
         * 回头看看,就知道切换到哪里了,当前现场保存在 job->fibrectx->fibre 中
         *
         * 步骤【33】
         * 暂停的 job 执行完成,上下文切换到目标 job->fibrectx->fibre
         */
        job->status = ASYNC_JOB_STOPPING;
        if (!async_fibre_swapcontext(&job->fibrectx,
                                     &ctx->dispatcher, 1)) {
            ERR_raise(ERR_LIB_ASYNC, ASYNC_R_FAILED_TO_SWAP_CONTEXT);
        }
    }
}

暂停

读到此处,不知是否会有这样的疑问?OpenSSL 异步模式,异步体现在哪里?上述步骤中,不就是通过一个新的上下文去执行了 sc->handshake_func 吗?这与步骤【1】的 else 中,直接执行 sc->handshake_func 有什么区别吗?如果你产生了这样的疑问,那就证明上面的步骤你应该是理解了。因为如果按照步骤这样一路看下来,确实不是异步模式,而是同步模式。

这是由于还有一个重要的机制没有介绍,那就是暂停,这个机制允许上下文执行到某个地方,切换出去,做别的事情,然后再切换回来必须执行。而这个机制起作用的地方出现在步骤【11】到步骤【12】之间,执行 job->func 中。

考虑一种情况,job->func 中,需要进行签名验签,加密解密,而这些算法是卸载到硬件的,调用算法需要等待硬件返回,而在这一段时间中,只能阻塞,导致 CPU 利用率低,例如如下代码。

void do_sign()
{
    // 获得一个硬件签名请求结构体并初始化
    struct req *r = sign_req_alloc();
    sign_req_init(r);

    // 利用硬件 API 发起签名请求
    // sign 中仅仅传递请求,就返回了,剩下就交给硬件
    sign(r);

    // 当硬件完成任务,会将 r->done 变为 true
    // 此处阻塞,不断的轮询 r->done 是否变为 true 来检测任务是否完成
    while (1) {
        if (r->done) {
            break;
        }
        usleep(0);
    }

    // 完成签名,退出
    return;
}

可以看出,如果硬件没有完成任务,就会一直执行 while(1),无法退出,导致无法处理其他任务。如果硬件性能很高,阻塞时间很短,可能影响会比较小,但是,如果硬件性能较差,阻塞时间长,就会导致 CPU 利用率很低。此时就可以使用 OpenSSL 异步模式中提供的 ASYNC_pause_job,如下代码。

void do_sign()
{
    // 获得一个硬件签名请求结构体并初始化
    struct req *r = sign_req_alloc();
    sign_req_init(r);

    // 利用硬件 API 发起签名请求
    // sign 中仅仅传递请求,就返回了,剩下就交给硬件
    sign(r);

    ASYNC_pause_job();

    /* 步骤【32】
     * do_sign 完成退出,会一直退到 async_start_func
     */

    // 完成签名,退出
    return;
}

仅仅将需要阻塞等待的代码,替换为 ASYNC_pause_job 即可。

ASYNC_pause_job

int ASYNC_pause_job(void)
{
    job = ctx->currjob;
    job->status = ASYNC_JOB_PAUSING;

    /* 上下文切换,会切换到步骤【13】的地方继续执行
     * 需要注意的是,job 的状态设置成了 ASYNC_JOB_PAUSING
     * 并且将当前的上下文保存在 job->fibrectx->fibre 中
     * 紧接着步骤【19】
     */
    if (!async_fibre_swapcontext(&job->fibrectx,
                                 &ctx->dispatcher, 1)) {
        ERR_raise(ERR_LIB_ASYNC, ASYNC_R_FAILED_TO_SWAP_CONTEXT);
        return 0;
    }

    /* 步骤【31】
     * 上下文切换之后,继续执行,就回到了 do_sign 中
     */
}

恢复

前文提到过,调用者需要有能力处理 SSL_do_handshake 返回值 -1,但 job 还存在的情况,这意味着 SSL_do_handshake 中通过 ASYNC_pause_job 暂停的 job。

而调用者需要在一个合适的时机,去继续运行该 job,如何确定什么时候是合适的时机,这是调用者需要考虑的问题,此处仅介绍如何恢复 job 的执行,很简单,再次调用 SSL_do_handshake 即可。

回到流程中,此时从步骤【24】开始。