在高性能网络编程中,epoll
是一种高效的 I/O 多路复用机制,广泛用于处理大量并发连接。尽管 Linux 内核提供了 epoll
的原生实现,但在某些场景中,用户态的 epoll
实现能够提供更灵活的控制和优化。本文将详细介绍一个用户态 epoll
实现的核心部分:nepoll_create
、nepoll_ctl
、nepoll_wait
和 epoll_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_malloc
为eventpoll
结构分配内存。 - 结构初始化: 初始化
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
是事件处理的核心,它将事件状态更新为就绪,并触发条件变量,唤醒等待的线程。