自定义 epoll 的实现

零基础的朋友,建议先阅读我之前写的文章:io 多路复用

先来看几个问题:

自定义协议栈为什么不能使用系统的epoll,而要去自定义epoll?

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

epoll 在自定义协议栈中的作用?

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

在这里插入图片描述

1、epoll 数据结构

epoll 采用 key-value 结构,通过 fd 找到对应的事件,需要存储大量的fd

选择什么样的数据结构来存储 fd 的集合?

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

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

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 体系下,多核竞争。

3、epoll 用户态接口

epoll 为用户态提供的接口有:epoll_create, epoll_ctl, eoll_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_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) {
	
	// 通过fd查找到协议栈中对应的tcb连接,返回 eventpoll 对象,fd -> host
	struct eventpoll *ep = (struct eventpoll *)get_hostinfo_fromfd(epfd);
	if (!ep || !events || maxevents <= 0) {
		errno = -EINVAL;
		return -1;
	}

	if (pthread_mutex_lock(&ep->cdmtx)) {
		if (errno == EDEADLK) {
			// assert(0);
		}
	}

	// 1、若就绪队列为空,timeout的值来判断是否进行等待,timeout = 0不等待
	while (ep->rdnum == 0 && timeout != 0) {
		
		ep->waiting = 1;	// 设置ep状态为正在等待
		// 1.1、等待timeout
		if (timeout > 0) {

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

			// 利用条件变量 + pthread_cond_timedwait
			// 在规定的时间内等待io事件的到来,实现等待规定的时间
			int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);
			if (ret && ret != ETIMEDOUT) {		
				pthread_mutex_unlock(&ep->cdmtx);
				return -1;
			}

			timeout = 0;
		} 
		// 1.2、一直等待
		else if (timeout < 0) {
			// 利用条件变量 + pthread_cond_wait
			// 一直等待io事件的到来,实现一直等待
			int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);
			if (ret) {
				pthread_mutex_unlock(&ep->cdmtx);

				return -1;
			}
		}

		ep->waiting = 0; // 设置ep状态为不等待

	}

	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 --;		// 要操作的结点数-1
		cnt ++;		// 返回的就绪io数量+1
		ep->rdnum --;	// 就绪队列中结点数量-1
	}
	
	// 释放锁
	pthread_spin_unlock(&ep->lock);

	// 返回就绪的io数量
	return cnt;
}

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 回调函数
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
epoll是一种I/O多路复用技术,主要用于在一个进程中管理多个socket的监听。它的作用是通过注册和监听多个socket,实现同时处理多个网络事件的能力。在常用的TCP/UDP程序中,如果只有一个socket,不需要使用epoll。但是如果有多个socket,就可以使用epoll来管理这些socket。 epoll主要由两个结构体组成:eventpoll和epitem。eventpoll是每一个epoll所对应的事件,而epitem是每一个IO所对应的事件。当调用epoll_ctl的EPOLL_CTL_ADD操作时,需要创建一个epitem来注册一个socket到epoll中。而通过调用epoll_wait方法可以获取已经监听到的事件。 下面是一个示例代码,展示了如何使用epoll实现socket监听: ``` // 创建一个epoll对象 int epollFd = epoll_create(1); // 创建一个socket int listenSocket = socket(AF_INET, SOCK_STREAM, 0); // 设置socket为非阻塞模式 fcntl(listenSocket, F_SETFL, O_NONBLOCK); // 创建一个epoll_event结构体 struct epoll_event listenEvent; listenEvent.data.fd = listenSocket; listenEvent.events = EPOLLIN | EPOLLET; // 监听读事件,并设置为边沿触发模式 // 将socket注册到epollepoll_ctl(epollFd, EPOLL_CTL_ADD, listenSocket, &listenEvent); // 开始监听事件 struct epoll_event events[MAX_EVENTS]; while (true) { int readyEventCount = epoll_wait(epollFd, events, MAX_EVENTS, -1); if (readyEventCount == -1) { // 发生错误,处理错误逻辑 break; } // 处理就绪的事件 for (int i = 0; i < readyEventCount; ++i) { if (events[i].data.fd == listenSocket) { // 监听socket有新的连接请求,处理连接逻辑 } else { // 其他socket有数据可读,处理读取逻辑 } } } // 关闭epoll和socket close(epollFd); close(listenSocket) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值