前言
本章主要对多路复用IO接口epoll进行详细的使用说明和其它多路复用IO接口(poll,select)与epoll的主要区别。建议在linux内核版本2.6以上使用。
区别
在linux中有三种多路复用模型分别是select、poll、epoll。
select能监听的文件描述符最大1024,单纯的改变进程的打开文件描述符个数并不能改变select监听文件的个数,如果要进行更改需要修改内和文件并重新编译内核。解决1024以下的客户端时使用select是很合适的,但如果链接的客户端过多,select采用轮询获取哪些文件描述符是可读或可写的,会大大降低服务器相应效率。
poll与select类似,但突破了监听1024个文件描述符的限制。
epoll是select和poll的增强版,epoll不需要对监听的文件描述符轮询判断是哪个文件描述符有IO事件,当有IO事件会被内核将发生事件的文件描述符通过epoll_wail返回。并且除了提供select/poll那种IO事件的水平触发外,还提供了边缘触发,给并发提供了更多的灵活性。
补充:当epoll和select/poll都监听1024以下相同的客户端时,并且监听的客户端都是活跃链接,两者的性能相差不大。epoll主要是减少了事件的轮询。但select夸平台windows下也可以使用,epoll只有linux中可以使用。
API
使用epoll离不开三和API系统调用分别是epoll_create、epoll_ctl、epoll_wait。它们都包含在sys/epoll.h头文件中。
epoll_create
原型:int epoll_create(int size)
功能:创建epoll实例。
size:标识这个监听的数目最大有多大。
返回值:成功,返回epoll文件描述符。错误,则返回-1,并且将errno设置为指示错误。
注意:
1 epoll_create()返回引用新epoll实例的文件描述符。该文件描述符用于随后的所有对epoll的调用接口。每创建一个epoll句柄,会占用一个fd,因此当不再需要时,应使用close关闭epoll_create()返回的文件描述符,否则可能导致fd被耗尽。当所有文件描述符引用已关闭的epoll实例,内核将销毁该实例并释放关联的资源以供重用。
2 在最初的epoll_create()实现中,size参数将调用者希望添加到epoll实例文件描述符的数量告知内核。内核使用该信息作为内部数据结构初始分配空间的提示,事件。 (如果有必要,如果调用方的使用超出了大小提示,内核将分配更多空间。)如今,此提示不再必需(内核无需提示即可动态调整所需数据结构的大小),但是大小必须仍大于零,以便当新的epoll应用程序在较旧的内核上运行时,请确保向后兼容。
epoll_ctl
原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能:控制epoll对象epfd监控的文件描述符上的事件:注册、修改、删除。
epfd: epoll_create创建的句柄。
op: 要执行的动作,用3个宏表示。
EPOLL_CTL_ADD:将新的文件描述符fd添加(注册)到epfd对象中。
EPOLL_CTL_MOD:修改已经注册在epfd中的fd文件描述符。并给出event的新设置。
EPOLL_CTL_DEL:从epfd中删除一个fd文件描述符。第三个event参数为NULL。
event: 需要设置的事件。类型struct epoll_event。
返回值:成功,0。失败,-1。
结构说明:
struct epoll_event
{
__uint32_t events; 根据下方 events 宏值进行设置
epoll_data_t data; 根据下方联合体,选择一种成员(或者方式)给监听的文件描述符携带数据
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
epoll_wait
原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
功能:等待epfd上所监控的文件描述符有事件的产生,并返回。
epfd: epoll_create创建的epoll实例
events: 用来从内核获取产生事件的集合(结构中包含事件,和设置时携带的数据。)
maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。如果事件数量大于这个值,剩余的事件将在下次调用wait时返回。
timeout: 超时时间。-1,永久阻塞。0,立即返回,不阻塞。>0, 指定超时时间,如果等待超过这个时间将返回,单位:微妙。
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1。
示例代码
server
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstdio>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/epoll.h>
using namespace std;
const unsigned int PORT = 9527;
const unsigned int MAXBUFLEN = 4096;
const unsigned int OPEN_EP_MAX = 4096;
int Listen();
int main(int argc, char* argv[])
{
int sfd = Listen();
if(sfd == -1)
{
perror("Listen");
return -1;
}
int efd = epoll_create(1024);
if(efd == -1)
{
perror("create epoll");
return -1;
}
epoll_event epevent;
epevent.events = EPOLLIN; // 默认水平触发,在这里可以设置sfd套接字为边缘触发。
epevent.data.fd = sfd;
if( epoll_ctl(efd,EPOLL_CTL_ADD, sfd, &epevent) == -1)
{
perror("epoll_ctl");
return -1;
}
epoll_event resEvent[OPEN_EP_MAX];
while(true)
{
int waitCount = epoll_wait(efd,resEvent,OPEN_EP_MAX,-1);
if(waitCount <= 0)
{
continue;
}
for(int i = 0; i < waitCount; ++i)
{
if(resEvent[i].events & EPOLLIN)
{
if(resEvent[i].data.fd == sfd)
{
sockaddr_in caddr;
socklen_t clen = sizeof(caddr);
int cfd = accept(sfd,(sockaddr*)&caddr,&clen);
if(cfd == -1)
{
perror("accpet");
continue;
}
epoll_event epinevent;
epinevent.events = EPOLLIN; // 默认水平触发,在这里可以设置sfd套接字为边缘触发。
epinevent.data.fd = cfd;
epoll_ctl(efd, EPOLL_CTL_ADD,cfd, &epinevent);
char clientIP[INET_ADDRSTRLEN] = {0};
cout << "received from: " << inet_ntop(AF_INET,&caddr.sin_addr, clientIP,sizeof(clientIP)) << " PROT: " << ntohs(caddr.sin_port) << flush;
}
else
{
int cfd = resEvent[i].data.fd;
char buf[MAXBUFLEN] = {0};
int rcnt = read(cfd,buf, MAXBUFLEN );
if(rcnt >0 )
{
cout << " MSG: " << buf << endl;
const char* sendMsg = "sender ok.";
write(cfd, sendMsg , strlen(sendMsg));
memset(buf,0,MAXBUFLEN);
}
else if( rcnt == 0)
{
// 客户端主动发起关闭,描述符可读数据为0. 此时将客户端关闭,为半链接状态。会不断的收到客户端发送数据包,服务器关闭对应套接字,完成四次分手,并从epoll中移除这个链接。
// 对于长时间与服务器链接但是没有数据传送的客户端,不活跃的客户端。可以通过时间戳设置套接字事件,当超过一定时间,服务器关闭这个套接字。
epoll_ctl(efd,EPOLL_CTL_DEL,cfd,NULL);
close(cfd);
cout << "client close." << endl;
}
}
}
}
}
close(sfd);
close(efd);
return 0;
}
int Listen()
{
int sfd = socket(AF_INET,SOCK_STREAM,0);
if(sfd == -1)
{
perror("create socket");
return -1;
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons( PORT );
if (bind(sfd, (sockaddr*) &saddr,sizeof(saddr)) == -1)
{
perror("bind error");
return -1;
}
if(listen(sfd,128) == -1)
{
perror("listen");
return -1;
}
return sfd;
}
client
在测试与服务器联通时也可以用linux提供的命令进行测试,nc ip port
执行:nc 127.0.0.1 9527
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 9527
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL)
{
write(sockfd, buf, strlen(buf));
n = read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
补充
事件触发模型
EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。用宏EPOLLET表示
Level Triggered (LT) 水平触发只要有数据都会触发。用宏EPOLLET表示。
两种宏通过在设置设置文件描述符事件时设置,如:
...
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; // ET 边沿触发,默认是水平触发,通过与其它事件按位或设置
...