Linux服务器之协程技术点篇 linux中协程的调度由操作系统控制
lipiwang 2024-11-01 14:10 7 浏览 0 评论
协程技术点分享
协程的实现之原语操作
问题:协程的内部原语操作有哪些?分别如何实现的?
协程的核心原语操作:create, resume, yield。协程的原语操作有create怎么没有exit?以NtyCo为例,协程一旦创建就不能有用户自己销毁,必须得以子过程执行结束,就会自动销毁协程的上下文数据。以_exec执行入口函数返回而销毁协程的上下文与相关信息。co->func(co->arg) 是子过程,若用户需要长久运行协程,就必须要在func函数里面写入循环等操作。所以NtyCo里面没有实现exit的原语操作。
create:创建一个协程。
1. 调度器是否存在,不存在也创建。调度器作为全局的单例。将调度器的实例存储在线程的私有空间pthread_setspecific。
2. 分配一个coroutine的内存空间,分别设置coroutine的数据项,栈空间,栈大小,初始状态,创建时间,子过程回调函数,子过程的调用参数。
3. 将新分配协程添加到就绪队列 ready_queue中
实现代码如下:
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg) {
assert(pthread_once(&sched_key_once, nty_coroutine_sched_key_creator) == 0);
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) {
nty_schedule_create(0);
sched = nty_coroutine_get_sched();
if (sched == NULL) {
printf("Failed to create scheduler\n");
return -1;
}
}
nty_coroutine *co = calloc(1, sizeof(nty_coroutine));
if (co == NULL) {
printf("Failed to allocate memory for new coroutine\n");
return -2;
}
//
int ret = posix_memalign(&co->stack, getpagesize(), sched->stack_size);
if (ret) {
printf("Failed to allocate stack for new coroutine\n");
free(co);
return -3;
}
co->sched = sched;
co->stack_size = sched->stack_size;
co->status = BIT(NTY_COROUTINE_STATUS_NEW); //
co->id = sched->spawned_coroutines ++;
co->func = func;
co->fd = -1;
co->events = 0;
co->arg = arg;
co->birth = nty_coroutine_usec_now();
*new_co = co;
TAILQ_INSERT_TAIL(&co->sched->ready, co, ready_next);
return 0;
}
yield: 让出CPU。
void nty_coroutine_yield(nty_coroutine *co)
参数:当前运行的协程实例
调用后该函数不会立即返回,而是切换到最近执行resume的上下文。该函数返回是在执行resume的时候,会有调度器统一选择resume的,然后再次调用yield的。resume与yield是两个可逆过程的原子操作。
resume:恢复协程的运行权
int nty_coroutine_resume(nty_coroutine *co)
参数:需要恢复运行的协程实例
调用后该函数也不会立即返回,而是切换到运行协程实例的yield的位置。返回是在等协程相应事务处理完成后,主动yield会返回到resume的地方。
协程的实现之切换
问题:协程的上下文如何切换?切换代码如何实现?
首先来回顾一下x86_64寄存器的相关知识。x86_64 的寄存器有16个64位寄存器,分别是:%rax, %rbx, %rcx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12,
%r13, %r14, %r15。
%rax 作为函数返回值使用的。
%rsp 栈指针寄存器,指向栈顶
%rdi, %rsi, %rdx, %rcx, %r8, %r9 用作函数参数,依次对应第1参数,第2参数。。。
%rbx, %rbp, %r12, %r13, %r14, %r15 用作数据存储,遵循调用者使用规则,换句话说,就是随便用。调用子函数之前要备份它,以防它被修改
%r10, %r11 用作数据存储,就是使用前要先保存原值。
上下文切换,就是将CPU的寄存器暂时保存,再将即将运行的协程的上下文寄存器,分别mov到相对应的寄存器上。此时上下文完成切换。如下图所示:
切换_switch函数定义:
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
参数1:即将运行协程的上下文,寄存器列表
参数2:正在运行协程的上下文,寄存器列表
我们nty_cpu_ctx结构体的定义,为了兼容x86,结构体项命令采用的是x86的寄存器名字命名。
typedef struct _nty_cpu_ctx {
void *esp; //
void *ebp;
void *eip;
void *edi;
void *esi;
void *ebx;
void *r1;
void *r2;
void *r3;
void *r4;
void *r5;
} nty_cpu_ctx;
_switch返回后,执行即将运行协程的上下文。是实现上下文的切换
_switch的实现代码:
0: __asm__ (
1: " .text \n"
2: " .p2align 4,,15 \n"
3: ".globl _switch \n"
4: ".globl __switch \n"
5: "_switch: \n"
6: "__switch: \n"
7: " movq %rsp, 0(%rsi) # save stack_pointer \n"
8: " movq %rbp, 8(%rsi) # save frame_pointer \n"
9: " movq (%rsp), %rax # save insn_pointer \n"
10: " movq %rax, 16(%rsi) \n"
11: " movq %rbx, 24(%rsi) # save rbx,r12-r15 \n"
12: " movq %r12, 32(%rsi) \n"
13: " movq %r13, 40(%rsi) \n"
14: " movq %r14, 48(%rsi) \n"
15: " movq %r15, 56(%rsi) \n"
16: " movq 56(%rdi), %r15 \n"
17: " movq 48(%rdi), %r14 \n"
18: " movq 40(%rdi), %r13 # restore rbx,r12-r15 \n"
19: " movq 32(%rdi), %r12 \n"
20: " movq 24(%rdi), %rbx \n"
21: " movq 8(%rdi), %rbp # restore frame_pointer \n"
22: " movq 0(%rdi), %rsp # restore stack_pointer \n"
23: " movq 16(%rdi), %rax # restore insn_pointer \n"
24: " movq %rax, (%rsp) \n"
25: " ret \n"
26: );
按照x86_64的寄存器定义,%rdi保存第一个参数的值,即new_ctx的值,%rsi保存第二个参数的值,即保存cur_ctx的值。X86_64每个寄存器是64bit,8byte。
Movq %rsp, 0(%rsi) 保存在栈指针到cur_ctx实例的rsp项
Movq %rbp, 8(%rsi)
Movq (%rsp), %rax #将栈顶地址里面的值存储到rax寄存器中。Ret后出栈,执行栈顶
Movq %rbp, 8(%rsi) #后续的指令都是用来保存CPU的寄存器到new_ctx的每一项中
Movq 8(%rdi), %rbp #将new_ctx的值
Movq 16(%rdi), %rax #将指令指针rip的值存储到rax中
Movq %rax, (%rsp) # 将存储的rip值的rax寄存器赋值给栈指针的地址的值。
Ret # 出栈,回到栈指针,执行rip指向的指令。
上下文环境的切换完成
协程的实现之定义
问题:协程如何定义? 调度器如何定义?
先来一道设计题:
设计一个协程的运行体R与运行体调度器S的结构体
1. 运行体R:包含运行状态{就绪,睡眠,等待},运行体回调函数,回调参数,栈指针,栈大小,当前运行体
2. 调度器S:包含执行集合{就绪,睡眠,等待}
这道设计题拆分两个个问题,一个运行体如何高效地在多种状态集合更换。调度器与运行体的功能界限。
运行体如何高效地在多种状态集合更换
新创建的协程,创建完成后,加入到就绪集合,等待调度器的调度;协程在运行完成后,进行IO操作,此时IO并未准备好,进入等待状态集合;IO准备就绪,协程开始运行,后续进行sleep操作,此时进入到睡眠状态集合。
就绪(ready),睡眠(sleep),等待(wait)集合该采用如何数据结构来存储?
就绪(ready)集合并不没有设置优先级的选型,所有在协程优先级一致,所以可以使用队列来存储就绪的协程,简称为就绪队列(ready_queue)。
睡眠(sleep)集合需要按照睡眠时长进行排序,采用红黑树来存储,简称睡眠树(sleep_tree)红黑树在工程实用为<key, value>, key为睡眠时长,value为对应的协程结点。
等待(wait)集合,其功能是在等待IO准备就绪,等待IO也是有时长的,所以等待(wait)集合采用红黑树的来存储,简称等待树(wait_tree),此处借鉴nginx的设计。
数据结构如下图所示:
Coroutine就是协程的相应属性,status表示协程的运行状态。sleep与wait两颗红黑树,ready使用的队列,比如某协程调用sleep函数,加入睡眠树(sleep_tree),status |= S即可。比如某协程在等待树(wait_tree)中,而IO准备就绪放入ready队列中,只需要移出等待树(wait_tree),状态更改status &= ~W即可。有一个前提条件就是不管何种运行状态的协程,都在就绪队列中,只是同时包含有其他的运行状态。
调度器与协程的功能界限
每一协程都需要使用的而且可能会不同属性的,就是协程属性。每一协程都需要的而且数据一致的,就是调度器的属性。比如栈大小的数值,每个协程都一样的后不做更改可以作为调度器的属性,如果每个协程大小不一致,则可以作为协程的属性。
用来管理所有协程的属性,作为调度器的属性。比如epoll用来管理每一个协程对应的IO,是需要作为调度器属性。
按照前面几章的描述,定义一个协程结构体需要多少域,我们描述了每一个协程有自己的上下文环境,需要保存CPU的寄存器ctx;需要有子过程的回调函数func;需要有子过程回调函数的参数 arg;需要定义自己的栈空间 stack;需要有自己栈空间的大小 stack_size;需要定义协程的创建时间 birth;需要定义协程当前的运行状态 status;需要定当前运行状态的结点(ready_next, wait_node, sleep_node);需要定义协程id;需要定义调度器的全局对象 sched。
协程的核心结构体如下:
typedef struct _nty_coroutine {
nty_cpu_ctx ctx;
proc_coroutine func;
void *arg;
size_t stack_size;
nty_coroutine_status status;
nty_schedule *sched;
uint64_t birth;
uint64_t id;
void *stack;
RB_ENTRY(_nty_coroutine) sleep_node;
RB_ENTRY(_nty_coroutine) wait_node;
TAILQ_ENTRY(_nty_coroutine) ready_next;
TAILQ_ENTRY(_nty_coroutine) defer_next;
} nty_coroutine;
调度器是管理所有协程运行的组件,协程与调度器的运行关系。
调度器的属性,需要在保存CPU的寄存器上下文 ctx,可以从协程运行状态yield到调度器运行的。从协程到调度器用yield,从调度器到协程用resume
以下为协程的定义。
typedef struct _nty_coroutine_queue nty_coroutine_queue;
typedef struct _nty_coroutine_rbtree_sleep nty_coroutine_rbtree_sleep;
typedef struct _nty_coroutine_rbtree_wait nty_coroutine_rbtree_wait;
typedef struct _nty_schedule {
uint64_t birth;
nty_cpu_ctx ctx;
struct _nty_coroutine *curr_thread;
int page_size;
int poller_fd;
int eventfd;
struct epoll_event eventlist[NTY_CO_MAX_EVENTS];
int nevents;
int num_new_events;
nty_coroutine_queue ready;
nty_coroutine_rbtree_sleep sleeping;
nty_coroutine_rbtree_wait waiting;
} nty_schedule;
协程的实现之调度器
问题:协程如何被调度?
调度器的实现,有两种方案,一种是生产者消费者模式,另一种多状态运行。
生产者消费者模式
逻辑代码如下:
while (1) {
//遍历睡眠集合,将满足条件的加入到ready
nty_coroutine *expired = NULL;
while ((expired = sleep_tree_expired(sched)) != ) {
TAILQ_ADD(&sched->ready, expired);
}
//遍历等待集合,将满足添加的加入到ready
nty_coroutine *wait = NULL;
int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1);
for (i = 0;i < nready;i ++) {
wait = wait_tree_search(events[i].data.fd);
TAILQ_ADD(&sched->ready, wait);
}
// 使用resume回复ready的协程运行权
while (!TAILQ_EMPTY(&sched->ready)) {
nty_coroutine *ready = TAILQ_POP(sched->ready);
resume(ready);
}
}
多状态运行
实现逻辑代码如下:
while (1) {
//遍历睡眠集合,使用resume恢复expired的协程运行权
nty_coroutine *expired = NULL;
while ((expired = sleep_tree_expired(sched)) != ) {
resume(expired);
}
//遍历等待集合,使用resume恢复wait的协程运行权
nty_coroutine *wait = NULL;
int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1);
for (i = 0;i < nready;i ++) {
wait = wait_tree_search(events[i].data.fd);
resume(wait);
}
// 使用resume恢复ready的协程运行权
while (!TAILQ_EMPTY(sched->ready)) {
nty_coroutine *ready = TAILQ_POP(sched->ready);
resume(ready);
}
}
最后推荐一个协程教程
关注后台私信:1,更多技术分享
相关推荐
- ubuntu单机安装open-falcon极度详细操作
-
备注:以下操作均由本人实际操作并得到验证,喜欢的同学可尝试操作安装。步骤一1.1环境准备(使用系统:ubuntu18.04)1.1.1安装redisubuntu下安装(参考借鉴:https://...
- Linux搭建promtail、loki、grafana轻量日志监控系统
-
一:简介日志监控告警系统,较为主流的是ELK(Elasticsearch、Logstash和Kibana核心套件构成),虽然优点是功能丰富,允许复杂的操作。但是,这些方案往往规模复杂,资源占用高,...
- 一文搞懂,WAF阻止恶意攻击的8种方法
-
WAF(Web应用程序防火墙)是应用程序和互联网流量之间的第一道防线,它监视和过滤Internet流量以阻止不良流量和恶意请求,WAF是确保Web服务的可用性和完整性的重要安全解决方案。它...
- 14配置appvolume(ios14.6配置文件)
-
使用AppVolumes应用程序功能,您可以管理应用程序的整个生命周期,包括打包、更新和停用应用程序。您还可以自定义应用程序分配,以向最终用户提供应用程序的特定版本14.1安装appvolume...
- 目前流行的缺陷管理工具(缺陷管理方式存在的优缺点)
-
摘自:https://blog.csdn.net/jasonteststudy/article/details/7090127?utm_medium=distribute.pc_relevant.no...
- 开源数字货币交易所开发学习笔记(2)——SpringCloud
-
前言码云(Gitee)上开源数字货币交易所源码CoinExchange的整体架构用了SpringCloud,对于经验丰富的Java程序员来说,可能很简单,但是对于我这种入门级程序员,还是有学习的必要的...
- 开发JAX-RPC Web Services for WebSphere(下)
-
在开发JAX-RPCWebServicesforWebSphere(上)一文中,小编为大家介绍了如何创建一个Web服务项目、如何创建一个服务类和Web服务,以及部署项目等内容。接下来小编将为大...
- CXF学习笔记1(cxf client)
-
webservice是发布服务的简单并实用的一种技术了,个人学习了CXF这个框架,也比较简单,发布了一些笔记,希望对笔友收藏并有些作用哦1.什么是webServicewebService让一个程序可...
- 分布式RPC最全详解(图文全面总结)
-
分布式通信RPC是非常重要的分布式系统组件,大厂经常考察的Dubbo等RPC框架,下面我就全面来详解分布式通信RPC@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》...
- Oracle WebLogic远程命令执行0day漏洞(CVE-2019-2725补丁绕过)预警
-
概述近日,奇安信天眼与安服团队通过数据监控发现,野外出现OracleWebLogic远程命令执行漏洞最新利用代码,此攻击利用绕过了厂商今年4月底所发布的最新安全补丁(CVE-2019-2725)。由...
- Spring IoC Container 原理解析(spring中ioc三种实现原理)
-
IoC、DI基础概念关于IoC和DI大家都不陌生,我们直接上martinfowler的原文,里面已经有DI的例子和spring的使用示例《InversionofControlContainer...
- Arthas线上服务器问题排查(arthas部署)
-
1Arthas(阿尔萨斯)能为你做什么?这个类从哪个jar包加载的?为什么会报各种类相关的Exception?我改的代码为什么没有执行到?难道是我没commit?分支搞错了?遇到问题无法在...
- 工具篇之IDEA功能插件HTTP_CLENT(idea2021插件)
-
工具描述:Java开发人员通用的开发者工具IDEA集成了HTTPClient功能,之后可以无需单独安装使用PostMan用来模拟http请求。创建方式:1)简易模式Tools->HTTPCl...
- RPC、Web Service等几种远程监控通信方式对比
-
几种远程监控通信方式的介绍一.RPCRPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果。这个请求包括一个参数集和一个文本集,通常形成“classname.meth...
- 《github精选系列》——SpringBoot 全家桶
-
1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)