在实际的开发中,我们经常会遇到这样的场景,我们需要接受多个端口的数据、多个终端的数据抑或是多个文件描述符对应的数据。那么,遇到这样的问题,你在程序中该怎么做呢?通常的做法,在程序中对数据交互的描述符进行轮询。那么问题来了,轮询的时间设置为多少呢?设置的太短,可以保证处理性能和速度,但是CPU的使用率太高,一旦处理的描述符数量多了起来,CPU可能就扛不住了。设置的时间太长,描述符处理的时间片太短,处于空闲的时间较长,性能和速度达不到要求。如果是服务器的话,面对多个用户的连接,处理速度和CPU使用性能是必须考虑的,而且最好要兼顾。这里就需要使用到I/O多路复用机制,这就是博主即将要和小伙伴们探讨的内容。
epoll简介
1、基本知识
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
2、epoll接口
epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, structepoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events,int maxevents, int timeout);
(1) int epoll_create(intsize);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_tdata; /* User data variable */
}
};
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)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
(3) int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
3、工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
程序实例
服务端程序
#include<stdio.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<time.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<error.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
const intMAXFD = FD_SETSIZE;//FD_SETSIZE 1024 可以监控的最大描述符数量
constchar *SERVERIP = "127.0.0.1";//服务端IP
constunsigned short PORT = 6666;//服务端端口号
typedefstruct server_st
{
int cli_num; //已连接客户端数量
int cli_fd[MAXFD]; //已连接客户端数组
struct epoll_event events[MAXFD]; //事件数组
int epollfd; //epoll_create创建的描述符
}server_st_t;
//打印出错信息并推出程序
#definehandle_error(msg)\
do {perror(msg);exit(EXIT_FAILURE);}while(0)
//定义类
classepollsocket
{
public:
//构造函数
epollsocket(const char *server_ip =SERVERIP, unsigned short port = PORT);
//析构函数
virtual ~epollsocket();
//客户处理函数(对外接口)
int handle_cli_proc();
private:
int server_init();//初始化server_st_t结构体
int server_uninit();//释放server_st_t结构体
int handle_create_proc();//创建服务端监听套接字
int handle_accept_proc();//处理客户端连接
int handle_recv_proc(int&cli_fd);//处理客户端数据发送
epollsocket(const epollsocket&ref);
epollsocket& operator=(constepollsocket &ref);
private:
server_st_t *m_server_st;
char m_server_IP[16];
unsigned short m_port;
int m_server_fd;
};
epollsocket::epollsocket(constchar *server_ip /*= SERVERIP*/, unsigned short port /*= PORT*/)
{
bzero(m_server_IP, sizeof(m_server_IP));//将类成员地址空间清零
memcpy(m_server_IP, server_ip,strlen(server_ip));//给类成员负值
m_port = port;
m_server_st = NULL;
handle_create_proc();//创建监听套接字
server_init();//初始化
}
epollsocket::~epollsocket()
{
//close(m_server_fd);
server_uninit();//释放结构体
}
intepollsocket::server_init()
{
m_server_st =(server_st_t*)malloc(sizeof(server_st_t));
if (NULL == m_server_st)handle_error("malloc");
//将结构体空间清零
bzero(m_server_st, sizeof(server_st_t));
//初始化已连接客户端数组
for (int i = 0; i<MAXFD; ++i)
{
m_server_st->cli_fd[i] = -1;
}
//创建epoll
m_server_st->epollfd =epoll_create(MAXFD);
//将监听描述符添加到epoll描述符中
struct epoll_event ev;
ev.data.fd = m_server_fd;
ev.events = EPOLLIN | EPOLLET;//监听读事件和边沿触发
epoll_ctl(m_server_st->epollfd,EPOLL_CTL_ADD, m_server_fd, &ev);
return 0;
}
intepollsocket::server_uninit()
{
//关闭epoll描述符
close(m_server_st->epollfd);
//关闭所有的描述符(包括监听描述符)
for (int i = 0; i < MAXFD; ++i)
{
int cli_fd = m_server_st->cli_fd[i];
if (-1 != cli_fd)
{
close(cli_fd);
}
}
if (NULL != m_server_st)
{
free(m_server_st);
}
m_server_st = NULL;
}
intepollsocket::handle_create_proc()
{
//创建TCP套接字
//参数1:协议族
//参数2:套接字类型
//参数3:使用的协议(0:使用套接字类型对应的默认协议)
if (-1 == (m_server_fd = socket(AF_INET,SOCK_STREAM, 0))) handle_error("socket");
//地址结构体
struct sockaddr_in serveraddr;
//将结构体变量空间清零
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;//协议族
//注意,设置端口和IP时,要将主机字节序转换为网络字节序
serveraddr.sin_port = htons(m_port);
//serveraddr.sin_addr.s_addr =htonl(INADDR_ANY);
inet_pton(AF_INET, m_server_IP,&serveraddr.sin_addr);
int op = true;
///*一个端口释放后会等待两分钟之后才能再被使用(TIME_WAIT状态),SO_REUSEADDR是让端口释放后立即就可以被再次使用*/
if (-1 == setsockopt(m_server_fd,SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)))handle_error("setsockopt");
//命名套接字
if (-1 == bind(m_server_fd, (structsockaddr*)&serveraddr, sizeof(serveraddr)))handle_error("serveraddr");
//将套接字由主动变为被动(接受客户端连接状态)
if (-1 == listen(m_server_fd, SOMAXCONN))handle_error("listen");
return 0;
}
intepollsocket::handle_accept_proc()
{
//地址结构体
struct sockaddr_in cli_addr;
socklen_t len =(socklen_t)sizeof(cli_addr);
//将结构体变量空间清零
bzero(&cli_addr, sizeof(cli_addr));
//接受客户端的连接请求
int cli_fd = accept(m_server_fd, (structsockaddr*)&cli_addr, &len);
if (-1 == cli_fd)handle_error("accept");
++m_server_st->cli_num;//已连接客户数加1
fprintf(stdout, "#%d %s:%d connected server!\n", m_server_st->cli_num,inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
//将新连接的客户端描述符添加到已连接客户端数组中
for (int i = 0; i<MAXFD; ++i)
{
if (-1 == m_server_st->cli_fd[i])
{
m_server_st->cli_fd[i] = cli_fd;
break;
}
}
//将新连接的客户端端描述符添加到epoll监控中
struct epoll_event ev;
ev.data.fd = cli_fd;
ev.events = EPOLLIN | EPOLLET;//监听读事件和边沿触发
epoll_ctl(m_server_st->epollfd,EPOLL_CTL_ADD, cli_fd, &ev);
}
intepollsocket::handle_recv_proc(int &cli_fd)
{
char buf[256] = {0};
//从网络中读取数据
int r = read(cli_fd, buf, sizeof(buf));
//如果读取出错
if (r <= 0)
{
for (int i = 0; i<MAXFD; ++i)
{
if (cli_fd ==m_server_st->cli_fd[i])
{
m_server_st->cli_fd[i] = -1;
break;
}
}
//关闭当前描述符(客户端断开)
close(cli_fd);
//将当前描述符从监控中删除
epoll_ctl(m_server_st->epollfd,EPOLL_CTL_DEL, cli_fd, NULL);
cli_fd = -1;//将描述符置为-1
//已连接客户数减1
--m_server_st->cli_num;
}
//如果接受成功,将数据写回网络
write(cli_fd, buf, sizeof(buf));
//清空缓存区
memset(buf, 0x00, sizeof(buf));
return 0;
}
intepollsocket::handle_cli_proc()
{
int ready = -1;
while (1)
{
//epoll阻塞等待描述符集合中是否有就绪描述符
//参数1:epoll_create创建的epoll描述符
//参数2:返回的已就绪事件数组
//参数3:返回的已就绪事件数组的长度
//参数4:最长等待时间(-1:无限等待,直到有描述符就绪;0:立即返回;等待指定时间,单位毫秒)
//一旦有描述符就绪则返回,返回值为以就绪描述符的个数
ready =epoll_wait(m_server_st->epollfd, m_server_st->events, MAXFD, 5000);
//epoll函数返回异常
if (-1 == ready) break;
//epoll函数等待超时
if (0 == ready)
{
fprintf(stdout, "epolltimeout\n");
continue;
}
//轮询已就绪事件数组
for (int i = 0; i<ready; ++i)
{
//处理读事件
if (m_server_st->events[i].events& EPOLLIN)
{
//如果有客户连接服务器,监听套接字就绪,执行以下代码
if (m_server_fd ==m_server_st->events[i].data.fd)
{
//fprintf(stdout,"connect\n");
//处理客户端的连接
handle_accept_proc();
//已经处理过来sfd描述符,read减1
//如果此时read<=0,表示所有就绪描述符已处理完毕,返回poll处继续等待就绪描述符
//没有客户端发送数据,退出本次循环
if (--ready <= 0)
{
break;
}
}
else//处理客户端数据接收
{
//处理客户端发送过来的数据
handle_recv_proc(m_server_st->events[i].data.fd);
}
}
else
{
//其他事件处理逻辑
}
}
}
return 0;
}
int main()
{
epollsocket epollsock;
epollsock.handle_cli_proc();
}
客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
int main()
{
//创建IPV4 TCP套接字
int sfd =socket(AF_INET, SOCK_STREAM, 0);
if (-1 ==sfd) perror("socket"), exit(EXIT_FAILURE);
//地址结构体
structsockaddr_in addr;
addr.sin_family = AF_INET;//协议族
addr.sin_port = htons(6666);//端口
//将地址串转换为网络字节序,存储到addr.sin_addr中
inet_aton("127.0.0.1", &addr.sin_addr);//连接的服务端IP地址
//连接服务器
if (-1 ==connect(sfd, (struct sockaddr *)&addr, sizeof(addr)))
{
perror("connect");
exit(EXIT_FAILURE);
}
charbuf[256] = {};
//从标准输入读取数据
while (NULL!= fgets(buf, sizeof(buf), stdin))
{
//将数据发送给服务器
write(sfd, buf, strlen(buf));
//清空缓存区
memset(buf, 0x00, sizeof(buf));
//读取服务器放送过来的数据
int r =read(sfd, buf, sizeof(buf));
//接受失败
if (r<= 0)
{
break;
}
//输出
fprintf(stdout, buf, r);
//清空缓存区
memset(buf, 0x00, sizeof(buf));
}
//关闭套接字描述符
close(sfd);
return 0;
}
程序运行结果
参考:epoll工作总结