目录
- 1 Linux的五种IO模型
- 1.1 模型调用的函数
- 1.1.1 recv函数
- 1.1.2 select函数
- 1.1.3 poll函数
- 1.1.4 epoll函数
- 1.1.5 sigaction函数
- 1.2 IO模型
- 1.2.1 阻塞IO模型
- 1.2.2 非阻塞IO模型
- 1.2.3 IO复用模型
- 1.2.4 信号驱动IO模型
- 1.2.5 异步IO模型
- 1.2.6 IO模型比较
- 2 Java的BIO、NIO、AIO
- 2.1 BIO(Blocking IO,同步阻塞式IO模型)
- 2.2 NIO(Non-blocking IO,同步非阻塞式IO模型)
- 2.3 AIO(Asynchronous IO,异步非阻塞式IO模型)
1 Linux的五种IO模型
五种IO模型包括:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO
1.1 模型调用的函数
使用到的部分函数
1.1.1 recv函数
recv函数用于从套接字(socket)接收数据。在TCP/IP协议栈中,当数据从网络到达时,它首先被放入内核的接收缓冲区中。然后,应用程序可以通过调用recv函数将这些数据从内核缓冲区复制到用户空间的缓冲区中,以便进一步处理。
参数和返回值:
- 参数:
套接字文件描述符:指定要接收数据的套接字。
缓冲区指针:指向用户空间缓冲区的指针,用于存放接收到的数据。
缓冲区大小:指定用户空间缓冲区的大小(以字节为单位)。
标志位:用于设置接收数据的特定方式(如是否阻塞、是否立即返回等)。 - 返回值:
成功时:返回实际接收到的字节数。
失败时:返回-1,并设置全局变量errno以指示错误类型。
1.1.2 select函数
在IO模型中,select函数是一个非常重要的系统调用,它允许一个程序同时监视多个文件描述符(通常是套接字描述符),以查看它们中的任何一个是否可以进行I/O操作(例如读、写或异常条件)。select函数在多种编程场景中都非常有用,特别是在需要处理多个并发连接的网络服务器中。
select函数通过三个文件描述符集(读集、写集和异常集)来监视多个文件描述符。调用select时,程序指定这三个集合,select函数将阻塞(除非设置了非阻塞标志),直到以下情况之一发生:
- 指定的读集合中的一个或多个文件描述符变为可读(例如,有数据可以读取)。
- 指定的写集合中的一个或多个文件描述符变为可写。
- 指定的异常集合中的一个或多个文件描述符发生异常条件。
当select返回时,它将更新这三个集合,以反映哪些文件描述符已准备好进行I/O操作。
select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64位机器默认2048个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。
参数和返回值:
- 参数:
nfds:要监视的文件描述符集合中的最大文件描述符加1。
readfds:指向文件描述符集合的指针,这些文件描述符被监视以查看它们是否可读。
writefds:指向文件描述符集合的指针,这些文件描述符被监视以查看它们是否可写。
exceptfds:指向文件描述符集合的指针,这些文件描述符被监视以查看是否发生异常条件。
timeout:指定select函数等待I/O操作发生的最长时间。如果为NULL,则select将无限期地等待。 - 返回值
返回值是准备好的文件描述符的总数。如果返回-1,则表示发生了错误。
1.1.3 poll函数
poll函数通过轮询的方式检测输入源(文件描述符)是否有数据到达或是否准备好进行I/O操作。它会遍历一个由pollfd结构体组成的数组,每个结构体包含了一个文件描述符、要监视的事件类型以及实际发生的事件类型。当调用poll函数时,它会阻塞(除非设置了非阻塞标志),直到指定的文件描述符之一准备好进行I/O操作或超时。
poll函数使用pollfd结构体来指定要监视的文件描述符和事件类型。pollfd结构体的定义通常如下:
java">struct pollfd {int fd; // 文件描述符short events; // 要监视的事件类型(如读、写、异常等)short revents; // 实际发生的事件类型,由内核设置
};
以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为O(n)。
参数和返回值
- 函数
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
-
参数
fds:指向pollfd结构体数组的指针。
nfds:数组中pollfd结构体的数量。
timeout:指定poll函数等待I/O操作发生的最长时间(以毫秒为单位)。如果为-1,则poll将无限期地等待;如果为0,则poll将立即返回,不阻塞。 -
返回值
返回-1:表示发生错误。
返回0:表示在指定的时间内没有任何事件发生。
返回正数:表示有事件发生,返回值是准备好进行I/O操作的文件描述符的数量。
1.1.4 epoll函数
epoll的工作原理主要基于Linux内核中的高效数据结构和事件驱动机制。事件驱动的,即如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为O(1)。
数据结构:
- 红黑树:epoll在内核中使用红黑树来管理所有注册的文件描述符(通常是socket)。红黑树是一种平衡二叉搜索树,它的查找、插入和删除操作的时间复杂度都是O(log n),这使得epoll能够高效地管理大量的文件描述符。
- 就绪列表:epoll还维护了一个就绪列表,用于存储那些已经就绪、有事件发生的文件描述符。就绪列表通常使用双向链表来实现,因为双向链表支持快速的插入和删除操作。当有事件发生时,内核会将相应的文件描述符从红黑树中取出,并加入到就绪列表中。
工作原理:
- 创建epoll实例:
使用epoll_create函数创建一个epoll实例,该函数返回一个文件描述符,用于后续操作。 - 注册事件:
使用epoll_ctl函数将感兴趣的文件描述符(通常是socket)及其事件(如读就绪、写就绪等)注册到epoll实例中。这一步可以添加新的事件、修改已注册的事件或删除事件。 - 等待事件:
使用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模型中的作用
- 设置信号处理函数:
在信号驱动I/O模型中,应用程序首先需要通过sigaction函数设置一个信号处理函数。这个处理函数将在接收到SIGIO信号时被调用。SIGIO信号通常用于通知应用程序某个文件描述符(如套接字)上有数据到达或可以进行I/O操作。 - 配置文件描述符:
除了设置信号处理函数外,应用程序还需要将相关的文件描述符(如套接字)配置为非阻塞模式,并使其能够接收SIGIO信号。这通常通过fcntl函数来实现,设置文件描述符的O_ASYNC和O_NONBLOCK标志。 - 接收并处理信号:
当数据到达配置为信号驱动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