I/O多路复用是在多线程或多进程编程中常用技术。主要是通过select/epoll/poll三个函数支持的。在此主要对select和epoll函数详细介绍。

select函数

  • 该函数运行进程指示内核等待多个事件中的任何一个发生,并只有一个或多个事件发生或经历一段指定的时间后才唤醒它。

  • 调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。我们感兴趣的描述符不局限于套接字,任何描述符都可以使用select来测试。

  • 函数原型:

    #include<sys/select.h>
    #include<sys/time.h> int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set
    *exceptset, const struct timeval *timeout);
    返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
    • 最后一个参数timeout,它告知内核等待所指定描述符中的任何一个就绪可花多长时间。该参数有三种可能:

      • 永远等待下去:仅在有一个描述符准备好I/O时才返回,将其设为空指针
      • 等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
      • 根本不等待:检查描述符后立即返回,这就是轮询。为此,该参数必须指向一个timeval结构,但是其中的值必须设置为0
    • 三个参数readset,writeset,exceptset指定我们要让内核测试读、写和异常条件的描述符。

    • 如何给这三个参数的每一个参数指定一个或多个描述符值是一个设计上的问题。select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。举例来说,假设使用32位整数,那么该数组的第一个元素对应于描述符031,第二个元素对应于描述符3263,以此类推。所有这些实现细节都与应用程序无关,它们隐藏在名为fd_set的数据类型和以下四个宏中:

      void FD_ZERO(fd_set *fdset);    //clear all bits in fdset
      void FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdset
      void FD_CLR(int fd, fd_set *fdset); //turn off the bit for fd in fdset
      int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset?

      我们分配一个fd_set数据类型的描述符集,并用这些宏设置或测试该集合中的每一位,也可以用C语言中的赋值语句把它赋值成另外一个描述符集。

      注意:前面所讨论的每个描述符占用整数数组中的一位的方法仅仅是select函数的可能实现之一。

    • maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加1。描述符0,1,2,...,直到maxfdp1 - 1均被测试。

    • select函数修改由指针readset,writeset和exceptset所指向的描述符集,因而这三个参数都是值-结果参数。该函数返回后,我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符所对应的位返回时均清成0.为此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1

select返回套接字的“就绪”条件

  • 满足下列四个条件之一的任何一个时,一个套接字准备好读:

    • 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对于这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们使用SO_RECVLOWAT套接字选项设置套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1
    • 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)
    • 该套接字时一个监听套接字且已完成的连接数不为0。
    • 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置为确切的错误条件。这些待处理错误也可以通过SO_ERROR套接字选项调用getsockopt获取并清除。
  • 下列四个条件的任何一个满足时,一个套接字准备好写:

    • 该套接字发送缓冲区中的可用字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞的,写操作将不阻塞并返回一个正值(如由传输层接收的字节数)。我们使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP而言,默认值为2048
    • 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号
    • 使用非阻塞式connect套接字已建立连接,或者connect已经已失败告终
    • 其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置为确切的错误条件。这些待处理错误也可以通过SO_ERROR套接字选项调用getsockopt获取并清除。
  • 如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。

  • 注意:当某个套接字上发生错误时,它将由select标记为既可读又可写

  • 接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select可读或可写条件之前有多少数据可读或有多大空间可用于写。

  • 任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(默认应该总是这种关系)就总是可写的,这是因为UDP套接字不需要连接。

poll函数

  • 函数原型:

    #include<poll.h>
    
    int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
    
        返回:若有就绪描述符则为数目,若超时则为0,若出错则为-1
  • 第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

    struct pollfd{
    int fd; //descriptor to check
    short event; //events of interest on fd
    short revents; //events that occurred on fd
    };

    要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。(每个描述符都有两个变量,一个为调用值,另一个为返回结果,从而避免使用值-结果参数。)

  • poll事件

epoll函数

  • epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大的差异。

    • 首先,epoll使用一组函数来完成任务,而不是单个函数。
    • 其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。
    • 但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表
  • epoll文件描述符使用如下方式创建:

    #include<sys/epoll.h>
    
    int epoll_create(int size);

    size参数完全不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

  • 下面的函数用来操作epoll的内核事件表:

    #include<sys/epoll.h>
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
        返回:若成功返回0,失败返回-1,并置errno

    fd参数是要操作的文件描述符,op参数则指定操作类型。操作类型有以下三类:

    • EPOLL_CTL_ADD, 往事件表中注册fd上的事件
    • EPOLL_CTL_MOD, 修改fd上的注册事件
    • EPOLL_CTL_DEL, 删除fd上的注册事件
  • event指定事件,它是epoll_event结构指针类型,epoll_event的定义如下:

    strcut epoll_event{
    __uint32_t events; //epoll事件
    epoll_data_t data; //用户数据
    };
    • 其中,events成员描述事件类型。epoll支持的事件类型同poll基本相同。表示epoll事件类型的宏在poll对应的宏前加上"E",比如epoll的数据可读事件是EPOLLIN。
    • epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。它们对于epoll的高效运作非常关键。
    • data成员用于存储用户数据,是一个联合体:
      typedef union epoll_data{
      void *ptr;
      int fd;
      uint32_t u32;
      uint64_t u64;
      }epoll_data_t;

      其中4个成员用得最多的是fd,它指定事件所从属的目标文件描述符。

  • epoll系列系统调用的主要接口是epoll_wait函数,它在一段超时时间内等待一组文件描述符上的事件,其原型如下:

    #include<sys/epoll.h>
    
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
        返回:若成功返回就绪的文件描述符个数,失败时返回-1,并置errnoo
  • maxevents参数指定最多监听多少个事件,它必须大于0

  • event_wait函数如果检测到事件,就将所有就绪事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。

  • 下面代码给出 poll和epoll在使用上的差别:

    //如何索引poll返回的就绪文件描述符
    int ret = poll(fds, MAX_EVENT_NUMBER, -1);
    //必须遍历所有已注册文件描述符并找到其中的就绪者
    for(int i = 0; i < MAX_EVENT_NUMBER; ++i){
    if(fds[i].revents & POLLIN) //判断第 i 个文件描述符是否就绪
    {
    int sockfd = fds[i].fd;
    //处理sockfd
    }
    } //如何索引epoll返回的文件描述符
    int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    //仅遍历就绪的ret个文件描述符
    for(int i = 0; i < ret; ++i){
    int sockfd = events[i].data.fd;
    //sockfd肯定就绪,直接处理
    }
  • LT和ET模式

    • LT(Level Trigger,电平触发)模式:是默认工作模式,在这种模式下的epoll相当于一个效率较高的poll。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件。
    • ET(Edge Trigger,边沿触发)模式。对于ET工作模式下的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
    • ET模式在很大程度上降低了同一个epoll事件被重复触发的次数。因此效率要比LT模式高。
    • 每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的时间而一直处于阻塞状态(饥渴状态)
  • EPOLLONESHOT事件

    • 即使使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中引起一个问题。比如一个线程(或进程)在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是出现了两个线程同时操作一个socket的场面。这当然不是我们期望的。我们期望的是一个socket连接在任一时刻都只被一个线程处理。
    • 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程时不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket.

网络编程:I/O复用的更多相关文章

  1. UNIX网络编程——I/O复用:select和poll函数

    我们看到TCP客户同时处理两个输入:标准输入和TCP套接字.我们遇到的问题是就在客户阻塞于(标准输入上)fgets调用,服务器进程会被杀死.服务器TCP虽然正确的给客户TCP发送了一个FIN,但是既然 ...

  2. 网络编程-I/O复用

    I/O模型 Unix下可用的I/O模型有五种: 阻塞式I/O 非阻塞式I/O I/O复用(select和poll.epoll) 信号驱动式I/O(SIGIO) 异步I/O(POSIX的aio_系列函数 ...

  3. 【Unix网络编程】chapter6IO复用

    chapter6  6.1 概述 I/O复用典型使用在下列网络应用场合. (1):当客户处理多个描述符时,必须使用IO复用 (2):一个客户同时处理多个套接字是可能的,不过不叫少见. (3):如果一个 ...

  4. socket网络编程-----I/O复用之poll函数

    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/soc ...

  5. socket网络编程-----I/O复用之select函数

    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/soc ...

  6. 网络编程——I/O复用

    int select( int nfds, fd_set FAR* readfds, fd_set * writefds, fd_set * exceptfds, const struct timev ...

  7. Linux网络编程——I/O复用之poll函数

    一.回顾前面的select select优点: 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点 select缺点: 1.每次调用 select(),都需要把 fd 集合从用户态拷贝到内核 ...

  8. Linux网络编程-IO复用技术

    IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...

  9. Linux网络编程——tcp并发服务器(poll实现)

    想详细彻底地了解poll或看懂下面的代码请参考<Linux网络编程——I/O复用之poll函数> 代码: #include <string.h> #include <st ...

  10. 【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数

    本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现 1. I/O复用技术 I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程.I/O ...

随机推荐

  1. java实现微信红包分配算法

    红包算法分析 有人认为,抢红包的额度是从0.01到剩余平均值*N(N是一个系数,决定最大的红包值)之间,比如一共发了10块钱,发了10个红包:第一个人可以拿到(0.01~1*N)之间的一个红包值,当然 ...

  2. 关于RestfulAPI与SpringMVC之间的传值

    ##默认读者已经搭建好的SpringMVC的环境,并且对SpringMVC有着一定的了解,并且已经对RestfulAPI有了一定的了解 1,首先将WEB-INF下的web.xml里面的url-part ...

  3. 异步编程的两种模型,闭包回调,和Lua的coroutine,到底哪一种消耗更大

    今天和人讨论了一下CPS变形为闭包回调(典型为C#和JS),以及Lua这种具有真正堆栈,可以yield和resume的coroutine,两种以同步的形式写异步处理逻辑的解决方案的优缺点.之后生出疑问 ...

  4. Android之利用正则表达式校验邮箱、手机号、密码、身份证号码等

    概述 现在Android应用在注册的时候基本会校验邮箱.手机号.密码.身份证号码其中一项或多项,特此收集了相关的正则表达式给大家分享.除了正则表达式,文章末尾提供Demo中有惊喜哦! 具体验证的图片效 ...

  5. svn检出服务器项目中出现的could not connect to server

    今天来新同事,新配的电脑装上SVN,Myeclipse后,检出服务器的项目老是出现文件不存在,could not connect to server.最后上网搜了下, 1.先用电脑ping服务器,看网 ...

  6. Mybatis基础学习(五)—缓存

    一.概述      mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能.           一级缓存是SqlSession级别的缓存.在操作数据库时需要 ...

  7. python 面向对象编程(二)

    在上一篇文章中谈到了类的基本定义和使用方法,这只体现了面向对象编程的三大特点之一:封装. 下面就来了解一下另外两大特征:继承和多态. 在Python中,如果需要的话,可以让一个类去继承一个类,被继承的 ...

  8. C#各个版本中的新增特性详解

    序言 自从2000年初期发布以来,c#编程语言不断的得到改进,使我们能够更加清晰的编写代码,也更加容易维护我们的代码,增强的功能已经从1.0搞到啦7.0甚至7.1,每一次改过都伴随着.NET Fram ...

  9. MySQL索引统计信息更新相关的参数

    MySQL统计信息相关的参数: 1. innodb_stats_on_metadata(是否自动更新统计信息),MySQL 5.7中默认为关闭状态 仅在统计信息配置为非持久化的时候生效. 也就是说在i ...

  10. Historical节点

    Historical节点 Historical 节点的作用是,load 历史数据提供查询. 运行类 io.druid.cli.Main server historical 装载和保存Segments ...