Dawn's Blogs

分享技术 记录成长

0%

软件架构设计读书笔记 (1) I/O

I/O

分类

缓冲 I/O 与直接 I/O

不管是文件 I/O 还是网络 I/O,最基本的分为两种类型:缓冲 I/O 和直接 I/O:

  • 缓冲 I/O:读写操作都进行三次数据拷贝操作,以读操作为例,数据从磁盘进入内核缓冲区(Page Cache,操作系统会把数据以 Page 为单位存储在操作系统内存空间中),从内核缓冲区进入用户缓冲区避免频繁的切换内核态,所以设立用户缓冲区,每次都读满缓冲区),最后从用户缓冲区进入应用程序内存

image-20230619111345104

  • 直接 I/O:直接 I/O 的读写操作会进行两次数据拷贝,与缓冲 I/O 不同的是,直接 I/O 中内核缓冲区直接与应用程序内存打交道

image-20230619111358424

内存映射文件与零拷贝

在读写操作时的拷贝消耗了 CPU 和内存资源,为了进一步减少拷贝,又引入了内存映射文件和零拷贝技术。

  • 内存映射文件:内存映射文件中,拷贝次数为一次。在应用程序中的地址指向系统内核缓冲区逻辑地址,所以在读操作时只需要将数据从磁盘读入内核缓冲区,应用程序通过内存映射就可以使用这些数据了。

image-20230619112432185

  • 零拷贝:零拷贝应用于避免数据从一个内核缓冲区拷贝到另一个内核缓冲区的场景(比如读取一个文件,然后将文件内容通过 socket 发送出去)。具体的做法是,直接使得一个内核缓冲区映射到另一个内核缓冲区中,两个内核缓冲区共用同一片内存空间。实际上这是有两次拷贝的,零拷贝指的是内存内部不需要拷贝操作。

image-20230619113753783

Golang 中内存映射文件使用 syscall.mmap,零拷贝使用 syscall.sendfile。

网络 I/O

当发生一次网络请求发生后,将会按顺序经历,等待数据从远程主机到达缓冲区将数据从缓冲区拷贝到应用程序地址空间两个阶段。

把网络 I/O 模型分为两类,五种模型:

  • 异步 I/O:异步 I/O 中数据到达缓冲区后,不需要由调用进程主动进行从缓冲区复制数据的操作,而是复制完成后由操作系统向线程发送信号
  • 同步 I/O:
    • 阻塞 I/O:等待数据到达时就进入阻塞,直到数据到达唤醒线程,线程唤醒后将数据复制到自己的内存空间。这种方法节省 CPU 资源,但缺点就是线程休眠所带来的上下文切换。
    • 非阻塞 I/O:非阻塞 I/O 中,线程不会进入阻塞状态,而是会一直轮询直到数据到达。这种方法会浪费 CPU 资源,不常用。
    • 多路复用 I/O:多路 I/O 就是一个线程在多个描述符(一个描述符看作是一个连接)上等待,而不是每一个描述符一个线程。具体又分为 select、poll、epoll:
      • select:有数据到达后,每一次轮询所有等待的描述符,找出数据进行操作。
      • poll:本质上和 select 没有区别,但是没有最大连接数的限制,因为是基于链表的。
      • epoll:会把哪个文件描述符发生的事件(读取或者写入)通知给我们,不用轮询。
    • 信号驱动 I/O:等待数据到达后会发出信号,应用程序会自行将数据复制到自己的内存空间中。

信号驱动 I/O 和异步 I/O 的区别:

区别在于从缓冲区获取数据这个步骤,信号驱动 I/O 是应用程序自己将数据复制到自己的内存区域。而异步 I/O 是已经复制完成后,才发出信号。

epoll 相比于 select 和 poll,性能是最好的,所以 epoll 是最常用的。

epoll 里面有两种模式,LT(水平触发)和 ET(边缘触发):

  • LT(水平触发):读缓冲区不为空,则一直触发;写缓冲区不满,则一直触发。对于 LT,在写完数据后要取消写事件,否则会进入写死循环。
  • ET(边缘触发):读缓冲区从空转为非空时触发一次;写缓冲区从满转换为非满时触发一次。对于 ET,要注意读完读缓冲区中未满的数据,否则会数据丢失。

LT 是更为常用的模式,因为 LT 更加安全不会丢失数据。

服务器 1+N+M 模型

通常服务器都会采用 epoll 进行编程,进一步分为三个步骤交给三种不同的线程完成,即服务器的 1+N+M 模型。

  • 监听线程:负责 accept 事件,和每一个新来的客户端建立连接之后,将 socket 交给 I/O 线程。完成之后,继续监听。
  • I/O 线程:负责 read/write 事件,即负责从 socket 中读取数据或者写入数据。将读取到的数据放入 Request 队列,交由 Worker 线程处理。从 Response 队列中读取 Worker 线程处理过的返回结果,写入到 socket 中。
  • Worker 线程:业务线程,根据收到的数据进行处理,并返回数据。

具体的实现可能会有差异,不一定与上方描述的完全一样。

image-20230619161051618