2.9 epoll的实现原理

news2024/11/25 22:38:02

目录

  • 一、epoll的数据结构
    • 1、epoll的数据结构选择
    • 2、epoll数据结构图
  • 二、epoll的线程安全
  • 三、epoll的内核回调
    • `epoll` 回调函数
    • `epoll`回调时机
  • 四、epoll的用户态接口
    • `epoll_create`
    • `epoll_ctl `
    • `epoll_wait`
  • 五、`epoll`的LT和ET


在前文已经基于dpdk实现了用户态协议栈,但是有个缺陷就是不能连接多服务端。这也就引出了本文的目的——如何实现自定的 epoll

为什么不用系统自带的epoll?

用户态协议栈是指运行在用户态的协议栈,与传统的内核态协议栈相比,它有许多优点,如灵活性、可扩展性、高性能等。因为可以避免内核态和用户态之间频繁的上下文切换,从而提高网络应用的处理性能。
在用户态协议栈中,一般需要自定义 I/O 多路复用机制,以便实现事件驱动的网络编程模型。传统的 Linux 系统提供了 epoll 这种高效的 I/O 多路复用机制,但是无法直接在用户态协议栈中使用。主要原因是,Linux 中的 epoll 实现涉及到了内核态和用户态之间的交互,如果要在用户态协议栈中使用 Linux 的 epoll,就必须频繁地进行系统调用和内存拷贝,这样会导致性能严重下降,并且违背了使用用户态协议栈的初衷。

因此在实现 epoll之前,需要先学习内核 epoll的原理。可以从四个方面入手:
1) epoll的数据结构
2) epoll的线程安全
3) epoll的内核回调
4) epoll的LT和ET

一、epoll的数据结构

1、epoll的数据结构选择

epoll需要为以下两类fd选择合适的数据结构
1)总集:所有fd的集合
2)就绪:就绪fd的集合

对于总集,首先需要明确几点:
1)每一个fd底层对应了一个tcb,包含了该连接的所有信息。也就是key—value的存储模式
2)对于io主要操作有:查找、删除、修改、插入。对于epoll总集,强查找的频率、数量会远高于其他操作。
3)io数量不确定,有时候多,有时候少。但不管什么时候,性能都不能太差。

因此,基于上面,总集可选择的数据结构有
1)hash —— O(1)
\quad 优点是查询速度快。缺点是创建时候需要指定hash的大小,但是不能太小,因为很可能会面临百万的fd。但是大多时候仅需要管理一小部分的fd,那就浪费了很大的空间。
2)rbtree —— O(lg N)
\quad 查找速度快,且不需分配初始大小。
3)链表 —— O(N)
\quad 随着链表越长,查找性能越来越差。
4)跳表
\quad 随着fd越来越多,分级实现越来越复杂。

而对于就绪fd,不需要查找,仅需挨个取出处理,因此选择双向链表即可。

2、epoll数据结构图

epoll的总集eventpoll和就绪epitem的数据结构如下,List 用来存储准备就绪的 IO,Rbtree 用来存储所有 io 的数据。
在这里插入图片描述
epitem:存储每个 io 对应的事件,每个注册到 eventpoll的 fd 对应1个 epitem

struct epitem {
	RB_ENTRY(epitem) rbn;
	LIST_ENTRY(epitem) rdlink;
	int rdy; //是否在就绪队列中
	
	int sockfd;		
	struct epoll_event event; //注册事件的类型
};

eventpoll:管理epoll 对象

struct eventpoll {
	int fd;

	ep_rb_tree rbr;
	int rbcnt;
	
	LIST_HEAD( ,epitem) rdlist;
	int rdnum;

	int waiting;

	pthread_mutex_t mtx; //rbtree update
	pthread_spinlock_t lock; //rdlist update
	
	pthread_cond_t cond; //block for event
	pthread_mutex_t cdmtx; //mutex for cond
	
};

二、epoll的线程安全

epoll为用户态提供三个接口
1)epoll_create:创建fd — 创建红黑树的根节点
2)epoll_ctl:控制fd — EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL分别对应增加、修改、删除结点
3)epoll_wait:把就绪队列的结点,通过指针赋值的方式,复制到用户态的events

epoll的线程安全,无非就是保证两个数据结构的能够多线程并行操作
1)多个线程可以同时操作红黑树,即调用epoll_ctl ——> 对红黑树加锁(互斥锁),锁根节点
2)多个线程可以同时复制,即调用epoll_wait ——> 对就绪队列加锁(自旋锁)

三、epoll的内核回调

epoll 回调函数

调用回调函数,在总集红黑树查找epitem
1)若存在,将新的要关注的事件掩码添加到已有的事件掩码
2)若不存在,加入到就绪队列,然后唤醒epoll_wait。epoll_wait运行后,将就绪队列里的epitem cpoy到用户态的events中,并删除就绪队列里相应的epitem

epoll_callback 是生产者,放入结点,唤醒 epoll_wait;epoll_wait 是消费者,消费结点。


int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {

	struct epitem tmp;
	tmp.sockfd = sockid;
	//在红黑树查找tmp
	struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
	if (!epi) {
		printf("rbtree not exist\n");
		return -1;
	}
	//已在就绪队列中,将新的事件掩码添加到已有的事件掩码
	if (epi->rdy) {
		epi->event.events |= event;
		return 1;
	} 

	printf("epoll_event_callback --> %d\n", epi->sockfd);
	
	//不在就绪队列,则将结点加入到就绪队列
	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;
}

epoll回调时机

一个有四个调用时机

  1. 三次握手完成之后
    tcp 三次握手,对端反馈 ack 后,socket 进入 rcvd 状态。需要将监听 socket 的
    event 置为 EPOLLIN,此时标识可以进入到 accept 读取 socket 数据。
  2. 接收数据回复ACK之后
    在 established 状态,收到数据以后,需要将 socket 的 event 置为 EPOLLIN 状态。
  3. 发送数据收到ACK之后
    检测 socket 的 send 状态,如果对端 cwnd>0 ,可以发送的数据。故需要将 socket
    置为 EPOLLOUT。
  4. 接收FIN回复ACK之后
    在 established 状态,收到 fin 时,此时 socket 进入到 close_wait。需要 socket 的 event置为 EPOLLIN。读取断开信息。

在这里插入图片描述

四、epoll的用户态接口

epoll_create

创建 eventpoll 结构体

int nepoll_create(int size) {

	if (size <= 0) return -1;

	//从位图中获取一个可用文件描述符fd
	int epfd = get_fd_frombitmap(); 
	
	struct eventpoll *ep = (struct eventpoll*)rte_malloc("eventpoll", sizeof(struct eventpoll), 0);
	if (!ep) { //若创建失败,从位图删除
		set_fd_frombitmap(epfd);
		return -1;
	}

	//获得指向全局结构体 ng_tcp_table 类型的指针
	struct ng_tcp_table *table = tcpInstance();
	table->ep = ep;
	
	//初始化红黑树和就绪队列
	ep->fd = epfd;
	ep->rbcnt = 0;
	RB_INIT(&ep->rbr);
	LIST_INIT(&ep->rdlist);

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

	if (pthread_mutex_init(&ep->cdmtx, NULL)) {
		pthread_mutex_destroy(&ep->mtx);
		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);
		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);
		free(ep);
		set_fd_frombitmap(epfd);
		return -2;
	}

	return epfd;

}

epoll_ctl

对红黑树进行增添,修改、删除


int nepoll_ctl(int epfd, int op, int sockid, struct epoll_event *event) {
	
	// 通过epfd获取所关联的内核对象,并强转为 eventpoll 结构体类型
	struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);
	if (!ep || (!event && op != EPOLL_CTL_DEL)) {
		errno = -EINVAL;
		return -1;
	}

	// 增加--EPOLL_CTL_ADD
	if (op == EPOLL_CTL_ADD) {

		pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = sockid;
		//在红黑树查找要增加的结点
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		if (epi) { //若存在,则返回
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}

		//若不存在,则创建新的结点epitem,并添加相应的sockfd、event,最后插入到红黑树中
		epi = (struct epitem*)rte_malloc("epitem", sizeof(struct epitem), 0);
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			rte_errno = -ENOMEM;
			return -1;
		}
		epi->sockfd = sockid;
		memcpy(&epi->event, event, sizeof(struct epoll_event));

		epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);

		ep->rbcnt ++;
		
		pthread_mutex_unlock(&ep->mtx);

	} else if (op == EPOLL_CTL_DEL) { //删除--EPOLL_CTL_DEL

		pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = sockid;
		//在红黑树查找要删除的结点
		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);

	} else if (op == EPOLL_CTL_MOD) { //修改--EPOLL_CTL_MOD

		struct epitem tmp;
		tmp.sockfd = sockid;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		if (epi) {
			epi->event.events = event->events;
			epi->event.events |= EPOLLERR | EPOLLHUP;
		} else {
			rte_errno = -ENOENT;
			return -1;
		}

	} 

	return 0;

}

epoll_wait

监控就绪队列,等待 fd 就绪。若有读写事件发生,从内核拷贝数据到用户空间,加入到事件列表中,并通知等待这些事件的进程或线程;若没有则阻塞等待。

int nepoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) {

	// 通过epfd获取所关联的内核对象,并强转为 eventpoll 结构体类型
	struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);;
	if (!ep || !events || maxevents <= 0) {
		rte_errno = -EINVAL;
		return -1;
	}

	//
	if (pthread_mutex_lock(&ep->cdmtx)) {
		if (rte_errno == EDEADLK) {
			printf("epoll lock blocked\n");
		}
	}

	// 1.若就绪队列为空,且timeout != 0 等待
	while (ep->rdnum == 0 && timeout != 0) {
		//设置状态为等待
		ep->waiting = 1;
		if (timeout > 0) { //timeout > 0,等待timeout
			struct timespec deadline;
			
			//计算定时器的超时时间
			clock_gettime(CLOCK_REALTIME, &deadline);
			if (timeout >= 1000) {
				int sec;
				sec = timeout / 1000;
				deadline.tv_sec += sec;
				timeout -= sec * 1000;
			}

			deadline.tv_nsec += timeout * 1000000;

			if (deadline.tv_nsec >= 1000000000) {
				deadline.tv_sec++;
				deadline.tv_nsec -= 1000000000;
			}

			//指定的时间deadline内等待条件变量cond的状态改变。
			int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);
			if (ret && ret != ETIMEDOUT) {
				printf("pthread_cond_timewait\n");
				
				pthread_mutex_unlock(&ep->cdmtx);
				
				return -1;
			}
			timeout = 0;
		} else if (timeout < 0) { //timeout < 0,一直等待

			int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);
			if (ret) {
				printf("pthread_cond_wait\n");
				pthread_mutex_unlock(&ep->cdmtx);

				return -1;
			}
		}
		ep->waiting = 0;  //更新状态为不等待

	}

	pthread_mutex_unlock(&ep->cdmtx);

	//对就绪队列加锁(自旋锁)
	pthread_spin_lock(&ep->lock);

	int cnt = 0;
	int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum);
	int i = 0;
	
	//2.从就绪队列中拷贝事件到用户态数组
	while (num != 0 && !LIST_EMPTY(&ep->rdlist)) { //EPOLLET
		//从就绪队列取出第一个结点
		struct epitem *epi = LIST_FIRST(&ep->rdlist);
		LIST_REMOVE(epi, rdlink);
		epi->rdy = 0;

		//将事件拷贝到用户态
		memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));
		
		num --;
		cnt ++;
		ep->rdnum --;
	}
	
	pthread_spin_unlock(&ep->lock);

	return cnt;
}

五、epoll的LT和ET

  • ET边沿触发,只触发一次
  • LT水平触发,如果有事件(比如数据recv_buffer没读完)就一直触发

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

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

相关文章

编译原理笔记(哈工大编译原理)(及格版)

文章目录 前言概论语言与文法基本概念字母表串字母表与串的联系 文法语言推导和规约句型与句子语言与字母表 文法的分类CFG的分析树 词法分析正则式正则定义有穷自动机&#xff08;FA&#xff09;DFANFAFA之间的等价性 从RE到DFARE转NFANFA确定化&#xff1a;子集法DFA最小化&a…

Vue.js 内部运行机制

在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」,…

spring boot 运行报错: 找不到或无法加载主类

原文地址&#xff1a;spring boot 运行报错: 找不到或无法加载主类 - 走看看 一&#xff1a;当在eclipse启动spring boot项目时出现问题&#xff1a; springboot错误: 找不到或无法加载主类 解决办法&#xff1a; 1&#xff0c;通过cmd命令行&#xff0c;进入项目目录进行&am…

Linux5.7 MySQL 高级(进阶) SQL 语句

文章目录 计算机系统5G云计算第四章 LINUX MySQL 高级(进阶) SQL 语句一、高级SQL 语句1&#xff09;SELECT2&#xff09;DISTINCT3&#xff09;WHERE4&#xff09;AND OR5&#xff09;IN6&#xff09;BETWEEN7&#xff09;通配符8&#xff09;LIKE9&#xff09;ORDER BY10&…

【软件测试面试题】offer又失之交臂?项目经验项目描述看这个篇就够了

前言 我们测试人员在找工作中&#xff0c;基本都会碰到让介绍项目的这种面试题。 如何正确介绍自己的项目&#xff1f;需要做哪些技术准备&#xff1f; 关于介绍自己的项目&#xff1f; 可以从以下几个方面来表述&#xff1a; 项目基本介绍&#xff1a;项目架构、项目业务流…

如何模拟一个僵尸进程

原理 子进程先于父进程退出&#xff0c;父进程还在继续运行&#xff0c;且没有调用wait函数。 实验代码 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h>#define _DEBUG_INFO #ifdef _DEB…

Zabbix“专家坐诊”第196期问答汇总

问题一 Q&#xff1a;统计一个主机群组里&#xff0c;值为A的某个监控项的个数&#xff0c;函数应该怎么写&#xff1f; A&#xff1a;参考&#xff1a; https://www.zabbix.com/documentation/6.0/zh/manual/config/items/itemtypes/calculated/aggregate。 Q&#xff1a;文…

零基础如何学习 Web 安全,如何让普通人快速入门网络安全?

前言 网络安全现在是朝阳行业&#xff0c;缺口是很大。不过网络安全行业就是需要技术很多的人达不到企业要求才导致人才缺口大 【一一帮助安全学习&#xff08;网络安全面试题学习路线视频教程工具&#xff09;一一】 初级的现在有很多的运维人员转网络安全&#xff0c;初级…

lammps案例:原子倒入容器

本文介绍lammps向体系内动态添加原子的一种方法。 在绝大多数的分子动力学模拟过程中&#xff0c;原子数量保持恒定。 如果需要按一定步数动态的减少原子&#xff0c;可参考&#xff1a; 删除蒸发原子 动态的增加原子&#xff0c;可以使用fix deposit沉积命令&#xff1a; 沉积…

《Stable Diffusion WebUI折腾实录》在Windows完成安装, 从社区下载热门模型,批量生成小姐姐图片

环境 操作系统: Windows11 显卡: RTX2060 6GB 显存 安装Python 下载 Python3.10.6 https://www.python.org/ftp/python/3.10.6/python-3.10.6-amd64.exe安装 注意勾选 Add Python 3.10.6 to PATH &#xff0c;然后一路下一步即可 打开powershell&#xff0c; 确认安装成功 …

Opencv项目实战:23 智能计数和表单信息

目录 0、项目介绍 1、效果展示 2、项目搭建 3、项目代码展示与部分讲解 拍照脚本data_collection.py 图片检测Picdetect.py 摄像头检测Videodetect.py 主函数CountMain.py 自定义模块tally.py 4、项目资源 5、项目总结 0、项目介绍 有一段时间没有更新专栏了&#…

“专精特新”企业数字化转型,如何激发增长新动能

随着数字技术的不断发展&#xff0c;越来越多的企业开始意识到数字化转型的重要性。对于专精特新的企业来说&#xff0c;数字经济的发展也同样给中小企业带来难得的发展机遇&#xff0c;数字化在助力中小企业降本、增效和提质方面发挥着日益重要的作用&#xff0c;数字化转型已…

英伟达发布GeForce 536.23游戏驱动,一键快速获取

英伟达又赶在6月发布了一款超强游戏驱动&#xff0c;NVIDIA GeForce 536.23 WHQL&#xff0c;并有两款游戏大作宣布&#xff0c;首发日即支持 NVIDIA DLSS 2 和 NVIDIA Reflex&#xff0c;驱动人生带大家一起了解一下这款NVIDIA GeForce 536.23 WHQ驱动&#xff0c;以及获取英伟…

Linux【系统学习】(命令及虚拟机篇-无shell)

目录 第 1 章 Linux 入门 1.1 概述 1.2 Linux 和 Windows 区别 ​编辑 1.3 CentOS 下载地址 第 2 章 VM 与 Linux 的安装 2.1 VMWare 安装 CentOS 安装 第 3 章 Linux 文件与目录结构 3.1 Linux 文件 3.2 Linux 目录结构 第 4 章 VI/VIM 编辑器&#xff08;重要&…

智能客服机器人:基于知识图谱的多轮对话系统

━━━━ 近年来&#xff0c;随着人工智能的快速发展&#xff0c;人机交互能力不断增强&#xff0c;其中问答技术能够在保证一定准确度的情况下极大地简化用户的搜索操作&#xff0c;在节约时间的同时&#xff0c;还能够加深用户对搜索事物的了解程度&#xff0c;百度公司的小…

一篇文章搞定《Android中View的绘制流程》

一篇文章搞定《CoordinatorLayout完成电商首页》 本文前言怎样到达ViewRootImpl过程如下&#xff1a;流程图小结&#xff1a; 到达ViewRootImpl做了什么第一步&#xff1a;setView()第二步&#xff1a;performTraversals()第三步&#xff1a;DecorView中的Measure()、Layout()、…

nginx配置代理报错

1.背景 因部署需要将项目用nginx进行二次转发访问&#xff0c;配置过程中出现各种报错&#xff0c;现将记录如下 Whitelabel Error PageThis application has no explicit mapping for /error, so you are seeing this as.... 2.nginx配置如下 upstream wuhan1 {#server 19…

【SpringBoot】整合Elasticsearch 操作索引及文档

官网操作文档&#xff1a;Elasticsearch Clients | Elastic 踩坑太多了。。。这里表明一下Spring Boot2.4以上版本可能会出现问题&#xff0c;所以我降到了2.2.1.RELEASE。对于现在2023年6月而言&#xff0c;Es版本已经到了8.8&#xff0c;而SpringBoot版本已经到了3.x版…

Hyper-v 虚拟机挂载物理硬盘的方法-Windows Server 2022/2025

起因&#xff1a; 之前我写过一篇介绍在KVM虚拟机体系下&#xff0c;如何直接挂载物理硬盘和物理分区的方法&#xff1a;KVM虚拟机直接挂栽物理硬盘分区的方法_给libvirt虚机挂载磁盘_lggirls的博客-CSDN博客。近期帮助一个朋友搭建局域网&#xff0c;其中有OA系统要用到window…

【安全】awvs使用(二)

目录 一、设置目标 二、设置登录 三、扫描引擎 四、开启扫描 五、扫描结束 六、报告 前言&#xff1a;怎么使用awvs进行扫描出报告呢&#xff1f; 一、设置目标 二、设置登录 因为扫描的网站需要登录的&#xff0c;这里需要设置这个 三、扫描引擎 四、开启扫描 翻译的页面…