NIO允许一个线程同时处理多个连接,而不会因为一个连接的阻塞而导致其他连接被阻塞。核心是依赖操作系统的多路复用
机制。
多路复用是一种操作系统的 I/O 处理机制,允许单个进程(或线程)同时监视多个输入或输出流的就绪状态。这样,一个进程就能够通过一个系统调用来等待多个事件,而不是为每个事件创建一个独立的进程或线程。
select: select
是一个系统调用,通过它可以同时监视多个文件描述符(通常是套接字)。当其中任何一个文件描述符准备好进行读取或写入时,select
就会返回,并告诉程序哪些文件描述符处于就绪状态。
poll: poll
也是一个系统调用,它和 select
类似,但是对文件描述符的管理更加灵活,而且没有文件描述符数目的限制。
epoll: epoll
是 Linux 中引入的一种多路复用机制,相对于 select
和 poll
具有更好的性能。epoll
使用事件通知的方式,只关心那些发生了变化的文件描述符,减少了遍历全部文件描述符的开销。
当客户端请求到达服务器时,整个流程可以分为以下几个步骤,涉及用户态和内核态的协同工作:
服务器启动: 服务器程序在用户态中启动,并创建一个监听 socket。这个监听 socket 负责接收客户端的连接请求。
监听连接: 服务器使用 select
或其他多路复用的系统调用,将监听 socket 添加到文件描述符集合中,然后阻塞等待事件发生。这时用户程序告诉内核要监听哪些文件描述符,而这些文件描述符通常是由 accept
等系统调用返回的新连接。
客户端连接: 当有客户端发起连接请求时,内核接收到连接请求,然后将新的连接 socket(客户端连接的文件描述符)添加到文件描述符集合中。此时内核通知用户程序,有文件描述符就绪。
处理连接: 用户程序从 select
返回后,检查文件描述符集合,确定哪些连接处于就绪状态。然后,用户程序可以通过 accept
接受新的连接,获得新的文件描述符,并处理与客户端的通信。
下面是一个简化的伪代码示例:
// 服务器启动
int listen_fd = create_and_bind_socket(port);
listen(listen_fd, SOMAXCONN);
// 设置监听 socket 到文件描述符集合
fd_set master_fds;
FD_ZERO(&master_fds);
FD_SET(listen_fd, &master_fds);
int max_fd = listen_fd;
while (true) {
fd_set read_fds = master_fds;
// 使用 select 监听文件描述符
int ready = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (ready == -1) {
// 处理错误
} else {
// 检查文件描述符集合,确定哪些连接就绪
for (int i = 0; i <= max_fd; ++i) {
if (FD_ISSET(i, &read_fds)) {
if (i == listen_fd) {
// 有新连接
int new_fd = accept(listen_fd, ...);
FD_SET(new_fd, &master_fds);
if (new_fd > max_fd) {
max_fd = new_fd;
}
} else {
// 有数据可读
handle_data(i);
}
}
}
}
}
在这个示例中,listen_fd
是监听 socket 的文件描述符,当有新的连接到达时,会使用 accept
获得新的文件描述符,然后将其添加到文件描述符集合中。select
会在有文件描述符就绪时返回,用户程序通过检查文件描述符集合确定哪些连接可以进行处理。
这个监听 socket 并不是客户端的连接请求,而是用于接受客户端连接的准备工作。客户端连接请求是在客户端发起连接时生成的。
文件描述符(File Descriptor)是用于标识已打开文件或I/O资源的整数。对于网络连接,文件描述符是内核用于跟踪每个连接的标识符。当一个客户端连接到服务器时,内核为这个连接分配一个文件描述符,通过这个文件描述符,内核能够管理和操作与客户端之间的I/O操作。
在 Linux 系统下,客户端与服务器之间的连接通常被抽象为文件描述符。这是因为内核为每个连接分配了一个文件描述符,通过这个文件描述符可以进行对应连接的读、写等I/O操作。文件描述符是一种通用的抽象,通过它,可以使用相同的接口进行文件、网络连接等各种I/O操作。
在 Linux 下,文件描述符(File Descriptor)并不是一个真正的文件,而是一个整数,用于标识已打开文件或 I/O 资源。每个客户端连接到服务器时,内核会为该连接分配一个文件描述符。这个文件描述符在内核中用于跟踪和管理该连接的相关信息,包括读写数据等 I/O 操作。
Linux 内核并不会创建一个真正的文件来存放客户端的请求内容、客户端的 IP 和端口等信息。相反,它在内核中维护了一个数据结构来表示每个连接的状态,这个数据结构包含了与连接相关的信息。这个信息通常被称为 socket(套接字),是用于在网络上进行通信的抽象。
当客户端发起连接时,内核会分配一个 socket,并分配一个文件描述符用于标识这个 socket。该文件描述符被传递给用户程序,用户程序可以通过这个文件描述符进行对应连接的读写操作。客户端的 IP 地址和端口等信息通常可以通过相应的系统调用获取,而不是通过创建一个文件。
总之,Linux 中的文件描述符不是一个实际的文件,而是用于标识和操作已打开的 I/O 资源,其中包括网络连接。相关的信息则在内核中以 socket 的形式存在,而不是在文件中。
在操作系统中,用户态(User Mode)和内核态(Kernel Mode)是指操作系统与应用程序之间的两个不同的运行级别或权限级别。这两个模式之间的切换是由操作系统内核控制的,而且涉及到处理器的特权级别。
用户态(User Mode):
内核态(Kernel Mode):
切换:
切换的目的:
总的来说,用户态和内核态的划分是为了保障系统的安全性和稳定性,确保应用程序不能随意访问和修改系统的关键资源。用户态和内核态之间的切换是由操作系统内核控制的,它会根据需要在两者之间进行切换。
在用户态,通过 select
系统调用的参数中的文件描述符集合(通常是 fd_set
)来告诉内核要监听哪些文件描述符。fd_set
是一个数据结构,它使用一个位图来表示文件描述符的状态,每个位表示一个文件描述符。
在调用 select
时,用户程序会将自己关心的文件描述符添加到 fd_set
中。在 select
返回后,用户程序可以检查 fd_set
来确定哪些文件描述符处于就绪状态。就绪状态通常表示有数据可读、有数据可写或者发生了错误。
下面是一个简化的示例:
#include
int main() {
fd_set read_fds;
FD_ZERO(&read_fds);
int sockfd = /* 创建并设置socket描述符 */;
FD_SET(sockfd, &read_fds);
// 设置超时时间为5秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 调用select,监听文件描述符
int ready = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (ready == -1) {
// 处理错误
} else if (ready == 0) {
// 超时
} else {
// 检查read_fds,确定哪些文件描述符就绪
if (FD_ISSET(sockfd, &read_fds)) {
// sockfd 就绪,可以进行读操作
}
}
return 0;
}
在这个示例中,通过 FD_SET
将 sockfd
添加到 read_fds
中,然后调用 select
来监听这个文件描述符。当 select
返回后,通过检查 FD_ISSET
可以确定 sockfd
是否处于就绪状态,进而进行相应的操作。
用户程序在调用 select
之前,需要设置好相应的文件描述符集合,并在 select
返回后,根据就绪状态进行处理。这种方式允许用户程序选择性地监听和处理多个文件描述符。
学习打卡:Java学习笔记-day04-NIO核心依赖多路复用小记