目录
一、epoll的数据结构
1.epoll数据结构的选择
2.epoll数据结构的分析
二、epoll接口函数
三、epoll锁机制
1.list加锁操作
2.rbtree的加锁操作
3.epoll_wait的加锁操作
四、epoll回调函数
五、水平触发和边沿触发
epoll是Linux下IO多路复用接口select/poll的增强版本,是linux平台高性能网络IO的必要组件。其有两种实现方式:内核态实现和用户态实现。内核态实现参考代码为fs/eventpoll.c,用户态参考实现代码为https://github.com/wangbojing/NtyTcp/blob/master/src/nty_e poll_rb.c。虽然其实现方式不一样,实现细节有差异,但是原理是一样的。
为什么epoll有两种实现方式呢?因为协议栈有用户态和内核态这两种实现方式,而epoll是基于协议栈实现的,所以epoll也有两种实现方式。
本文主要参考用户态的epoll实现代码来学习epoll原理。epoll的原理可以从五个方面来理解:
1.epoll的数据结构。rbtree用于存储
2.epoll接口函数。用于供应用程序使用epoll功能。
3.epoll的线程安全。SMP的运行,以及防止死锁。
4.epoll协议栈回调。
5.epoll的LT(水平触发)和ET(边沿触发)。
epoll需要处理大量fd。无论是epoll回调,还是epoll_ctl的ADD、DEL和MOD操作,都有查找操作。所以我们需要选择查找频率很高的数据结构。基于此,可供选择的数据结构有:
(1)哈希表:由于fd的数量是不确定的,而哈希表的大小是确定的。 使用哈希表可能会出现哈希表太大或太小的问题。
(2)B/B+树:查找效率没有红黑树高,其层高适合做磁盘索引。
(3)红黑树:查找性能稳定,合适存储
epoll主要有两个结构体 :eventpoll 与 epitem 。
一个eventpoll对象代表一个epoll实例,其由epoll_create创建。
一个epitem对象代表一对
结构体定义代码如下:
#define RB_ENTRY(type) \
struct { \
struct type *rbe_left; /* left element */ \
struct type *rbe_right; /* right element */ \
struct type *rbe_parent; /* parent element */ \
int rbe_color; /* node color */ \
}
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
struct epitem {
RB_ENTRY(epitem) rbn;
LIST_ENTRY(epitem) rdlink;
int rdy; //exist in ready list
int sockfd;
struct epoll_event event;
};
struct eventpoll {
ep_rb_tree rbr;//red black tree root node
int rbcnt;// red black node count
LIST_HEAD( ,epitem) rdlist;//ready list node head
int rdnum;// ready list node number
int waiting;// When rdnum is zero and timeout is non-zero,waiting is 1, otherwise is 0
pthread_mutex_t mtx; //Being used when update rbtree
pthread_spinlock_t lock; //Being used when update rdlist
pthread_cond_t cond; //block for event
pthread_mutex_t cdmtx; //mutex for cond
};
数据结构如下图所示:
list 用来存储准备就绪的IO。当内核 IO 准备就绪的时候,则会执行 epoll_event_callback 的回调函数,将rbtree中 epitem对象 添加到 list 中,也就是说list和rbtree共用epitem对象。当 epoll_wait 激活重新运行的时候 ,将 list 的 epitem对象从list逐一 删除并将该对象的event值拷贝到 events 参数中 ,但是此epitem对象仍然还在rbtree中。
rbtree 用来存储所有 IO的数据,对此红黑树的操作主要通过epoll_ctl函数进行。
1.int epoll_create(int size);
参数size在此处是个无效的值,只要其大于0即可。
epoll_create主要用来创建eventpoll 实例并对其进行初始化,同时返回一个指向epoll实例的文件描述符。
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl主要是对epitem对象进行操作:
3.int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait函数主要是把就绪链表中的事件信息拷贝到events数组中,并返回就绪事件个数。
epoll从以下几个方面是需要加锁保护的 。List 的操作 ,rbtree 的操作 ,epoll_wait 的等待。
List使用最小粒度的锁 spinlock,便于在SMP下添加操作的时候, 能够快速操作list 。
避免SMP 体系下多核竞争 。 此处采用自旋锁,不适合采用睡眠锁。
a.list添加结点时加锁代码:
pthread_spin_lock(&ep->lock);
epi->rdy = 1;
LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
ep->rdnum ++;
pthread_spin_unlock(&ep->lock);
b.list删除结点时加锁代码:
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);
a.rbtree添加结点时加锁操作代码:
pthread_mutex_lock(&ep->mtx);
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (epi) {
nty_trace_epoll("rbtree is exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
epi = (struct epitem*)calloc(1, sizeof(struct epitem));
if (!epi) {
pthread_mutex_unlock(&ep->mtx);
errno = -ENOMEM;
return -1;
}
epi->sockfd = sockid;
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);
b.rbtree删除结点时加锁代码
pthread_mutex_lock(&ep->mtx);
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (!epi) {
nty_trace_epoll("rbtree no exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
if (!epi) {
nty_trace_epoll("rbtree is no exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
ep->rbcnt --;
free(epi);
pthread_mutex_unlock(&ep->mtx);
c.rbtree修改结点时,用户态代码没有加锁,但是内核态代码是加锁了的。个人认为加锁更合理。
epoll_wait加锁代码如下:
if (pthread_mutex_lock(&ep->cdmtx)) {
if (errno == EDEADLK) {
nty_trace_epoll("epoll lock blocked\n");
}
assert(0);
}
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) {
nty_trace_epoll("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) {
nty_trace_epoll("pthread_cond_wait\n");
pthread_mutex_unlock(&ep->cdmtx);
return -1;
}
}
ep->waiting = 0;
}
pthread_mutex_unlock(&ep->cdmtx);
在tcp的协议栈中,有四个地方会触发epoll的回调函数:
在此四处添加epoll 的回调函数,即可使得 epoll 正常接收到 IO 事件。
水平触发:此时只要event设置了EPOLLIN,就能不断的触发epoll回调函数。
边沿触发:与之前的event相比,如果event 发生改变就会触发epoll回调函数 。 在此情形下变化只发生一次,故只调用一次 epoll 回调函数 。
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接,详细查看详细的服务:
服务器高级架构体系:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂