一.基本概念                                                         

我们通俗一点讲:

Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!

阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。当你去写一个阻塞的文件描述符时,如果在该文件描述符上没有空间(通常是缓冲区)可写,那么它会一直阻塞,直到有空间可写。以上的读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据,还包括接收连接accept(),发起连接connect()等操作...

非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样,卡在那里不动!!!

二.几种IO模型的触发方式                          

select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

这里我们要探讨epoll()的水平触发和边缘触发,以及阻塞IO和非阻塞IO对它们的影响!!!下面称水平触发为LT,边缘触发为ET。

对于监听的socket文件描述符我们用sockfd代替,对于accept()返回的文件描述符(即要读写的文件描述符)用connfd代替。

我们来验证以下几个内容:

1.水平触发的非阻塞sockfd

2.边缘触发的非阻塞sockfd

3.水平触发的阻塞connfd

4.水平触发的非阻塞connfd

5.边缘触发的阻塞connfd

6.边缘触发的非阻塞connfd

以上没有验证阻塞的sockfd,因为epoll_wait()返回必定是已就绪的连接,设不设置阻塞accept()都会立即返回。例外:UNP里面有个例子,在BSD上,使用select()模型。设置阻塞的监听sockfd时,当客户端发起连接请求,由于服务器繁忙没有来得及accept(),此时客户端自己又断开,当服务器到达accept()时,会出现阻塞。本机测试epoll()模型没有出现这种情况,我们就暂且忽略这种情况!!!

三.验证代码                                                         

文件名:epoll_lt_et.c

 /*
  *url:http://www.cnblogs.com/yuuyuu/p/5103744.html
  *
  */

 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <arpa/inet.h>
 #include <netinet/in.h>
 #include <sys/socket.h>
 #include <sys/epoll.h>

 /* 最大缓存区大小 */
 #define MAX_BUFFER_SIZE 5
 /* epoll最大监听数 */
 #define MAX_EPOLL_EVENTS 20
 /* LT模式 */
 #define EPOLL_LT 0
 /* ET模式 */
 #define EPOLL_ET 1
 /* 文件描述符设置阻塞 */
 #define FD_BLOCK 0
 /* 文件描述符设置非阻塞 */
 #define FD_NONBLOCK 1

 /* 设置文件为非阻塞 */
 int set_nonblock(int fd)
 {
     int old_flags = fcntl(fd, F_GETFL);
     fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
     return old_flags;
 }

 /* 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) */
 void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type)
 {
     struct epoll_event ep_event;
     ep_event.data.fd = fd;
     ep_event.events = EPOLLIN;

     /* 如果是ET模式,设置EPOLLET */
     if (epoll_type == EPOLL_ET)
         ep_event.events |= EPOLLET;

     /* 设置是否阻塞 */
     if (block_type == FD_NONBLOCK)
         set_nonblock(fd);

     epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event);
 }

 /* LT处理流程 */
 void epoll_lt(int sockfd)
 {
     char buffer[MAX_BUFFER_SIZE];
     int ret;

     memset(buffer, , MAX_BUFFER_SIZE);
     printf("开始recv()...\n");
     ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, );
     printf("ret = %d\n", ret);
     )
         printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     else
     {
         )
             printf("客户端主动关闭!!!\n");
         close(sockfd);
     }

     printf("LT处理结束!!!\n");
 }

 /* 带循环的ET处理流程 */
 void epoll_et_loop(int sockfd)
 {
     char buffer[MAX_BUFFER_SIZE];
     int ret;

     printf("带循环的ET读取数据开始...\n");
     )
     {
         memset(buffer, , MAX_BUFFER_SIZE);
         ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, );
         )
         {
             if (errno == EAGAIN || errno == EWOULDBLOCK)
             {
                 printf("循环读完所有数据!!!\n");
                 break;
             }
             close(sockfd);
             break;
         }
         )
         {
             printf("客户端主动关闭请求!!!\n");
             close(sockfd);
             break;
         }
         else
             printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     }
     printf("带循环的ET处理结束!!!\n");
 }

 /* 不带循环的ET处理流程,比epoll_et_loop少了一个while循环 */
 void epoll_et_nonloop(int sockfd)
 {
     char buffer[MAX_BUFFER_SIZE];
     int ret;

     printf("不带循环的ET模式开始读取数据...\n");
     memset(buffer, , MAX_BUFFER_SIZE);
     ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, );
     )
     {
         printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     }
     else
     {
         )
             printf("客户端主动关闭连接!!!\n");
         close(sockfd);
     }

     printf("不带循环的ET模式处理结束!!!\n");
 }

 /* 处理epoll的返回结果 */
 void epoll_process(int epollfd, struct epoll_event *events, int number, int sockfd, int epoll_type, int block_type)
 {
     struct sockaddr_in client_addr;
     socklen_t client_addrlen;
     int newfd, connfd;
     int i;

     ; i < number; i++)
     {
         newfd = events[i].data.fd;
         if (newfd == sockfd)
         {
             printf("=================================新一轮accept()===================================\n");
             printf("accept()开始...\n");

             /* 休眠3秒,模拟一个繁忙的服务器,不能立即处理accept连接 */
             printf("开始休眠3秒...\n");
             sleep();
             printf("休眠3秒结束!!!\n");

             client_addrlen = sizeof(client_addr);
             connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
             printf("connfd = %d\n", connfd);

             /* 注册已链接的socket到epoll,并设置是LT还是ET,是阻塞还是非阻塞 */
             addfd_to_epoll(epollfd, connfd, epoll_type, block_type);
             printf("accept()结束!!!\n");
         }
         else if (events[i].events & EPOLLIN)
         {
             /* 可读事件处理流程 */

             if (epoll_type == EPOLL_LT)
             {
                 printf("============================>水平触发开始...\n");
                 epoll_lt(newfd);
             }
             else if (epoll_type == EPOLL_ET)
             {
                 printf("============================>边缘触发开始...\n");

                 /* 带循环的ET模式 */
                 epoll_et_loop(newfd);

                 /* 不带循环的ET模式 */
                 //epoll_et_nonloop(newfd);
             }
         }
         else
             printf("其他事件发生...\n");
     }
 }

 /* 出错处理 */
 void err_exit(char *msg)
 {
     perror(msg);
     exit();
 }

 /* 创建socket */
 int create_socket(const char *ip, const int port_number)
 {
     struct sockaddr_in server_addr;
     ;

     memset(&server_addr, , sizeof(server_addr));
     server_addr.sin_family = AF_INET;
     server_addr.sin_port = htons(port_number);

     )
         err_exit("inet_pton() error");

     )) == -)
         err_exit("socket() error");

     /* 设置复用socket地址 */
     )
         err_exit("setsockopt() error");

     )
         err_exit("bind() error");

     ) == -)
         err_exit("listen() error");

     return sockfd;
 }

 /* main函数 */
 int main(int argc, const char *argv[])
 {
     )
     {
         fprintf(stderr, ]);
         exit();
     }

     int sockfd, epollfd, number;

     sockfd = create_socket(argv[], atoi(argv[]));
     struct epoll_event events[MAX_EPOLL_EVENTS];

     /* linux内核2.6.27版的新函数,和epoll_create(int size)一样的功能,并去掉了无用的size参数 */
     )) == -)
         err_exit("epoll_create1() error");

     /* 以下设置是针对监听的sockfd,当epoll_wait返回时,必定有事件发生,
      * 所以这里我们忽略罕见的情况外设置阻塞IO没意义,我们设置为非阻塞IO */

     /* sockfd:非阻塞的LT模式 */
     addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);

     /* sockfd:非阻塞的ET模式 */
     //addfd_to_epoll(epollfd, sockfd, EPOLL_ET, FD_NONBLOCK);

252
     )
     {
         number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -);
         )
             err_exit("epoll_wait() error");
         else
         {
             /* 以下的LT,ET,以及是否阻塞都是是针对accept()函数返回的文件描述符,即函数里面的connfd */

             /* connfd:阻塞的LT模式 */
             epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_BLOCK);

             /* connfd:非阻塞的LT模式 */
             //epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_NONBLOCK);

             /* connfd:阻塞的ET模式 */
             //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);

             /* connfd:非阻塞的ET模式 */
             //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_NONBLOCK);
         }
     }

     close(sockfd);
     ;
 }

四.验证                                                                

1.验证水平触发的非阻塞sockfd,关键代码在247行。编译运行

代码里面休眠了3秒,模拟繁忙服务器不能很快处理accept()请求。这里,我们开另一个终端快速用5个连接连到服务器:

我们再看看服务器的反映,可以看到5个终端连接都处理完成了,返回的新connfd依次为5,6,7,8,9:

上面测试完毕后,我们批量kill掉那5个客户端,方便后面的测试:

 $:..};do kill %$i;done

2.边缘触发的非阻塞sockfd,我们注释掉247行的代码,放开250行的代码。编译运行后,用同样的方法,快速创建5个客户端连接,或者测试5个后再测试10个。再看服务器的反映,5个客户端只处理了2个。说明高并发时,会出现客户端连接不上的问题:

3.水平触发的阻塞connfd,我们先把sockfd改回到水平触发,注释250行的代码,放开247行。重点代码在263行。

编译运行后,用一个客户端连接,并发送1-9这几个数:

再看服务器的反映,可以看到水平触发触发了2次。因为我们代码里面设置的缓冲区是5字节,处理代码一次接收不完,水平触发一直触发,直到数据全部读取完毕:

4.水平触发的非阻塞connfd。注释263行的代码,放开266行的代码。同上面那样测试,我们可以看到服务器反馈的消息跟上面测试一样。这里我就不再截图。

5.边缘触发的阻塞connfd,注释其他测试代码,放开269行的代码。先测试不带循环的ET模式(即不循环读取数据,跟水平触发读取一样),注释178行的代码,放开181行的代码。

编译运行后,开启一个客户端连接,并发送1-9这几个数字,再看看服务器的反映,可以看到边缘触发只触发了一次,只读取了5个字节:

我们继续在刚才的客户端发送一个字符a,告诉epoll_wait(),有新的可读事件发生:

再看看服务器,服务器又触发了一次新的边缘触发,并继续读取上次没读完的6789加一个回车符:

这个时候,如果继续在刚刚的客户端再发送一个a,客户端这个时候就会读取上次没读完的a加上次的回车符,2个字节,还剩3个字节的缓冲区就可以读取本次的a加本次的回车符共4个字节:

我们可以看到,阻塞的边缘触发,如果不一次性读取一个事件上的数据,会干扰下一个事件!!!

接下来,我们就一次性读取数据,即带循环的ET模式。注意:我们这里测试的还是边缘触发的阻塞connfd,只是换个读取数据的方式。

注释181行代码,放开178的代码。编译运行,依然用一个客户端连接,发送1-9。看看服务器,可以看到数据全部读取完毕:

细心的朋友肯定发现了问题,程序没有输出"带循环的ET处理结束",是因为程序一直卡在了88行的recv()函数上,因为是阻塞IO,如果没数据可读,它会一直等在那里,直到有数据可读。如果这个时候,用另一个客户端去连接,服务器不能受理这个新的客户端!!!

6.边缘触发的非阻塞connfd,不带循环的ET测试同上面一样,数据不会读取完。这里我们就只需要测试带循环的ET处理,即正规的边缘触发用法。注释其他测试代码,放开272行代码。编译运行,用一个客户端连接,并发送1-9。再观测服务器的反映,可以看到数据全部读取完毕,处理函数也退出了,因为非阻塞IO如果没有数据可读时,会立即返回,并设置error,这里我们根据EAGAIN和EWOULDBLOCK来判断数据全部读取完毕了,可以退出循环了:

这个时候,我们用另一个客户端去连接,服务器依然可以正常接收请求:

五.总结                                                                

1.对于监听的sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。

2.对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。

3.对于读写的connfd,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。

实例浅析epoll的水平触发和边缘触发,以及边缘触发为什么要使用非阻塞IO的更多相关文章

  1. epoll的水平触发和边缘触发,以及边缘触发为什么要使用非阻塞IO

    转自:http://www.cnblogs.com/yuuyuu/p/5103744.html 一.基本概念                                               ...

  2. linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO(转载)

      IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file ...

  3. 转一贴,今天实在写累了,也看累了--【Python异步非阻塞IO多路复用Select/Poll/Epoll使用】

    下面这篇,原理理解了, 再结合 这一周来的心得体会,整个框架就差不多了... http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架, ...

  4. Python异步非阻塞IO多路复用Select/Poll/Epoll使用,线程,进程,协程

    1.使用select模拟socketserver伪并发处理客户端请求,代码如下: import socket import select sk = socket.socket() sk.bind((' ...

  5. Linux非阻塞IO(八)使用epoll重新实现非阻塞的回射服务器

    本文无太多内容,主要是几个前面提到过的注意点: 一是epoll的fd需要重新装填.我们将tcp_connection_t的指针保存在数组中,所以我们以这个数组为依据,重新装填fd的监听事件. //重新 ...

  6. Linux非阻塞IO(七)使用epoll重新实现客户端

    使用poll与epoll的区别主要在于: poll可以每次重新装填fd数组,但是epoll的fd是一开始就加入了,不可能每次都重新加入 于是采用这种策略: epoll除了listenfd一开始就监听r ...

  7. epoll水平/边缘触发模式下阻塞/非阻塞EPOLLOUT事件触发条件及次数

    在IO多路复用技术中,epoll默认的事件触发模式为Level_triggered(水平触发)模式,即当被监控的文件描述符上有可读/写事件发生时,epoll_wait()会通知处理程序去读写.如果这次 ...

  8. socket阻塞与非阻塞,同步与异步、I/O模型,select与poll、epoll比较

    1. 概念理解 在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式: 同步/异步主要针对C端: 同步:      所谓同步,就 ...

  9. 关于同步,异步,阻塞,非阻塞,IOCP/epoll,select/poll,AIO ,NIO ,BIO的总结

    相关资料 IO基本概念 Linux环境 同步异步阻塞非阻塞 同步与异步 阻塞与非阻塞 IO模型Reference Link 阻塞IO模型 非阻塞IO模型 IO复用模型 信号驱动异步IO模型 异步IO模 ...

随机推荐

  1. Jconsole远程监控tomcat 的JVM内存(linux、windows)

    Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到.它用于连接正在运行的本地或者远程的JVM,对运行在java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界 ...

  2. Spring4学习笔记 - Bean的生命周期

    1 Spring IOC 容器对 Bean 的生命周期进行管理的过程: 1)通过构造器或工厂方法创建 Bean 实例 2)为 Bean 的属性设置值和对其他 Bean 的引用 3)调用 Bean 的初 ...

  3. MP4和HR-HDTV压制教程

    写在前 几年前还没工作的时候,长期混迹于百度“恐怖片吧”和“电锯惊魂吧”.因喜欢看电影,也自学了RMVB内嵌字幕的压制. 偶然机会加入@谢耳朵字幕组,因RMVB过于陈旧,人人影视所有美剧也全面抛弃了R ...

  4. HTML <fieldset> 标签将表单内的相关元素分组

    <fieldset> 标签将表单内容的一部分打包,生成一组相关表单的字段. 当一组表单元素放到 <fieldset> 标签内时,浏览器会以特殊方式来显示它们,它们可能有特殊的边 ...

  5. SharePoint 2013 为站点配置基于主机标头的双域名

    SharePoint的应用中,经常需要配置双域名,为不同的认证方式提供访问入口,下面简单介绍下,如何以主机标头的方式为SharePoint配置双域名: 配置基于主机标头的双域名 1.原本可以访问的测试 ...

  6. 通过API找出Autodesk Vault中某个用户组可以访问的Vault

    首先在Vault Explorer中可以这样查看和更改某个用户组有权访问的vault Tools –> Administration –> Global Settings –> Gr ...

  7. 高性能JS笔记4——算法和流程控制

    一.循环 for.while.do while三种循环的性能都没有多大区别.foreach 的性能较其他三种差 . 既然循环没有多大区别,注意循环内的代码控制. 减少迭代次数. 减少迭代工作量. 推荐 ...

  8. 兼容Android的水波纹效果

    Android的水波纹效果只有高版本才有,我们希望自己的应用在低版本用低版本的阴影,高版本用水波纹,这怎么做呢?其实,只要分drawable和drawablev21两个文件夹就好了. 普通情况下的se ...

  9. 颜色线性渐变-CAGradientLayer

    我们先来看一下效果图吧: 其实,就是一个颜色的线性渐变,使用CAGradientLayer很容易就能实现.由于代码很简单,就不做过多讲解了,直接看代码吧. import UIKit class Vie ...

  10. iOS指南针

    前言: 这个小项目使用到了CoreLocation框架里面的设备朝向功能,对CoreLocation感兴趣的可以翻一下之前的文章 在另一个博客站有朋友发现一个尴尬的问题(图片的东西2个方向是不对的), ...