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】开始。