I/O模型

268 阅读5分钟

阻塞I/O

read/write 阻塞I/O

非阻塞I/O

read/write(O_NONBLOCK) 非阻塞I/O一般和轮询一起使用,轮询的过程中会造成CPU资源的浪费 非阻塞I/O

多路复用I/O

select/poll/epoll 多路复用I/O的优势是它可以同时处理多个连接,如果处理的连接数不是很高的话,使用多路复用I/O不一定比使用多线程 + 阻塞I/O的性能更好,可能延迟还更大。 多路复用I/O

信号I/O

SIGIO 等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。 信号I/O

异步I/O

aio_read/aio_write 告知内核启动某个操作,并让内核在整个操作(包括将内核复制到我们自己的缓冲区)完成后通知我们。 异步I/O

总结

阻塞I/O和非阻塞I/O的区别 调用阻塞I/O会一直阻塞住对应的进程直到操作完成,而非阻塞I/O在内核还准备数据的情况下会立刻返回。 同步I/O和异步I/O的区别 同步I/O做I/O操作(包括将内核复制到我们自己的缓冲区)的时候会将进程阻塞。

select/poll/epoll

select

select实现

  1. 使用copy_from_user从用户空间拷贝fd_set到内核空间
  2. 注册回调函数__pollwait
  3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
  4. 以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
  5. __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
  6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  7. 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
  8. 把fd_set从内核空间拷贝到用户空间。

select缺点

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

select有点

  1. 可移植性好,支持全平台
  2. 超时时间支持微秒级,适合实时性要求高的场景

poll

poll和select的实现方式差不多,只是描述fd的集合方式不同,poll使用pollfd结构而不是select的fd_set结构,其他都差不多。

poll特点

  1. poll没有文件描述符数量的限制
  2. poll仅支持一些平台。

epoll

epoll实现

select和poll只提供了1个函数,而epoll提供了3个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

  1. 每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  2. epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表,epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。
  3. epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048。

epoll特点

  1. 只能运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
  2. 需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
  3. 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。