epoll是Linux内核为处理高并发而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本。这里主要讲epoll和另外两个的区别,另外再把epoll的一个简单运用实例说说。
(一)epoll 有select,poll的主要区别:
一、相比于select与poll, epoll最大的好处在于它不会随着监听fd数目的增长而降低效率;
二、内核中的select与poll的实现是采用轮询来处理的,轮询的fd数据越多,自然耗时也越多;
三、epoll的实现是基于回调的,如果fd有期望的事件发生就通过回调函数将其加入epoll的就绪队列当中,也就是
说它只关心活跃的fd
四、内核/用户空间拷贝问题,当内核把fd消息通知给用户空间时,selec和poll采用了内存拷贝的方法,而epoll采
用了共享内存的方式。
(二)epoll 相关系统调用
epoll的接口非常简单,一共就三个函数:
一、(1)int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这里其实是指定hash表的容量。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)int epoll_create(int flags);
在比较新的Linux的版本中,出现了 int epoll_create(int flags); 这里的flags 一般选EPOLL_CLOEXEC,表示当进程被替换的时候文件描述符会被关闭。
二、 int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll的事件注册函数,即注册要监听的事件类型。
第一个参数是epoll_create()的返回值,
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
<span style="font-size:18px;">struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
typedef union epoll_data {
void *ptr; //这里是个指针,主要用来存放复杂的文件描述符
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;</span>
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level
Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再
次把这个socket加入到EPOLL队列里
三、int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待事件的产生。参数events 用来从内核得到事件的集合,maxevents 告之内核这个events 有多大,这个
maxevents 的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1一
直等待,即阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
(三)epoll 实例
这里以一个简单的客户端回射为例,程序的运行环境是debian(linux的一个发行版本),从上到下分别是头文件"net.h",epoll_server.cpp,select_client.cpp
服务器端:epoll实现的,干两件事分别为:1.等待客户端的链接,2.接收来自客户端的数据并且回射;
客户端:select实现,干两件事为:1.等待键盘输入,2.发送数据到服务器端并且接收服务器端回射的数据;
/*****************
@author:shaosli
@data: 2015/7/28
@funtions:network file
***************************/
#include <stdio.h>
#ifndef _NET_L
#define _NET_L
#include <iostream>
#include <vector>
#include <algorithm>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h> //epoll ways file
#include <sys/socket.h>
#include <fcntl.h> //block and noblock
#include <stdlib.h>
#include <error.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
using namespace std;
#define hand_error(msg) do{perror(msg); exit(EXIT_FAILURE);}while(0)
#endif
服务器端代码:
/****************
@data: 2015/7/28
@authour:shaosli
@function: this cpp is used to test epoll
**************************/
#include "../public/net.h"
#define MAX_EVENTS 10000
int setblock(int sock)
{
int ret = fcntl(sock, F_SETFL, 0);
if (ret < 0 )
hand_error("setblock");
return 0;
}
int setnoblock(int sock) //设置非阻塞模式
{
int ret = fcntl(sock, F_SETFL, O_NONBLOCK );
if(ret < 0)
hand_error("setnoblock");
return 0;
}
int main()
{
int listenfd;
listenfd = socket( AF_INET, SOCK_STREAM,0 ); //create a socket stream
if( listenfd < 0 )
hand_error( "socket_create");
setnoblock(listenfd);
int on = 1;
if( setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))< 0)
hand_error("setsockopt");
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(5188); //here is host sequeue
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if( bind( listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0)
hand_error("bind");
int lisId = listen(listenfd, SOMAXCONN);
if( lisId < 0) //LISTEN
hand_error("listen");
struct sockaddr_in peer_addr; //用来 save client addr
socklen_t peerlen;
//下面是一些初始化,都是关于epoll的。
vector<int> clients;
int count = 0;
int cli_sock = 0;
int epfd = 0; //epoll 的文件描述符
int ret_events; //epoll_wait()的返回值
struct epoll_event ev_remov, ev, events[MAX_EVENTS]; //events 用来存放从内核读取的的事件
ev.events = EPOLLET | EPOLLIN; //边缘方式触发
ev.data.fd = listenfd;
epfd = epoll_create(MAX_EVENTS); //create epoll,返回值为epoll的文件描述符
//epfd = epoll_create1(EPOLL_CLOEXEC); //新版写法
if(epfd < 0)
hand_error("epoll_create");
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //添加时间
if(ret < 0)
hand_error("epoll_ctl");
while(1)
{
ret_events = epoll_wait(epfd, events, MAX_EVENTS, -1); //类似于select函数,这里是等待事件的到来。
if(ret_events == -1)
{
cout<<"ret_events = "<<ret_events<<endl;
hand_error("epoll_wait");
}
if( ret_events == 0)
{
cout<<"ret_events = "<<ret_events<<endl;
continue;
}
cout<<"ret_events = "<<ret_events<<endl;
for( int num = 0; num < ret_events; num ++)
{
cout<<"num = "<<num<<endl;
if(events[num].events == listenfd) //client connect;
{
cout<<"listen sucess and listenfd = "<<listenfd<<endl;
cli_sock = accept(listenfd, (struct sockaddr*)&peer_addr, &peerlen);
if(cli_sock < 0)
hand_error("accept");
cout<<"count = "<<count++;
printf("ip=%s,port = %d\n", inet_ntoa(peer_addr.sin_addr),peer_addr.sin_port);
clients.push_back(cli_sock);
setnoblock(cli_sock); //设置为非阻塞模式
ev.data.fd = cli_sock;
ev.events = EPOLLIN | EPOLLET ;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, cli_sock, &ev)< 0)
hand_error("epoll_ctl");
}
else if( events[num].events & EPOLLIN)
{
cli_sock = events[num].data.fd;
if(cli_sock < 0)
hand_error("cli_sock");
char recvbuf[1024];
memset(recvbuf, 0 , sizeof(recvbuf));
int num = read( cli_sock, recvbuf, sizeof(recvbuf));
if(num == -1)
hand_error("read have some problem:");
if( num == 0 ) //stand of client have exit
{
cout<<"client have exit"<<endl;
close(cli_sock);
ev_remov = events[num];
epoll_ctl(epfd, EPOLL_CTL_DEL, cli_sock, &ev_remov);
clients.erase(remove(clients.begin(), clients.end(), cli_sock),clients.end());
}
fputs(recvbuf,stdout);
write(cli_sock, recvbuf, strlen(recvbuf));
}
}
}
return 0;
}
客户端代码:
/****************
@data: 2015/7/18
@authour:shaosli
@function: this is a client
**************************/
#include "../public/net.h"
int main()
{
int sock;
sock = socket( AF_INET, SOCK_STREAM,0 ); //create a socket stream
if( sock< 0 )
hand_error( "socket_create");
struct sockaddr_in my_addr;
//memset my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(5188); //here is host sequeue
//my_addr.sin_addr.s_addr = htonl( INADDR_ANY );
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int conn = connect(sock, (struct sockaddr *)&my_addr, sizeof(my_addr)) ;
if(conn != 0)
hand_error("connect");
char recvbuf[1024] = {0};
char sendbuf[1024] = {0};
fd_set rset;
FD_ZERO(&rset);
int nready = 0;
int maxfd;
int stdinof = fileno(stdin);
if( stdinof > sock)
maxfd = stdinof;
else
maxfd = sock;
while(1)
{
//select返回后把原来待检测的但是仍没就绪的描述字清0了。所以每次调用select前都要重新设置一下待检测的描述字
FD_SET(sock, &rset);
FD_SET(stdinof, &rset);
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
cout<<"nready = "<<nready<<endl;
if(nready == -1 )
break;
else if( nready == 0)
continue;
else
{
if( FD_ISSET(sock, &rset) ) //检测sock是否已经在集合rset里面。
{
int ret = read( sock, recvbuf, sizeof(recvbuf)); //读数据
if( ret == -1)
hand_error("read");
else if( ret == 0)
{
cout<<"sever have close"<<endl;
close(sock);
break;
}
else
{
fputs(recvbuf,stdout); //输出数据
memset(recvbuf, 0, strlen(recvbuf));
}
}
if( FD_ISSET(stdinof, &rset)) //检测stdin的文件描述符是否在集合里面
{
if(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
int num = write(sock, sendbuf, strlen(sendbuf)); //写数据
cout<<"sent num = "<<num<<endl;
memset(sendbuf, 0, sizeof(sendbuf));
}
}
}
}
return 0;
}
代码在我自己的系统上测试过。