怎么做到高性能网络IO?

为什么要做高性能网络IO。主要是解决c10,c10M问题

最开始的时候我们走的内核协议栈,走内核协议栈其实性能比较低,因为我们之前介绍的时候需要拷贝两次

但是我们采用用户态协议栈可以少拷贝一次,可以大大提高效率,

步骤:

1)客户端请求数据,先经过网卡,服务器需要从网卡copy数据到内核协议栈(tcp/bsd)。

2)再从内核协议栈copy数据到应用程序。

由此可见,客户端与应用程序之间的数据交互,多了两次数据拷贝的操作,在大量数据并发的情况下,必将会严重影响性能。

优化思路:可以跳过内核协议栈,去除拷贝操作,数据直接从网卡到应用程序,这种方式称为零拷贝。

但是我们这个在做完用户态协议栈在配合reactor的时候,会出现一个问题,是什么问题呢?

问题是:我们的epoll并不会通知这个事件

那么我们是怎么看待这个问题的呢,我们如果不了解epoll的原理和底层的话,我们一下也不知道为什么epoll不会通知,其实是因为epoll的通知是由内核通知的,但是我们旁路之后,不走内核协议栈,那么内核协议栈就不会通知数据了

图例:(我们是在内核里创建了红黑树和fd的一些结构体信息,然后提供系统调用给用户)

协议栈解析出有数据来,通知到epoll中。应用程序操作epoll

 所以我们在走用户态协议栈的时候,就不能用系统自带的epoll了,需要自己再用户态实现一个

 epoll,进行管理

我们怎么设计epoll呢,我们采用红黑树结构是最好的,排除掉哈希表,优先队列,链表

就是红黑树最好了,为什么,因为红黑树这个数据结构更适合增删改查,效率也高,并且有

带有二叉搜索树的性质,所以很好用

需要查找性能高的数据结构,可选的数据结构有

  • hash:fd 数量不确定,创建 hash 消耗大量的内存。若 fd 数量较少时,内存浪费多,性能低
  • b/b+ 树:查找性能低于红黑树,降低树高,用于磁盘 io
  • rbtree:查找性能高,效率稳定,这里选用红黑树

还有一点就是epoll 监听的是系统 fd。而在自定义用户态协议栈的过程中,我们定义的 fd 只是个 int 值,并不指向内核打开文件表中对应的 i-node 结点

epoll 通过 fd 检测协议栈中的 tcb 有无事件发生,并对这些 fd 进行管理。

 

自定义epoll 的主要结构体有

  • epitem:存储每个 io 对应的事件,每个注册到 epoll 池的 fd 对应1个 epitem

// 自定义的 epitem
struct epitem {
	RB_ENTRY(epitem) rbn;		// 红黑树的结点
	LIST_ENTRY(epitem) rdlink;	// 就绪队列,双向链表结点
	int rdy; 				   // 是否在就绪队列中
	
    int sockfd;				   // 事件对应的sockfd
	struct epoll_event event;	// 注册事件的类型 
};

eventpoll:用于管理1个 epoll 对象

// 自定义的 eventpoll
struct eventpoll {
	int fd;		    // epfd
	ep_rb_tree rbr;	// 红黑树的根结点
	int rbcnt;	    
	
	LIST_HEAD( ,epitem) rdlist;	// 就绪队列头结点
	int rdnum;					

	int waiting;	// epoll_wait判断是否正在等待

	pthread_mutex_t mtx; 	 //rbtree update
	pthread_spinlock_t lock; //rdlist update
	
	pthread_cond_t cond; 	//block for event,用于epoll_wait的超时等待
	pthread_mutex_t cdmtx; 	//mutex for cond
};

 红黑树和双向链表共用结点 epitem。 

双向链表采用的是就绪队列,在处理事件的时候,可以按先来先服务策略进行处理时间

2、epoll 锁机制

考虑两个公共资源:红黑树和就绪队列。

  • 红黑树:mutex,互斥锁
  • 就绪队列:spinlock,采用自旋锁,避免 SMP 体系下,多核竞争。

 我们的红黑树的删除和修改和插入都是采用互斥锁的,因为不用锁的的话会发生线程安全问题,比如我们将epoll交给多个线程管理,那么当事件就绪的时候就会有惊群效应,如果此时

不加锁的话,那么多个线程会同时去处理这个事件,那么就会出现线程安全问题

3、epoll 用户态接口

epoll 为用户态提供的接口有:epoll_createepoll_ctleoll_wait

 

3.1、epoll_create 的实现

功能: 创建 eventpoll 结构体

int epoll_create(int size) {
	
	if (size <= 0) return -1;

	// 从位图中获取新的fd,fd从3开始依次递增	
	int epfd =get_fd_frombitmap();

	struct eventpoll *ep = (struct eventpoll*)rte_calloc("eventpoll",1, sizeof(struct eventpoll), 0);
	if (!ep) {
		// 创建失败,将fd从位图中删除
		set_fd_frombitmap(epfd);
		return -1;
	}

	// 初始化红黑树和就绪队列
	ep->rbcnt = 0;
	RB_INIT(&ep->rbr);
	LIST_INIT(&ep->rdlist);

	if (pthread_mutex_init(&ep->mtx, NULL)) {
		rte_free(ep);
		set_fd_frombitmap(epfd);
		return -2;
	}

	if (pthread_mutex_init(&ep->cdmtx, NULL)) {
		pthread_mutex_destroy(&ep->mtx);
		rte_free(ep);
		set_fd_frombitmap(epfd);
		return -2;
	}

	if (pthread_cond_init(&ep->cond, NULL)) {
		pthread_mutex_destroy(&ep->cdmtx);
		pthread_mutex_destroy(&ep->mtx);
		rte_free(ep);
		set_fd_frombitmap(epfd);
		return -2;
	}

	if (pthread_spin_init(&ep->lock, PTHREAD_PROCESS_SHARED)) {
		pthread_cond_destroy(&ep->cond);
		pthread_mutex_destroy(&ep->cdmtx);
		pthread_mutex_destroy(&ep->mtx);
		rte_free(ep);

		set_fd_frombitmap(epfd);
		return -2;
	}

	return epfd;
}

 

3.2、epoll_ctl 的实现

功能:对红黑树进行增添,修改、删除。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) {
	
    // 通过fd查找到协议栈中对应的tcb连接,返回 eventpoll 对象,fd -> host
	struct eventpoll *ep = (struct eventpoll *)get_hostinfo_fromfd(epfd);
	// 若ep对象为空,或没有要设置的事件(del除外)
	if (!ep || (!event && op != EPOLL_CTL_DEL)) {
		errno = -EINVAL;
		return -1;
	}

	///1、ADD 操作
	if (op == EPOLL_CTL_ADD) {
		
		pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = fd;
         // 在红黑树查找该结点 
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		
		// 若红黑树已经存在该结点,返回
		if (epi) {
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}
		// 不存在,则创建 epitem 结点,并为其添加sockfd和事件
		epi = (struct epitem*)rte_calloc("epitem",1, sizeof(struct epitem), 0);
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			errno = -ENOMEM;
			return -1;
		}	
		epi->sockfd = fd;
		memcpy(&epi->event, event, sizeof(struct epoll_event));

		// 插入到红黑树中
		epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
		assert(epi == NULL);
		// 红黑树结点数量增加
        ep->rbcnt ++;
		
		pthread_mutex_unlock(&ep->mtx);

	} 
	// 2、DEL 操作
	else if (op == EPOLL_CTL_DEL) {

		pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = fd;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		
        // 若红黑树中不存在该结点,直接返回
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}
		// 存在该结点,则从红黑树中删除
		epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}
		// 红黑树结点数量减少
		ep->rbcnt --;
         // 释放结点空间
		rte_free(epi);
		
		pthread_mutex_unlock(&ep->mtx);

	} 
    // 3、MOD 操作
    else if (op == EPOLL_CTL_MOD) {

		struct epitem tmp;
		tmp.sockfd = fd;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		// 该结点存在,则修改
		if (epi) {
			epi->event.events = event->events;
			epi->event.events |= EPOLLERR | EPOLLHUP;
		} 
		// 不存在,返回-1
		else {
			errno = -ENOENT;
			return -1;
		}

	} 
    // 4、非法操作
    else {
		assert(0);
	}

	return 0;
}

3.3、epoll_wait 的实现
功能:等待 fd 就绪,监控就绪队列,若有数据,从内核拷贝数据到用户空间;若没有数据,阻塞。

等待的实现方法

等待规定的时间,条件变量 + pthread_cond_timedwait
一直等待(阻塞),条件变量 + pthread_cond_wait
 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) {
	
    // 通过fd查找到协议栈中对应的tcb连接,返回 eventpoll 对象,fd -> host
	struct eventpoll *ep = (struct eventpoll *)get_hostinfo_fromfd(epfd);
	// 若ep对象为空,或没有要设置的事件(del除外)
	if (!ep || (!event && op != EPOLL_CTL_DEL)) {
		errno = -EINVAL;
		return -1;
	}

	///1、ADD 操作
	if (op == EPOLL_CTL_ADD) {
		
		pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = fd;
         // 在红黑树查找该结点 
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		
		// 若红黑树已经存在该结点,返回
		if (epi) {
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}
		// 不存在,则创建 epitem 结点,并为其添加sockfd和事件
		epi = (struct epitem*)rte_calloc("epitem",1, sizeof(struct epitem), 0);
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			errno = -ENOMEM;
			return -1;
		}	
		epi->sockfd = fd;
		memcpy(&epi->event, event, sizeof(struct epoll_event));

		// 插入到红黑树中
		epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
		assert(epi == NULL);
		// 红黑树结点数量增加
        ep->rbcnt ++;
		
		pthread_mutex_unlock(&ep->mtx);

	} 
	// 2、DEL 操作
	else if (op == EPOLL_CTL_DEL) {

		pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = fd;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		
        // 若红黑树中不存在该结点,直接返回
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}
		// 存在该结点,则从红黑树中删除
		epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}
		// 红黑树结点数量减少
		ep->rbcnt --;
         // 释放结点空间
		rte_free(epi);
		
		pthread_mutex_unlock(&ep->mtx);

	} 
    // 3、MOD 操作
    else if (op == EPOLL_CTL_MOD) {

		struct epitem tmp;
		tmp.sockfd = fd;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		// 该结点存在,则修改
		if (epi) {
			epi->event.events = event->events;
			epi->event.events |= EPOLLERR | EPOLLHUP;
		} 
		// 不存在,返回-1
		else {
			errno = -ENOENT;
			return -1;
		}

	} 
    // 4、非法操作
    else {
		assert(0);
	}

	return 0;
}

4、epoll 回调

4.1、epoll 回调函数的实现


当内核 io 准备就绪的时候,执行 epoll 回调函数,将 epitem 添加到 rdlist 中,唤醒 epoll_wait。当 epoll_wait 被激活重新运行的时候,将 rdlist 的 epitem 逐一拷贝到 events 中,同时删除 rdlist 中对应的结点。换句话说, epoll_callback 是生产者,放入结点,唤醒 epoll_wait;epoll_wait 是消费者,消费结点。

// 从协议栈回调到epoll,把fd和对应的事件拷贝到应用程序
static int nepoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {
	
	struct epitem tmp;
	tmp.sockfd = sockid;
	// 在红黑树中查找 epitem
	struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
	if (!epi) {
		return -1;
	}
	// 已经在就绪队列中,只添加事件
	if (epi->rdy) {
		epi->event.events |= event;
		return 1;
	} 
	
	// 不在就绪队列,则将结点加入到就绪队列
	pthread_spin_lock(&ep->lock);
	epi->rdy = 1;
	LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
	ep->rdnum ++;
	pthread_spin_unlock(&ep->lock);

	pthread_mutex_lock(&ep->cdmtx);

	// 就绪队列中增加结点,唤醒epoll_wait
	pthread_cond_signal(&ep->cond);
	pthread_mutex_unlock(&ep->cdmtx);
	return 0;

}
4.2、epoll 回调的时机


触发 epoll 回调4个时机,需要在这些地方添加 epoll 回调函数,使得 epoll 可以正常接收数据。

三次握手中,在 syn-rcvd 状态,对端返回 ack 后,tcb 结点放入到全连接队列,将对应的 sockfd 的置为 EPOLLIN 状态,等待 accept 取出,触发 epoll 回调。
 

if (stream->status == TCP_SYN_RCVD) {
    // 进入到 ESTABLISHED 状态
    stream->status = TCP_STATUS_ESTABLISHED;
	// 设置 epoll 回调函数,等待 accept
}

在 established 状态,收到数据后,将 sockfd 置为 EPOLLIN 状态,等待读取数据,触发epoll 回调

if (tcphdr->tcp_flags & TCP_PSH_FLAG) {
    // 建立连接后,push 接收数据,设置 epoll 回调函数
} 

在 established 状态,收到 fin 时,进入到 close_wait 状态。将 sockfd 的 event 置为 EPOLLIN,读取断开信息,触发 epoll 回调

if (tcphdr->tcp_flags & TCP_FIN_FLAG) {
    // 收到 fin,进入到 CLOSE_WAIT 状态
    stream->status = TCP_STATUS_CLOSE_WAIT; 
    // 设置 epoll 回调函数,读取断开信息
}
  • 检测 socket 的 send 状态,如果对端 cwnd>0, 可以发送数据,将 sockfd 置为 EPOLLOUT,等待发送数据

5、epoll 事件通知机制


水平触发(LT),有事件,则一直触发;边缘触发(ET),只触发一次,关注的是 io 状态的变化。

实现的关键是内核 io 就绪时,epoll 回调函数的执行次数。

LT,检测 recvbuffer 有数据则调用 epoll 回调函数
ET,从协议栈中检测到recvbuffer中接收数据就调用 epoll 回调函数
 

我们后面还可以用io_uring来处理,先不介绍了

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于粒子群算法优化概率神经网络PNN的分类预测 - 附代码

基于粒子群算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于粒子群算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于粒子群优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络…

职业迷茫,我该如何做好职业规划

案例25岁男&#xff0c;入职2月&#xff0c;感觉自己在混日子&#xff0c;怕能力没有提升&#xff0c;怕以后薪资也提不起来。完全不知道应该往哪个方向进修&#xff0c;感觉也没有自己特别喜欢的。感觉自己特别容易多想&#xff0c;想多年的以后一事无成的样子。 我觉得这个案…

Tektronix(泰克)示波器TBS1102B测试电压

对于 Tektronix TBS1102B 示波器来说&#xff0c;测试电压的步骤基本如下&#xff1a; 连接测量点&#xff1a; 将被测电路的测量点连接到示波器的输入通道。使用正确的探头并确保连接的极性正确。 选择通道&#xff1a; 选择示波器上的通道&#xff0c;你想要测量的电压可能连…

20231112_DNS详解

DNS是实现域名与IP地址的映射。 1.映射图2.DNS查找顺序图3.DNS分类和地址4.如何清除缓存 1.映射图 图片来源于http://egonlin.com/。林海峰老师课件 2.DNS查找顺序图 3.DNS分类和地址 4.如何清除缓存

单链表按位序与指定结点 删除

按位序删除(带头结点) #define NULL 0 #include<stdlib.h>typedef struct LNode {int data;struct LNode* next; }LNode, * LinkList;//按位序删除&#xff08;带头结点&#xff09; bool ListInsert(LinkList& L, int i, int& e) {if (i < 1)return false;L…

【Spring Cloud】声明性REST客户端:Feign

Spring Cloud Feign ——fallback 服务降级 1. Feign 简介2. Feign 的基础使用2.1 普通 HTTP 请求2.2 Feign 远程调用上传文件接口 1. Feign 简介 Feign 是一个声明式的 HTTP 客户端&#xff0c;它简化了编写基于 REST 的服务间通信代码的过程。在 Spring Cloud 中&#xff0c…

如何从零开始手写一个消息中间件(从宏观角度理解消息中间件的技术原理)

如何从零开始手写一个消息中间件&#xff08;从宏观角度理解消息中间件的技术原理&#xff09; 什么是消息中间件消息中间件的作用逐一拆解消息中间件的核心技术消息中间件核心技术总览IOBIONIOIO多路复用AIOIO多路复用详细分析selectpollepoll Java中的IO多路复用 协议序列化消…

【算法每日一练]-快速幂,倍增,滑动窗口(保姆级教程 篇1) #麦森数 #青蛙跳

之前是考试准备&#xff0c;所以有几天没更新&#xff0c;今天开始继续更新 目录 快速幂模板 题目&#xff1a;麦森数 思路&#xff1a; 题目&#xff1a;青蛙跳 思路&#xff1a; 快速幂模板 #include <bits/stdc.h> #define ll long long using namespa…

.net在使用存储过程中IN参数的拼接方案,使用Join()方法

有时候拼接SQL语句时&#xff0c;可能会需要将list中的元素都加上单引号&#xff0c;并以逗号分开&#xff0c;但是Join只能简单的分开&#xff0c;没有有单引号&#xff01; 1.第一种拼接方案 List<string> arrIds new List<string>(); arrIds.Add("aa&qu…

JavaScript_动态表格_删除功能

1、动态表格_删除功能 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>动态表格_添加和删除功能</title><style>table{border: 1px solid;margin: auto;width: 100%;}td,th{text-align: …

杂记 | 使用FRP搭建内网穿透服务(新版toml配置文件,搭配反向代理食用)

文章目录 01 需求与回顾02 下载程序包03 编辑.toml文件3.1 编辑frps.toml3.2 编辑frpc.toml 04 启动服务4.1 启动服务端4.2 启动客户端 05 配置反向代理&#xff08;可选&#xff09;06 windows设置为默认启动&#xff08;可选&#xff09;6.1 创建启动脚本6.2 设置为开机自启 …

【Java 进阶篇】Java与JQuery选择器:解锁前端开发的魔法大门

在前端开发的世界中&#xff0c;选择器是我们与HTML文档进行互动的钥匙&#xff0c;而Java和JQuery则为我们提供了强大的工具&#xff0c;使得前端开发不再是一个艰深的谜题。本篇博客将围绕Java与JQuery选择器展开&#xff0c;深入解析选择器的奥秘&#xff0c;为你打开前端开…

你一定要学会的Java语法 -- 【继承】

书接上回&#xff0c;我们已经学完了类和对象&#xff0c;今天内容可能有一点难&#xff0c;相信自己能跨过这道坎。 目录 一. 继承 1.什么是继承 2. 继承的概念 3. 继承的语法 4.父类成员访问 子类和父类成员变量同名 子类和父类成员方法同名 5.super关键字 6.子类构…

003、Nvidia Jetson Nano Developer KIT(b01)-深度学习环境配置

之——深度学习环境 杂谈 网上到处淘金&#xff0c;pytorch、opencv、torchvision。 正文 1.各种依赖库 1.1 pytorch的底层依赖库 sudo apt install build-essential make cmake cmake-curses-gui -ysudo apt install git g pkg-config curl -ysudo apt install libatlas-ba…

Java图像编程之:Graphics

一、概念介绍 1、Java图像编程的核心类 Java图像编程的核心类包括&#xff1a; BufferedImage&#xff1a;用于表示图像的类&#xff0c;可以进行像素级的操作。Image&#xff1a;表示图像的抽象类&#xff0c;是所有图像类的基类。ImageIcon&#xff1a;用于显示图像的类&a…

计算机中丢失msvcr120.dll文件怎么修复?找不到msvcr120.dll五种完美修复方案

今天我想和大家分享的是关于“msvcr120.dll丢失的问题的5个解决方法”。在我们日常的工作生活中&#xff0c;或许大家都曾遇到过这样的问题&#xff0c;那么&#xff0c;了解它的解决方法是非常必要的。 首先&#xff0c;让我们来了解一下msvcr120.dll是什么文件。简单来说&am…

“艾迪-东软杯”第六届武汉理工大学新生程序设计竞赛

A.Capoos Acronym Zero 题目描述 yz 和他的朋友 ea 和 zech 一起养了一群 Capoo。 这些 Capoo 非常聪明&#xff0c;但不知道为什么&#xff0c;它们并没有从三人那里学到怎么写算法题&#xff0c;而是出于某种原因开始研究语言学&#xff0c;并发明了一套自己的暗语。这门暗语…

二分图判定和二分图最大匹配

1.二分图的定义 二分图是一种特殊的无向图&#xff0c;它的节点可以被划分为两个互不相交的集合&#xff0c;使得同一集合中的任意两个节点之间没有边相连&#xff0c;而不同集合中的节点之间都有边相连。 换句话说&#xff0c;如果一个无向图可以被划分为两个集合&#xff0…

Keil文本对齐

摘要&#xff1a;通常我们写代码的时候&#xff0c;尤其是缩进和{}的使用&#xff0c;很多都需要自己手动去调整&#xff0c;如果有一个自动格式化代码的工具&#xff0c;每次编辑完代码&#xff0c;然后一键给将代码格式化&#xff0c;即省时又美观。为了解决这个问题&#xf…

面向对象高级

本期对应知识库&#xff1a;&#xff08;持续更新中&#xff01;&#xff09; 面向对象高级 (yuque.com) ​​​​​​​尚硅谷_宋红康_对象内存解析.pptx static 适用于公用变量 开发中&#xff0c;变量 经常把一些常量设置为静态static 例如 PI 方法 经常把工具类中的方…