一. 关于epoll
对于IO复用模型,前面谈论过了关于select和poll函数的使用,select提供给用户一个关于存储事件的数据结构fd_set来统一监测等待事件的就绪,分为读、写和异常事件集;而poll则是用一个个的pollfd类型的结构体管理事件的文件描述符和事件所关心的events,并通过结构体里面的输出型参数revents来通知用户事件的就绪状态;
但是对于上述两种函数,都是需要用户遍历所有的事件集合来确定到底是哪一个或者是哪些事件已经就绪可以进行数据的处理了,因此当要处理等待的事件比较多时,就会有数据复制和系统遍历的开销导致效率并不高效;针对select和poll的缺点,另外一种相对高效的处理IO复用的函数就出现了,那就是epoll;
二. epoll相关函数的使用
首先,和select及poll函数不同的是,epoll并没有直接的一个用epoll来命名的函数使用,而是分别提供出来三个函数:epoll_create、epoll_ctl和epoll_wait;
1. epoll_create
2. epoll_create函数创建一个epoll的“实例”,请求内核分配一个指定大小的空间用于事件的后台存储,函数参数size只是一个关于内核如何维护内部结构的提示,不过现在这个size已经被忽略并不需要在意了;
3. 函数成功会返回一个引用新创建的epoll实例的一个文件描述符,用于随后调用其他的epoll函数的结构,如果不再需要的话,应当使用close函数关闭,这时内核会销毁该epoll实例并释放相关资源;如果函数失败会返回-1并置相应的错误码;
4.
5.
2. epoll_ctl
函数参数中,
epfd是用epoll_create创建出来的epoll文件描述符,用来操纵epoll实例;
op是要对创建出的epoll实例进行操作,而op的操作选项有如下三种宏:
EPOLL_CTL_ADD用于在epfd标识的epoll实例中添加登记要处理的事件;
EPOLL_CTL_MOD用于更改特定的文件描述符所关心的事件;
EPOLL_CTL_DEL用于删除在epoll实例中登记的事件,标识并不需要再关心了;
fd是指要进行数据IO的事件的文件描述符,也就是用户需要进行操作的事件的文件描述符;
event是一个epoll_event的结构体,用于存放需要对fd进行操作的相关信息:
结构体中,
events表示文件描述符fd所对应的事件所关心的操作,是相应的比特位的设置,有如下几种宏:
如上的宏中,最主要使用的有如下几种:
EPOLLIN表示fd可以进行数据的读取;
EPOLLOUT表示fd可以进行数据的写入;
EPOLLPRI表示当前有紧急数据可供读取;
EPOLLERR表示当前事件发生错误;
EPOLLHUP表示当前事件被挂断;
EPOLLET将相关的文件描述符设置为边缘触发,因为默认是水平触发的;对于LT和ET模式下面会讨论;
对于结构体中的data则是一个联合,用于表示有关文件描述符操作的数据信息:
ptr是指向数据缓冲区的一个指针;
fd是相应操作的文件描述符;
epoll_ctl函数成功返回0,失败返回-1并置相应的错误码;
3. epoll_wait
如果说上面的epoll_create和epoll_ctl是为了进行相关事件的操作而进行的准备工作,那么真正和select及poll函数一样用来进行多个事件的等待就绪则就是epoll_wait函数了:
函数参数中,
epfd是用epoll_create创建出的epoll实例的文件描述符;
events是上述的一个结构体的指针,这里一般是一个数组的首地址,是一个输入输出型参数,当作为输入时,是用户提供给系统一个用来存放就绪事件的地址空间,而作为输出型参数时,系统会将就绪的事件放入其中供用户提取,因此不可以为NULL;
maxevents是events的大小;
timeout则是设置等待的超时时间,单位为毫秒;
这里值得一提的是,既然epoll是select和poll的改进,那么其最主要的高效就是体现在epoll_wait的返回值:
・ 函数失败返回-1并置相应的错误码;
・ 函数返回0表示超时,预定时间内并没有事件就绪;
・ 当函数返回值大于0时,是告诉用户当前事件集中已经就绪的IO事件的个数,并且将其按序从头开始排列在了用户提供的空间events内,因此,不需要像select和poll那样遍历整个事件集找出就绪的事件,只需要在相应的数组中从头访问固定的返回值的个数就拿到了所有就绪的事件了;
三. 栗子时间
同样的,使用epoll相关的接口函数,可以自主来编写一个基于TCP协议的服务端,其基本步骤如下:
1. 首先,先要创建出一个监听socket,绑定好本地网络地址信息并将其处于监听状态,但是这里,为了使其更为高效,还需要调用setsockopt函数来将其属性设定为SO_REUSEADDR,使其地址信息可被重用;
2. 调用epoll_create创建出一个关于epoll实例的文件描述符,用于以后操作epoll相关函数;
3. 调用epoll_ctl函数,将监听socket登记添加到epoll实例中;
4. 定义一个epoll_event结构体数组,用户指定大小,供系统存放就绪的IO事件;
5. 调用epoll_wait进行事件的就绪等待,并接收其返回值;
6. 当epoll_wait返回时,对返回的事件一一进行判断处理,如果是监听事件就绪,表明有连接请求需要处理,并将新的套接字添加进epoll实例中;如果是其他socket就绪,表明数据就绪可以进行读取和写入了;
7. 当连接的一端关闭或者epoll实例使用完毕的时候,需要调用close函数关闭相应的文件描述符回收资源;
server客户端程序设计如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
#define _BACKLOG_ 5
#define _MAX_NUM_ 20
#define _DATA_SIZE_ 1024
typedef struct data_buf
{
int _fd;
char _buf[_DATA_SIZE_];
}data_buf_t, *data_buf_p;
void Usage(const char *argv)
{
assert(argv);
printf("Usage: %s [ip] [port]\n", argv);
exit(0);
}
int set_non_block(int fd)
{
int old_fl = fcntl(fd, F_GETFL);
if(old_fl < 0)
{
perror("fcntl");
return -1;
}
if(fcntl(fd,F_SETFL, old_fl|O_NONBLOCK))
{
perror("fcntl");
return -1;
}
return 0;
}
static intCreateListenSock(int ip, int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
exit(1);
}
int opt = 1;
if(setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
perror("setsockopt");
exit(2);
}
if(set_non_block(sock) < 0)
printf("set nonblockfailed...\n");
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = ip;
if(bind(sock, (structsockaddr*)&server, sizeof(server)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock, _BACKLOG_) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
ssize_tMyRead(int fd, char *buf, size_t size)
{
assert(buf);
int index = 0;
ssize_t ret = 0;
while((ret = read(fd, buf+index,size-index)))
{
if(errno == EAGAIN)
{
printf("read errno:%d\n", errno);
perror("read");
break;
}
index += ret;
}
return (ssize_t)index;
}
ssize_tMyWrite(int fd, char* buf, size_t size)
{
assert(buf);
int index = 0;
ssize_t ret =-1;
while((ret = write(fd, buf+index,size-index)))
{
if(errno == EAGAIN)
{
printf("write errno:%d\n", errno);
perror("write");
break;
}
index += ret;
}
return (ssize_t)ret;
}
voidepoll_server(int listen_sock)
{
int epoll_fd = epoll_create(256);
if(epoll_fd < 0)
{
perror("epoll_create");
exit(5);
}
struct epoll_event ep_ev;
ep_ev.events = EPOLLIN | EPOLLET;
//ep_ev.events = EPOLLIN;
ep_ev.data.fd = listen_sock;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD,listen_sock, &ep_ev) < 0)
{
perror("epoll_ctl");
exit(6);
}
struct epoll_event evs[_MAX_NUM_];
int maxnum = _MAX_NUM_;
int timeout = 10000;
int ret = 0;
while(1)
{
switch((ret = epoll_wait(epoll_fd, evs,maxnum, timeout)))
{
case -1:
perror("epoll_wait");
break;
case 0:
printf("timeout...\n");
break;
default:
{
int i = 0;
for(; i < ret; ++i)
{
if((evs[i].data.fd ==listen_sock) && (evs[i].events & EPOLLIN))
{
struct sockaddr_inclient;
socklen_tclient_len = sizeof(client);
int accept_sock =accept(listen_sock, (struct sockaddr*)&client, &client_len);
if(accept_sock <0)
{
perror("accept");
continue;
}
printf("connect with a client...[fd]:%d [ip]:%s [port]:%d\n", accept_sock, inet_ntoa(client.sin_addr),ntohs(client.sin_port));
if(set_non_block(accept_sock) < 0)
printf("set nonblock failed...\n");
ep_ev.events =EPOLLIN | EPOLLET;
//ep_ev.events =EPOLLIN;
ep_ev.data.fd =accept_sock;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accept_sock, &ep_ev) < 0)
{
perror("epoll_ctl");
close(accept_sock);
}
}
else
{
if(evs[i].events& EPOLLIN)
{
data_buf_p _data =(data_buf_p)malloc(sizeof(data_buf_t));
if(!_data)
{
perror("malloc");
continue;
}
_data->_fd =evs[i].data.fd;
printf("read from fd: %d\n", _data->_fd);
ssize_t size =MyRead(_data->_fd, _data->_buf, sizeof(_data->_buf)-1);
//ssize_t size =read(_data->_fd, _data->_buf, sizeof(_data->_buf)-1);
if(size < 0)
printf("read error...\n");
else if(size ==0)
{
printf("client closed...\n");
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL);
close(_data->_fd);
free(_data);
}
else
{
(_data->_buf)[size] = '\0';
printf("client# %s", _data->_buf);
fflush(stdout);
ep_ev.dataNaNr = _data;
ep_ev.events = EPOLLOUT | EPOLLET;
//ep_ev.events = EPOLLOUT;
epoll_ctl(epoll_fd,EPOLL_CTL_MOD, _data->_fd, &ep_ev);
}
}
elseif(evs[i].events & EPOLLOUT)
{
data_buf_p _data =(data_buf_p)evs[i].dataNaNr;
MyWrite(_data->_fd, _data->_buf, strlen(_data->_buf));
//char *msg ="HTTP/1.1 200 OK\r\n\r\nHello, what can i do for you ? :)\r\n";
//MyWrite(_data->_fd, msg, strlen(msg));
//write(_data->_fd, _data->_buf, strlen(_data->_buf));
//write(_data->_fd, msg, strlen(msg));
epoll_ctl(epoll_fd, EPOLL_CTL_DEL,_data->_fd, NULL);
close(_data->_fd);
free(_data);
}
else
{}
}
}
}
break;
}
}
}
int main(intargc, char *argv[])
{
if(argc != 3)
Usage(argv[0]);
int port = atoi(argv[2]);
int ip = inet_addr(argv[1]);
int listen_sock = CreateListenSock(ip,port);
epoll_server(listen_sock);
close(listen_sock);
return 0;
}
四. 水平触发和边缘触发
当epoll_wait在进行多个事件的等待时,如果有数据发送到缓冲区中时,则表示当前事件处于就绪状态,则需要返回来通知用户“有数据来了,可以进行处理了”,那么对于系统通知用户的方式,就分为水平式触发和边缘式触发:
水平触发(Level Trigger)简称LT,其特点是当数据到来的时候会通知用户,如果用户一次数据处理并没有将缓冲区中的数据全部取走还留有一部分,那么下一次再进行相同事件的epoll_wait的时候系统会认为事件仍然是就绪的,还会继续通知用户来取走剩下的数据,因此,水平触发的特点是:只要数据缓冲区中有数据,当前的IO事件始终都是就绪的,epoll_wait始终会返回有效值通知用户程序;
边缘触发(Edge Triggered)简称ET,当有数据到来的时候仍然会返回通知用户程序,但是和水平触发不同的是,如果用户在通知一次后对数据的IO处理并不完全,也就是一次处理之后缓冲区中还留有数据,那么再次返回进行epoll_wait的时候就不会再表明当前事件是就绪的了,只有当这个事件再次有数据到达时才会再一次通知用户程序来处理数据,因此,边缘触发的特点是:只有当数据到来的时候系统才会通知用户程序且只会通知一次,如果还有数据没有处理完,只有等到再次有数据到来的时候才会再次满足事件就绪,epoll_wait返回通知用户程序处理数据;
这里需要注意的是:对于边缘式触发,因为只有当数据到来时系统才会通知用户程序一次,如果当前的IO接口工作于阻塞模式,那么当一个事件被阻塞的时候,其他事件的就绪也就只会被通知一次但并得不到处理,因此会导致多数据的堆积,所以,当使用边缘式触发的时候:
・ 最好将当前的IO接口设定为非阻塞的;
・ 当一个IO事件进行数据的读取和写入的时候,最好一次性就将缓冲区中的数据全部都处理完;因此,对于数据的读取,可以用一个循环来每次读取特定的长度,当最后一次读取的长度小于特定的长度时,就可以认为当前缓冲区的数据已经全部读取完毕终止循环;但是,不可避免的是,如果最后一次的读取恰好也就是特定的长度,那么在此进行读取缓冲区中数据为0,就会返回一个EAGAIN的错误码,这个就可以作为循环的终止条件;
将IO接口设置为非阻塞的,可以调用fcntl函数:
函数参数中,
fd表示要进行操作的文件描述符;
cmd表示要进行的操作;
至于后面的参数,则有cmd来决定;
在这里要设置文件接口为非阻塞的,首先要将cmd设置为F_GETFL,表示获取当前文件描述符的标志,因为重新设定时需要用到;之后需要再次调用fcntl函数,将cmd设定为F_SETFL,要重新设置文件描述符的标志,其中有一个选项就是O_NONBLOCK;
对于fcntl函数的返回值,根据操作的不同而不同:
对比水平触发和边缘触发,可以发现水平触发对于数据的处理来说是更安全更可靠的,而边缘触发是要更为高效的,因此,选择哪种通知方式,可以依情况而定;