epoll简介:
- epoll的移植性:epoll是Linux下独有的I/O复用函数,比如在QNX等平台上,就不支持epoll,但是select、poll出现的较早,有较多的平台支持,论移植性,select、poll > epoll;
- epoll的基本使用:epoll使用一组函数完成任务。首先我们通过
int epoll_create()
创建一个epoll对象,然后将我们关心的所有文件描述符通过int epoll_ctl(...)
将文件描述符以及注册到文件描述符上的事件类型放在一个内核事件表中,如果某个文件描述符上对应的事件类型发生了,int epoll_wait(...)
将返回,并且只将发生的事件返回给用户(就绪的事件存放在结构体epoll_event类型的数组中),这样用户就可以根据事件的类型做出相应的动作; - epoll事件分类: 常见事件可读、可写、异常、是否开启ET、是否开启EPOLLONESHOT;
- ET和LT工作模式: LT 边沿触发,是epoll的默认工作方式,采用LT模式的事件发生后,程序可以不立即对数据采取处理措施,这轮如果不处理,下一次epoll_wait返回后,还会接着通知用户此事件发生,再处理也不晚;ET却相反,事件发生以后,epoll_wait返回通知用户,如果我们本次通知不处理数据,epoll之后将不会返回此事件,因为只会通知一次,所以在编程上要求我们,对于注册了ET事件类型的事件,有数据处理,如recv数据,应该循环处理,保证数据的完整性。
epoll系列系统调用
#include <sys/epoll.h>
int epoll_create( int size );
,创建一个epoll文件系统(也叫内核事件表、红黑树),如果失败返回-1,参数size,int size
提示内核这个事件表需要多大,但是目前并不起作用;int epoll_ctl( int epollfd , int op, int fd ,struct epoll_event * event);
,epollfd实际上关联一个内核事件表,即int epoll_create( int size );
返回值;fd我们关注的事件的描述符,将来要添加到内核事件表中的,如listenfd,writefd,readfd,exceptionfd等;参数op指的是要将这个fd添加、修改还是删除从这个内核事件表上;参数event描述这个fd上的事件类型(读写异常等);返回0表示操作成功,返回-1表示失败;
typedef union epoll_data
{
void *ptr;
int fd; //事件对应的fd,使用最多,指定epoll_event描述的是哪个文件描述符的事件
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; //描述事件类型,如EPOLLIN、EPOLLOUT等
epoll_data_t data; //存储用户数据
} __EPOLL_PACKED;
//使用案例:
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLOUT;
event.enents |= EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
int epoll_wait( int epollfd, struct epoll_event * events, int maxevents, int timeout );
,参数events是一个结构体数组,类型为epoll_event,将来有就绪事件发生会直接将就绪的事件从内核事件表复制存放在这里,返回值用户可以直接根据返回值或者错误码判断接下来的逻辑处理;参数maxevents,指定最多监听多少个事件,它必须大于0;参数timeout,单位毫秒,当timeout == -1时,epoll_wait将永远阻塞,可以理解为一个慢系统调用,当timeout==0,不管有没有事件发生,将立刻返回。
//使用实例:
//...
int listenfd = socket(PF_INET,SOCK_STREAM,0);
//...
epoll_event events[100];
//...
while (1){
int ret = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
if(ret < 0){
perror("epoll wait failed");
break;
}
for(int i = 0;i<ret;i++){
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connectfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
}
else if(events[i].events & EPOLLIN){
//recv data
}else{
//something else
}
}
epoll使用实战:非阻塞文件描述符、ET、LT模式对于数据的处理
#include <sys/socket.h>
#include <sys/types.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <assert.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFD);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
void addfd(int epollfd, int fd, bool enable_et) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if (enable_et) {
event.events |= EPOLLET; //如果启用ET,就给对应事件添加EPOLLET,默认是LT
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
//电平触发,默认工作方式
void lt(epoll_event* events, int number, int epollfd, int listenfd) {
char buf[BUFFER_SIZE];
for (int i = 0; i < number; i++) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) {
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connectfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connectfd, false);//禁用ET,connectfd开启LT模式
}
else if (events[i].events & EPOLLIN) {//数据可读
//如果数据没有读完,epoll_wait下次返回还会执行这段代码
printf("event trigger once \n");
memset(buf, 0, BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
if (ret <= 0) {
close(sockfd);
continue;
}
printf("get %d bytes of content:%s\n", ret, buf);
}
else {
printf("something else happend \n");
}
}
}
//边沿触发 添加事件时,给对应的事件fd添加上EPOLLET,event.events |= EPOLLET
void et(epoll_event* events, int number, int epollfd, int listenfd) {
char buf[BUFFER_SIZE];
for (int i = 0; i < number; ++i) {
int sockfd = events[i].data.fd;
//判断是哪种类型的文件描述符,判断是哪种事件发生
if (sockfd == listenfd) {
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connectfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connectfd, true);//对connfd开启ET模式
}
else if (events[i].events & EPOLLIN) {
//这段代码对应的fd,下次epoll_wait如果返回依然有数据,这个fd不会被再次提醒
//所以我们需要循环的一次把数据读完,对于IO密集型的业务,可以开启一个线程去执行读操作
while (1) {
memset(buf, 0, BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
if (ret < 0) {
//对于非阻塞IO,下面条件说明数据已经全部被读完。这样,如果该文件描述符再有事件有数据,epoll_wait还是会返回
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
printf("please read later\n");
break;
}
close(sockfd);
break;
}
else if (ret == 0) {
close(sockfd);
}
else {
printf("get %d bytes of content:%s\n", ret, buf);
}
}
}
else {
printf("something else happend \n");
}
}
}
const char* ip = "127.0.0.1";
int port = 6000;
int main() {
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
if (ret == -1) {
perror("bind failed");
exit(-1);
}
ret = listen(listenfd, 5);
assert(ret != -1);
//存放就绪事件数组,epoll_wait返回后,将就绪事件存放到该数组
//也就是说,该数组中的事件一定是发生了,就绪了
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
//添加listenfd到epoll红黑树上
addfd(epollfd, listenfd, true);
while (1) {
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (ret < 0) {
printf("epoll wait failed\n");
break;
}
//else 返回就绪事件fd个数
#ifdef LT_MODEL
lt(events, ret, epollfd, listenfd); //lt
#else
et(events, ret, epollfd, listenfd); //et
#endif
}
close(listenfd);
return 0;
}
ET模式从理论的角度看 ,降低了同一个epoll事件被重复触发的次数,效率较高。但是这不是绝对的,因为LT有LT的应用场景,ET有ET的应用场景,谁效率高,可说不准呢,不能一昧的就认为ET效率一定比LT高。
参考:<<Linux高性能服务器编程>>