超越内核:DPDK与用户态Epoll

在高性能网络编程中,epoll 是一种高效的 I/O 多路复用机制,广泛用于处理大量并发连接。尽管 Linux 内核提供了 epoll 的原生实现,但在某些场景中,用户态的 epoll 实现能够提供更灵活的控制和优化。本文将详细介绍一个用户态 epoll 实现的核心部分:nepoll_createnepoll_ctlnepoll_waitepoll_event_callback 函数。

1、定义相关操作

用户态 epoll 的实现,使用红黑树来管理事件和套接字。主要定义了事件类型、控制命令、数据结构和红黑树的相关操作。

1)定义事件类型 EPOLL_EVENTS

enum EPOLL_EVENTS {
    EPOLLNONE    = 0x0000,
    EPOLLIN      = 0x0001,
    EPOLLPRI     = 0x0002,
    EPOLLOUT     = 0x0004,
    EPOLLRDNORM  = 0x0040,
    EPOLLRDBAND  = 0x0080,
    EPOLLWRNORM  = 0x0100,
    EPOLLWRBAND  = 0x0200,
    EPOLLMSG     = 0x0400,
    EPOLLERR     = 0x0008,
    EPOLLHUP     = 0x0010,
    EPOLLRDHUP   = 0x2000,
    EPOLLONESHOT = (1 << 30),
    EPOLLET      = (1 << 31)
};
  • 事件类型: 定义了多种 I/O 事件类型,包括输入、输出、紧急数据、错误和挂起等。
  • EPOLLONESHOT 和 EPOLLET: 分别代表了“一次性”事件和“边缘触发”模式的标志位。

2) epoll 控制命令

#define EPOLL_CTL_ADD  1
#define EPOLL_CTL_DEL  2
#define EPOLL_CTL_MOD  3
  • 控制操作: 定义了添加、删除和修改监视事件的操作类型。

3)数据结构 

1.epoll_data_t

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
  • 通用数据结构: 用于存储与事件相关联的数据,可以是指针、文件描述符或整数类型。

2.epoll_event

struct epoll_event {
    uint32_t events;
    epoll_data_t data;
};
  • 事件数据结构: 包含事件类型和关联数据。

3.epitem

struct epitem {
    RB_ENTRY(epitem) rbn;
    LIST_ENTRY(epitem) rdlink;
    int rdy; //exist in list 

    int sockfd;
    struct epoll_event event; 
};
  • 事件项结构: 表示一个具体的事件,包含套接字描述符和事件类型。使用红黑树节点和链表节点来管理。

4.红黑树操作

static int sockfd_cmp(struct epitem *ep1, struct epitem *ep2) {
    if (ep1->sockfd < ep2->sockfd) return -1;
    else if (ep1->sockfd == ep2->sockfd) return 0;
    return 1;
}

RB_HEAD(_epoll_rb_socket, epitem);
RB_GENERATE_STATIC(_epoll_rb_socket, epitem, rbn, sockfd_cmp);
  • 比较函数: 用于比较两个事件项的套接字描述符,以便在红黑树中进行排序。
  • 红黑树生成: 创建一个类型为 epitem 的红黑树,并指定节点比较函数。

5.事件轮询器结构

struct eventpoll {
    int fd; // 文件描述符,通常用于标识这个事件轮询器

    ep_rb_tree rbr; // 红黑树,用于高效地管理和查找事件项(epitem)
    int rbcnt; // 红黑树中节点的数量,用于快速获取事件数量

    LIST_HEAD(, epitem) rdlist; // 双向链表头,用于存储就绪的事件项
    int rdnum; // 链表中就绪事件项的数量

    int waiting; // 表示是否有线程正在等待此事件轮询器上的事件

    pthread_mutex_t mtx; // 用于同步对红黑树的更新操作的互斥锁
    pthread_spinlock_t lock; // 用于同步对就绪列表的更新操作的自旋锁

    pthread_cond_t cond; // 条件变量,用于阻塞和唤醒等待事件的线程
    pthread_mutex_t cdmtx; // 与条件变量相关的互斥锁,用于保护条件变量的操作
};
  • 事件轮询器 eventpoll: 包含文件描述符、红黑树、就绪列表、计数器和同步对象。这些组件协同工作,确保事件的有效管理和线程安全的访问。

2、nepoll_create

nepoll_create 函数用于创建一个新的事件轮询器(eventpoll 结构),这是管理所有事件的基础结构。 

int nepoll_create(int size) {

	if (size <= 0) return -1;

	// epfd --> struct eventpoll
	int epfd = get_fd_frombitmap(); //tcp, udp
	
	struct eventpoll *ep = (struct eventpoll*)rte_malloc("eventpoll", sizeof(struct eventpoll), 0);
	if (!ep) {
		set_fd_frombitmap(epfd);
		return -1;
	}

	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;

}
工作流程
  • 参数验证: 确保传入的大小有效(size > 0)。
  • 文件描述符分配: 使用 get_fd_frombitmap 分配一个新的文件描述符 epfd
  • 内存分配: 使用 rte_malloceventpoll 结构分配内存。
  • 结构初始化: 初始化 eventpoll 结构中的红黑树、链表和各种锁/条件变量。
  • 错误处理: 在初始化过程中,如果出现任何错误,都会释放已分配的资源,并返回相应的错误码。

通过 nepoll_create 函数,我们创建了一个 eventpoll 实例,它将用于管理所有注册的事件。

3、nepoll_ctl

nepoll_ctl 函数用于向 eventpoll 中添加、删除或修改事件。 

int nepoll_ctl(int epfd, int op, int sockid, struct epoll_event *event) {
	
	struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);
	if (!ep || (!event && op != EPOLL_CTL_DEL)) {
		errno = -EINVAL;
		return -1;
	}

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

		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) {

		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 --;
		free(epi);
		
		pthread_mutex_unlock(&ep->mtx);

	} else if (op == 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 实例、事件指针和操作类型的有效性。
  • 添加事件 (EPOLL_CTL_ADD):
    • 检查红黑树中是否已有相同的 sockid,若有则返回错误。
    • 创建并初始化新的 epitem 结构,并将其插入红黑树。
  • 删除事件 (EPOLL_CTL_DEL):
    • 从红黑树中查找并移除对应的 epitem
    • 释放内存并更新计数器。
  • 修改事件 (EPOLL_CTL_MOD):
    • 更新已存在的事件类型,并标记错误和挂起状态。

nepoll_ctl 是管理 epoll 中事件的核心函数,确保了事件的增删改查操作的高效性和安全性。

4、nepoll_wait

 nepoll_wait 函数用于等待事件的发生,并将就绪的事件返回给调用者。

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

	

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

	
	while (ep->rdnum == 0 && timeout != 0) {

		ep->waiting = 1;
		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;
			}

			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) {

			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;
	
	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;
}
工作流程
  • 参数验证: 确保 eventpoll 实例和事件数组的有效性,且 maxevents 参数大于 0。
  • 锁定条件变量: 使用互斥锁 ep->cdmtx 锁定等待事件的条件变量。
  • 等待事件:
    • 如果没有就绪事件且未超时,则根据 timeout 参数决定是进行定时等待还是无限期等待。
    • 超时或者事件发生时,解除等待状态。
  • 处理就绪事件:
    • 使用自旋锁锁定 rdlist(就绪事件列表),从中取出就绪事件,将其复制到 events 数组中。
    • 更新计数器,并解锁。

nepoll_wait 函数是事件等待的核心,通过条件变量和自旋锁的组合实现了高效的事件检测和处理。

5、epoll_event_callback

epoll_event_callback 函数用于处理事件的回调,将事件标记为就绪并通知等待该事件的线程。 

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

	struct epitem tmp;
	tmp.sockfd = sockid;
	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);

	pthread_cond_signal(&ep->cond);
	pthread_mutex_unlock(&ep->cdmtx);

}
工作流程
  • 查找事件项: 通过 sockid 在红黑树中查找对应的 epitem 结构。如果未找到,返回错误。
  • 更新事件:
    • 如果事件已标记为就绪(rdy 为 1),将新的事件标志添加到现有事件中,并返回。
    • 如果事件尚未就绪,将其标记为就绪,插入到就绪事件列表 rdlist 中。
  • 通知等待线程: 使用条件变量通知等待该事件的线程,解锁互斥锁。

epoll_event_callback 是事件处理的核心,它将事件状态更新为就绪,并触发条件变量,唤醒等待的线程。

https://xxetb.xetslk.com/s/2sff5t 

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值