select函数有效地解决了多个I/O端口的复用问题,但是select函数存在两个缺陷:
一是进程所能同时打开的文件描述符个数受FD_SETSIZE大小的限制;
二是每个select函数返回可用的文件描述符集合后,应用都必须对所有已注册的文件描述符进行遍历对比,以确定哪个描述符上发生了事件,从而对其进行读写操作。
于是随着文件描述符的增加,系统的性能线性下降,从Linux内核2.6开始,提供了一种新的I/O模型,称为事件I/O(epoll)。epoll有效解决了select存在的问题,称为Linux平台上逐渐流行的编程模式。
二、关于epoll函数的3个系统调用
1、epoll_create
#include<sys/epoll.h>
int epoll_create(int size)
返回值:文件描述符表示成功; -1表示错误; errno记录错误号
epoll_create()函数调用成功,则初始化一个epoll实例,并返回一个和此实例相关联的文件描述符,该文件描述符实际并不指向任何真实的文件,仅作为句柄用于后续使用此epoll实例,size参数表示应用预计需要内核监视的文件描述符个数,注意该参数并不表示最大监视文件描述符个数,而只是一个告诉内核的提示数目,内核可以根据此size参数分配合适的内部数据结构,因此该数据越准确,就越能获得更好的性能。当发生错误时,epoll_create返回-1,并且设置错误代码errno为以下几个值:
a. EINVAL 参数size不是一个整数
b. ENFILE 系统已经分配了最大限度的文件描述符个数了
c. ENOMEM 无足够内存完成此操作
2、epoll_ctl
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
epfd是epoll_create()的返回值;op表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;
fd是需要监听的fd;
event是告诉内核需要监听什么事,struct epoll_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 */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
3、epoll_wait
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
返回值:准备好的文件描述符个数表示成功,-1表示错误,errno记录错误号
调用epoll_waith后,进程将等待事件的发生,直到timeout参数设定的超时值到时为止,当成功返回后,则返回值为发生了所监视事件的文件描述符个数,并且参数events指向被返回的事件,本次epoll_wait最多可以返回maxevents个事件,因此可以通过遍历的方式逐个处理发生了事件的哪些文件描述符,另外,如果epoll_wait返回了maxevents个事件,并不表示当前只有maxevents个事件发生,而只是说本次调用能处理的事件个数为maxevents,剩下已产生但是未处理的事件存放到epoll上下文的事件队列中,因此将在下次调用epoll_wait并返回时进行处理,epoll_wait调用失败返回-1,并设置错误码如下值:
a. EBADF epfd为一个非法文件描述符
b. EFAULT 进程没有events参数所指向内存的写权限
c. EINTR epoll_wait系统调用被信号中断
d. EINVAL epfd不是合法epoll文件描述符,或者maxevents小于或者等于0
注意:若参数timeout为0,则epoll_wait立即返回,即使没有任何事件发生,并且返回值为0,若参数为-1,则不返回直到发生事件为止。
epoll.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define MAXFD 10
void epoll_add(int epfd,int fd)//往内核事件表中添加文件描述符
{
struct epoll_event ev;
ev.events = EPOLLIN;//注册读事件,设置关注
ev.data.fd = fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev )== -1)
//EPOLL_CTL_ADD为操作方法添加
{
perror("epoll_ctl add error");
}
}
void epoll_del(int epfd,int fd)//从内核事件表中移除文件描述符
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1 )
//EPOLL_CTL_ADD为操作方法移除
{
perror("epoll_ctl del error");
}
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
assert(res != -1);
listen(sockfd,5);
assert(sockfd != -1);
int epfd = epoll_create(MAXFD);
//系统调用,在内核空间中创建内核事件表,实际为一种红黑树的数据结构
if(epfd == -1)
{
perror("epoll_create failed");
}
epoll_add(epfd,sockfd);//把事件和文件描述符添加到红黑树中的结点中去
struct epoll_event events[MAXFD];
//因为epoll_wait()会把就绪的文件描述符存在一个数组中,所以定义这个数组
while(1)
{
int n = epoll_wait(epfd,events,MAXFD,5000);
//检查就绪并且将其添加到events这个数组中
if(n == -1) //失败
{
perror("epoll_wait error");
continue;
}
else if(n == 0)//超时
{
printf("time out\n");
continue;
}
//有n个数据元素就绪
else
{
int i = 0;
for(;i < n;i++)
{
int fd = events[i].data.fd;
if(events[i].events & POLLIN)
//因为有多种事件,因此要检查是哪种类型的时间,在此关注检查读事件
{
if(fd == sockfd) //监听套接字
{
int len = sizeof(caddr);
int c = accept(sockfd,&caddr,&len);
if(c <= 0)
{
continue;
}
printf("accept c=%d\n",c);
epoll_add(epfd,c); //新的连接产生,添加到内核事件表中
}
else
{
char buff[128] = {0};
int num = recv(fd,buff,127,0)
if( num <= 0) //对方关闭
{
//先epoll_del()移除再close()关闭,因为epoll_del()中epoll_ctl()这
//个系统调用要用到fd,所以不能先关闭,如果先关闭的话,就找不到了,在执
//行时会提示为无效描述符close()不会使值发生变化,但会使得值无效
epoll_del(epfd,fd);
close(fd);
printf("one client close\n");
continue;
}
printf("recv(%d):%s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
}
运行结果: