I/O复用场景:

  • 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。
  • 一个客户同时处理多个套接字。
  • 一个TCP服务器既要处理监听套接字,又要处理已连接套接字。
  • 一个服务器既要处理TCP,又要处理UDP。
  • 一个服务器要处理多个服务或者多个协议。

Unix下的五种可用I/O模型的基本区别:

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O复用(select和poll)
  • 信号驱动式I/O(SIGIO)
  • 异步I/O(POSIX的aio_系列函数)

阻塞式I/O模型

非阻塞I/O模型

I/O复用模型

信号驱动式I/O模型

异步I/O模型

各种I/O模型对比

同步I/O和异步I/O对比

  • 同步I/O操作导致请求进程阻塞,直到I/O操作完成;
  • 异步I/O操作不导致请求进程阻塞。

根据上述定义,前四种I/O模型为同步I/O模型,因为真正的I/O操作将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。

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
struct timeval{long tv_sec;
long tv_usec;
}
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd,fd_set *fdset);
void FD_CLR(int fd,fd_set *fdset);
int FD_ISSET(int fd,fd_set *fdset);

最后一个参数有三种可能:
(1)永远等待:尽在有一个描述符准备好I/O时才返回。为此,把参数设为NULL。
(2)等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指定的时间数。
(3)根本不等待:检查描述符后立即返回,这称为轮询。为此,该参数的定时器值指定的秒和微秒数必须为0。

支持的异常条件:
(1)某个套接字带外数据的到达。
(2)某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。

头文件<sys/select.h>中定义的FD_SETSIZE常值是数据类型fd_set中的描述符总数,其值通常是1024。
描述符集内任何与未就绪描述符对应的位返回时均清0。为此,每次调用select函数时,都得再次把所有描述符集内所关心的位均置1。

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

(2)下列四个条件中的任何一个满足时,一个套接字准备好写。
a)该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前们把这样的套接字设置成非阻塞(第16章),写操作将不阻塞并返回一个正值的字节数)。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对UDP套接字而言,其默认值通常为2048.
b)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号(5.12节)。
c)使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
d)其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1 (也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
(3)如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件特处理。

当某个套接字发生错误时,它将由select标记为即可写又可读。

select的缺点:
(1)每次调用select 函数时,都需要把fd集合从用户态复制到内核态,这个开销在fd较多时会很大,同时每次调用select函数都需要在内核中遍历传递进来的所有fd,这个开销在fd较多时也很大。
(2)单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过先修改宏定义然后重新编译内核来调整这一限制,但这样非常麻烦而且效率低下。
(3)select函数在每次调用之前都要对传人的参数进行重新设定,这样做也比较麻烦。
(4)在Linux上,select函数的实现原理是其底层使用了poll函数。

shutdown函数

终止网络连接的通常方法是调用close函数。不过close有两个限制,却可以使用shutdowm来避免。

(1)close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。我们已在4.8节讨论过这一点。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列(图2-5中由FIN开

(2) close终止读和写两个方向的数据传送。既然TCP连接是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。这就是我们在前一节中遇到的str _cli函数在批量输入时的情况。图6- 12展示了这样的情况下典型的函数调用。

SHUT_ RD 关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对对TCP套接字这样调用shutdown的数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。

SHUT_WR 关闭连接的写这一半——对 于TCP套接字,这称为半关闭(half-close, 见TCPv1的18.5节)。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。我们已经说过,不管套接字描述符的引用计数是否等于0,这样的写半部关闭照样执行。进程不能再对这样的套接字调用任何写函数。

SHUT_RDWR 连接的读半部和写半部都关闭——这与调用shutdown两次等效;第一次调用指定SHUT_RD,第二次调用指定SHUT_WR。

close的c’z取决于SO_LINGER套接字选项的值。

pselect函数

#include<sys/select.h>
#include<signal.h>
#include<time.h>
int pselect(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timespec*timeout,const sigset_t *sigmask);返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1struct timespec{time_t tv_sec;
long tv_nsec;
}

pselect函数增加第六个参数:一个指向信号掩码的指针,该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止的信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。

poll函数

#include<poll.h>
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
struct pollfd{int fd;
short events;
short revents;
};

结构数组的个数由nfds指定

  • 所有正规TCP数据和所有UDP数据都被认为是普通数据。
  • TCP的带外数据(第24章)被认为是优先级带数据。
  • 当TCP连接的读半部关闭时(譬如收到了一个来自对端的FIN),也被认为是普通数据,随后的读操作将返回0。
  • TCP连接存在错误既可认为是普通数据,也可认为是错误(POLLERR)。无论哪种情况,随后的读操作将返回-1,并把errno设置成合适的值。这可用于处理诸如接收到RST或发生超时等条件。
  • 在监听套接字上有新的连接可用既可认为是普通数据,也可认为是优先级数据。大多数实现视之为普通数据。
  • 非阻塞式connect的完成被认为是使相应套接字可写。
    poll相比于select的优点:
    (1) poll不要求开发者计算最大文件描述符加1的大小;
    (2)与select相比,poll在处理大数量的文件描述符时速度更快;
    (3) poll没有最大连接数的限制,因为其存储fd的数组没有长度限制;
    (4)在调用poll函数时,只需对参数进行一次设置就好了。
    poll的缺点:
    (1)在调用poll函数时,不管有没有意义,大量fd的数组在用户态和内核地址空间之间被整体复制。
    (2)与select函数一样,poll函数返回后,需要遍历fd集合来获取就绪的fd,这样会使性能下降;
    (3)同时连接的大量客户端在某一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll

要使用epoll模式,则必须先创建一个epollfd,需要用到epoll_create函数:

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

参数size从Linux2.6.8以后就不再使用了,但是必须为它设置一个大于0的值。若epoll_create函数调用成功,则返回一个非负值的epollfd,否则返回-1。
有了epollfd之后,我们需要将检测事件的其他fd绑定到这个epollfd上,或者修改一个板顶上去的fd的事件类型,或者在不需要时将fd从epollfd上解绑,这都可以使用epoll_ctl函数完成:

int epoll_ctl(int epfd, int op, int fd,struct epoll_event* event);

对其中的参数说明如下:
(1)epfd:即上面的epollfd
(2)op:操作类型,取值有EPOLL_CTL_ADD、EPOLL_CTL_MOD和EPOLL_CTL_DEL,分别表示在epollfd上添加、修改和移除fd,当取值是EPOLL_CTL_DEL时,第四个参数event忽略不计,可以设置为NULL;
(3)fd:即要操作的fd。
(4)event:这是一个epoll_event结构体的地址。epoll_event结构体定义如下:

struct epoll_event
{uint32_t events;/* 需要检测的fd事件标志*/epoll_data_t data;/* 用户自定义的数据*/
}

epoll_event结构体的data字段类型是epoll_data_t,我们可以利用这个字段设置一个自定义数据,它在本质上是一个Union对象,在64位操作系统中大小是8字节,定义如下:

typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
}epoll_data_t;

(5)函数返回值:epoll_ctl若调用成功返回0,失败则返回-1;可以通过errno错误码获取具体的错误原因。
创建epollfd,设置好某个fd需要检测的事件并将该fd绑定到epollfd上,就可以调用epoll_wait检测事件了。

int epoll_wait(int epfd, struct epoll_event* events,int maxevents,int timeout);返回值:成功则返回有事件的fd数量;若返回0,表示超时,失败则返回-1;

参数event是一个epoll_event结构数组的首地址,它是一个输出参数,在函数调用成功后,在event中存放的是与就绪事件相关的epoll_event结构体数组;参数maxevents是数组元素的个数;timeout是超时时间,单位为0毫秒,如果将其设置为0,则epoll_wait会立即返回。

通过对poll与epoll_wait 雨数的介绍可以发现:我们在epol_wait函数调用完成后,通过参数event拿到所有有事件就绪的fd(参数event仅仅是个输出参数);而poll事件集合参数( poll 函数的第1个参数)在调用前后数量都不会改变,只不过调用通过pollfd结构体的events字段设置待检测的事件,调用后通过pollfd结构体的revents字段检测就绪的事件(参数fds既是入参也是出参)。
poll 函数的效率不一定不如epoll_wait 函数,一般在fd数量比较多但就某段时间内就绪事件fd数量较少的情况下,epoll_wail 函数才会体现它的优势,也就是说socket连接数量较大而活跃的连接较少时,epoll 模型更高效。

与pol模式的事件宏相比,epoll模式新增了一个事件宏EPOLLET.即边缘触发模式(Edge Tgger, ET), 我们称默认的模式为水平触发模式(Level Tigger LT)。这两种模式的区别在于:

(1)对于水平触发模式,一个事件只要有,就会一直触发;
(2)对于边缘触发模式,在一个事件从无到有时才会触发。

这两个词汇来自电学术语,我们可以将fd上有数据的状态认为是高电平状态,将没有数据的状态认为是低电平状态,将fd可写状态认为是高电平状态,将fd不可写状态认为是低电平状态。那么水平模式的触发条件是处于高电平状态,而边缘模式的触发条件是新来的一次电信号将当前状态变为高电平状态。

水平模式的触发条件:①低电平→高电平;②处于高电平状态。
边缘模式的触发条件:低电平→高电平。

socket可读事件的水平模式触发条件:①socket上无数据—>socket 上有数据; ②sock处于有数据状态。

socket可读事件的边缘模式触发条件:①socket上无数据—>socket 上有数据;②sock又新来一次数据。

socket可写事件的水平模式触发条件:①socket可写—>socket 不可写;②socket 不可写一>socket可写。

socket可写事件的边缘模式触发条件: socket 不可写一>socket 可写。

也就是说,对于一个非阻塞socket,如果使用epoll边缘模式检测数据是否可读,触发可读事件后,一定要一次性地把socket上的数据收取干净。也就是说,一定要循调用recv函数直到recv出错,错误码是EWOULDBLOCK ( EAGAIN也一样,此时表示socket上的本次数据已经读完);如果使用水平模式,则我们可以根据业务一次性地收固定的字节数,或者到收完为止。

(1)在LT模式下,读事件触发后可以按需收取想要的字节数,不用把本次接收的数据收取干净(即不用循环到recv或者read 函数返回-1,错误码为EWOULDBLOCK或EAGAIN);在ET模式下,读事件时必须把数据收取干净,因为我们不一定再有机会收取数据了,即使有机会,也可能因为没有及时处理上次没读完的数据,造成客户端响应延迟。

(2)在LT模式下,不需要写事件时一定要及时移除,避免不必要地触发且浪费CPU资源;在ET模式下,写事件触发后,如果还需要下一次的写事件触发来驱动任务(例如分送上次剩余的数据),则我们需要继续注册一次检测可写事件。

(3)LT模式和ET模式各有优缺点,无所谓孰优孰劣。使用LT模式时,我们可以自由决定每次收取多少字节(对于普通socket)或何时接收连接(对于监听socket),但是可能会导致多次触发;使用ET模式时,我们必须每次都将数据接收完(对于普通socket)或立即调用accept接受连接(对于监听socket),其优点是触发次数少。

epoll模型的EPOLLONESHOT选项,如果某个socket注册了该标志,则其监听的事件(例如EPOLLIN)在触发一次后再也不会触发,除非重新注册监听该事件类型。
在一些特殊的应用场景中,如果涉及多个线程同时处理某个socket 上的事件,则为了避免数据乱序,我们不得不使用复杂的多线程同步机制;但是有了EPOLLONESHOT选项,我们就可以减少线程同步逻辑了。以EPOLLIN事件处理为例,多个线程同时从一个socket 上读数据,可以使某个线程先处理,在该线程处理完之后再重新给该socket器加读事件,这样读事件再次触发时,就可以被其他线程继续处理了。这种做法在本质上记是保证同一个时刻只有一个线程在处理某个socket上的事件。当然,多个线程同时操个二个socket本来就是一.种不好的表现,我们在实际开发时应该尽量避免。

I/O复用:select、poll和epoll函数相关推荐

  1. Linux下select, poll和epoll IO模型的详解

    http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll 介绍 Epoll 可是当前在 Linux 下开发大规模并发网络程序的热 ...

  2. Linux中select poll和epoll的区别

    首先给大家分享一个巨牛巨牛的人工智能教程,是我无意中发现的.教程不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵段子,像看小说一样,哈哈-我正在学习中,觉得太牛了,所以分享给大家!点这里可以跳转 ...

  3. select poll 与epoll模型的总结

    select()和poll() IO多路复用模型 select优点: 1.一次可以等待多个文件描述符,减少了平均等待时间 2.客户越来越多时,减轻了进程调度的压力(相较于多进程多线程服务器) sele ...

  4. 三分钟看Netty(3) select poll VS epoll

    2019独角兽企业重金招聘Python工程师标准>>> 前言 上一节主要讲述了BIO和NIO的区别.BIO每一步都是阻塞式的:NIO仅在select的时候阻塞,并且在获取到IO权限后 ...

  5. I/O复用的 select poll和epoll的简单实现

    http://www.cnblogs.com/wj9012/p/3876734.html 一个tcp的客户端服务器程序 服务器端不变,客户端通过I/O复用轮询键盘输入与socket输入(接收客户端的信 ...

  6. Linux IO复用:select、poll、epoll的理解与对比

    目录 IO多路复用 select系统调用 poll系统调用 Epoll *系统调用 Epoll vs select/poll 相关文章 Linux(实际上是Unix)的一个基本概念是Unix / Li ...

  7. Linux多线程编程----IO【select、poll、epoll】

    IO操作多   速度就下降 IO数据的 读和写 IO的完成 必须等到 读事件(如磁盘 拷贝  每次要从磁盘查找数据) 和 写事件 (允许写 如写太快 写满就要马上阻塞)的就绪 IO是否高效 :主要看一 ...

  8. Linux系统编程——I/O多路复用select、poll、epoll

    参考:https://segmentfault.com/a/1190000003063859 Linux下的I/O复用与epoll详解:https://www.cnblogs.com/lojunren ...

  9. 浅谈Python-IO多路复用(select、poll、epoll模式)

    1. 什么是IO多路复用 在传统socket通信中,存在两种基本的模式, 第一种是同步阻塞IO,其线程在遇到IO操作时会被挂起,直到数据从内核空间复制到用户空间才会停止,因为对CPython来说,很多 ...

最新文章

  1. 基于FPGA的几种排序算法总结
  2. jquery ajax异步调用
  3. 电脑声音太小如何增强_感觉手机音量太小了?教你这样设置,声音立马大上许多...
  4. POJ 3122 分披萨(二分查找)
  5. andoridstudio run图标是灰色两步解决
  6. 52.3. HAVING
  7. [APIO2013]机器人(斯坦纳树)
  8. ​让AI触类旁通93种语言:Facebook最新多语种句嵌入来了
  9. 微波雷达感应模块技术,实时智能检测人体存在,静止微小动静感知
  10. Q1营收利润大增,Take-Two如何掘金“次世代”?
  11. matlab仿真ppt,Matlab系列之Simulink仿真教程.ppt
  12. it培训机构包就业是啥套路?it培训骗局,it培训班学出来有用吗?
  13. 云控系统都支持哪些安卓手机装机步骤
  14. 【我的新颖社区社交产品架构构思设想】
  15. 【钉钉】通过钉钉机器人抓取群消息
  16. 不用运动快速有效减肥——红光光浴#大健康#红光光浴#红光#种光光学
  17. Windows下tracert命令
  18. 2021年材料员-通用基础(材料员)考试题库
  19. ae渲染存在偏移_(图文+视频)C4D+AE野教程:一起来制作一个MG方块动画吧
  20. 遥感影像云检测-云检测数据集信息及下载

热门文章

  1. lms算法的verilog实现_最小均方算法(LMS Algorithm)理论及DSP实现
  2. IPhoneApp发布:手机号速查
  3. Qt 制作安装程序(使用 binarycreator.exe)
  4. d313(d3131)
  5. 网络RJ45水晶头制作(图解)
  6. 举例说明一下常见的弱口令_幼儿语言表达弱,该怎么训练?
  7. CodeM2018美团 初赛A轮 题目二 下棋
  8. java zhs16gbk_JAVA-----乱码的处理 乱码的解决方法总结
  9. uniapp 小程序使用腾讯地图搜索位置地点,获取省、市、县地区码的方法
  10. AcWing 158. 项链