阻塞I/O
read/write
非阻塞I/O
read/write(O_NONBLOCK)
非阻塞I/O一般和轮询一起使用,轮询的过程中会造成CPU资源的浪费
多路复用I/O
select/poll/epoll
多路复用I/O的优势是它可以同时处理多个连接,如果处理的连接数不是很高的话,使用多路复用I/O不一定比使用多线程 + 阻塞I/O的性能更好,可能延迟还更大。
信号I/O
SIGIO
等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
异步I/O
aio_read/aio_write
告知内核启动某个操作,并让内核在整个操作(包括将内核复制到我们自己的缓冲区)完成后通知我们。
总结
阻塞I/O和非阻塞I/O的区别 调用阻塞I/O会一直阻塞住对应的进程直到操作完成,而非阻塞I/O在内核还准备数据的情况下会立刻返回。 同步I/O和异步I/O的区别 同步I/O做I/O操作(包括将内核复制到我们自己的缓冲区)的时候会将进程阻塞。
select/poll/epoll
select
select实现
- 使用copy_from_user从用户空间拷贝fd_set到内核空间
- 注册回调函数__pollwait
- 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
- 以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
- __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
- poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
- 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
- 把fd_set从内核空间拷贝到用户空间。
select缺点
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
select有点
- 可移植性好,支持全平台
- 超时时间支持微秒级,适合实时性要求高的场景
poll
poll和select的实现方式差不多,只是描述fd的集合方式不同,poll使用pollfd结构而不是select的fd_set结构,其他都差不多。
poll特点
- poll没有文件描述符数量的限制
- poll仅支持一些平台。
epoll
epoll实现
select和poll只提供了1个函数,而epoll提供了3个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
- 每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
- epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表,epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。
- epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048。
epoll特点
- 只能运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
- 需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
- 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。