poll和epoll及实现epoll网络服务器

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优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作
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的优点

  1. 文件描述符数目无上限:通过epoll_ctl()来注册一个文件描述符, 内核中使用红黑树的数据结构来管理所有需要监控的文件描述符
  2. 基于事件的就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于callback的回调机制,迅速激活这个文件描述符。这样随着文件描述符数量的增加, 也不会影响判定就绪的性能
  3. 维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中。这样调用epoll_wait获取就绪文件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1)
  4. 不存在内存映射机制:操作系统将就绪的节点拷贝至用户的缓冲区,不可能把自己的数据暴露给用户。

epoll的工作方式

epoll有2种工作方式-水平触发(LT)和边缘触发(ET) 。

水平触发Level Triggered 工作模式(默认状态下):
  1. 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理,或者只处理一部分。
  2. 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪。
  3. 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
  4. 支持阻塞读写和非阻塞读
边缘触发Edge Triggered工作模式 :
  1. 当epoll检测到socket上事件就绪时, 必须立刻处理
  2. ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
  3. ET的性能比LT性能更高( epoll_wait 返回的次数少了很多).
  4. 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地址:
这里写图片描述
运行程序:
这里写图片描述
网页搜索:
这里写图片描述
这里写图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值