以下内容均为本人学习笔记,若有不当,感谢指出
在网络编程中,poll可能实际中用到的不是很多,所以作为了解性内容
上上篇为 IO多路转接之select:select
上一篇为 IO多路转接之poll:poll
多路转接之epoll
epoll是在2.5.44内核中引进的,被公认为是最好的多路IO就绪通知方法
一、epoll的相关接口
创建一个文件句柄
#include <sys/epoll.h>
int epoll_create(int size);
创建一个 epoll 对象,这里类似于创建管道,但是这里返回的是一个标识该软件资源的文件描述符。
这里的参数 size 在linux2.6.8之后就忽略了,但是建议传参为256,128避免平台不同出错
- 既然返回的是epoll句柄,用完之后记得关闭
epoll的事件注册函数
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epoll不同于select 是在监听事件是才告诉内核要监听哪些文件描述符,哪种类型的事件,而是在这里先调用注册函数注册要监听的文件描述符和事件类型。
- 第一个参数为要epoll_create()返回的epoll句柄
第二个参数为动作
EPOLL_CTL_ADD: 注册新的 fd 到 opfd 中
EPOLL_CTL_MOD: 修改已注册的 fd 的监听事件
EPOLL_CTL_DEL: 从epfd中删除该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 为一个位图,每一位表示不同的事件,类似于 pollfd 里面的结构,用来表示监听事件集合
- data 为联合体,用来保存用户自定制数据,传什么类型的数据,就对联合体里面的哪个数据进行赋值
- 这里的data 在一般情况下用保存对应的文件描述符(后面会提到)
收集epoll监控事件中已经发送的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
- 第一个参数为我们创建的epoll模型
- 第二个参数为事件数组
- 第三个为数组大小
- 第四个参数为超时时间
二、epoll的工作原理
1.创建epoll模型
调用epoll_create()
之后,内核会做3件事情
(1)在操作系统底层(硬件驱动,网卡)构建回调机制
(2)在操作系统层构建一颗红黑树(一种相对平衡的二叉搜索树),树的每个节点用来保存用户关心的事件(即用户关心的文件描述符和所关心的事件类型)
(3)在操作系统层构建一个就绪队列,保存众多事件中已经就绪的事件
2.用户控制事件
(1) 用户通过调用epoll_ctl()实现
实现告诉操作系统,你现在要关心的文件描述符和关心的事件类型
(2)操作系统会将这一事件保存在红黑树中
3.内核激活事件
(1)操作系统得知网卡(文件)上面有数据就绪时(硬件机制),激活该事件,将其存入就绪队列中
(2)用户调用epoll_wait()
返回时,返回的为就绪队列中就绪的事件
我们说epoll_wait()
的实现是O(1)
的时间复杂度,只需要关注就绪队列是否为空,不为空就将事件复制到用户态
实例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
//基于epoll实现的http服务器
void Handle_events(int epfd,epoll_event events[],int maxevent,int listen_socket)
{
if(events == NULL || maxevent <=0)
{
return;
}
//因为执行到这一步,说明一定有事件就绪
//我们只需要关心是哪类事件就绪
int i = 0;
for(i = 0; i < maxevent; ++i)
{
char buf[1024 * 10] = {0};
if(events[i].data.fd == listen_socket && (events[i].events & EPOLLIN))
{
//(a)说明listen_socket已经就绪
sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int acc_sock = accept(listen_socket,(sockaddr *)&peer,&peer_len);
if(acc_sock < 0)
{
perror("accept");
continue;
}
else
{
//将已经建立连接的文件描述符加入到epfd中,并且设置为关心读事件
//让epoll_wait()监听该文件的读事件就绪
epoll_event event;
event.data.fd = acc_sock;
event.events = EPOLLIN;
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,acc_sock,&event);
if(ret < 0)
{
perror("epoll_ctl add");
continue;
}
}
continue;
}
else
{
//(b)先关心读事件
if(events[i].events & EPOLLIN)
{
//这里只读取一次,但是这样其实是不安全的,因为一次并不能保证把缓冲区所有是数据都读走
//有可能造成粘包问题
ssize_t read_size = read(events[i].data.fd,buf,sizeof(buf)-1);
if(read_size < 0)
{
perror("read");
close(events[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
continue;
}
if(read_size == 0)
{
printf("client sidconnect!\n");
close(events[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
continue;
}
buf[read_size] = '\0';
printf("%s",buf);
//将已经读完的文件描述符加入到epfd中,并且设置为关心写事件
//让epoll_wait()监听该文件的写事件就绪
epoll_event event;
event.data.fd = events[i].data.fd;
event.events = EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_ADD,events[i].data.fd,&event);
}
else
{
//(c)再关心写事件
if(events[i].events & EPOLLOUT)
{
///这里对应所有的响应都恢复一个html页面
const char * recv = "HTTP/1.1 200 OK\r\n\r\n<html><h1>hello world</h1><html>";
write(events[i].data.fd,recv,strlen(recv));
//短连接,一次响应之后将连接断开
close(events[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
}
}
}
}
}
//启动服务器
int Server_Start(const char * ip,const short port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
int ret = bind(sock,(sockaddr *)&addr,sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sock ,5);
if(ret < 0)
{
perror("listen");
return -1;
}
return sock;
}
//main()函数
int main(int argc,char * argv[])
{
//判断命令行参数
if(argc != 3)
{
printf("Usage :./server [ip] [port]\n");
return 1;
}
//1.启动服务器
int listen_socket = Server_Start(argv[1],atoi(argv[2]));
if(listen_socket < 0)
{
printf("server start faild\n");
return 2;
}
printf("server start ok\n");
//2.开始事件循环
//(a)构造epoll对象
int epfd = epoll_create(256);
if(epfd < 0)
{
perror("epoll_create");
return 3;
}
//(b)注册事件
epoll_event event;//定义events结构
event.data.fd = listen_socket;//将用户要监听的文件描述符赋值
event.events = EPOLLIN;//为该文件描述符只注册读事件
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,listen_socket,&event);
if(ret < 0)
{
perror("epoll_ctl add\n");
return 4;
}
//(c)进行循环处理
while(1)
{
//调用epoll_wait()开始监听
epoll_event events[128];
int size = epoll_wait(epfd,events,sizeof(events)/sizeof(events[0]),-1);//超时时间为1s监听
switch(size)
{
case -1:
perror("epoll_wait");
break;
case 0:
printf("time out\n");
break;
default://返回值大于0 的说明一定有事件就绪了
Handle_events(epfd,events,sizeof(events)/sizeof(events[0]),listen_socket);
break;
}
perror("epoll_wait");
printf("time out\n");
}//end while()
close(listen_socket);
close(epfd);
}//end main()
epoll特点总结
对比于我们之前学的select 和 poll
epoll的优点
- 文件描述符没有上限,这一点和poll类似,都是采用了数组来存储,而select 是受fd_set结构大小的限制
- 基于事件的就绪通知方式,操作系统通过硬件机制一旦监听的某个文件描述符就绪,会采用类似于callback回调机制,激活这个文件描述符,这样随着文件描述符数目的增多,也不会影响判定就绪的性能
- 内核维护就绪队列,当文件描述符就绪,操作系统会将这个事件放在就绪队列中,用于epoll_wait()返回,实现epoll_wait()O(1)的时间复杂度
- 内存映射机制,内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝数据到内存(这个观点被很多人认可,但是我觉得存在疑点)
对上面的内存映射机制有以下疑点:
- 我们再使用这些接口的时候,并没有用到内存映射的相关接口,而是在调用epoll_wait()时,我们自己将开辟好的空间作为输出型参数,最后将内核数据拷贝到用户态
- 操作系统是不相信任何用户的,假如采用内存映射机制维护就绪队列的话,用户可以直接修改内核的数据,我们都知道,访问内核数据必须通过系统给我们提供的系统调用接口
完。