Jinyun's Notes 🚀

没什么天赋,爱好也不多,但愿坚持做些喜欢的事情

0%

Linux 网络 IO 模型

202101210847.png

🍹 Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件),对一个 File 的读写操作会调用内核提供的系统命令,返回一个 File Descriptor(FD 文件描述符)。而对一个 Socket 的读写也会有响应的描述符,称为 Socket File Descriptor(Socket 文件描述符),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区等一些属性)。

根据 UNIX 网络编程对 IO 模型的分类,UNIX 提供了 5 种 IO 模型。

设备处理速度

  • 内存读数据:纳秒级别;
  • 千兆网卡读数据:微妙级别,1 微秒等于 1000 纳秒,网卡比内存慢了千倍;
  • 磁盘读数据:毫秒级别。1 毫秒等于 10 万纳秒 ,硬盘比内存慢了 10 万倍;
  • CPU 一个时钟周期 1 纳秒上下,内存算是比较接近 CPU 的,其他设备都等不起;

CPU 处理数据的速度远大于 IO 准备数据的速度 。所以理论上 任何编程语言 都会遇到这种 CPU 处理速度和 IO 速度不匹配的问题,在网络编程中如何进行网络 IO 优化,怎么高效地利用 CPU 进行网络数据处理就变得非常重要。

程序空间与内核空间

五种模型经历的两个阶段:

  • 等待数据准备好(Waiting for the data to be ready);
  • 将准备好的数据,从内核空间复制到进程空间(Copying the data from the kernel to the process);

五种模型概念

Linux 在处理文件和网络连接时,都需要打开和关闭 FD(File Descriptor)。每个进程都会有默认的 FD:

  • 0:标准输入 stdin;
  • 1:标准输出 stdout;
  • 2:错误输出 stderr;

阻塞 IO 模型

阻塞 IO 模型(Blocking IO)是最常用的 IO 模型,缺省情况下,所有文件操作都是阻塞的。以套接字为例:在进程空间中调用 recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或发生错误时才返回。在此期间一直会等待,进程从调用 recvfrom 开始到它返回的整段时间都是被阻塞的。即 recvfrom 的调用会被阻塞。

非阻塞 IO 模型

非阻塞 IO 模型(Non-blocking IO),recvfrom 从应用层到内核的时候,如果缓冲区没有数据的话,就直接返回一个 EWOULDBLOCK 错误,一般对非阻塞 IO 模型进行轮询检查这个状态,看内核是不是有数据到来。即反复调用 recvfrom 等待成功指示(轮询)。

IO 复用模型

IO 复用模型(IO Multiplexing),Linux 提供 select/poll,进程通过将一个或者多个 fd 传递给 select 或 poll 系统调用,阻塞在 select 操作上,阻塞在 select 操作上,这样 select/poll 可以帮我们侦测到多个 fd 是否处于就绪状态。select/poll 是顺序扫描 fd 是否就绪,而且支持的 fd 数量有限,因此它的使用受到了一些制约。Linux 还提供了 epoll 系统调用,epoll 使用基于事件驱动方式代替顺序扫描,因此性能更高。当有 fd 就绪时,立即回调函数 callback。即阻塞在 select/poll/epoll,以及数据复制拷贝的这段时间。

信号驱动 IO 模型

信号驱动 IO 模型(Signal Driven IO),首先开启信号曲驱动 IO 功能,并通过系统调用 Sigaction 执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个 SIGIO 信号,通过信号回调通知应用程序调用 recvfrom 来读取数据,并通知主循环函数处理数据。即阻塞在数据复制拷贝的这段时间。

异步 IO

异步 IO(Asynchronous IO),告知内核启动某个文件,并让内核整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号模型的组要区别是:信号驱动 IO 由内核通知我们何时可以开始一个 IO 操作:异步 IO 模型由内核通知我们 IO 操作何时已完成。

五种模型的区别

  • 阻塞 IO、非阻塞 IO、多路复用 IO、信号驱动 IO 都是同步 IO,五种模型在内核数据 Copy 到用户空间时都是阻塞的。
  • 阻塞与非阻塞 IO:发起 IO 请求是否会被阻塞,如果阻塞就是传统的阻塞 IO,不如不阻塞就是非阻塞 IO;
  • 同步与异步 IO:如果实际的 IO 读写阻塞请求过程,那么就是同步 IO,如果不阻塞,而是操作系统协助做完 IO 操作再将结果返回给你,那么就是异步 IO;

IO 多路复用技术

应用场景

IO 多路复用就是通过一种机制,一个进程可以监视多个文件描述符,一旦某个描述符就绪(读就绪或写就绪),能够通知程序进行相应的读写操作。

  • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用 IO 复用;
  • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现;
  • 服务器需要同时处理多个处于监听状态或多个连接状态的套接字;
  • 服务器需要同时处理多个服务或多个协议的套接字;

与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

使用技术

select

  • 良好跨平台支持;
  • 单个进程可监视的 FD 数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 查看。32 位机默认是 1024 个。64 位机默认是 2048 个;
  • 对 Socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次 select() 都要通过遍历 FD_SETSIZE 个 Socket 来完成调度,不管哪个 Socket 是活跃的,都遍历一遍。这会浪费很多 CPU 时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是 epoll 与 kqueue 做的;
  • 需要维护一个用来存放大量 FD 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大;
  • 可以在一个线程内同时处理多个 Socket 的 IO 请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是 fork 出多个进程来处理每个客户连接。现在,我们同样可以使用 select 来处理多客户问题,而不用 fork;

用 select 处理带外数据,网络程序中,select 能处理的异常情况只有一种:socket 上接收到带外数据。

带外数据:带外数据(out—of—band data),有时也称为加速数据(expedited data),是指连接双方中的一方发生重要事情,想要迅速地通知对方。这种通知在已经排队等待发送的任何普通(有时称为『带内』)数据之前发送。带外数据设计为比普通数据有更高的优先级。带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。

poll

  • 没有最大文件描述符数量的限制;
  • 和 select 一样,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大;
  • 同 select 一样,内核需要将消息传递到用户空间,都需要内核拷贝动作;

epoll

  • 没有最大并发连接的限制,能打开的 FD 的上限远大于 1024(1G 的内存上能监听约 10 万个端口);
  • IO 效率不会随着 FD 数目的增加而线性下将,只有活跃可用的 FD 才会调用 callback 函数;
  • 内存拷贝,利用 mmap 文件映射内存加速与内核空间的消息传递。epoll 使用 mmap 减少复制开销,即 epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次;
  • 通过内核和用户空间共享一块内存来实现,不需要内核拷贝动作;
  • epoll 拥有更加简单的 API;

epoll 对文件描述符的操作有两种模式:

  • LT(Level Trigger,水平触发)模式:是缺省的工作方式,并且同时支持 block 和 non-block socket。当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件,这种机制可以比较好的保证每个数据用户都处理掉了;
  • ET(Edge Trigger,边缘触发)模式:是高速工作方式,只支持 non-block socket。,当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。简而言之,就是内核通知过的事情不会再说第二遍,数据错过没读,你自己负责。这种机制确实速度提高了,但是风险相伴而行;

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

技术区别

selectpollepoll
事件集合通过 3 个参数分别传入感兴趣的可读可写及异常事件。内核通过对这些参数的在线修改来反馈其中的就绪事件,这这使得用户每次调用 select 都要重置这 3 个 参数统一处理所有事件类型,因此只需要一个事件参数,用户通过 pollfd.events 传入感兴趣的事件,内核通过修改 pollfd.revents 参数反馈其中就绪的事件内核通过一个事件直接管理用户感兴趣的所有事件,因此每次调用 epoll_wait 时,无需反复传入用户感兴趣的事件,epoll_wait 系统调用的参数 events 仅用来反馈就绪的事件
工作原则采用轮询方式检测就绪事件,时间复杂度:O(n)采用轮询方式检测就绪事件,时间复杂度:O(n)采用回调方式检测就绪事件,时间复杂度:O(1)
最大连接1024无上限无上限
工作模式LTLTLT 和 ET
文件描述每次调用,每次拷贝每次调用,每次拷贝通过 mmap 的内存映射技术,降低拷贝的资源消耗
本笔记是笔者在学习和工作中的一些整理,如对您有用,请鼓励我继续写作