I/O多路转接-----epoll服务器
在前面的两篇博客中,我们介绍了最早期的select和改进版的poll;但是,他两都没有改进的就是,想要快速知道事件就绪并没有得到改进,两个全部是遍历数组,我们都知道它的时间复杂度就是O(N);效率不是很高,时间复杂度达到O(1)才是高效的;
epoll介绍
epoll是Linux特有的I/O复用函数,它在实现和使用上与select、poll有很大差异,首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里面的一个事件表中,从而无需向select和poll那样每次都要重复的传入文件描述符集或事件集。
其实,简单的来理解epoll服务器,它的底层就是由一颗红黑树和一个队列组成的,当调用epoll_create函数时就是创建了一个空的红黑树和空的队列,当调用epoll_ctl函数的时候,就是在红黑树上面进行节点操作,当调用epoll_wait函数时就是把红黑树中的指定节点放到队列中去,这样就可以利用队列先进先出的特点迅速的找到就绪节点,相比较二巷比数组遍历一次要节省不少时间;
epoll函数调用
1、epoll_create()
#include<sys/epoll.h>
int epoll_create(int size);
//size参数现在并不起什么作用,只是给内核一个提示,告诉它事件表需要多大,该函数返回的文件描述符将作用其他所有epoll系统调用的第一个参数,已指定要访问的内核事件表
2、epoll_ctl()
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:参数指定事件,是一个结构体
struct epoll_event
{
_uint32_t events; //epoll事件
epoll_data_t data; //用户数据
}
data成员用于存储用户数据,其类型:
typedef union epoll_data
{
void* ptr;
inf fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
3、epoll_wait()
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
从后往前讨论参数:
timeout:超时设置;
maxevents:最多监听多少个事件,必须大于0;(类似于数组的最大值)
epoll_wait函数如果检测到了事件,就将所有就绪事件ongoing内核事件表中赋值到它的第二个参数events指向的数组中。这个数组直营与出输出epoll_wait检测到的就绪事件。
LT和ET模式
epoll对文件描述符的操作有两种方式LT(水平触发)和ET(边缘触发),LT是默认的工作模式,这种模式epoll相当于一个高效的poll,当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符,ET模式是epoll的高效工作模式。
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不利己处理该事件,这样,就当程序次啊一次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,知道该事件被处理。而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用不在向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此次奥绿要比LT模式高
ET模式的epoll服务器
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>
#include <pthread.h>
#define MAX_READY_EVENTS 64
//将文件描述符设置为非阻塞的
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, int 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);
}
static void usage(const char* proc)
{
printf("Usage %s: [local_ip] [local_port]\n", proc);
}
int startup(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("sock");
return 2;
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(sock, 10) < 0)
{
perror("listen");
return 4;
}
return sock;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1], atoi(argv[2]));
int epfd = epoll_create(256);
if(epfd < 0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev) < 0)
{
perror("epoll_ctl");
return 6;
}
int timeout = 1000;
int nums = -1;
struct epoll_event revs[MAX_READY_EVENTS];
while(1)
{
switch(nums = epoll_wait(epfd, revs, MAX_READY_EVENTS, /*timeout*/-1))
{
case 0:
{
printf("timeout...\n");
}
break;
case -1:
{
perror("epolly_wait");
return 7;
}
break;
default:
{
int i = 0;
for(; i < nums; i++)
{
int fd = revs[i].data.fd;
if((fd == listen_sock) && (revs[i].events == EPOLLIN))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);
if(new_sock < 0)
{
perror("accept");
return 8;
}
printf("get new client: [%s:%d]\n",inet_ntoa(client.sin_addr), ntohs(client.sin_port));
struct epoll_event ev1;
ev1.events = EPOLLIN;
ev1.data.fd = new_sock;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev1) < 0)
{
perror("epoll_ctl");
return 9;
}
addfd(epfd, new_sock, 1);//开启ET模式
}// listen_sock can read
else if((fd != listen_sock) && (revs[i].events & EPOLLIN))
{
//ET模式下下面代码不会重复触发,所以我们循环读取数据
while(1)
{
char *buf[1024];
int s = read(fd, buf, sizeof(buf)-1);
if(s < 0)
{
if((errno == EAGAIN) || (errno == EWOULDBLOCK))
{
break;
}
close(fd);
break;
}
else if(s == 0)
{
printf("client quit...\n");
close(fd);
}
else
{
buf[s] = 0;
printf("client ####:%s", buf);
}
}
}//other events can read
else if((fd != listen_sock) && (revs[i].events & EPOLLOUT))
{
char* msg = "";
write(fd, msg, strlen(msg));
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}//events can write
}//for
}//default
break;
}
}
return 0;
}
epoll的优缺点
epoll在调用epoll_create时操作系统会创建一颗红黑树存放socket和一个队列存放就绪事件
- 支持一个进程打开大数目的socket描述符(FD)
- IO效率不随FD数目增加而线性下降
- 使用mmap加速内核与用户空间的消息传递。
三种模式服务器的比较
mmap
转载一篇写的很好的博客学习学习:mmap是什么,为什么,怎么用
推荐阅读
-
一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生
-
Linux I/O多路复用详解及实例
-
python 之 并发编程(非阻塞IO模型、I/O多路复用、socketserver的使用)
-
【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生
-
I/O多路复用
-
linux进程间通信---本地socket套接字(五)---多路IO转接服务器实现一个server对应多个client---poll实现
-
7.10 第九章I/O复用高级函数 select poll epoll(lt et)
-
select模块(I/O多路复用)
-
I/O多路转接之epoll(实现epoll版本的TCP服务器)
-
I/O多路转接——select、poll 和 epoll