在服务器高并发场景中,多线程的线程栈资源占用和内核态上下文切换开销会成为性能瓶颈,基于汇编实现的C/C++协程可以在用户态完成调度,大幅降低资源消耗。协程本质是用户态的轻量级线程,核心能力是保存当前执行上下文并切换到其他协程执行,而上下文的保存恢复需要依赖汇编直接操作寄存器和栈空间。

协程核心概念与服务器场景适配
协程相比线程的优势在于:协程的栈空间可以自定义大小,默认几KB即可满足需求,而线程栈默认通常是几MB;协程切换不需要进入内核态,仅需要保存恢复少量寄存器和栈状态,切换开销仅为线程的十分之一甚至更低。在服务器处理大量网络连接时,每个连接对应一个协程,能轻松支撑数十万并发连接,而多线程模型往往在几千连接时就会出现资源不足的问题。
协程上下文结构设计
协程上下文需要保存所有会被调用者修改的寄存器,以及栈指针和返回地址,在x86_64架构下,常用的需要保存的寄存器包括rbx、rbp、r12到r15,还有栈指针rsp和指令指针rip。我们定义如下的上下文结构体:
// 协程上下文结构体,保存x86_64架构下需要恢复的寄存器
typedef struct coroutine_context {
void *rip; // 指令指针,存储协程下次执行的地址
void *rsp; // 栈指针,存储协程栈的当前位置
void *rbx; // 被调用者保存寄存器
void *rbp; // 栈基址寄存器
void *r12; // 被调用者保存寄存器
void *r13; // 被调用者保存寄存器
void *r14; // 被调用者保存寄存器
void *r15; // 被调用者保存寄存器
} coroutine_context_t;
汇编实现上下文切换
上下文切换是协程的核心,需要实现两个功能:保存当前协程的上下文到结构体,从目标协程的上下文结构体恢复执行状态。我们编写两个汇编函数,分别是保存上下文的save_context和恢复上下文的restore_context。
保存上下文汇编实现
保存上下文时,需要将当前寄存器的值存入传入的上下文结构体指针指向的内存中:
; 保存上下文函数,参数:rdi = 上下文结构体指针
save_context:
mov [rdi], rsp ; 保存当前栈指针到上下文的rsp字段
mov [rdi+8], rbx ; 保存rbx寄存器
mov [rdi+16], rbp ; 保存rbp寄存器
mov [rdi+24], r12 ; 保存r12寄存器
mov [rdi+32], r13 ; 保存r13寄存器
mov [rdi+40], r14 ; 保存r14寄存器
mov [rdi+48], r15 ; 保存r15寄存器
; 返回地址已经在栈上,不需要额外保存,切换时直接从栈弹出即可
xor rax, rax ; 返回0表示保存成功
ret
恢复上下文汇编实现
恢复上下文时,从目标上下文结构体中取出寄存器值,然后跳转到目标协程的执行地址:
; 恢复上下文函数,参数:rdi = 目标上下文结构体指针
restore_context:
mov rsp, [rdi] ; 恢复栈指针
mov rbx, [rdi+8] ; 恢复rbx寄存器
mov rbp, [rdi+16] ; 恢复rbp寄存器
mov r12, [rdi+24] ; 恢复r12寄存器
mov r13, [rdi+32] ; 恢复r13寄存器
mov r14, [rdi+40] ; 恢复r14寄存器
mov r15, [rdi+48] ; 恢复r15寄存器
; 此时栈上已经存放了目标协程的返回地址,直接ret即可跳转到目标执行位置
xor rax, rax ; 返回0表示恢复成功
ret
协程管理与调度实现
有了上下文切换能力后,我们需要实现协程的创建、销毁和调度逻辑,这里实现一个简单的服务器协程调度器,支持协程的创建和轮询执行。
协程结构体定义
// 协程状态枚举
typedef enum coroutine_status {
COROUTINE_READY, // 就绪状态
COROUTINE_RUNNING, // 运行状态
COROUTINE_SUSPENDED, // 挂起状态
COROUTINE_DEAD // 结束状态
} coroutine_status_t;
// 协程结构体
typedef struct coroutine {
coroutine_context_t ctx; // 协程上下文
void *stack; // 协程栈空间指针
size_t stack_size; // 协程栈大小
coroutine_status_t status; // 协程状态
void (*entry)(void *arg); // 协程入口函数
void *arg; // 入口函数参数
struct coroutine *next; // 下一个协程指针,用于调度链表
} coroutine_t;
协程创建函数
创建协程时需要分配栈空间,初始化上下文的入口地址和栈指针:
#include <stdlib.h>
#include <string.h>
// 创建协程,entry为入口函数,arg为参数,stack_size为栈大小
coroutine_t *coroutine_create(void (*entry)(void *arg), void *arg, size_t stack_size) {
coroutine_t *co = (coroutine_t *)malloc(sizeof(coroutine_t));
if (!co) return NULL;
memset(co, 0, sizeof(coroutine_t));
co->stack = malloc(stack_size);
if (!co->stack) {
free(co);
return NULL;
}
co->stack_size = stack_size;
co->entry = entry;
co->arg = arg;
co->status = COROUTINE_READY;
// 初始化上下文:栈顶存放入口函数地址,栈指针指向栈顶下方8字节(模拟函数调用后的栈状态)
co->ctx.rsp = (char *)co->stack + stack_size - 8;
*(void **)co->ctx.rsp = entry; // 栈顶存放入口函数地址,ret时会跳转到该地址执行
return co;
}
协程调度器实现
调度器维护一个就绪协程链表,轮询执行所有就绪状态的协程,遇到阻塞操作就挂起协程,切换执行其他协程:
// 调度器结构体
typedef struct scheduler {
coroutine_t *current; // 当前运行的协程
coroutine_t *ready_list; // 就绪协程链表头
} scheduler_t;
// 初始化调度器
void scheduler_init(scheduler_t *sched) {
memset(sched, 0, sizeof(scheduler_t));
}
// 将协程加入就绪链表
void scheduler_add_ready(scheduler_t *sched, coroutine_t *co) {
co->next = sched->ready_list;
sched->ready_list = co;
co->status = COROUTINE_READY;
}
// 协程让出CPU,切换到下一个就绪协程
void coroutine_yield(scheduler_t *sched) {
coroutine_t *cur = sched->current;
if (!cur) return;
// 保存当前协程上下文
save_context(&cur->ctx);
cur->status = COROUTINE_SUSPENDED;
// 切换到下一个就绪协程
coroutine_t *next = sched->ready_list;
if (next) {
sched->ready_list = next->next;
sched->current = next;
next->status = COROUTINE_RUNNING;
restore_context(&next->ctx);
}
}
服务器场景中的协程使用示例
在服务器网络处理中,每个客户端连接对应一个协程,协程处理完一次请求后主动让出CPU,等待下一个请求事件到来再恢复执行,下面是一个简单的模拟服务器处理逻辑:
#include <stdio.h>
#include <unistd.h>
// 模拟处理客户端连接请求的协程入口
void handle_client(void *arg) {
int client_fd = *(int *)arg;
printf("协程开始处理客户端 %d 的请求n", client_fd);
// 模拟读取请求数据,这里用sleep模拟阻塞等待
sleep(1);
printf("协程处理完客户端 %d 的请求,准备让出CPUn", client_fd);
// 这里需要传入调度器指针,实际实现中可以通过线程局部变量传递调度器
// coroutine_yield(sched);
}
int main() {
scheduler_t sched;
scheduler_init(&sched);
// 创建3个模拟客户端连接的协程
for (int i = 0; i < 3; i++) {
int client_fd = i + 1;
coroutine_t *co = coroutine_create(handle_client, &client_fd, 4096);
if (co) {
scheduler_add_ready(&sched, co);
}
}
// 启动调度循环
while (sched.ready_list) {
coroutine_t *co = sched.ready_list;
sched.ready_list = co->next;
sched.current = co;
co->status = COROUTINE_RUNNING;
// 恢复协程执行
restore_context(&co->ctx);
// 协程执行完后销毁
free(co->stack);
free(co);
}
return 0;
}
注意事项与优化方向
- 栈溢出问题:需要给协程栈设置合理的警戒页,当栈使用超过阈值时触发异常,避免栈溢出覆盖其他内存。
- 汇编兼容性:不同CPU架构的寄存器数量和调用约定不同,上述代码仅适配x86_64架构,ARM架构需要重新编写汇编上下文切换代码。
- 调度策略优化:可以根据服务器场景实现优先级调度、事件驱动的协程唤醒,比如网络数据到达时再唤醒对应的处理协程,减少无效轮询。
- 错误处理:实际使用中需要增加内存分配失败、上下文切换异常等错误处理逻辑,提升协程库的健壮性。