剖析IO原理和零拷贝机制

发布于:2025-02-24 ⋅ 阅读:(18) ⋅ 点赞:(0)

1 Linux的五种IO模型

五种IO模型包括:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO

1.1 模型调用的函数

使用到的部分函数

1.1.1 recv函数

recv函数用于从套接字(socket)接收数据。在TCP/IP协议栈中,当数据从网络到达时,它首先被放入内核的接收缓冲区中。然后,应用程序可以通过调用recv函数将这些数据从内核缓冲区复制到用户空间的缓冲区中,以便进一步处理。

参数和返回值

  1. 参数:
    套接字文件描述符:指定要接收数据的套接字。
    缓冲区指针:指向用户空间缓冲区的指针,用于存放接收到的数据。
    缓冲区大小:指定用户空间缓冲区的大小(以字节为单位)。
    标志位:用于设置接收数据的特定方式(如是否阻塞、是否立即返回等)。
  2. 返回值:
    成功时:返回实际接收到的字节数。
    失败时:返回-1,并设置全局变量errno以指示错误类型。

1.1.2 select函数

在IO模型中,select函数是一个非常重要的系统调用,它允许一个程序同时监视多个文件描述符(通常是套接字描述符),以查看它们中的任何一个是否可以进行I/O操作(例如读、写或异常条件)。select函数在多种编程场景中都非常有用,特别是在需要处理多个并发连接的网络服务器中。

select函数通过三个文件描述符集(读集、写集和异常集)来监视多个文件描述符。调用select时,程序指定这三个集合,select函数将阻塞(除非设置了非阻塞标志),直到以下情况之一发生:

  • 指定的读集合中的一个或多个文件描述符变为可读(例如,有数据可以读取)。
  • 指定的写集合中的一个或多个文件描述符变为可写。
  • 指定的异常集合中的一个或多个文件描述符发生异常条件。

当select返回时,它将更新这三个集合,以反映哪些文件描述符已准备好进行I/O操作。

select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64位机器默认2048个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。

参数和返回值

  1. 参数:
    nfds:要监视的文件描述符集合中的最大文件描述符加1。
    readfds:指向文件描述符集合的指针,这些文件描述符被监视以查看它们是否可读。
    writefds:指向文件描述符集合的指针,这些文件描述符被监视以查看它们是否可写。
    exceptfds:指向文件描述符集合的指针,这些文件描述符被监视以查看是否发生异常条件。
    timeout:指定select函数等待I/O操作发生的最长时间。如果为NULL,则select将无限期地等待。
  2. 返回值
    返回值是准备好的文件描述符的总数。如果返回-1,则表示发生了错误。

1.1.3 poll函数

poll函数通过轮询的方式检测输入源(文件描述符)是否有数据到达或是否准备好进行I/O操作。它会遍历一个由pollfd结构体组成的数组,每个结构体包含了一个文件描述符、要监视的事件类型以及实际发生的事件类型。当调用poll函数时,它会阻塞(除非设置了非阻塞标志),直到指定的文件描述符之一准备好进行I/O操作或超时。

poll函数使用pollfd结构体来指定要监视的文件描述符和事件类型。pollfd结构体的定义通常如下:

struct pollfd {
    int fd;         // 文件描述符
    short events;   // 要监视的事件类型(如读、写、异常等)
    short revents;  // 实际发生的事件类型,由内核设置
};

以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为O(n)。

参数和返回值

  1. 函数
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
  1. 参数
    fds:指向pollfd结构体数组的指针。
    nfds:数组中pollfd结构体的数量。
    timeout:指定poll函数等待I/O操作发生的最长时间(以毫秒为单位)。如果为-1,则poll将无限期地等待;如果为0,则poll将立即返回,不阻塞。

  2. 返回值
    返回-1:表示发生错误。
    返回0:表示在指定的时间内没有任何事件发生。
    返回正数:表示有事件发生,返回值是准备好进行I/O操作的文件描述符的数量。

1.1.4 epoll函数

epoll的工作原理主要基于Linux内核中的高效数据结构和事件驱动机制。事件驱动的,即如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为O(1)。

数据结构:

  1. 红黑树:epoll在内核中使用红黑树来管理所有注册的文件描述符(通常是socket)。红黑树是一种平衡二叉搜索树,它的查找、插入和删除操作的时间复杂度都是O(log n),这使得epoll能够高效地管理大量的文件描述符。
  2. 就绪列表:epoll还维护了一个就绪列表,用于存储那些已经就绪、有事件发生的文件描述符。就绪列表通常使用双向链表来实现,因为双向链表支持快速的插入和删除操作。当有事件发生时,内核会将相应的文件描述符从红黑树中取出,并加入到就绪列表中。

工作原理:

  1. 创建epoll实例:
    使用epoll_create函数创建一个epoll实例,该函数返回一个文件描述符,用于后续操作。
  2. 注册事件:
    使用epoll_ctl函数将感兴趣的文件描述符(通常是socket)及其事件(如读就绪、写就绪等)注册到epoll实例中。这一步可以添加新的事件、修改已注册的事件或删除事件。
  3. 等待事件:
    使用epoll_wait或epoll_pwait函数等待事件的发生。这些函数会阻塞调用线程,直到有注册的事件发生或超时。当事件发生时,函数会返回发生事件的文件描述符数量,并将事件信息存储在提供的epoll_event结构体数组中。

1.1.5 sigaction函数

在I/O模型中,特别是在信号驱动I/O模型中,sigaction函数扮演着重要的角色。信号驱动I/O模型是Unix/Linux系统中一种处理I/O操作的方式,它使用信号来通知应用程序数据已经准备好可以进行处理。在这个过程中,sigaction函数被用来设置信号处理函数,以便在接收到特定信号(如SIGIO)时执行相应的操作。

sigaction在信号驱动I/O模型中的作用

  1. 设置信号处理函数:
    在信号驱动I/O模型中,应用程序首先需要通过sigaction函数设置一个信号处理函数。这个处理函数将在接收到SIGIO信号时被调用。SIGIO信号通常用于通知应用程序某个文件描述符(如套接字)上有数据到达或可以进行I/O操作。
  2. 配置文件描述符:
    除了设置信号处理函数外,应用程序还需要将相关的文件描述符(如套接字)配置为非阻塞模式,并使其能够接收SIGIO信号。这通常通过fcntl函数来实现,设置文件描述符的O_ASYNC和O_NONBLOCK标志。
  3. 接收并处理信号:
    当数据到达配置为信号驱动I/O模式的文件描述符时,内核会发送SIGIO信号给应用程序。应用程序的信号处理函数随后被调用,可以在该函数中执行读取数据或其他相关操作。

1.2 IO模型

1.2.1 阻塞IO模型

在这里插入图片描述
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。

1.2.2 非阻塞IO模型

在这里插入图片描述
进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

对于上面的阻塞IO模型来说,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。

1.2.3 IO复用模型

在这里插入图片描述
多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;

如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;

相比于阻塞IO模型,多路复用只是多了一个select/poll/epoll函数。select函数会不断地轮询自己所负责的文件描述符/套接字的到达状态,当某个套接字就绪时,就对这个套接字进行处理。select负责轮询等待,recvfrom负责拷贝。当用户进程调用该select,select会监听所有注册好的IO,如果所有IO都没注册好,调用进程就阻塞。

1.2.4 信号驱动IO模型

在这里插入图片描述
模型也分为两个阶段:
数据准备阶段:未阻塞,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。
数据拷贝阶段:阻塞用户进程,等待数据拷贝。

1.2.5 异步IO模型

在这里插入图片描述
当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。

1.2.6 IO模型比较

在这里插入图片描述

2 Java的BIO、NIO、AIO

2.1 BIO(Blocking IO,同步阻塞式IO模型)

工作机制:在BIO模型中,每个客户端连接都会在一个独立的线程中处理。这个线程在处理IO操作时会阻塞,直到操作完成。因此,每个连接都需要一个独立的线程。当连接数较多时,会消耗大量的内存和CPU资源。
应用场景:适用于连接数少的场景。在这种情况下,程序编写相对简单,但对服务器的资源要求较高。在JDK 1.4之前,BIO是Java中唯一的IO模型选择。

2.2 NIO(Non-blocking IO,同步非阻塞式IO模型)

核心组件:NIO包含三大核心组件,即通道(Channel)、缓冲区(Buffer)和选择器(Selector)。
工作机制:在NIO模型中,一个线程可以处理多个连接。客户端连接请求会注册到多路复用器(Selector)上。多路复用器检测到某个连接有IO事件(如读、写、连接等)时,就会处理该事件。这种非阻塞模式使得主线程在未发生数据读写事件时无需阻塞,可以继续执行其他任务,从而增强了服务器的并发处理能力。
应用场景:适用于连接数多的场景,如聊天服务器、服务器间通讯等。然而,由于NIO的编程模型相对复杂,因此程序编写难度较高。NIO从JDK 1.4版本开始被支持。

2.3 AIO(Asynchronous IO,异步非阻塞式IO模型)

核心组件:AIO引入了异步通道的概念,并使用Future或CompletionHandler来处理异步操作的结果。
工作机制:在AIO模型中,读写异步通道会立刻返回,而不需要等待IO操作完成。读写的数据由Future或CompletionHandler进一步处理。当操作系统完成IO操作后,会主动通知应用程序,或者调用应用程序注册的回调函数来处理结果。
应用场景:也适用于连接数多的场景,但更加偏向于异步操作多的场景。AIO作为NIO的改进和增强,随JDK 1.7版本更新被集成在JDK的nio包中,因此也被称为NIO 2.0。

细化文章推荐:Java的BIO、NIO、AIO