I/O多路转接之poll
poll函数原型
参数解释
参数 | 解释 |
---|---|
fds | 是⼀个poll函数监听的结构列表 |
nfds | 表示fds数组的长度 |
timeout | 表示poll函数的超时时间, 单位是毫秒(ms) |
pollfd结构
那么fds是一个什么样的结构呢?如下:
其中各成员表示的意义如下:
fd:文件描述符
events:监听的事件集合
revents:返回的事件集合
通过上图,events和revents是short类型的,那么它们是怎么记录事件的呢?
这是它们的取值:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据可读(包括普通数据和优先数据) | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起 | 否 | 是 |
POLLNVAL | 文件描述符没被打开 | 否 | 是 |
上表中都是一些宏,我们看一下它们的大小:
events和revents是通过判断以上各取值对应的比特位来记录事件的。
返回值
返回值 | 意义 |
---|---|
小于0 | 表示出错 |
等于0 | 表示poll函数等待超时 |
大于0 | 表示poll由于监听的文件描述符就绪而返回 |
与select的比较
1、解决了select调用之前需要重新设置的问题
2、文件描述符上限问题,可以认为文件描述符无上限,但进程的文件描述符是有上限的。
其余基本相同。
poll的优缺点
优点:
1. poll使用一个pollfd的指针实现
2. pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比 select更方便.
3. poll并没有最大数量限制 (但是数量过大后性能也是会下降).
缺点:
1. 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
2. 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
3. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
I/O多路转接之epoll
epoll有三个系统调用,每个函数实现它特定的功能:
(1)epoll_create
功能:创建一个epoll模型
参数:自从Linux2.6.8之后,size常数是被忽略的,用完之后,必须用close()关闭。
返回值:
成功返回文件描述符,
失败返回-1。
(2)epoll_ctl
功能:epoll的事件注册函数.
参数:
参数 | 解释 |
---|---|
epfd | 上一步创建的epoll模型 |
op | 表示动作,用三个宏来表示 |
fd | 需要监听的fd |
event | 告诉内核需要监听什么事 |
op的取值
值 | 作用 |
---|---|
EPOLL_CTL_ADD | 注册新的fd到epfd中 |
EPOLL_CTL_MOD | 修改已经注册的fd的监听事件 |
EPOLL_CTL_DEL | 从epfd中删除一个fd |
struct epoll_event结构
event的取值
events可以是以下几个宏的集合:
事件 | 描述 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读 |
EPOLLOUT | 表示对应的文件描述符可以写 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读 |
EPOLLERR | 表示对应的⽂件描述符发生错误 |
EPOLLHUP | 表示对应的文件描述符被挂断 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式 |
(3)epoll_wait
功能:收集在epoll监控的事件中已经发送的事件.
参数:
参数 | 解释 |
---|---|
events | 是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存) |
maxevents | 通知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size |
timeout | 是超时时间 (毫秒,0会⽴立即返回,-1是永久阻塞) |
返回值:
如果函数调用成功,返回对应I/O上已就绪的文件描述符数目,
返回0表示已超时,
返回小于0表示函数失败
epoll模型
我们通过介绍三个系统调用,来了解epoll模型。
(1)创建epoll模型
我们先前已经提过,epoll_create调用会创建一个epoll模型,那么epoll模型是什么样的呢?
epoll模型包括三个部分,红黑树,回调机制,就绪队列。
所以说,创建一个epoll模型,操作系统要做三件事情:
1. 建红黑树,用来保存要监控的哪些文件描述符的哪些事件,其key值为文件描述符。
2. 构建底层驱动到操作系统的一种回调机制,确保有事件就绪时驱动会通知操作系统。
3. 在操作系统内部构建就绪队列,保存就绪的文件描述符的事件的节点
注:
select和poll都是操作系统去检测事件是否发生,需要自己去维护要关心的文件描述符及事件,而epoll是当底层数据就绪时,让驱动来通知操作系统,且epoll让操作系统通过红黑树帮我们维护。
(2)完成事件注册
调用epoll_ctl,即将我们关心的文件描述符告诉操作系统,操作系统会将我们要关心的文件描述符及事件添加到红黑树中,至此我们就不需要管理它们了,由操作系统帮我们管理。
(3)等待文件描述符就绪,检查事件是否就绪
调用epoll_wait,检查就绪队列是否为空,如果不为空,就绪队列中保存的就是已经就绪的事件;然后操作系统将数据按顺序放置在用户提供的缓冲区,同时将事件数量返回给用户。
如下图所示:
epoll的优点
- 文件描述符数目无上限:通过epoll_ctl()来注册一个文件描述符, 内核中使用红黑树的数据结构来管理所有需要监控的文件描述符
- 基于事件的就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于callback的回调机制,迅速激活这个文件描述符。这样随着文件描述符数量的增加, 也不会影响判定就绪的性能
- 维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中。这样调用epoll_wait获取就绪文件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1)
- 不存在内存映射机制:操作系统将就绪的节点拷贝至用户的缓冲区,不可能把自己的数据暴露给用户。
epoll的工作方式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET) 。
水平触发Level Triggered 工作模式(默认状态下):
- 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理,或者只处理一部分。
- 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪。
- 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
- 支持阻塞读写和非阻塞读
边缘触发Edge Triggered工作模式 :
- 当epoll检测到socket上事件就绪时, 必须立刻处理
- ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多).
- Nginx默认采用ET模式使用epoll. 只支持非阻塞的读写
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式
注:select和poll是工作在LT模式下. epoll既可以支持LT, 也可以支持ET.
实现epoll网络服务器
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<sys/epoll.h>
#define MAX 128
int startup(int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(2);
}
int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=htonl(INADDR_ANY);
local.sin_port=htons(port);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(3);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(4);
}
return sock;
}
void serverIO(struct epoll_event *revs,int num,int epfd,int listen_sock)
{
int i=0;
struct epoll_event ev;
for(;i<num;i++){
int fd=revs[i].data.fd;
//read ready
if(revs[i].events & EPOLLIN){
if(fd==listen_sock){
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_sock=accept(fd,(struct sockaddr*)&client,&len);
if(new_sock<0)
{
perror("accept");
continue;
}
printf("get new client [%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events=EPOLLIN;
ev.data.fd=new_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
}
else{
char buf[10240];
ssize_t s=read(fd,buf,sizeof(buf)-1);
if(s>0){
buf[s]=0;
printf("client:>%s\n",buf);
ev.events=EPOLLOUT;
ev.data.fd=fd;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
}
else if(s==0){
close(fd);
printf("client quit!\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
else{
perror("read");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}
}
//write ready
if(revs[i].events & EPOLLOUT){
const char *msg="HTTP/1.0 200 OK\r\n\r\n<html><h1>hello my epoll server!</h1></html>";
write(fd,msg,strlen(msg));
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
}
}
int main(int argc,char *argv[])
{
if(argc!=2)
{
printf("Usage:%s [port]\n",argv[0]);
return 1;
}
int listen_sock=startup(atoi(argv[1]));
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;
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
int num=0;
int timeout=-1;
struct epoll_event revs[MAX];
for(;;)
{
switch((num=epoll_wait(epfd,revs,MAX,timeout))){
case -1:
perror("epoll_wait");
break;
case 0:
printf("timeout...");
break;
default:
serverIO(revs,num,epfd,listen_sock);
break;
}
}
return 0;
}
测试一:
telnet工具
我们等会用远程登录工具telnet测试,第一次使用需要安装:
测试二:
网页测试:
用ifconfig查看IP地址:
运行程序:
网页搜索: