apue高级IO
非阻塞 I/O
之前讨论的所有函数都是阻塞的,如read(2)
函数读取设备时,设备中如果没有充足的数据,那么read(2)
函数就会阻塞等待,直到有数据可读再返回。当 IO 操作时出错时,需要判断是真的出错还是假错。
两种假错的情况:
- EINTR:被信号打断,阻塞时会遇到。
- EAGAIN:非阻塞形式操作失败。
遇到这两种假错的时候我们需要重新再操作一次(占用CPU),所以通常对假错的判断是放在循环中的。例如read(2)
函数使用非阻塞方式读取数据时,如果没有读取到数据,errno
为EAGAIN
,表明是非阻塞方式读取返回了,并非设备有问题或读取失败。
阻塞与非阻塞是使用的同一套函数,flags
特殊要求指定为O_NONBLOCK
就可以了。
1 | fd = open("/etc/service", O_RDONLY | O_NONBLOCK); //以非阻塞方式打开 |
IO多路转接
IO密集型的任务,可以使用IO多路转接。select()
,poll()
,epoll()
先布置监视任务(设置自己关心/感兴趣的事件/文件描述符),监视多个IO(文件描述符),当某个文件描述符发生了我们感兴趣的事情(读/写)的时候再去操作(读/写)。
select(2)
移植性高,各个平台都支持它,这也是它相对于poll(2)
唯一的优点。
select(2)
关心的是事件,poll()
关心的是文件描述符。
select(2)
和poll()
都是可移植的,epoll()
是Linux平台实现的方言。epoll()
是利用poll()
封装的。
select
1 | //IO多路转接(IO复用) |
select(2)
可相当于一个安全可靠的休眠。某些平台的sleep()
用的是信号,不是很安全。
1 | ./io/adv/select/relay.c |
poll
1 | //等待某个事件在指定的文件描述符上发生 |
关心的事件和发生的事件是分开的两个成员,所以关心的事件发生一次后不用重新设置。
epoll
1 | //创建一个epoll文件描述符(创建数组) |
1 | //控制epoll文件描述符 |
1 | //往外取正在发生的事件 |
poll
是自己定义一个数组,每个元素就是一个文件描述符以及其关心和发生的事件,epoll
是内核帮你维护了这样一个数组。epoll_create()
就是创建这个数组,再用其他函数去访问/修改该数组。
select&poll&epoll
select()
缺点:
1.fd_set
使用数组实现,fd_size
有限制 1024bitmap
,fd[i] = accept()
2.fdset
不可重用,新的fd进来,重新创建
3.用户态和内核态拷贝bitmap
产生开销
4.O(n)
时间复杂度的轮询(发生事件后的轮询)
poll
基于结构体存储fd,解决了select
的缺点1,2
1 | struct pollfd{ |
epoll
:用户态和内核态不拷贝bitmap
;返回结果不需要轮询,时间复杂度为O(1)
,解决select
的缺点1,2,3,4
epoll_create 创建一个白板 存放fd_events
epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符
两种触发模式:
LT:水平触发
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。
下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,
因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。