epoll 系列系统调用(I/O复用函数)
目录
- 内核事件表
- epoll_wait函数
- LT 和 ET模式
- EPOLLONESHOT事件
内核事件表
epoll 是liunx特有的I/O复用函数 。它在实现和使用上与select 。poll有很大差异。
首先,epoll 使用一组函数来完成任务,而不是单个函数
其次,epoll把用户关心的文件描述符上面的事件放在内核里的一个事件表中,
从而无须向select和poll那样每次调用都要重复传入文件描述符 或者事件集 。
但是epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
这个文件描述符使用如下 epoll_create 函数来创建:
#include<sys/epoll.h>
int epoll_create(int size )
size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大
该函数返回的文件描述符将作用于其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
下面的函数用来操作 epoll的内核事件表:
#include <sys/epoll.h>
int epoll_ctl(int epfd,int op, int fd ,struct epoll_event *event)
fd参数是要操作的文件描述符 ,op参数则指定操作类型 。操作类型有如下三种
- EOPLL_CTL_ADD, 往事件表中注册 fd上的事件
- EOPLL_CTL_MOD , 修改fd上的注册事件。
- EOPLL_CTL_DEL , 删除fd上的注册事件。
event参数指定事件,它是epoll_event结构指针类型。 epoll_event的定义如下:
struct epoll_event
{
_uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
}
其中events成员描述事件类型。epoll 支持的事件类型和poll基本相同。表示epoll 事件类型的宏是在poll对应的宏前加上"E",
比如epoll的数据可读事件是EPOLLIN。 但是epoll有关额外的事件类型–EPOLLET 和EPOLLONESHOT。 它们对于epoll 的高效运行非常关键。 将在后面讨论。
data成员用于存储用户数据,其类型Eopll_data_t定义如下:
typedef union epoll_data
{
void * ptr ;
int fd ;
uint32_t u32;
uint64_t u64;
}epoll_data_t ;
epoll_data_t 是个联合体,其四个成员 中使用最多的是fd,它是指定事件所从属的目标文件描述符。
ptr成员可用来指定与fd相关的用户数据。但由于epoll_data_t 是联合体, 我们不能同时使用 其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问。只能使用其他手段。
比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd
epoll成功时返回0 失败则返回-1 并设置相关的errno
epoll_wait函数
epoll 系列函数调用的主要接口是epoll_wait函数 。它在一段超时时间 内等待一组文件描述符上面的事件,其原型如下
#include <sys/epoll.h>
int epoll_wait(int epfd ,struct epoll_event *events,int maxevents, int timeout);
该函数成功时返回就绪的文件描述符的个数,失败时 返回-1 并设置相关的errno。
关于该函数的参数 从后往前讨论。
- timeout参数的函数和poll接口的timeout参数 相同.
- maxevents参数指定最多的监听多少个事件,它必须大于0
- epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中(由epfd参数指定) 中复制到它的第二个参数 events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪时间,而不像select和poll的数组参数那样即用于传入用户注册事件,又用户输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述的效率。
下面展示了这个差别
/* 如何索引 poll返回的就绪文件 描述符*/
int ret = poll(fds ,MAX_EVENT_NUMBER ,-1)
/* 必须遍历到所有已经注册文件描述符 并找到其中就绪者 (当然可以利用ret来做点优化)*/
for(int i = 0; i,MAX_EVENT_NUMBER;++i)
{
if(fds[i].revents & POLLIN) /* 判断第i 个 文件描述符是否就绪*/
{
int sockfd = fds[i].fd ;
/* 处理 sockfd */
}
}
/*如何索引到epoll 返回的就绪文件描述符 */
int ret =epoll_wait(epollfd, events,MAX_EVENT_NUMBER, -1);
/* 仅遍历就绪的ret个文件描述符 */
for(int i =0 ;i<ret ;i++)
{
int sockfd = events[i].data.fd;
/*sockfd 肯定就绪,直接处理*/
}
LT 和 ET模式
epoll对文件描述符的操作 有两种模式:LT (Level Trigger ,电平触发)模式和ET (Edge Trigger,边沿触发)模式。LT模式是默认的工作模式,这种模式下的eopll相关于一个效率较高的poll。当往epoll内核事件上注册一个文件描述符上的EPOLLET事件时 ,epoll将以ET模式来操作当前文件描述符.ET模式是epoll的高效工作模式。
— 对于采用LE工作模式的文件描述符 ,当epoll_wait检测到其上有事件发生并将此事件通知给应用程序后,应用程序可以不立刻处理改时间。这样,当应用程序下一次调用epoll_wait时,epoll函数在想应用程序告知 此事件。知道该事件被处理为止。
— 而对于采用ET工作模式的文件描述符。当epoll_wait检测到其上有时间发生并将此时间通知给应用程序后,应用程序必须立刻处理该事件 ,因为后续的epoll_wait调用将不再向应用程序通知此之间。可见ET模式在很大程度上降低了 同一个epoll事件被重复触发的次数。
因此效率要比LT模式高。
代码可以演示差距/结果待补充/
/*
show the diff about ET and LT
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <stdbool.h>
#include <pthread.h>
#include <libgen.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
/* 将文件描述符设置为非阻塞的*/
int setnonblocking (int fd)
{
int old_option = fcntl(fd ,F_GETFL) ;
int new_option = old_option | O_NONBLOCK;
fcntl(fd ,F_SETFL, new_option );
return old_option;
}
/* 将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中 ,参数enable_et指定是否对fd启用ET模式 */
void addfd(int epollfd ,int fd ,bool enable_et)
{
struct epoll_event event;
event.data.fd = fd;
event.events =EPOLLIN;
if(enable_et)
{
event.events |= EPOLLET;
}
epoll_ctl(epollfd ,EPOLL_CTL_ADD,fd,&event );
setnonblocking(fd);
}
/** LT模式工作流程 */
void lT(struct 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 connfd = accept(listenfd ,(struct sockaddr *) &client_address, &client_addrlength);
addfd (epollfd ,connfd ,false) ; /* 对connfd禁用了 ET模式*/
}
else if( events[i].events & EPOLLIN)
{
/* 只要socket 读缓存中还有数据没有读出 ,这段代码就会被触发*/
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");
}
}
}
/* ET模式的工作流程 */
void et(struct 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 connfd = accept(listenfd ,(struct sockaddr *) &client_address, &client_addrlength);
addfd (epollfd ,connfd ,false) ; /* 对connfd禁用了 ET模式*/
}
else if( events[i].events & EPOLLIN)
{
/* 这段代码不会被重复触发 ,所以我们循环读取数据 以确保socket读取缓存中的所有数据读取*/
printf("event trigger once \n");
while(1)
{
memset(buf, '\0', BUFFER_SIZE);
int ret = recv (sockfd ,buf ,BUFFER_SIZE-1,0);
if(ret < 0)
{
/*对于非阻塞IO ,下面的条件成立表示数据已经全部读取完毕 ,此后,epoll能够再次触发 sockfd上EPOLLIN事件 以驱动下一次读操作 */
if((errno == EAGAIN) || (errno == EWOULDBLOCK))
{
printf("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");
}
}
}
int main(int argc , char * argv[])
{
if(argc <= 2)
{
printf("usage : %s ip_address port_number \n", basename(argv[0]));
return 1;
}
const char * ip = argv [1];
int port =atoi(argv[2]);
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(ret >= 0);
ret =bind(listenfd ,(struct sockaddr *) &address ,sizeof(address ));
assert(ret != -1);
ret = listen(listenfd ,5);
assert(ret != -1);
struct epoll_event events[ MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd ,listenfd ,true);
while(1)
{
int ret = epoll_wait(epollfd , events, MAX_EVENT_NUMBER, -1);
if(ret < 0)
{
printf("epoll failure \n");
break;
}
lT(events ,ret,epollfd ,listenfd); /*使用 LT模式*/
//et(events ,ret,epollfd ,listenfd ); /*使用ET模式*/
}
close (listenfd );
return 0;
}
EPOLLONESHOT事件
- 即使我们使用ET模式,一个socket上面的某个事件还是可能被多次触发,这在并发的程序中就会引起一个文件,比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket又有新的数据可出(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据,于是就出现两个线程同时操作一个socket的局面。这当然不是我们锁期望的。我们期望的是一个socket连接在任意时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现。
- 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读可写或者异常的事件,且只触发一次,除非我们使用epoll_ctl函数重置了该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时。其他线程是不可能有机会操作该socket。但是反过来思考 ,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就立刻重置这个socket上的EPOLLONESHOT事件 ,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket