epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,epoll更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。同时 epoll 的性能更好,因此在工作中首选 epoll。后面会介绍select和epoll的区别。
#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_create(int size); 创建一个句柄,size告诉内核这个监听的数据一共有多大。不同于select的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在 linux 下如果查看/proc/进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 不同于select函数在监听时告诉内核要监听什么类型事件,而epoll是先注册监听的事件类型。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 队列里 |
epoll的两种触发模式:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待事件的发生,类似于select调用。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。通用头文件head.h
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ARGS_CHECK(argc,num) {if(argc!=num) {printf("error args\n");return -1;}}
#define ERROR_CHECK(ret,retval,func_name) {if(ret==retval) {printf("errno=%d,",errno);fflush(stdout);perror(func_name);return -1;}}
#define THREAD_ERR_CHECK(ret,func_name) {if(ret!=0) {printf("%s failed,%d %s\n",func_name,ret,strerror(ret));return -1;}}
服务器端server.c
#include"head.h"int main(int argc,char** argv)
{ARGS_CHECK(argc,3);int sfd; sfd = socket(AF_INET,SOCK_STREAM,0); //socket描述符ERROR_CHECK(sfd,-1,"socket");printf("sfd = %d\n",sfd);int reuse = 1,ret;//bind绑定之前,先设定一下端口重用ret = setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(int));ERROR_CHECK(ret,-1,"setsockopt");struct sockaddr_in ser_addr; //定义服务端描述结构体bzero(&ser_addr,sizeof(ser_addr));//清空结构体ser_addr.sin_family=AF_INET;//代表要进行ipv4通信ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序ret = bind(sfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)); //绑定sfdERROR_CHECK(ret,-1,"bind");ret = listen(sfd,10); //对sfd进行监听ERROR_CHECK(ret,-1,"listen");int new_fd; //新的传输文件的描述符struct sockaddr_in client_addr;bzero(&client_addr,sizeof(client_addr));socklen_t addr_len=sizeof(client_addr);new_fd = accept(sfd,(struct sockaddr*)&client_addr,&addr_len);//接收队列中的建立连接请求,没有就阻塞ERROR_CHECK(new_fd,-1,"accept");printf("client ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));//编写即时聊天char buf[128] = {0};int epfd = epoll_create(1);//创建一个句柄,即epoll描述符struct epoll_event event,evs[2];//先注册标准输入输出event.data.fd=STDIN_FILENO;event.events=EPOLLIN;//监控是否可读//用epfd描述符注册EPOLL_CTL_ADD一个监听STDIN_FILENO的事件eventret = epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&event);ERROR_CHECK(ret,-1,"epoll_ctl");//再用该结构体注册new_fd,和标准输入用同一个epoll描述符event.data.fd=new_fd;event.events = EPOLLIN;ret = epoll_ctl(epfd,EPOLL_CTL_ADD,new_fd,&event);ERROR_CHECK(ret,-1,"epoll_ctl");int ready_fd_num,i;while(1){//监听描述对应的事件,-1表示时间不确定,evs用于获取内核中epfd对应的2个事件ready_fd_num = epoll_wait(epfd,evs,2,-1);for(i = 0;iif(evs[i].data.fd==STDIN_FILENO){ //如果是标准输入,就将内容读到缓冲区,并且发送给客户端bzero(buf,sizeof(buf));ret=read(STDIN_FILENO,buf,sizeof(buf));if(!ret){printf("服务端想断开连接\n");return 0;}send(new_fd,buf,strlen(buf),0);}if(evs[i].data.fd==new_fd){ //如果new_fd可读,则读入到缓冲区,并且打印出来bzero(buf,sizeof(buf));//服务器接收数据ret=recv(new_fd,buf,sizeof(buf),0);ERROR_CHECK(ret,-1,"recv");if(!ret) { //代表对方断开了printf("客户端断开了连接\n");return 0;}printf("客户端:%s\n",buf);}}}return 0;
}
客户端client.c
#include"head.h"int main(int argc,char** argv)
{ARGS_CHECK(argc,3);int sfd;sfd=socket(AF_INET,SOCK_STREAM,0);//初始化一个网络描述符,对应了一个缓冲区ERROR_CHECK(sfd,-1,"socket");printf("sfd=%d\n",sfd);struct sockaddr_in ser_addr;bzero(&ser_addr,sizeof(ser_addr));//清空ser_addr.sin_family=AF_INET;//代表要进行ipv4通信ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序//客户端就要去连接服务器int ret=connect(sfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));ERROR_CHECK(ret,-1,"connect");//编写即时聊天char buf[128]={0};fd_set rdset;while(1){//清空集合并写入要监控的描述符FD_ZERO(&rdset);FD_SET(STDIN_FILENO,&rdset);FD_SET(sfd,&rdset);//监控哪一个描述符就绪ret=select(sfd+1,&rdset,NULL,NULL,NULL);if(FD_ISSET(STDIN_FILENO,&rdset))//如果标准输入可读{bzero(buf,sizeof(buf));ret=read(STDIN_FILENO,buf,sizeof(buf));if(!ret){printf("I want go\n");break;}send(sfd,buf,strlen(buf)-1,0);//发送对应的字符串到对端,不带\n}if(FD_ISSET(sfd,&rdset))//如果sfd可读{bzero(buf,sizeof(buf));//服务器接收数据ret=recv(sfd,buf,sizeof(buf),0);ERROR_CHECK(ret,-1,"recv");if(!ret)//代表对方断开了{printf("byebye\n");break;}printf("%s\n",buf);}}close(sfd);
}

epoll和select都是Linux下的I/O多路复用机制,但是它们的实现方式不同。select使用的是轮询的方式,而epoll使用的是事件通知的方式。因此,epoll在处理大量连接时,效率更高,而且不会随着连接数的增加而降低效率。另外,epoll支持水平触发和边缘触发两种模式,而select只支持水平触发模式。
select和epoll都是用于I/O多路复用的机制,但是它们有一些区别:
select的优点:
select是标准的系统调用,几乎所有的操作系统都支持它;
select支持的文件描述符数量没有上限,可以处理大量的连接;
select可以同时处理多种类型的I/O事件,包括读、写和异常事件。
select的缺点:
select每次调用都需要将所有的文件描述符从用户空间复制到内核空间,效率较低;
select返回的文件描述符集合是一个线性的数组,每次遍历都需要遍历整个数组,效率较低;
select对文件描述符的监控是“水平触发”,即只要文件描述符上有数据可读或可写,就会一直通知应用程序,这会导致应用程序频繁地被唤醒。
epoll的优点:
epoll是Linux特有的系统调用,效率较高;
epoll使用“事件触发”的方式,只有当文件描述符上有数据可读或可写时才会通知应用程序,避免了频繁唤醒应用程序的问题;
epoll支持的文件描述符数量没有上限,可以处理大量的连接。
epoll的缺点:
epoll只能在Linux系统上使用,不具有通用性;
epoll的API比select复杂,使用起来较为困难;
epoll不能同时处理异常事件,需要额外的处理。