一、I/O复用的特点

  • 能同时监听多个文件描述符
  • 自身是阻塞的
  • 当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符

由于其第三个特点,所以服务器程序看起来仍像是串行工作的,如果要实现并发,只能使用多进程或多线程等编程手段。

二、select系统调用

/* 进程阻塞在select调用上监听多个事件,并在事件就绪或超时后返回 */
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); /* 参数说明 */
// nfds:被监听的文件描述符的总数
// readfds:指向可读事件对应的文件描述符集合
// writefds:指向可写事件对应的文件描述符集合
// exceptfds:指向异常事件对应的文件描述符集合
// timeout:select函数的超时时间

1. 功能:在一段指定时间内,监听用户感兴趣的那些文件描述符上的可读、可写和异常事件。

2. nfds参数:select函数监听的文件描述符的总数

假设在我们感兴趣的文件描述符集合中,文件描述符的最大值为maxfd,那么select函数监听的文件描述符总数nfds = maxfd + 1,因为文件描述符是从0开始计数的。这意味着,select函数监听的文件描述符的个数nfds可能超过我们感兴趣的文件描述符的个数。

{我们感兴趣的文件描述符} ⊆ {select函数监听的文件描述符}

3. 中间三个指针参数:分别指向可读、可写和异常事件对应的文件描述符集合

  • 调用select函数时,通过这三个参数传入我们感兴趣的文件描述符
  • select函数返回时,内核将修改它们所指向的文件描述符集合来通知哪些文件描述符已经就绪

当select函数返回时,那些未就绪的文件描述符对应的位均被置零。所以,每次重新调用select函数时,这三个参数指向的文件描述符集均需重新置位。

4. timeout参数:select函数的超时时间

内核将修改它以告诉应用程序select等待了多久。不过一般把它设置为NULL,让select一直阻塞到有事件就绪。

5. fd_set结构体

struct fd_set {
int fds_bits[FD_SETSIZE / 32]; // 因为int类型占32位,故该数组的总位数等于FD_SETSIZE
};

fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位标记一个文件描述符。

故fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select函数能同时处理的文件描述符的总量。  

/* 访问fd_set结构体中的位 */
FD_ZERO(fd_set *fdset); // 清除fdset的所有位
FD_SET(int fd, fd_set *fdset); // 设置fdset的位fd
FD_CLR(int fd, fd_set *fdset); // 清除fdset的位fd
int FD_ISSET(int fd, fd_set *fdset); // 测试fdset的位fd是否被设置

6. select函数可被信号中断,此时select立即返回-1,并设置errno为EINTR。

7. select函数的缺点

①监听的文件描述符的数目太多。比如我感兴趣的两个文件描述符的值分别为1和711,这种情况下,select函数将监听712个文件描述符。

②一次监听的文件描述符的总量受FD_SETSIZE的限制。

③每次重新调用select函数时,总要重新设置感兴趣的文件描述符。

④当select成功返回后,我们需要遍历所有我们感兴趣的文件描述符及对应的事件,以找出就绪的文件描述符及对应的事件。

⑤采用轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序。  

三、poll系统调用

/* 在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout); /* 参数说明 */
// fds:指定所有我们感兴趣的文件描述符上发生的可读、可写和异常事件
// nfds:被监听事件集合fds的大小
// timeout:poll函数的超时时间

1. 功能:与select类似,poll也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。与select不同,poll不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。

2. pollfd结构体

struct pollfd {
int fd; // 文件描述符
short events; // 注册的事件
short revents; // 实际发生的事件,由内核填充
};

events成员:告诉poll监听fd上的哪些事件,它是一系列事件的按位或  

revents成员:由内核修改,以通知应用程序fd上实际发生了哪些事件

poll函数没有改变events成员。这与select函数不同,select函数修改其参数以指示哪些描述符上相应的事件已就绪。

3. poll事件类型

事件 是否能够输入至events 是否能够从revents输出 说明
POLLIN 数据可读(除高优先级数据,相当于POLLRNORM|POLLRDBAND)
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读(Linux不支持)
POLLPRI 高优先级数据可读,如TCP带外数据
POLLOUT 数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLRDHUP TCP连接被对方关闭或对方关闭了写操作
POLLERR   错误
POLLHUP   挂起,如管道的写端被关闭后,读端描述符将收到此事件
POLLNVAL   文件描述符没有打开
  • 在Linux内核2.6.17之前,还没有POLLRDHUP事件,故当时为了区分socket上接收到的是有效数据还是对方关闭连接的请求,需要参考recv调用的返回值。
  • 文件尾端与挂起并无联系,如我们正从终端输入数据,并键入文件结束符,那么revents中的POLLIN就会打开,而POLLHUP则不会打开。
  • 当一个文件描述符被挂起后,就不能再写该描述符,但是有可能仍然可以从该描述符读取到数据。

4. poll函数的缺点

①当poll成功返回后,我们需要遍历所有我们注册的事件(包括就绪的和未就绪的),以找出就绪的文件描述符及对应的事件。

②采用轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序。

相对于select,poll的优势之处:

  • 内核每次修改的是revents成员,而events成员保持不变,因此下次调用poll时无需重置pollfd类型的事件集参数。
  • 用nfds参数指定最多监听多少个文件描述符和事件,该数值能达到系统允许打开的最大文件描述符数目。

四、epoll系列系统调用

1. 内核事件表:epoll把用户感兴趣的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集

2. epoll需要使用一个额外的文件描述符来唯一标识上述的内核事件表

/* 创建一个用于标识内核事件表的文件描述符 */
int epoll_create(int size); /* 参数说明 */
// size:内核事件表的大小,现在并不起作用

补:epoll_create返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

3. 操作epoll的内核事件表

/* 控制往内核事件表中添加、修改、删除事件 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /* 参数说明 */
// op:指定操作类型,包含EPOLL_CTL_ADD、EPOLL_CTL_MOD和EPOLL_CTL_DEL
// fd:要操作的文件描述符
// event:指定事件
  • EPOLL_CTL_ADD:往事件表中注册fd上的事件
  • EPOLL_CTL_MOD:修改fd上的注册事件
  • EPOLL_CTL_DEL:删除fd上的注册事件

4. epoll_event结构体

struct epoll_event {
__uint32_t events; // epoll事件
epoll_data_t data; // 用户数据
}; union epoll_data_t {
void *ptr; // 指定与fd相关的用户数据
int fd; // 指定事件所从属的目标文件描述符
uint32_t u32;
uint64_t u64;
};

epoll支持的事件类型和poll基本相同,只是多了两个额外的事件类型——EPOLLET和EPOLLONESHOT,它们对于epoll的高效运作非常关键。

表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是EPOLLIN。

5. epoll_wait:如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中

/* 在指定时间内等待一组文件描述符上的事件 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); /* 参数说明 */
// events:指向由epoll_wait检测到的就绪事件
// maxevents:最多监听多少个事件,它必须大于0
// timeout:epoll_wait函数的超时时间

events指向的结构数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。

这极大地提高了应用程序索引就绪文件描述符的效率。

6. 示例——poll和epoll在索引就绪文件描述符上的差别

/* 索引poll返回的就绪文件描述符 */
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
// 必须遍历所有已注册文件描述符并找到其中的就绪者
forr(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(epfd, events, MAX_EVENT_NUMBER, -1);
// 仅遍历就绪的ret个文件描述符
for(int i = 0; i < ret; ++i) {
int sockfd = events[i].data.fd;
/* ...处理sockfd... */
}

补:epoll_wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到O(1)。

7. LT模式 & ET模式——epoll操作文件描述符的两种模式  

LT是默认的工作模式,ET则是epoll的高效工作模式。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。

①采用LT工作模式的文件描述符:

  • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。
  • 当应用程序下一次调用epoll_wait时,epoll_wait会再次向应用程序通告此事件,直到该事件被处理。

②采用ET工作模式的文件描述符:

  • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件。
  • 当应用程序下一次调用epoll_wait时,epoll_wait不会再次向应用程序通告此事件。
  • 可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

注:每个使用ET模式的文件描述符都应该是非阻塞的。否则,读或写操作将会因为没有后续的事件而一直处于阻塞状态。

8. EPOLLONESHOT事件

即使使用了ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引发一个问题。如一个进程(线程)在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个进程(线程)被唤醒来读取这些新的数据,于是出现了两个进程(线程)同时操作一个socket的局面。而我们期望的是一个socket连接在任一时刻都只被一个进程(线程)处理。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个进程(线程)在处理某个socket时,其他进程(线程)是不可能有机会操作该socket的。此外,注册了EPOLLONESHOT事件的socket一旦被某个进程(线程)处理完毕,就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作进程(线程)有机会继续处理这个socket。 

  

9. epoll的优点

epoll的优点 poll的缺点 select的缺点

epoll_wait直接从内核事件表中取得用户注册的事件,无须每次调用都传入用户感兴趣的事件

epoll_wait的events参数仅用于返回epoll_wait检测到的就绪事件

每次调用都需通过pollfd.events传入用户感兴趣的事件

pollfd数组既用于传入用户注册的事件,又用于输出内核检测到的就绪事件

每次调用都需通过3个文件描述符集传入用户感兴趣的事件

3个文件描述符集既用于传入用户注册的事件,又用于输出内核检测到的就绪事件

应用程序索引就绪文件描述符的时间复杂度为O(1) 应用程序索引就绪文件描述符的时间复杂度为O(n) 应用程序索引就绪文件描述符的时间复杂度为O(n)
采用回调方式来检测就绪事件,算法时间复杂度为O(1) 采用轮询方式来检测就绪事件,算法时间复杂度为O(n) 采用轮询方式来检测就绪事件,算法时间复杂度为O(n)
支持ET高效模式 只支持LT工作模式 只支持LT工作模式
允许监听的最大文件描述符数量可达到系统允许打开的最大文件描述符数目   允许监听的最大文件描述符数量受到限制
    每次重新调用都需重新设置用户感兴趣的事件
    监听的文件描述符数量远远超过用户感兴趣的文件描述符数量

五、余音绕梁

1. 三种I/O复用检测就绪事件的实现原理有何不同,epoll_wait函数是如何达到O(1)的时间效率的?

select和epoll采用轮询的方式,即扫描整个注册文件描述符集合,以找到其中就绪的文件描述符,然后将它们返回给用户程序。

epoll_wait则采用回调的方式,即内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机将该就绪事件队列中的内容拷贝到用户空间。因此,epoll_wait无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度是O(1)。

2. 关于红黑树的内核实现(红黑树 + 双向链表 + 回调函数)

参见博客:https://blog.csdn.net/russell_tao/article/details/7160071

7. I/O复用的更多相关文章

  1. 从I/O复用谈epoll为什么高效

    上一篇文章中,谈了一些网络编程的基本概念.在现实使用中,用的最多的就是I/O复用了,无非就是select,poll,epoll 很多人提到网络就说epoll,认为epoll效率是最高的.单纯的这么认为 ...

  2. if __name__== "__main__" 的意思(作用)python代码复用

    if __name__== "__main__" 的意思(作用)python代码复用 转自:大步's Blog  http://www.dabu.info/if-__-name__ ...

  3. Atitit.java c#.net php项目中的view复用(jsp,aspx,php的复用)

    Atitit.java c#.net php项目中的view复用(jsp,aspx,php的复用) 1.1. Keyword1 1.2. 前言1 2. Java项目使用.Net的aspx页面view1 ...

  4. SAP CRM 复用视图

    在设计任何视图或组件的时候,我们需要以可复用的方式来设计它.UI组件设计的主要目标即可复用. 例如:几乎每个事务都要处理合作伙伴(客户).如果我们想要在Web UI显示那些合作伙伴,需要设计一个视图. ...

  5. Andriod 自定义控件之创建可以复用的组合控件

    前面已学习了一种自定义控件的实现,是Andriod 自定义控件之音频条,还没学习的同学可以学习下,学习了的同学也要去温习下,一定要自己完全的掌握了,再继续学习,贪多嚼不烂可不是好的学习方法,我们争取学 ...

  6. Android 5.X新特性之RecyclerView基本解析及无限复用

    说到RecyclerView,相信大家都不陌生,它是我们经典级ListView的升级版,升级后的RecyclerView展现了极大的灵活性.同时内部直接封装了ViewHolder,不用我们自己定义Vi ...

  7. UITableView cell复用出错问题 页面滑动卡顿问题 & 各杂七杂八问题

    UITableView 的cell 复用机制节省了内存,但是有时对于多变的自定义cell,重用时会出现界面出错(例如复用出错,出现cell混乱重影).滑动卡顿等问题,这里只简单敲下几点复用出错时的解决 ...

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

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

  9. 第3月第27天 uitableviewcell复用

    1. 有需求cell一行放两个子view,也可以放3个.子view控件都是一样,只有大小区分.需要复用吗? 复用实现:创建时创建3个,拿到数据时是两个就设置两个的frame,是3个就设置3个的fram ...

  10. 前后端分离中,Gulp实现头尾等公共页面的复用

    前言 通常我们所做的一些页面,我们可以从设计图里面看出有一些地方是相同的.例如:头部,底部,侧边栏等等.如果前后端分离时,制作静态页面的同学,对于这些重复的部分只能够通过复制粘贴到新的页面来,如果页面 ...

随机推荐

  1. RandomAccessFile类——高效快捷地读写文件

    RandomAceessFile类 RandomAccessFile类是一个专门读写文件的类,封装了基本的IO流,在读写文件内容方面比常规IO流更方便.更灵活.但也仅限于读写文件,无法像IO流一样,可 ...

  2. vue 复习(2)v-bind的应用 v-bind:classv-binf:style

    dasdclass与style绑定v-bind 1. 绑定HTML Class 对象语法 有些时候我们想动态的切换class的类名.在原生的js或jq中我们就要通过事件来动态的改变class类名,但在 ...

  3. LL(1)文法--递归下降程序

    递归下降程序 递归下降程序一般是针对某一个文法的.而递归下降的预测分析是为每一个非终结符号写一个分析过程,由于文法本身是递归的,所以这些过程也是递归的. 以上是前提. Sample 假如给的是正规式子 ...

  4. Excel工作表保护的密码破解与清除...假装自己破解密码系列?

    有一次我女朋友让我帮忙解一个excel表格的保护密码,然后~用了宏 网上下载来的Excel经常会有工作表保护,也就是无法修改,妄图做任何修改的时候你就会看见这句话: 您试图更改的单元格或图表位于受保护 ...

  5. CentOS7.6离线安装Redis5.0.4

    安装gcc-c++: 检查是否存在gcc-c++:rpm -qa|grep gcc-c++ 如果不存在就下载Linux-GC-C++文件: 访问镜像网站:http://mirrors.aliyun.c ...

  6. echarts使用方法

    1.引入完整版echarts.min.js. 2.为ECharts准备一个具备大小(宽高)的Dom . <div id="main" style="width: 6 ...

  7. 记如何用树莓派3开一个无线AP

    开热点 忘掉自己手动配置吧 create_ap git clone git@github.com:oblique/create_ap.git cd create_ap sudo make instal ...

  8. string 模块

    string 说明:string 模块保留了很多有用的常量和类,用来处理 string 和 unicode 对象. 作用:包含处理文本的常量和类. capwords( ) capwords( ) 的作 ...

  9. 《Nginx高性能Web服务器》系列分享专栏

    <Nginx高性能Web服务器>系列分享专栏 [作者:Poechant] Nginx是目前最流行的基于BSD-like协议.轻量级.高性能的HTTP服务器.反向代理服务器和电子邮件(SMT ...

  10. (转载)Javascript异步编程的4种方法

    你可能知道,Javascript语言的执行环境是"单线程"(single thread). 所谓"单线程",就是指一次只能完成一件任务.如果有多个任务,就必须排 ...