协程框架nty_co

news2024/11/19 9:28:39

一、为什么要有协程?

以DNS请求为例子,客户端向服务器发送域名,服务器回复该域名对应得IP地址。

我们想要以同步的编程方式获得异步的性能!!!

 在Linux下,常使用IO多路复用器epoll来管理客户端连接,其主循环框架如下

while (1){
    int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);

    int i=0;
    for (i=0; i<nready; i++){

        int sockfd = events[i].data.fd;
        if (sockfd == listenfd){

            int connfd = accept(listenfd, addr, &addr_len);
            
            setnonblock(connfd); //置为非阻塞

            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD,connfd,&ev);
        }else{
            handel(sockfd); //进行读写操作
        }
    }

}

在通过 accept 建立服务端与客户端的连接之后,需要行读写操作,也就是 handel 函数。根据同步和异步,有两种不同的处理方式。

同步的处理方式

 异步的处理方式

 可见,同步和异步主要区别在于对于 handle 函数的处理。同步在需要等待 handle 函数处理完成,主循环才能继续执行,阻塞了 epoll_wait。而异步是单独为 handle 函数创建一个线程异步处理,主循环不需要等待 handle 函数。(同步异步的本质是在IO操作的时候的处理方式的不同)

但是问题在于线程的创建、销毁,十分消耗资源。面对来自客户端的数百万连接,每一条都创建线程,很容易把服务器干崩溃。

因此就有了协程,在一个线程里面创建多个协程,共享一个线程的资源,但又能异步(看起来)处理事务。

二、协程的实现原理


前面说到,协程能异步处理事务,这只是看起来而已。协程的异步处理在于对CPU的调度,即需要的时候切入获取CPU操作权,不需要的时候让出CPU操作权。

 

这边涉及到以下几个问题:
1、切换的时候怎么做到跟切换前一致?
2、有协程1、协程2、协程3,……,怎么决定由那个协程执行?

首先第一个问题,就是协程切换前后需要进行上下文切换。有汇编、ucontext、longjmp / setjmp。当然,汇编效果最快。

其次第二个问题,协程是一种用户态的轻量级线程,协程的调度完全由用户控制。也就是说,由我们自定义的调度器管理。
在讲调度规则之前,我们需要先了解一下协程创建后会有哪些状态:
1、新创建的协程,创建完成后,加入到就绪集合,等待调度器的调度;
2、协程在运行完成后,进行 IO 操作,此时 IO 并未准备好,进入等待状态集合;
3、IO 准备就绪,协程开始运行,后续进行 sleep 操作,此时进入到睡眠状态集合。

 

 

在协程的上下文 IO 异步操作(nty_recv,nty_send)函数,步骤如下:
1)将 sockfd 添加到 epoll 管理中。
2)进行上下文环境切换,由协程上下文 yield 到调度器的上下文。
3)调度器获取下一个协程上下文。Resume 新的协程

IO 异步操作的上下文切换的时序图如下:

就绪:都准备好了,就等着执行。就绪(ready)集合并不没有设置优先级的选型,所有在协程优先级一致,所以可以使用队列来存储就绪的协程,简称为就绪队列

等待:没准备好,比如IO操作的recv,信息还没来,recv就还没准备好。等待(wait)集合,其功能是在等待 IO 准备就绪,等待 IO 也是有时长的,所以等待(wait)集合采用红黑树的来存储,简称等待树(wait_tree)

睡眠:指协程主动挂起,等待某个时间后再恢复执行。比如等待IO我们可以设置一个时间,时间内还是没触发,那就算过期超时了。睡眠(sleep)集合需要按照睡眠时长进行排序,采用红黑树来存储,简称睡眠树(sleep_tree)红黑树在工程实用为<key, value>, key 为睡眠时长,value 为对应的协程结点。

因此,基于以上,协程如何被调度?有两种
1、 生产者消费者模式

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);
	}
}

 2、多状态运行

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);
	}
}

三、NtyCo 的接口(纯C的协程框架)

大致介绍一下协程工作的流程:
1、为accept事件创建一个协程co1,并注册监听事件到co1的epoll,加入等待队列,然后yield,让出CPU控制权
2、为recv事件创建一个协程co2,并注册监听事件到co2的epoll,加入等待队列,然后yield,让出CPU控制权
3、为send事件创建一个协程co3,并注册监听事件到co3的epoll,加入等待队列,然后yield,让出CPU控制权
(以上设置默认睡眠时间,同步加入睡眠队列)
(调度器接手)
4、遍历睡眠集合,使用 resume 恢复过期协程 expired 的协程运行权
5、遍历就绪集合,使用 resume 恢复 ready 的协程运行权
6、遍历等待集合,使用 resume 恢复 wait 的协程运行权

四、测试结果

4台Ubuntu虚拟机,其中一台服务端4核12G,另外三台1核4G。测试并发连接。
需要做一些配置测试搭建百万并发项目

五、我们的系统代码怎么改成支持协程呢?

    (也就是如何与posix api兼容

NtyCo的hook

如果我们自己写的代码要引入协程,最傻的办法就是一个函数一个函数的改过来,把每个recv改成nty_recv,这样非常耗时耗力,于是hook就起到了非常好的作用。

  我们可以使用hook,帮助我们不用再封装posix api接口取个别的名字的函数,可以直接用和那些posix api接口同名并且不会冲突的函数(recv()、send()等等),并且功能由我们来具体实现。
  hook提供了两个接口;1. dlsym()是针对系统的,系统原始的api。2. dlopen()是针对第三方的库。
void *dlsym(void *handle, const char *symbol); 头文件是#include <dlfcn.h>

我们用dlsym来处理

在使用hook之前,我们一定要定义一个#define _GNU_SOURCE,定义这个

我们才能使用扩展库,如果不定义的话就不能用

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include<mysql/mysql.h>
//
// Created by 68725 on 2022/7/17.
//

typedef int (*connect_t)(int, struct sockaddr *, socklen_t);

connect_t connect_f;

typedef ssize_t (*recv_t)(int, void *buf, size_t, int);

recv_t recv_f;

typedef ssize_t (*send_t)(int, const void *buf, size_t, int);

send_t send_f;

typedef ssize_t (*read_t)(int, void *buf, size_t);

read_t read_f;

typedef ssize_t (*write_t)(int, const void *buf, size_t);

write_t write_f;

int connect(int fd, struct sockaddr *name, socklen_t len) {
    printf("in connect\n");
    return connect_f(fd, name, len);
}

ssize_t recv(int fd, void *buf, size_t len, int flags) {
    printf("in recv\n");
    return recv_f(fd, buf, len, flags);
}

ssize_t send(int fd, const void *buf, size_t len, int flags) {
    printf("in send\n");
    return send_f(fd, buf, len, flags);
}
ssize_t read(int fd, void *buf, size_t len) {
    printf("in read\n");
    return read_f(fd, buf, len);
}

ssize_t write(int fd, const void *buf, size_t len) {
    printf("in write\n");
    return write_f(fd, buf, len);
}

static int init_hook() {
    connect_f = dlsym(RTLD_NEXT, "connect");
    recv_f = dlsym(RTLD_NEXT, "recv");
    send_f = dlsym(RTLD_NEXT, "send");
    read_f = dlsym(RTLD_NEXT, "read");
    write_f = dlsym(RTLD_NEXT, "write");
}

void main() {
    init_hook();
    MYSQL *m_mysql = mysql_init(NULL);
    if (!m_mysql) {
        printf("mysql_init failed\n");
        return;
    }
    if (!mysql_real_connect(m_mysql, "192.168.109.1", "root", "123456", "cdb", 3306, NULL, 0)) {
        printf("mysql_real_connect failed\n");
        return;
    }
    else {
        printf("mysql_real_connect success\n");
    }
}
//gcc -o hook hook.c -lmysqlclient -I /usr/include/mysql/ -ldl

如果跟mysql,redis建立连接进行io操作,但是不去修改它们提供的客户端源码开发包的时候,就会发现连不上去,因为其源码用的是posix api,recv和send。而协程用的是nty_recv()和nty_send()。两者之间没有关联。 

所以解决方法就用hook函数进行替换

 这样,在执行协程之前,我们先利用hook进行系统函数替换,下面是替换的原理图

 当我们替换掉之后,我们的mysql和redis也就支持我们自己定义的函数了,那么mysql在recv和send的时候也就是用的我们协程支持的recv和send等操作了

那怎么测试呢?

测试mysql和redis要测试二大要点

1.增删改查

2.存储过程

六、协程多核模式

解决协程多核的问题有两种方式

  1. 多进程(实现起来容易,对协程代码本身不用去改)
  2. 多线程(复杂,需要对调度器进行加锁)

  那么做多线程对调度器进行加锁,锁放在哪呢?锁放在调度器结构体里面,因为调度器是全局唯一的,那么要锁哪里呢?<取协程,恢复协程>,这里需要加锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1155241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLO算法改进4【中阶改进篇】:添加DeformableConvolution卷积模块

论文地址: https://arxiv.org/abs/1811.11168 源码地址:https://github.com/msracver/Deformable-ConvNets 传统的卷积操作是将特征图分成一个个与卷积核大小相同的部分,然后进行卷积操作,每部分在特征图上的位置都是固定的。这样,对于形变比较复杂的物体,使用这种卷积的…

C#学习相关系列之多线程---lock线程锁的用法

一、lock的作用 Lock可以看成在操作系统中的临界区&#xff0c;Lock区域内的代码表示临界区&#xff0c;使得同一时间只有一个线程能够进入Lock所包含的函数中&#xff0c;实现原子操作&#xff0c;保护同一资源只有一个线程进行修改&#xff0c;实现不同线程中数据的同步。 …

【数据结构】数组和字符串(十二):顺序存储字符串的基本操作(串长统计、查找、复制、插入、删除、串拼接)

文章目录 4.3 字符串4.3.1 字符串的定义与存储4.3.2 字符串的基本操作&#xff08;顺序存储&#xff09;1. 串长统计2. 串定位3. 串复制4. 串插入5. 串删除6. 串拼接7.主函数8. 代码整合及优化 4.3 字符串 字符串(String)是由零个或多个字符(char)顺序排列组成的有限序列&#…

基于RFID技术的优化医药供应链管理解决方案

一、社会背景和挑战 随着全球假药问题的严重性日益凸显&#xff0c;医疗产品的追溯和管理变得越来越重要。据世界卫生组织报告&#xff0c;全球假药比例已超过10%&#xff0c;而中国每年至少有20万人死于假药和不当用药。在国际上&#xff0c;医疗产品的追溯体系已成为监管机构…

总线类设备驱动——IIC

目录 一、本章目标 二、IIC设备驱动 2.1 I2C协议简介 2.2 LinuxI2C驱动 2.3 I2C 设备驱动实例 一、本章目标 一条总线可以将多个设备连接在一起&#xff0c;提高了系统的可扩展性能。这个互联的系统通常由三部分组成:总线控制器、物理总线(一组信号线) 和设备。总线控制器…

视频监控平台EasyCVR分组接口出现“pending”报错,该如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台可拓展性强、视频能力灵活&#xff0c;能对外分发RTMP、RTSP、…

直线导轨的误差匹配度如何?

直线导轨的误差匹配度是评估导轨之间配合精度的重要指标&#xff0c;导轨之间的配合精度越高&#xff0c;误差匹配度就会越好&#xff0c;反之则越差。 在直线导轨的生产和加工过程中&#xff0c;每个导轨都会产生一定误差&#xff0c;例如平行误差、垂直误差、轨面平整度、滑块…

什么是跨域问题?如何解决?

跨域问题指的是不同站点之间,使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。但这个保护机制也带来了新的问题,它的问题是给不同站点之间的正常调用,也带来的阻碍,那怎么解决这个问题呢?接下来…

2023软件测试八股文最新版(含答案+文档)

一、Web 自动化测试 1、Selenium 中 hidden 或者是 display &#xff1d; none 的元素是否可以定位到&#xff1f; 不能&#xff0c;可以写 JavaScript 将标签中的 hidden 先改为 0&#xff0c;再定位元素 2、Selenium 中如何保证操作元素的成功率&#xff1f;也就是说如何保…

连续分析:提高应用效率和成本效益的关键

作者&#xff1a;John Knoepfle 最近&#xff0c;Elastic Universal Profiling 已经正式发布。 它是我们可观察性解决方案的一部分&#xff0c;允许用户在生产环境中进行整个系统的连续分析。 如果你不熟悉连续分析&#xff0c;你可能想知道通用分析是什么以及为什么你应该关心…

【1++的Linux】之信号(一)

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的Linux】 文章目录 一&#xff0c;关于信号二&#xff0c;深剖信号的产生1. 键盘组合建产生信号2.核心转储3. 系统调用接口产生信号4. 由软件条件产生信号5. 硬件异常产生信号 一&#xff0c;…

TSINGSEE青犀AI视频识别技术+危化安全生产智慧监管方案

一、背景分析 石油与化学工业生产过程复杂多样&#xff0c;涉及的物料易燃易爆、有毒有害&#xff0c;生产条件多高温高压、低温负压&#xff0c;现场危险化学品存储量大、危险源集中&#xff0c;重特大安全事故多发。打造基于工业互联网的安全生产新型能力&#xff0c;提高危…

storage数据存储问题,不能存undefined

这篇文章分享一下自己使用sessionStorage遇到的一个小问题&#xff0c;以后遇到要避坑。 需求是easyui表格的单元格编辑&#xff0c;点击保存的时候会结束当前行的编辑&#xff0c;然后修改editingId&#xff08;当前编辑行记录的ID&#xff09;。 待解决问题 如图&#xff0c…

操作系统的内存管理之虚拟空间

操作系统的内存管理&#xff0c;主要分为三个方面。 第一&#xff0c;物理内存的管理&#xff0c;相当于会议室管理员管理会议室。 第二&#xff0c;虚拟地址的管理&#xff0c;也即在项目组的视角&#xff0c;会议室的虚拟地址应该如何组织。 第三&#xff0c;虚拟地址和物…

vcomp140.dll丢失是什么意思,vcomp140.dll丢失这几个方法都能修复好

vcomp140.dll是什么&#xff1f; vcomp140.dll是一个动态链接库&#xff08;Dynamic Link Library&#xff09;&#xff0c;它主要用于支持Microsoft Visual C 2015编程语言的运行。这个文件包含了编译器相关的函数和资源&#xff0c;对于使用Visual C 2015开发的程序和游戏来…

Android图形系统之HWComposer、ComposerHal、ComposerImpl、Composer、Hwc2::Composer实例总结(十四)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

一款Nacos漏洞自动化工具

1、参考GitHub - charonlight/NacosExploitGUI: Nacos漏洞综合利用GUI工具&#xff0c;集成了默认口令漏洞、SQL注入漏洞、身份认证绕过漏洞、反序列化漏洞的检测及其利用 0x01 前言 ​ 本工具已经集成Nacos常见漏洞的检测及其利用&#xff0c;工具为GUI版本&#xff0c;简单…

[迁移学习]DA-DETR基于信息融合的自适应检测模型

原文标题为&#xff1a;DA-DETR: Domain Adaptive Detection Transformer with Information Fusion&#xff1b;发表于CVPR2023 一、概述 本文所描述的模型基于DETR&#xff0c;DETR网络是一种基于Transformer的目标检测网络&#xff0c;详细原理可以参见往期文章&#xff1a;…

k8s 资源预留

KUBERNETES资源管理之–资源预留 Kubernetes 的节点可以按照 Capacity 调度。node节点本身除了运行不少驱动 OS 和 Kubernetes 的系统守护进程&#xff0c;默认情况下 pod 能够使用节点全部可用容量&#xff0c; 除非为这些系统守护进程留出资源&#xff0c;否则它们将与 pod 争…

创造产业链协同优势后,凌雄科技在DaaS行业转动成长飞轮

企业服务领域&#xff0c;一直存在一种共识&#xff1a;做好很难&#xff0c;但一旦服务模式跑通了&#xff0c;得到了市场的认可&#xff0c;要滚起雪球就会事半功倍。 重资产、重运营的DaaS&#xff08;设备及服务&#xff09;赛道&#xff0c;是个非常典型的细分领域。在这…