7. I/O复用
一、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复用的更多相关文章
- 从I/O复用谈epoll为什么高效
上一篇文章中,谈了一些网络编程的基本概念.在现实使用中,用的最多的就是I/O复用了,无非就是select,poll,epoll 很多人提到网络就说epoll,认为epoll效率是最高的.单纯的这么认为 ...
- if __name__== "__main__" 的意思(作用)python代码复用
if __name__== "__main__" 的意思(作用)python代码复用 转自:大步's Blog http://www.dabu.info/if-__-name__ ...
- 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 ...
- SAP CRM 复用视图
在设计任何视图或组件的时候,我们需要以可复用的方式来设计它.UI组件设计的主要目标即可复用. 例如:几乎每个事务都要处理合作伙伴(客户).如果我们想要在Web UI显示那些合作伙伴,需要设计一个视图. ...
- Andriod 自定义控件之创建可以复用的组合控件
前面已学习了一种自定义控件的实现,是Andriod 自定义控件之音频条,还没学习的同学可以学习下,学习了的同学也要去温习下,一定要自己完全的掌握了,再继续学习,贪多嚼不烂可不是好的学习方法,我们争取学 ...
- Android 5.X新特性之RecyclerView基本解析及无限复用
说到RecyclerView,相信大家都不陌生,它是我们经典级ListView的升级版,升级后的RecyclerView展现了极大的灵活性.同时内部直接封装了ViewHolder,不用我们自己定义Vi ...
- UITableView cell复用出错问题 页面滑动卡顿问题 & 各杂七杂八问题
UITableView 的cell 复用机制节省了内存,但是有时对于多变的自定义cell,重用时会出现界面出错(例如复用出错,出现cell混乱重影).滑动卡顿等问题,这里只简单敲下几点复用出错时的解决 ...
- Linux网络编程-IO复用技术
IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...
- 第3月第27天 uitableviewcell复用
1. 有需求cell一行放两个子view,也可以放3个.子view控件都是一样,只有大小区分.需要复用吗? 复用实现:创建时创建3个,拿到数据时是两个就设置两个的frame,是3个就设置3个的fram ...
- 前后端分离中,Gulp实现头尾等公共页面的复用
前言 通常我们所做的一些页面,我们可以从设计图里面看出有一些地方是相同的.例如:头部,底部,侧边栏等等.如果前后端分离时,制作静态页面的同学,对于这些重复的部分只能够通过复制粘贴到新的页面来,如果页面 ...
随机推荐
- list 去重复元素
public static List removeDuplicate(List list){ List listTemp = new ArrayList(); for(int i=0;i<lis ...
- CentOS 7 Minimal 安装JDK 1.8
真好最近比较闲,打算在linux 的CentOS 7 Minimal版本试着搭建hadoop环境学习学习,当然第一步就是在CentOS 7 Minimal 安装JDK 1.8环境.其实老早就打算了解一 ...
- 课时87. !important(掌握)
1.什么是important 作用:用于提升某个直接选中标签的选择器中的某个属性的优先级,可以将被指定的属性的优先级提升为最高. 注意点: 1.important只能用于直接选中,不能用于间接选中 p ...
- [示例] 用代码设置 ListView 颜色 (只适用 Win 平台,无需修改官方源码)
如果可以使用代码随意设置 ListView 的颜色,而不用加载额外的 Style 及修改官方的源码,那该有多好?! 其实 Style 提供了很强了扩充性及可塑性,可以很容易的去操作它. 下面以 Lis ...
- STM32使用FatFs
1.定义一些变量在我们代码开始的部分,先定义一些变量供我们使用.这里选择几个来解析一下.第一个FIL file;这个变量是文件的结构体变量,记录了我们打开的文件的信息.使用f_open等函数的时候都要 ...
- Python编程小坑
在Pycharm中编辑文本文件,如果使用\r\n会产生两个空行? exit("xxx),会导致Process finished with exit code 1,所以如果要输入信息然后退出, ...
- 20155223 2016-2017-2 《Java程序设计》第10周学习总结
20155223 2016-2017-2 <Java程序设计>第10周学习总结 教材学习内容总结 Java Socket编程 网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向 ...
- 20155322 2017-2018-1《信息安全系统设计》第九周 Linux命令:pwd命令学习与简单实现
pwd命令学习 功能 Linux中用 pwd 命令来查看"当前工作目录"的完整路径. 格式 pwd [选项] 实例 用 pwd 命令查看默认工作目录的完整路径: 用 pwd 命令查 ...
- 20155322 2016-2017-2 《Java程序设计》第9周学习总结
20155322 2016-2017-2 <Java程序设计>第9周学习总结 教材学习内容总结 第9周学习的主要内容是课本的第十六.第十七.第十八章,老师的教学指导上主要要求学习以下知识点 ...
- C语言复习20170821
函数 函数头部参数表里的变量称为形参,也是内部变量,只能在函数体内访问. 形参的作用是实现主调函数与被调函数之间的联系,通常将函数所处理的数据,影响函数功能的因素或者函数处理的结果作为形参.没有形参的 ...