思考理解linux的io模型

218 阅读5分钟

我正在参加「掘金·启航计划」。 
想要更好的理解linux的IO模型,首先对于IO的整个过程有一个大概的认识。
IO,Input/Output。本质,是计算机的核心-cpu和内存 与外部设备进行数据交换的过程。比如 数据从磁盘写入内存,或内存的数据写回到磁盘等。
那么,如何交互来达到数据的交换呢?

  • 用户进程通过系统调用,用户态进入内核态。
    • 而划分内核空间和用户空间,是为了保护内核
  • 数据会先被操作系统拷贝到内核缓冲区,再从内核缓冲区拷贝到操作系统的地址空间。

截屏2022-11-27 16.16.27.png

阻塞和非阻塞

阻塞是指io操作需要彻底完成才返回用户空间,过程中用户进程(发起请求io的一方)一直处于阻塞状态。
非阻塞io是指io操作被调用后先返回一个状态值,不需要等io彻底完成。

截屏2022-11-27 16.16.57.png

阻塞io

当用户进程发起系统调用进入内核态,系统内核此时需要准备数据

  • 当数据还没到达时,系统内核需要等待足够的数据到来。此时用户进程处于阻塞状态。

当数据准备完成后,系统内核会将数据从内存空间拷贝到用户空间。

非阻塞io

当用户进程发起read操作,如果此时内核中的数据还没准备就绪,就会返回一个错误,而不是block用户进程。用户接收到错误,知道数据未准备就绪,会再次发送read操作。

多路复用io

多路复用也是事件驱动,有时也称异步阻塞io。
为了提高性能,新增一种系统调用,该系统调用可以检测IO文件描述符的状态。在linux中一般为epoll/poll函数。
通过该系统调用,一个用户进程可以监听多个文件描述符。当文件描述符准备就绪时(内核缓冲区数据准备就绪),就会向用户进程返回文件描述符的状态,用户进程根据所返回的文件描述符去进行相应的io调用。
发起新的io系统调用时,内核等待数据过程中可以立即返回,用户线程不会阻塞。
举例来说,发起一个多路复用io的read读操作的系统调用流程如下:

  • 选择器注册。将需要read操作的目标socket网络连接提前注册到选择器中,才可以开启整个io多路复用模型的轮训。
  • 用户线程发起io系统调用,内核此时数据尚未就绪,因此会立即返回。当用户线程的选择器中所注册的内核缓冲区数据准备就绪后,内核会将socket加入对应的就绪列表。select/epoll是采用轮询的方法来查找达到io操作就绪的socket连接。
  • 用户线程获取就绪列表后,根据其中的socket连接进行read。此时用户进程阻塞,内核复制数据从内核缓冲区到用户缓冲区。
  • 复制完成后,内核返回结果,用户结束阻塞。

select

io系统调用使用文件描述符来指代打开的文件。 socket是一种常见的文件类型,用来和另一个进程进行跨网络通信的文件。

select方法监听服务端socket是否可读,当所监听的所有socket都不可读,用户线程会阻塞,直到存在可读的socket,select唤醒用户线程。 对于select来说,所处理的fd_set是一个比特位结构(bitmap),每个比特位表示所监听的fd。使用一个bitmap数组来存储所有监听的fd。

  • select和poll大致相同,不同在于poll使用的底层结构是链表,而select使用的是数组。

缺点

  • 每次调用select(),都需要把文件描述符从用户态拷贝到内核态
    • 用户程序会把需要监听的文件描述符从用户空间拷贝到内核空间
  • select支持的文件描述符 默认最大为1024

epoll

  1. 执行epoll_create(),创建eventpoll的结构体。涉及到了红黑树和双向链表。其中红黑树保存和管理所有相关的文件描述符,而双向链表保存和管理处于就绪状态的文件描述符。
struct eventpoll{ ... 
// 红黑树的根节点,该树存储所有添加到epoll的需要监控的socket
// o(logn) 对socket进行增删改查
struct rb_root rbr; 
// 双向链表存放已就绪的socket 
struct list_head rdlist; 
}
  1. 执行epoll_ctl(),将要监控的文件描述符从用户空间拷贝到内核空间,将被监听的fd作为一个节点加入红黑树。
  2. 当一个socket的读事件或写时间就绪,就将该socket的文件描述符添加到双向链表。
  3. 调用epoll_wait(),检查就绪链表是否存在节点,并将就绪链表中的fd拷贝到用户空间,如果就绪链表为空,epoll_wait()会一直阻塞。

对比

  • epoll没有可监控的最大文件描述符的限制
  • select的每次调用都需要将fd从用户态拷贝到内核态,但是epoll只需要在刚开始确定监听文件的时候拷贝到内核空间,并使用红黑树管理。
  • 当socket轮询响应就绪fd后,还需要遍历所有的文件描述符来返回。而epoll通过双向链表会直接返回就绪的fd。

Level trigger

水平触发通知时,用户进程可以随时检查文件描述符的状态,因此可以重复检查io状态,执行更多的io。

Edge Trigger

边缘触发只有在io事件发生时用户进程才会收到通知

异步IO

按照之前的io模型,是由用户线程发起io请求,调用内核缓存区。但在异步io中,用户线程成为了被调用者,用户线程发起io请求后就可以进行其他操作,当内核缓冲区中数据准备就绪后,会主动通知用户进程。

参考

juejin.cn/post/701206…
blog.csdn.net/liyifan687/…
www.zbpblog.com/blog-211.ht…