本文对epoll的注意点做一个小结。
接口
epoll常见的接口有三个:
1.创建epoll的实例,在内核中初始化相应的数据结构
// 创建epoll instance
// linux 2.6.8之后,size参数没有实际意义。但是,需要比0大
int epoll_create(int size);
2.向epoll instance注册,修改,删除相应的fd及其事件。这点有别于select,后者每次监听的时候,从用户空间拷贝监听的fd到内核空间。当fd较多的时候,这个开销是非常大的。但是,epoll在是在监听之前,直接想epoll instance的内核数据结构,注册相应的fd,修改或者删除也是直接操作内核的数据接口。所以,节省了大量时间。
// 向epoll instance注册/修改/删除,fd以及相应的事件
// EPOLL_CTL_ADD:register,向epoll注册fd,fd关联的事件
// EPOLL_CTL_MOD:change,修改已经注册到epoll的fd关联的事件
// EPOLL_CTL_DEL:remove,删除epoll当中的fd,关联的事件自动失效
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event* event);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
ev.events = EPOLLIN; // 可以设置ET LT
ev.data.fd = listen_sock; // 这里必须保存fd,否则,fd事件触发拷贝到用户空间后,不知道是哪一个fd
if (epoll_ctl(epollfd,
EPOLL_CTL_ADD,
listen_sock,
&ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
3 . 监听多个fd,等待事件触发。
// events是传出参数,保存触发的事件数组。
// maxevents是事件数组的大小
// 返回值:nready>0表示返回的有效事件个数。nready=0,表示超时.
// 即timeout之间没有数据读入
// nready < 0出错
int epoll_wait(int epfd,
struct epoll_event* events,
int maxevents,
int timeout);
注意点
ET (边沿触发)
这种模式只支持non block socket, 只有当socket从未就绪变为就绪时,内核才会通过epoll告诉你。直到,socket重新变为未就绪状态。所以,读写的时候都需要小心处理。
为什么ET必须和non-blocking socket联合使用?
这么考虑,缓冲区只有32byte,现在发过来36byte数据。
那么,第一次读取缓冲区大小的字节即32byte,发现返回值是32。证明,可能还有数据没有读取完毕。那么,继续读取,第二次读取缓冲区大小的字节,即32byte。发现读取到4byte,表明读取完毕。
考虑这种情形,缓冲区32byte,发送了32byte。那么,第一次读取32byte数据,没问题。第二次,继续读取发现缓冲区为空。!!!此时,如果要是阻塞模式,缓冲区为空进行读取,那么线程被挂起。没有办法再监听别的socket,这么做不对。所以,必须设置为非阻塞模式。
读取代码
while(rs)
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
// 由于是非阻塞的模式,所以当 errno 为 EAGAIN 时,表示当前缓冲区已无数据可读
// 在这里就当作是该次事件已处理处.
if(errno == EAGAIN)
break;
else
return;
}
else if(buflen == 0)
{
// 这里表示对端的 socket 已正常关闭.
}else {
if(buflen == sizeof(buf)
rs = 1; // 需要再次读取
else
rs = 0;
}
}
写代码
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char *p = buffer;
while(1)
{
tmp = send(sockfd, p, total, 0);
if(tmp < 0)
{
// 当 send 收到信号时,可以继续写,但这里返回-1.
if(errno == EINTR)
return -1;
// 当 socket 是非阻塞时,如返回此错误,表示写缓冲队列已满,
// 在这里做延时后再重试.
if(errno == EAGAIN)
{
usleep(1000);
continue;
}
else return -1;
}
if((size_t)tmp == total)
return buflen;
else {
total -= tmp;
p += tmp;
}
}// while
return tmp;
}
内核实现
- 红黑树:用来存储epoll_ctl注册的socket。用epoll_ctl向内核注册socket的同时,会向内核注册一个回调函数。告诉内核,当这个socket的事件触发了,就把它放入就绪链表当中。
- 就绪链表:内核维护一个就绪链表,当socket事件发生时,就会把它插入到就绪链表当中。返回到user space.