自实现一个基于信号协程框架

275 阅读12分钟

引言

Go协程介绍

Go 协程(Goroutine)是 Go 语言实现高并发的核心机制,它不同于传统的操作系统线程(Thread),是一种用户级轻量级线程。每个 Goroutine 的初始栈大小仅为 2KB(可通过运行时调整),且由 Go 运行时(Runtime)直接管理,而非操作系统内核。这种设计使得 Goroutine 的创建、销毁和切换成本极低,轻松支持百万级并发。

没接触过GO或没听说过协程可以看看视频

到底该怎么理解协程?

GO协程的调度策略:

  • 协作式调度:Goroutine 主动让出 CPU(如 runtime.Gosched() 或通道阻塞)。
  • 抢占式调度(Go 1.14+):通过 SIGURG 信号 强制挂起长时间运行的 Goroutine,防止饿死。

GO协程是一种有栈协程

什么是有栈协程?

有栈协程(Stackful Coroutine)是具备独立栈空间的协程,其特点包括:

  • 任意嵌套挂起:可在多层函数调用中挂起,恢复时能正确回到调用点。
  • 显式/隐式切换:支持开发者手动让出(如 yield)或由运行时自动抢占。
  • 典型实现:Go 的 Goroutine、Lua 的协程(配合 LuaJIT)、C++ 的 Boost.Context。

对比无栈协程(Stackless Coroutine,如 Python 生成器):

特性有栈协程无栈协程
栈空间独立分配,可动态扩展无独立栈,依赖闭包状态
挂起点任意函数调用层级仅限顶层生成器函数
切换开销较高(需保存完整上下文)较低(仅保存局部状态)
典型应用场景通用并发模型简单迭代器/状态机

这里说的函数栈空间,是指函数栈帧,由RSP,RBP寄存器控制

CPU眼里的:{函数括号} | 栈帧 | 堆栈 | 栈变量

基于信号的抢占式协程切换原理

有栈协程的切换原理与操作系统线程切换在核心逻辑上高度相似,但实现层级和开销存在本质差异。操作系统线程切换由内核调度器通过硬件中断(如时钟中断)触发,需陷入内核态保存/恢复全套寄存器(通用寄存器、栈指针、指令指针等)、更新内存映射(CR3寄存器)及线程控制块(TCB),这一过程涉及特权级切换和缓存失效,单次开销常达微秒级。有栈协程切换则完全在用户态完成,由协程库(而非内核)直接通过汇编指令保存/恢复关键寄存器(如%rip、%rsp、%rbp)和切换私有栈空间(通过修改栈指针实现),无需特权切换或内存映射更新,单次切换可控制在百纳秒内。两者均通过“保存现场-加载新现场”实现执行流跳转,但协程因规避了内核边界 crossing 和复杂的权限检查,成为更轻量的“用户态线程”——正如线程是进程的轻量化,协程则是线程的进一步抽象,通过牺牲全局抢占性换取极致的切换效率。

简单协程实现

Linux信号处理

Linux 信号是一种异步事件通知机制,用于进程间通信或响应特定系统事件。其本质是内核向进程发送的软中断(编号 1~64),典型场景包括:

  • 用户输入SIGINT(Ctrl+C)、SIGTSTP(Ctrl+Z)。
  • 硬件异常SIGSEGV(段错误)、SIGFPE(除零错误)。
  • 定时器触发SIGALRM(定时器到期)、SIGVTALRM(虚拟时间计时器)。

注意:

信号处理函数执行时,可能中断任意线程的正常执行流,因此禁止调用非异步安全函数(如 mallocprintf)。协程调度器若在信号处理函数中触发切换,需确保所有操作仅涉及寄存器操作或原子变量。

定时器信号

此时实现中,打算使用定时器信号处理来实现定时中断协程效果

#include <iostream>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include "sys/ucontext.h"
#include <sys/time.h>
#include "cstring"
#include "task.h"

void signal_handler(int signum, siginfo_t *info, void *ucontext) {
    // 协程切换操作...
}


/**
 * 
 * 设置信号处理函数
*/
void setup_signal_handler() {
    struct sigaction sa;
    sa.sa_sigaction = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO ; // 使用 SA_SIGINFO 获取上下文

    // 注册 SIGALRM 信号处理
    if (sigaction(SIGALRM, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    // 设置定时器触发信号
    struct itimerval timer = {
        .it_interval = {.tv_sec = 0, .tv_usec = 100000}, // 100ms
        .it_value = {.tv_sec = 0, .tv_usec = 100000}
    };
    setitimer(ITIMER_REAL, &timer, NULL);
}

CPU寄存器保存与恢复

进入信号处理函数时,参数ucontext保存了CPU进入信号处理函数前的各种寄存器,若我们修改这个寄存器数组里面的数组,Linux在执行完信号处理函数恢复主流程时,把ucontext中的寄存器状态恢复到CPU上,所以我们可以在信号处理函数中保存中断前CPU现场,并恢复另一个协程的CPU现场

#define MAX_COROUTINES 10      // 系统支持的最大协程数量


// 协程状态机定义
typedef enum {
    COROUTINE_READY,    // 就绪态(可被调度)
    COROUTINE_RUNNING,  // 运行态(正在执行)
    COROUTINE_BLOCKED   // 阻塞态(等待资源)
} coroutine_state;

// 协程控制块(Coroutine Control Block)
typedef struct {
    ucontext_t context;    // 协程上下文(保存寄存器等状态)
    char* stack;           // 协程栈空间指针(需手动分配)
    coroutine_state state; // 当前状态
    void (*func)(void*);   // 协程入口函数指针
    void *arg;             // 传递给入口函数的参数
} coroutine_t;

// 调度器结构(全局单例)
typedef struct {
    coroutine_t **coroutines = new coroutine_t*[10]; // 协程指针数组(硬编码大小为10)
    int num_coroutines = 0;     // 当前管理的协程数量
    int current_index = 0;      // 当前正在运行的协程索引
} scheduler_t;

scheduler_t scheduler; // 全局调度器实例

// 保存寄存器状态到目标上下文
// current: 目标上下文结构
// uc:      当前上下文来源
void save_registers(ucontext_t *current, ucontext_t *uc) {
    current->uc_mcontext.gregs[REG_RAX] = uc->uc_mcontext.gregs[REG_RAX];
    current->uc_mcontext.gregs[REG_RCX] = uc->uc_mcontext.gregs[REG_RCX];
    current->uc_mcontext.gregs[REG_RDX] = uc->uc_mcontext.gregs[REG_RDX];
    current->uc_mcontext.gregs[REG_RSI] = uc->uc_mcontext.gregs[REG_RSI];
    current->uc_mcontext.gregs[REG_RDI] = uc->uc_mcontext.gregs[REG_RDI];
    current->uc_mcontext.gregs[REG_R8] = uc->uc_mcontext.gregs[REG_R8];
    current->uc_mcontext.gregs[REG_R9] = uc->uc_mcontext.gregs[REG_R9];
    current->uc_mcontext.gregs[REG_R10] = uc->uc_mcontext.gregs[REG_R10];
    current->uc_mcontext.gregs[REG_R11] = uc->uc_mcontext.gregs[REG_R11];
    current->uc_mcontext.gregs[REG_RBX] = uc->uc_mcontext.gregs[REG_RBX];
    current->uc_mcontext.gregs[REG_RBP] = uc->uc_mcontext.gregs[REG_RBP];
    current->uc_mcontext.gregs[REG_R12] = uc->uc_mcontext.gregs[REG_R12];
    current->uc_mcontext.gregs[REG_R13] = uc->uc_mcontext.gregs[REG_R13];
    current->uc_mcontext.gregs[REG_R14] = uc->uc_mcontext.gregs[REG_R14];
    current->uc_mcontext.gregs[REG_R15] = uc->uc_mcontext.gregs[REG_R15];
    current->uc_mcontext.gregs[REG_RSP] = uc->uc_mcontext.gregs[REG_RSP];
    current->uc_mcontext.gregs[REG_RIP] = uc->uc_mcontext.gregs[REG_RIP]; 
    current->uc_mcontext.gregs[REG_EFL] = uc->uc_mcontext.gregs[REG_EFL]; 
}

// 从上下文恢复寄存器状态
// current: 源上下文
// uc:      目标上下文(即将激活的上下文)
void restore_registers(ucontext_t *current, ucontext_t *uc) {
    // 逆向操作,将保存的寄存器值写回新上下文
    uc->uc_mcontext.gregs[REG_RAX] = current->uc_mcontext.gregs[REG_RAX];
    uc->uc_mcontext.gregs[REG_RCX] = current->uc_mcontext.gregs[REG_RCX];
    uc->uc_mcontext.gregs[REG_RDX] = current->uc_mcontext.gregs[REG_RDX];
    uc->uc_mcontext.gregs[REG_RSI] = current->uc_mcontext.gregs[REG_RSI];
    uc->uc_mcontext.gregs[REG_RDI] = current->uc_mcontext.gregs[REG_RDI];
    uc->uc_mcontext.gregs[REG_R8] = current->uc_mcontext.gregs[REG_R8];
    uc->uc_mcontext.gregs[REG_R9] = current->uc_mcontext.gregs[REG_R9];
    uc->uc_mcontext.gregs[REG_R10] = current->uc_mcontext.gregs[REG_R10];
    uc->uc_mcontext.gregs[REG_R11] = current->uc_mcontext.gregs[REG_R11];
    uc->uc_mcontext.gregs[REG_RBX] = current->uc_mcontext.gregs[REG_RBX];
    uc->uc_mcontext.gregs[REG_RBP] = current->uc_mcontext.gregs[REG_RBP];
    uc->uc_mcontext.gregs[REG_R12] = current->uc_mcontext.gregs[REG_R12];
    uc->uc_mcontext.gregs[REG_R13] = current->uc_mcontext.gregs[REG_R13];
    uc->uc_mcontext.gregs[REG_R14] = current->uc_mcontext.gregs[REG_R14];
    uc->uc_mcontext.gregs[REG_R15] = current->uc_mcontext.gregs[REG_R15];
    uc->uc_mcontext.gregs[REG_RSP] = current->uc_mcontext.gregs[REG_RSP];
    uc->uc_mcontext.gregs[REG_RIP] = current->uc_mcontext.gregs[REG_RIP];
    uc->uc_mcontext.gregs[REG_EFL] = current->uc_mcontext.gregs[REG_EFL];
}

// 信号处理函数(驱动协程切换的核心)
// signum:   信号编号(如SIGALRM)
// info:     信号附加信息
// ucontext: 当前线程的上下文信息
void signal_handler(int signum, siginfo_t *info, void *ucontext) {
    // 获取当前正在运行的协程
    int idx = scheduler.current_index;
    coroutine_t *current = scheduler.coroutines[idx];
    auto uc = (ucontext_t*)ucontext; // 转换为上下文指针

    // 保存当前协程的执行上下文
    if (current->state == COROUTINE_RUNNING) {
        save_registers(&current->context, uc); // 寄存器状态保存
        current->state = COROUTINE_READY;      // 状态转为就绪
    }

    coroutine_t *next = current; // 默认继续执行当前协程

    // 简单轮询调度算法:选择下一个协程(环形队列)
    scheduler.current_index = (scheduler.current_index + 1) % scheduler.num_coroutines;
    next = scheduler.coroutines[scheduler.current_index];

    
    // 准备切换到下一个协程
    current = next; // 更新当前协程指针

    // 恢复目标协程的寄存器状态
    restore_registers(&current->context, uc);

    // 更新协程状态为运行态
    current->state = COROUTINE_RUNNING;
}

协程初始化

初始化有栈协程需要分配栈空间,协程是独立执行流,需要私有栈空间存储:

  • 函数调用链(返回地址、栈帧)
  • 局部变量
  • 寄存器保存区域
    没有独立栈会导致多个协程的栈数据互相覆盖,产生不可预知的错误。

RSP/RBP 需要指向 STACK_SIZE - 1因为在 x86_64 架构中,栈是向下增长的,即从高地址向低地址增长

#define STACK_SIZE (1024 * 64) // 协程栈大小

void coroutine_init(void (*func)(void*), void *arg) {
    coroutine_t *co = new coroutine_t{};  // 创建协程控制块
    co->func = func;                      // 绑定入口函数
    co->arg = arg;                        // 保存参数指针
    co->state = COROUTINE_READY;          // 初始状态为就绪态

    // 分配协程栈空间(16字节对齐)
    if (posix_memalign((void**) &co->stack, 16, STACK_SIZE) != 0) {
        throw std::runtime_error("申请内存失败");
    }

    // 初始化栈指针和入口地址
    co->context.uc_mcontext.gregs[REG_RSP] = (long long)co->stack + STACK_SIZE - 1;  // 栈指针指向栈顶
    co->context.uc_mcontext.gregs[REG_RBP] = (long long)co->stack + STACK_SIZE - 1;  // 基址指针初始化
    co->context.uc_mcontext.gregs[REG_RIP] = (long long)func;                        // 指令指针指向入口函数

    // 将新协程加入调度器
    scheduler.coroutines[scheduler.num_coroutines] = co;
    scheduler.num_coroutines++;
}

编写协程函数

编写task 1, 2, 3分别执行不同的计算任务,并且这些计算任务都有递归调用,用来验证协程栈空间是否设置正确。

#include <stdio.h>
#include <unistd.h>

#define COLOR_RESET   "\033[0m"    // 重置所有属性
#define COLOR_RED     "\033[31m"   // 红色文本
#define COLOR_GREEN   "\033[32m"   // 绿色文本
#define COLOR_YELLOW  "\033[33m"   // 黄色文本
#define COLOR_BLUE    "\033[34m"   // 蓝色文本
#define COLOR_MAGENTA "\033[35m"   // 洋红色文本
#define COLOR_CYAN    "\033[36m"   // 青色文本
#define COLOR_WHITE   "\033[37m"   // 白色文本

// 背景颜色
#define BG_RED     "\033[41m"      // 红色背景
#define BG_GREEN   "\033[42m"      // 绿色背景
#define BG_YELLOW  "\033[43m"      // 黄色背景
#define BG_BLUE    "\033[44m"      // 蓝色背景
#define BG_MAGENTA "\033[45m"      // 洋红色背景
#define BG_CYAN    "\033[46m"      // 青色背景
#define BG_WHITE   "\033[47m"      // 白色背景

// 递归计算斐波那契数列的第n项
unsigned long long fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// 递归判断一个数是否为质数
int is_prime_helper(int n, int divisor) {
    if (divisor * divisor > n) return 1; // 如果除数平方大于n,说明n是质数
    if (n % divisor == 0) return 0;      // 如果n能被divisor整除,说明n不是质数
    return is_prime_helper(n, divisor + 1); // 递归检查下一个除数
}

int is_prime(int n) {
    if (n <= 1) return 0;
    if (n <= 3) return 1;
    if (n % 2 == 0) return 0; // 偶数不是质数
    return is_prime_helper(n, 3); // 从3开始递归检查
}

// 计算斐波那契数列的任务(递归实现)
void task1(void*) {
    while (1) {
        int n = 40; // 计算斐波那契数列的第40项
        unsigned long long result = fibonacci(n);
        printf(BG_RED "协程一" COLOR_RESET "计算斐波那契数列第%d项: %llu\n", n, result);
        usleep(50000);
    }
}

// 计算质数的任务(递归实现)
void task2(void*) {
    while (1) {
        int count = 0;
        for (int i = 0; i < 10000; i++) { 
            if (is_prime(i)) {
                count++;
            }
        }
        printf(BG_GREEN "协程二" COLOR_RESET "计算0到10000之间的质数个数: %d\n", count);
        usleep(50000);
    }
}

// 计算斐波那契数列的任务(与task1不同,递归实现)
void task3(void*) {
    while (1) {
        int n = 35; // 计算斐波那契数列的第35项
        unsigned long long result = fibonacci(n);
        printf( BG_CYAN "协程三" COLOR_RESET "计算斐波那契数列第%d项: %llu\n", n, result);
        usleep(50000);
    }
}

主函数

int main() {

    coroutine_init(task1, nullptr);

    coroutine_init(task2, nullptr);

    coroutine_init(task3, nullptr);


    setup_signal_handler();

    while (1)
    {
    
    }
    
    return 0;
}

运行结果

可以看到3个协程正在交替执行代码

协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程一计算斐波那契数列第40项: 102334155
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229

参考

到底该怎么理解协程?

CPU眼里的:{函数括号} | 栈帧 | 堆栈 | 栈变量

Linux系统之信号及处理流程(图详解)