一、select的缺陷
1、高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:返回的活跃连接 ==select(全部待监控的连接)。
2、什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。
3、 此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数(1024)。
4、内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。看到这里,您可能要要问了,你为什么不提poll?笔者认为select与poll在内部机制方面并没有太大的差异。相比于select机制,poll只是取消了最大监控文件描述符数限制,并没有从根本上解决select存在的问题。
接下来我们看张图,当并发连接为较小时,select与epoll似乎并无多少差距。可是当并发连接上来以后,select就显得力不从心了。
二、epoll
1、到Linux 2.6才出现由内核直接支持的实现方法epoll,它几乎具备之前所说的一切优点,被公认为Linux 2.6下性能最好的多路I/O就绪通知方法。
2、相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
3、epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,它就不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但代码实现相当复杂。
4、epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
5、另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行描述,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知
三、epoll函数
1.int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好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_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
// 感兴趣的事件和被触发的事件
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)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3.int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
收集在epoll监控的事件中已经发生的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时
epfd:epoll_create()的返回值。
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
maxevents:指定最多监听多少个事件
timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。
返回值:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
四、epoll工作原理
1、epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
2、另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
五、例子:
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/poll.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include<netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include<sys/time.h>
#include <sys/epoll.h>
#include <sys/select.h>
#define BUF_SIZE 100
#define SERV_PORT 8888
#define MAX_LISTEN_QUE 100
#define MAX_EVENTS 100
//服务器向客户端发送数据
void *ServerToClient(void *arg)
{
char wdbuf[BUF_SIZE] = {0};
int timep;
int writefd = *(int*)arg;
struct tcp_info info;
int len=sizeof(info);
int status = 0;
while(1)
{
status = getsockopt(writefd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len);
if((info.tcpi_state == TCP_ESTABLISHED) ) //判断客户端未断开 else 断开
{
memset(wdbuf, 0, BUF_SIZE );
timep = time(NULL);
snprintf(wdbuf, sizeof(wdbuf), "%s", ctime((time_t *)&timep));
write(writefd, wdbuf, strlen(wdbuf));
printf("writefd:%d\n", writefd);
info.tcpi_state = 0;
sleep(5);
}
else
{
printf("write close:%d\n", writefd);
pthread_exit(0);
}
}
}
int mz_ipv4_tcp_create_socket(void)
{
int listenfd, sockfd, opt = 1;
struct sockaddr_in server;
socklen_t len;
int timep;
int ret;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0){
perror("Create socket fail.");
return -1;
}
if((ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) < 0){
perror("Error, set socket reuse addr failed");
return -1;
}
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(SERV_PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
len = sizeof(struct sockaddr);
if(bind(listenfd, (struct sockaddr *)&server, len)<0){
perror("bind error.");
return -1;
}
listen(listenfd, MAX_LISTEN_QUE);
return listenfd;
}
int mz_process_data(int sockfd)
{
int bytes, flag = 1;
char buf[100];
while(1)
{
//bytes = recv(sockfd, buf, sizeof(buf), 0);
//memset(buf, 0, sizeof(buf));
bytes = read(sockfd, buf, sizeof(buf));
if(bytes < 0)
{
perror("recv err:");
return -1;
}
if(bytes == 0)
return -2;
printf("sockfd:%d bytes:%d : %s",sockfd, bytes, buf);
}
//send(sockfd, buf, len, 0);
return 0;
}
/*
int epoll_create ( int size );
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
fd:要操作的文件描述符
op:指定操作类型
操作类型:
EPOLL_CTL_ADD:往事件表中注册fd上的事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件
event:指定事件,它是epoll_event结构指针类型
epoll_event定义:
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。
maxevents:指定最多监听多少个事件
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
*/
int main(int argc, char *argv[])
{
int listenfd, sockfd;
int epollfd, fds;
struct epoll_event ev, events[MAX_EVENTS]; //ev用于注册事件,数组用于返回要处理的事件
int i, rv;
struct sockaddr_in client;
int len;
len = sizeof(struct sockaddr_in);
epollfd = epoll_create(MAX_EVENTS); //新建epoll描述符
if(epollfd < 0)
{
perror("epoll_create err:");
return -1;
}
listenfd = mz_ipv4_tcp_create_socket();
fcntl(listenfd, F_SETFL, O_NONBLOCK); //设置文件状态为不阻塞
ev.data.fd = listenfd;
ev.events = EPOLLIN;
rv = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev); //EPOLL_CTL_ADD添加待监控者
if(rv < 0)
{
perror("epoll_ctl err:");
return -1;
}
pthread_t tid;
while(1)
{
fds = epoll_wait(epollfd, events, MAX_EVENTS, -1); //返回活跃的客户端连接个数,timeout==-1阻塞等待:epoll将会把发生的活跃事件赋值到events数组中
if(fds < 0){
perror("epoll_wait err:");
return -1;
}
for(i = 0; i < fds; i++)
{
if(events[i].data.fd == listenfd)
{
sockfd = accept(listenfd, (struct sockaddr *)&client, &len);
if(sockfd < 0)
{
perror("accept err:");
continue;
}
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET; //监听读状态同时设置ET模式
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
pthread_create(&tid, NULL, ServerToClient, (void*)&sockfd); //创建线程并发送时间信息给客户端
continue;
}else
{
rv = mz_process_data(events[i].data.fd);
if(rv == -2)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
printf("close socket: %d\n", events[i].data.fd);
close(events[i].data.fd);
continue;
}
}
}
}
}