聊一聊I/O那些事儿

在Linux/Unix系统中,对于1次IO读取操作,数据并不会由磁盘/Socket直接拷贝到应用程序的缓存区(用户空间)。

数据的流转顺序为:

磁盘/Socket–>内核空间–>用户空间。

所以,数据读取可以看成2个过程:

  • Waiting for the data to be ready(等待数据到达内核缓冲区)
  • Copying the data from the kernel to the process(从内核缓冲区拷贝数据到应用程序缓冲区)

基础概念

同步和异步

同步和异步关注的是消息通信机制

同步指的是发出1个调用时,在没有得到结果之前,该调用就不返回,即调用者主动等待返回结果。

而异步恰恰相反,发出调用后,会立即返回,调用者无需等待。任务完成后,被调用者将结果通过状态、通知或回调来告知给调用者。

通知调用者的3种方式:

  • 状态,监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率比较低;
  • 通知,被调用者将任务执行完成后,发出通知告知调用者(一般通过消息队列),无需消耗太多性能;
  • 回调,被调用者将任务执行完成后,调用调用者提供的回调函数告知结果。

阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

系统IO模型

阻塞式 I/O 模型(blocking I/O)

在这里插入图片描述
在阻塞式 I/O 模型中,应用程序在从调用 recvfrom 开始到它返回有数据报准备好这段时间是阻塞的,recvfrom 返回成功后,应用进程开始处理数据报。

  • 优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。
  • 缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,这种模型在实际生产中很少使用。

非阻塞式 I/O 模型(non-blocking I/O)

在这里插入图片描述
在非阻塞式 I/O 模型中,应用程序把一个套接口设置为非阻塞,就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠。而是返回一个错误,应用程序基于 I/O 操作函数将不断的轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。

  • 优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。
  • 缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。

I/O 复用模型(I/O multiplexing)

在这里插入图片描述
在 I/O 复用模型中,会用到 Select 或 Poll 函数或 Epoll 函数(Linux 2.6 以后的内核开始支持),这两个函数也会使进程阻塞,但是和阻塞 I/O 有所不同。这两个函数可以同时阻塞多个 I/O 操作,而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

  • 优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源。
  • 缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处理需要 2 次系统调用,占用时间会有增加。

信号驱动式 I/O 模型(signal-driven I/O)

在这里插入图片描述
在信号驱动式 I/O 模型中,应用程序使用 Socket 进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。

  • 优点:线程并没有在等待数据时被阻塞,可以提高资源的利用率。
  • 缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。

但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。

异步 I/O 模型(asynchronous I/O)

在这里插入图片描述
由 POSIX 规范定义,应用程序告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到应用程序的缓冲区)完成后通知应用程序。这种模型与信号驱动模型的主要区别在于:信号驱动 I/O 是由内核通知应用程序何时启动一个 I/O 操作,而异步 I/O 模型是由内核通知应用程序 I/O 操作何时完成。

  • 优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。
  • 缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 IO 复用模型模式为主。

五种I/O模型总结

在这里插入图片描述
这五种 I/O 模型中,前四种属于同步 I/O,因为其中真正的 I/O 操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配。

IO多路复用的3种实现

Select

运行机制

select是通过Long型数组fd_set来实现的,fd_set种的每个元素均与1个文件描述符(Socket、文件句柄、设备句柄等)绑定,绑定过程由应用程序负责。当应用程序执行select()系统调用时,内核会根据IO状态来修改fd_set中的元素,由此来通知应用进程哪一个文件或Socket可读。

select的优势在于可以通过单个线程来管理多个文件或Socket请求,而在同步阻塞模型中,必须通过多线程的方式来实现该目的。

劣势
  • 每次调用select,均需要将fd_set由用户态拷贝到内核态,当fd_set元素较多时,拷贝开销较大;
  • 每次调用select,内核均需要遍历fd_set中的所有元素,当fd_set元素较多时,遍历开销较大;
  • 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且该阈值是基于宏来控制的,大小不可改变(32位系统限制为1024,64位系统限制为2048)。

Poll

运行机制

poll本质上和select没有区别,依然需要进行数据结构的复制,依然是基于轮询来实现,但区别就是,select使用的是fd数组,而poll则是维护了一个链表,所以从理论上,poll方法中,单个进程能监听的fd不再有数量限制。但是轮询,复制等select存在的问题,poll依然存在。

EPoll

运行机制

epoll就是对select和poll的改进了。它的核心思想是基于事件驱动来实现的,就是给每个fd注册一个回调函数,当fd对应的设备发生IO事件时,就会调用这个回调函数,将该fd放到一个链表中,然后由客户端从该链表中取出一个个fd,以此达到O(1)的时间复杂度。

epoll操作实际上对应着有三个函数:epoll_create,epoll_ctr,epoll_wait。

  • epoll_create

epoll_create相当于在内核中创建一个存放fd的数据结构。在select和poll方法中,内核都没有为fd准备存放其的数据结构,只是简单粗暴地把数组或者链表复制进来;而epoll则不一样,epoll_create会在内核建立一颗专门用来存放fd结点的红黑树,后续如果有新增的fd结点,都会注册到这个epoll红黑树上。

  • epoll_ctr

另一点不一样的是,select和poll会一次性将监听的所有fd都复制到内核中,而epoll不一样,当需要添加一个新的fd时,会调用epoll_ctr,给这个fd注册一个回调函数,然后将该fd结点注册到内核中的红黑树中。当该fd对应的设备活跃时,会调用该fd上的回调函数,将该结点存放在一个就绪链表中。这也解决了在内核空间和用户空间之间进行来回复制的问题。

  • epoll_wait

epoll_wait的做法也很简单,其实直接就是从就绪链表中取结点,这也解决了轮询的问题,时间复杂度变成O(1)。

水平触发和边缘触发

水平触发的意思就是说,只要条件满足,对应的事件就会一直被触发。所以如果条件满足了但未进行处理,那么就会一直被通知。

边缘触发的意思就是说,条件满足后,对应的事件只会被触发一次,无论是否被处理,都只会触发一次。

而对于select和poll来说,其触发都是水平触发。而epoll则有两种模式:·EPOLLLT和EPOLLET。

  • EPOLLLT(默认状态):也就是水平触发。在该模式下,只要这个fd还有数据可读,那么epoll_wait函数就会返回该fd
  • EPOLLET(高速模式):也就是边缘触发。在该模式下,当被监控的fd上有可读写事件发生时,epoll_wait会通知程序去读写,若本次读写没有读完所有数据,或者甚至没有进行处理,那么下一次调用epoll_wait时,也不会获取到该fd。这种效率比水平触发的要高,系统中不会充斥着大量程序不感兴趣的fd,不感兴趣直接忽视就行,下次不会再触发。

总结

在这里插入图片描述

  • select,poll是基于轮询实现的,将fd_set从用户空间复制到内核空间,然后让内核空间以poll机制来进行轮询,一旦有其中一个fd对应的设备活跃了,那么就把整个fd_set返回给客户端(复制到用户空间),再由客户端来轮询每个fd的,找出发生了IO事件的fd;
  • epoll是基于事件驱动实现的,加入一个新的fd,会调用epoll_ctr函数为该fd注册一个回调函数,然后将该fd结点注册到内核中的epoll红黑树中,当IO事件发生时,就会调用回调函数,将该fd结点放到就绪链表中,epoll_wait函数实际上就是从这个就绪链表中获取这些fd;
  • epoll分为EPOLLLT(水平触发,默认状态)和EPOLLET(边缘触发,效率高);
  • 并不是所有的情况中epoll都是最好的,比如当fd数量比较小的时候,epoll不见得就一定比select和poll好。

参考文献

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值