写在前面:文章掺杂了个人理解,请谨慎参考
1. select、poll、epoll区别
三个系统调用均用于感知多个socket是否有数据到来
1.1 select、poll:
暴力做法:当一个进程A调用recv阻塞,等待数据时,其实是睡眠在了recv所等待的socket(这里假设有fd1、fd2)的等待队列上,当网卡接收到数据,并得知该数据发给fd1时,会触发中断,fd1会从等待队列上唤醒A,使得A重新进入运行状态,而A被唤醒后,得知至少有一个socket上有数据到来,便执行程序遍历一遍监听的socket列表,就可以得到就绪的socket。
性能开销:每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。
上述就是select的实现大致过程,出于效率的考量,通过bitmask规定了 Select 的最大监视数量,所以在设计之初,就默认认为select的最大范围是1024;
poll:更换数据结构,由bitmask更换为结构体数组,一个结构体对应一个socket,所以不限制上限数目,但是出于操作系统的内存限制,实际上poll监听的socket数目也是有限的。
1.2 epoll
在理解上述数据到来时,进程如何感知到是哪个socket有数据的过程后,来看一下epoll所做的改进。
epoll在内核态中有两个重要的数据结构,①采用红黑树实现的eventpoll对象:用于保存所监听的socket fd②采用双向链表实现的rdlist对象:用于保存可读写的socket,并给用户态进程返回读写就绪的fd集合。
- 在select的实现过程中,有一个重要的环节是,由中断唤醒对应的进程来处理可读写的socket;在epoll中,所做的优化是:触发中断时,重写了中断处理程序,首先将可读写的socket添加到rdlist中,再唤醒进程;因此进程唤醒后,可以直接从rdlist中得到可读写的socket。
- 采用了红黑树的结构,也使得对socket的增删改查时间维持在对数时间内。
2. epoll中的ET、LT触发方式
接下来,结合epoll高效的原理分析,继续简要分析一下epoll所提供的两种模式ET和LT的实现原理。
LT和ET的区别
LT模式:如果数据可读,epoll_wait 会通知用户空间,即使在之前的通知后数据尚未被完全读取。(举例来说,如果某次来了100 Bytes,read可能读了60 Bytes后,因为内核调度的原因,剩下的数据没来得及读完,后续再次调用read时,余下的40 Bytes也会被继续读取)
ET模式:与LT不同,如果当时这40 Bytes不读取的话,之后再次调用read时便不会再被读取。
因此选择不同的模式,编写代码时有区别:
/*******
***LT***
*******/
int nfds, epfd;
struct epoll_event events[MAX_EVENTS];
while (1) {
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; ++i) {
if (events[i].events & EPOLLIN) {
char buffer[1024];
ssize_t bytes_read = read(events[i].data.fd, buffer, sizeof(buffer));
// 处理读取的数据...
}
// 其他事件处理...
}
}
/*******
***ET***
*******/
int nfds, epfd;
struct epoll_event events[MAX_EVENTS];
while (1) {
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; ++i) {
if (events[i].events & EPOLLIN) {
while (1) {
char buffer[1024];
ssize_t bytes_read = read(events[i].data.fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
if (errno != EAGAIN) {
// 读取出现错误...
}
break; // EAGAIN 表示所有数据都已读取完毕
} else if (bytes_read == 0) {
// 连接已关闭...
break;
}
// 处理读取的数据...
}
}
// 其他事件处理...
}
}
实现原理
在内核态中,epoll_wait在给用户态进程返回rdlist数据后,不同的模式下有不同的后续处理:
LT模式:对于没有读完缓冲区的fd,继续放在rdlist中不做处理;
ET模式:一旦某事件被报告给用户态程序并从就绪列表中移除;