当前位置: 首页 > news >正文

linux网络编程7——协程设计原理与汇编实现

文章目录

  • 协程设计原理与汇编实现
    • 1. 协程概念
    • 2. 协程的实现
      • 2.1 setjmp
      • 2.2 ucontext
      • 2.3 汇编实现
      • 2.4 优缺点
      • 2.5 实现协程原语
        • 2.5.1 create()
        • 2.5.2 yield()
        • 2.5.3 resume()
        • 2.5.4 exit()
        • 2.5.5 switch()
        • 2.5.6 sleep()
      • 2.6 协程调度器
    • 3. 利用hook使用协程版本的库函数
    • 学习参考

协程设计原理与汇编实现

本文介绍了协程的概念、特征、优势、以及其实现原理。

1. 协程概念

协程是一种轻量级的用户态线程。它允许在单个线程内执行多个任务,使得程序可以在不同的函数之间灵活地切换,以便更好地利用 CPU 资源。这种机制特别适合 IO 密集型任务(如网络请求、文件读写)和异步编程场景。协程可以被暂停和恢复,避免了阻塞等待,同时不需要系统级线程的切换成本。

协程的实现在底层是由执行流的跳转切换机制实现的。一般情况,有一个协程调度器作为每个协程挂起时要切换回的代码。

应用场景

  • webserver
  • kv存储
  • 图床,网络层

同步和异步

"同步"和"异步"主要是指在执行任务时,任务与调用方的相互关系。在同步操作中,调用方会等待任务执行完毕然后继续执行。在异步操作中,调用方会立即返回并继续执行后续的操作,不会等待任务执行完,任务执行完可以通过回调、事件等方式通知调用方。

异步的好处

  • 多线程并发,充分利用cpu,性能好。

异步的坏处

  • 代码复杂,不好理解,需要设置回调函数或者使用事件机制。

协程的好处

  • 同步的编程方式,实现异步的性能。

互联网中协程可能被用到的场景

  1. 浏览器网页加载发送异步HTTP请求时可能用到了协程。
  2. 淘宝商店界面加载商品信息
  3. 直播界面加载评论和视频流
  4. 贴吧加载新的帖子回复
  5. bilibili异步加载新的回复
  6. 网络游戏中加载各种位置信息
  7. 微信聊天时,需要异步加载和发送信息
  8. 音视频通话异步加载流媒体
  9. chatgpt异步发送和接收问答消息
  10. github的git仓库托管服务器可能使用协程处理用户的push、pull等请求

2. 协程的实现

2.1 setjmp

setjmplongjmp 提供了一种低级的非局部跳转机制,适用于需要在 C 程序中实现复杂控制流或异常处理的情况。但由于它们带来的复杂性和潜在风险,使用时需要小心,确保不会影响程序的可维护性和可读性。

代码示例

#include <setjmp.h>
#include <stdio.h>jmp_buf env1, env2, env3;// coroutine1
void func1(void)
{int cur = 0;int ret = setjmp(env1);if (ret == 0)longjmp(env3, 1);printf("func1: %d [%d]\n", ret, cur++);if (ret < 20){longjmp(env2, ++ret);    }
}// coroutine1
void func2(void)
{int cur = 0;int ret = setjmp(env2);printf("func2: %d [%d]\n", ret, cur++);if (ret < 20){longjmp(env1, ++ret);    }
}int main()
{int ret = setjmp(env3);if (ret == 0)func1();elsefunc2();return 0;
}

从实现代码中可以看到setjmp机制需要我们自己保证协程所在的栈空间已被建立,并且还没有退出。协程所在的函数需要先手动执行,才能进行调度。协程的调度也比较麻烦。

2.2 ucontext

ucontext 是一种用于实现协程和用户态线程的机制。它在一些类 Unix 系统(例如 Linux)中提供了在用户态创建、切换和恢复上下文的接口。ucontext 通过保存和恢复 CPU 寄存器、堆栈指针等状态,允许程序在不同执行流之间切换,适用于实现协程和轻量级任务调度等。

其中保存协程上下文信息的结构体ucontext_t为

#include <ucontext.h>typedef struct ucontext {ucontext_t *uc_link;       // 执行结束后切换到的上下文sigset_t uc_sigmask;       // 信号屏蔽字stack_t uc_stack;          // 栈信息(地址和大小)mcontext_t uc_mcontext;    // 寄存器状态
} ucontext_t;

ucontext API 提供了几个主要函数来创建和切换上下文:

  1. getcontext(ucontext_t *ucp):获取当前上下文并保存到 ucp
  2. setcontext(const ucontext_t *ucp):恢复指定上下文并跳转到该上下文。
  3. makecontext(ucontext_t *ucp, void (*func)(), int argc, ...):为 ucp 配置要执行的函数 func 及其参数。
  4. swapcontext(ucontext_t *oucp, const ucontext_t *ucp):保存当前上下文到 oucp,然后切换到 ucp 上下文。

代码示例

#include <ucontext.h>
#include <stdio.h>ucontext_t ctx[2];
ucontext_t main_ctx;int count = 0;// coroutine1
void func1(void)
{int cur = 0;while (count++ < 20){printf("func1: %d [%d]\n", count, cur++);// yieldswapcontext(&ctx[0], &ctx[1]);}
}// coroutine1
void func2(void)
{int cur = 0;while (count++ < 20){printf("func2: %d [%d]\n", count, cur++);// yieldswapcontext(&ctx[1], &ctx[0]);}
}int main()
{char stack1[2048] = {0};char stack2[2048] = {0};getcontext(&ctx[0]);ctx[0].uc_stack.ss_sp = stack1;ctx[0].uc_stack.ss_size = sizeof(stack1);// 执行完之后跳转的地方ctx[0].uc_link = &main_ctx;makecontext(&ctx[0], func1, 0);getcontext(&ctx[1]);ctx[1].uc_stack.ss_sp = stack2;ctx[1].uc_stack.ss_size = sizeof(stack2);ctx[1].uc_link = &main_ctx;makecontext(&ctx[1], func2, 0);printf("start\n");swapcontext(&main_ctx, &ctx[0]);return 0;
}

ucontext 机制虽然强大,但需要谨慎使用。现代开发中,通常使用其他更高层的协程库,如 libco、libuv 或 Boost.Context 等。

2.3 汇编实现

使用汇编语言来实现协程的切换:主要操作为恢复和保存寄存器的值

int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);__asm__(
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"
"       movq (%rsp), %rax       # save insn_pointer      \n"
"       movq %rax, 16(%rsi)                              \n"
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       movq 56(%rdi), %r15                              \n"
"       movq 48(%rdi), %r14                              \n"
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"
"       movq %rax, (%rsp)                                \n"
"       ret                                              \n"
);

上面的_switch函数实现了协程上下文的切换,和线程切换所作的工作类似

2.4 优缺点

  1. setjmp实现方式复杂,但是跨平台性好
  2. ucontext实现方式简单,但是跨平台性一般
  3. 汇编实现方式复杂,跨平台型差,但是效率高

2.5 实现协程原语

2.5.1 create()

主要工作是创建一个保存协程上下文的数据结构。一个协程的上下文必须包括如下信息:

  • 协程运行的函数和参数信息

  • cpu寄存器上下文

  • 运行时栈上下文

  • 协程状态

  • 协程id

  • 协程所属的调度器

  • 其他信息

一个示例如下:

struct _coroutine_context
{ucontext_t ctx;			// 里面包括寄存器状态和栈上下文proc_coroutine func;	// 协程运行的函数和参数信息void *arg;void *data;coroutine_status status;	// 协程状态scheduler *sched;			// 所属的调度器uint64_t id;
};

创建协程所作的主要工作包括:

  • 分配一个协程上下文并初始化
  • 获取并设置调度器
  • 将改协程加入调度器进行管理
2.5.2 yield()

主要工作是调用swapcontext()或者_switch()切换会协程调度器。

2.5.3 resume()

主要工作是恢复协程的执行。

2.5.4 exit()

主要工作是协程从调度器中删除,然后释放协程上下文。

2.5.5 switch()

协程切换,主要是切换协程的寄存器。

2.5.6 sleep()

让协程停止执行一段时间。

2.6 协程调度器

协程调度器管理协程,包括一个就绪协程队列,一个sleep协程的集合,一个运行时协程队列,一个等待协程集合。可以采用事件机制,当某事件发生时(例如某fd可读),可以将相应的协程从等待集合中取出并恢复执行。

其核心代码如下

while (1)
{// 检查sleep集合,查看是否有协程超时coroutine_context *expired;while ((expired = check_expired(sched))){resume(expired);}// 检查wait结合,查看是否有协程有监听的事件发生coroutine_context *waked;int nready = epoll_wait(epfd, events, EVENTS_SIZE, 1);for (int i = 0; i < nready; ++i){waked = wait_search(events[i].data.fd);resume(waked);}// 恢复ready队列中的协程的运行coroutine_context *rdy;while (!is_ready_empty(sched)){rdt = ready_pop(sched);resyme(rdt);}
}

3. 利用hook使用协程版本的库函数

利用运行时动态链接,可以在运行时将一个函数替换为为使用协程的版本。

例如,以下代码将read函数在运行时替换为了另一个函数:

#include <dlfcn.h>
#include <unistd.h>typedef ssize_t (*readf_t)(int fd, void *buf, size_t count);readf_t readf;void init_hook()
{readf = (readf_t)dlsym(RTLD_NEXT, "read");
}ssize_t read(int fd, void *buf, size_t count)
{if (!readf) init_hook();// 如果对应的fd不可读,那么就挂起协程yield_if_not_ok(fd, POLLIN | POLLERR | POLLHUP);return readf(fd, buf, count);
}

学习参考

学习更多相关知识请参考零声 github。


http://www.mrgr.cn/news/61427.html

相关文章:

  • Selenium 进行网页自动化操作的一个示例,绕过一些网站的自动化检测。python编程
  • 《透过财报看企业》
  • linux-supervisor(进程控制系统)
  • 21天学通C++第八章——指针
  • 鸿蒙面试 2025-01-09
  • Python之装饰器
  • 【网络】传输层协议TCP
  • Training language models to follow instructions with human feedback解读
  • 国密和国际密
  • 拥塞控制与TCP子问题(粘包问题,异常情况等)
  • 2024/10/29 英语每日一段
  • PyMol3.0 Educational Version激活教程(激活一次可用半年)
  • LCR 027. 回文链表 不利用额外空间实现快慢指针
  • OSError: no library called “cairo-2“ was found no library called “cairo“ was
  • 84674
  • msvcr100.dll丢失怎样修复,介绍6个简单靠谱的方法
  • 基于SSM+微信小程序的汽车维修管理系统(汽车5)
  • Qt编程技巧小知识点(6)根据 *IDN? 对程控仪器连接状态进行确认
  • leetcode hot100【LeetCode 543. 二叉树的直径】java实现
  • 离散数学实验五c语言(并查集处理,Kruskal算法求最小生成树)
  • binlog 介绍
  • C# OpenCvSharp DNN UNet 推理
  • 2024年【通信安全员ABC证】最新解析及通信安全员ABC证新版试题
  • qt的c++环境配置和c++基础【正点原子】嵌入式Qt5 C++开发视频
  • 【AIGC】2024-arXiv-Lumiere:视频生成的时空扩散模型
  • 开始菜单增强工具 StartAllBack v3.7.10.4910 直装激活版