文章目录
IO复用
I/O (数据交换过程)
1. 背景
I/O即Input/Output,由于程序和运行时数据是在内存中驻留的,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要I/O接口
I/O编程中涉及到流,其是相对于内存而言的,所以,Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。
2. 概念
I/O操作就是在运行代码的过程中,可能需要对文件读写,即将文件输入到内存和将代码结果输出到外设(网络、磁盘)的过程。
3. 分类
- 网络I/O:通过网络进行数据的拉取和输出;
- 磁盘I/O:主要是对磁盘进行读写工作
五种I/O类型
1. 阻塞式I/O (同步I/O)
进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作(默认情况)
应用进程通过系统调用recvfrom接收数据,但由于内核还未准备好数据报,应用进程就会阻塞住,知道内核准备好数据报,recvfrom完成数据报复制工作,应用进程才能结束阻塞状态。
优点:
设备文件不可操作时,可进入休眠状态,将CPU资源让出;当设备文件可以操作的时候,就必须唤醒进程,一般在中断函数中完成唤醒工作。
缺点:
耗费时间,适合并发低,时效性要求低的情况。
2. 非阻塞式I/O(同步I/O)
应用进程与内核交互,目的未到达之前,不再一味地等待,而是直接返回;
通过轮询的方式,不停的去问内核数据有没有准备好。如果一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。
前三次调用recvfrom时,没有数据可返回,内核立即返回EWOULDBOLOCK错误;
第四次调用recvfrom时,已有一个数据准备好,将其从内核复制到应用进程缓冲区,recvfrom成功返回。
轮询(polling)
应用进程持续轮询内核,以查看某个操作是否就绪
缺点:
往往消耗大量CPU时间
3. I/O复用 (select和poll) (同步I/O)
通过调用select或poll,阻塞在这两个系统调用中的某一个上,而不是阻塞在真正的I/O系统调用上。
阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,调用recvfrom把所读数据报复制到应用进程缓存区。
多个进程I/O注册到同一个select,当用户进程调用select,select监听所有注册好的I/O
- 若所有的被监听I/O需要的数据未准备好,则阻塞
- 任意一个所需数据准备好后,select调用返回
- 用户进程通过recvfrom进行数据拷贝
优点:
可以等待多个描述符就绪
4. 信号驱动式I/O (SIGIO)(同步I/O)
-
开启套接字信号驱动I/O功能,通过sigaction系统调用安装一个信号处理函数,该系统函数立即返回,不阻塞;
-
数据报准备好后,内核为该进程产生一个SIGIO信号递交给进程;
-
可以在信号处理函数中调用recvfrom读取数据报,通知主循环数据已准备好待处理;
-
可以立即通知主循环,读取数据报
优点:
等待数据报到达期间,进程不被阻塞。主循环可以继续执行,等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
5. 异步I/O (POSIX的aio_系列函数)
用户进程告知内核启动某个操作,并由内核在整个操作中完成后通知用户进程
与信号I/O驱动的区别
- 信号驱动I/O是由内核通知我们何时可以启动一个I/O操作
- 异步I/O是由内核通知我们I/O操作何时完成
步骤
- 用户进程调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,告诉内核整个操作完成时如何通知我们,然后就立刻去做其他事情;
- 当内核受到aio_read后,会立即返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户空间,然后再通知进程本次I/O已完成
各种I/O模型的比较
同步I/O
导致请求进程阻塞,直到I/O操作完成;
四种同步模型的区别在于:第一阶段等待数据的处理方式不同,第二阶段均为将数据从内核空间复制到用户空间缓冲区期间,进程阻塞于recvfrom调用。
异步I/O
不导致请求进程阻塞
I/O 复用函数
多路复用接口select/poll/epoll,内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件:
(1)select—>两次遍历 + 两次拷贝
- 把已连接的socket放在一个文件描述符集合,调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生;
- 通过遍历,有事件产生就把此socket标记为可读/可写,然后再整个拷贝回用户态;
- 用户态还需要遍历找到刚刚标记的socket。
(2)poll
动态数组,以链表形式来组织,相比于select,没有文件描述符个数这个限制,当然还会受到系统文件描述符限制。
(3)epoll(event poll)—>红黑树
- 在内核里使用红黑树来个弄脏进程所有待检测的文件描述字
- 调用epoll_ctl()函数,把需要监控的socket加入内核中的红黑树里;(红黑树的增删查时间复杂度是 O ( l o g n ) O(logn) O(logn),不需要每次操作都传入整个集合,只需要传入一个待检测的socket,减少了内核和用户空间的大量数据拷贝和内存分配)
- epoll使用事件驱动的机制,内核里维护了一个链表来记录就绪事件(当某个socket有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中)
- 当用户调用epoll_waiit()函数时,只会返回有事件发生的文件描述符的个数,不需要像select/poll那样轮询扫描整个socket集合,大大提高了检测的效率。
(4)epoll触发机制
-
epoll支持的时间触发模式:边缘触发ET; 水平触发LT
-
边缘触发模式ET
当被监控的socket描述符上有可读事件发生时,服务器只会从epoll_wait中苏醒一次,即使经常没有调用read函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读完,只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
-
水平触发模式LT
当被监控的socket上有可读事件发生时,服务器不断地从epoll_wait中苏醒,直到内核缓冲区数据被read函数读完才结束,目的是告诉我们有数据,只要满足事件的条件,比如内核中有数据需要读取,就一直不断地把这个事件传递给用户。
(5)select和poll的区别
- select 和 poll 采⽤轮询的⽅式检查就绪事件,每次都要扫描整个⽂件描述符,复杂度O(N);
- epoll 采⽤回调⽅式检查就绪事件,只会返回有事件发⽣的⽂件描述符的个数,复杂度O(1);
- select 只⼯作在低效的LT模式, epoll 可以在 ET ⾼效模式⼯作;
- epoll 是 Linux 所特有,⽽ select 则应该是 POSIX 所规定,⼀般操作系统均有实现;
- select 单个进程可监视的fd数量有限,即能监听端⼝的⼤⼩有限, 64位是2048; epoll 没有最⼤并发连接的限制,能打开的 fd 的上限远⼤于2048(1G的内存上能监听约10万个端⼝) ;
- select:内核需要将消息传递到⽤户空间,都需要内核拷⻉动作; epoll通过内核和⽤户空间共享⼀块内存来实现的。