1、EPOLL简介
epoll是linux下的一个处理多路I/O复用的机制,基于事件通知,能够高效的处理多个SOCKET连接。
2、基本函数
使用epoll,基本的函数只有三个:
(1)创建:epoll_create
(2)控制:epoll_ctl
(3)监听:epoll_wait
2.1 创建
函数原型:
int epoll_create(int size)
函数打开一个epoll的文件描述符,用于其他epoll函数调用。关闭的时候使用close
函数。
2.2 控制
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
函数用于 添加/删除/修改 需要监听的SOCKET。
参数1是epoll_create
返回的句柄。
参数2在三个宏定义中选择:
EPOLL_CTL_ADD:添加需要监听的SOCKET和事件
EPOLL_CTL_MOD:修改监听的事件
EPOLL_CTL_DEL:移除监听目标
参数3是操作的目标SOCKET。
参数4是操作的事件参数:
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 */
};
需要关注的事件:
EPOLLIN:当目标SOCKET有数据到来时触发。
EPOLLOUT:当目标SOCKET可写进触发。
EPOLLERR:出错时触发。
EPOLLHUP:对方SOCKET挂起时触发。
EPOLLET:使用边缘触发(EdgeTriggered)方式。默认是水平触发(LevelTriggered)。
2.3 监听
函数原型:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数2是一个epoll_event数组,用来保存收到的信息。
参数3是数组大小。
参数4是超时时间,单位毫秒。
返回值是有事件发生的SOCKET数量。
3 例子
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <errno.h>
#include <sys/ioctl.h>
#define PORT 2999
#define EPOLL_SIZE 2048
#define BUFFER_SIZE 1024
int epoll_fd;
int listen_fd;
int create_listen_socket()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1){
printf("created socket error\n");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(addr);
if (-1 == bind(fd, (struct sockaddr*)&addr, len)){
printf("bind socket fail\n");
close(fd);
return -1;
}
listen(fd, 10);
printf("waiting for connections...\n");
return fd;
}
void my_epoll_add(int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; //使用边缘触发方式,监听读数据
ev.data.fd = fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);
}
void on_accept()
{
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int clientFd = accept(listen_fd, (struct sockaddr*)&clientAddr, &clientLen);
if (clientFd > 0){
//边缘触发需要SOCKET非阻塞
int flag = 1;
ioctl(clientFd, FIONBIO, &flag);
my_epoll_add(clientFd);
printf("new client accept...\n");
}
}
int on_read(int fd)
{
char buffer[BUFFER_SIZE];
int n;
for(;;){
n = read(fd, buffer, BUFFER_SIZE);
if (n == -1){
if (errno == EAGAIN){
//表示读完此次数据
break;
}
return -1;
}else if(n == 0){
return -1;
}else{
printf("recv:%s\n", buffer);
}
}
return 0;
}
int main(int argn, char **argv)
{
listen_fd = create_listen_socket();
if (listen_fd == -1){
return 0;
}
epoll_fd = epoll_create(EPOLL_SIZE);
my_epoll_add(listen_fd);
int i, count, fd, ev, timeout = 1000;
struct epoll_event events[EPOLL_SIZE];
for (;;){
count = epoll_wait(epoll_fd, events, EPOLL_SIZE, timeout);
if (count > 0){
for (i = 0; i < count; i++){
fd = events[i].data.fd;
if (fd == listen_fd){//有连接到来
on_accept();
continue;
}
ev = events[i].events;
if ((ev & EPOLLHUP) || (ev & EPOLLERR)){//连接关闭
close(fd);
printf("client %d close\n", fd);
continue;
}
if (ev & EPOLLIN){ //表示有数据可读
if( -1 == on_read(fd)){
printf("client %d close\n", fd);
close(fd);
}else{
//把状态修改为epollout,下一循环就能触发写事件。
events[i].events = EPOLLOUT | EPOLLET;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &events[i]);
}
}else if(ev & EPOLLOUT){
//处理写数据
//修改状态
events[i].events = EPOLLIN | EPOLLET;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &events[i]);
}
}
}
}
close(epoll_fd);
close(listen_fd);
return 0;
}
4 水平触发 、边缘触发以及EPOLLOUT
边缘触发,就是状态发生改变时才触发。
水平触发,则是状态存在就一直会触发。
对于EPOLLIN来说,就是水平触发的话,只要socket里有数据可读,还没读完,则每次调用epoll_wait都会触发。
而在边缘触发下,如果一次调用没有把数据都读完的话,则下次调用不会触发,要直到下次对方有数据进来时才能继续。而要想一次把数据读完,则必须把socket设置成非阻塞,要不然程序就卡在read的地方了。
对于EPOLLOUT来说,比较难理解。因为读数据是被动的,可以监听很正常,而发数据应该是个主动的过程,如何算作监听呢?
在水平触发下,如果设置了EPOLLOUT,那么只要socket缓冲区足够空间,就会一直触发可写状态。
而边缘触发下,设置了EPOLLOUT,会在连接时触发一次,然后在下次状态由不可写转化为可写时才触发。写缓冲区满了的情况下会出现不可写的状态。
更多的时候,是程序自己调用epoll_ctl改变状态来触发写事件,统一发送数据,从而可以减少调用内核write的次数。