当前位置: 首页 > news >正文

redis高级篇之IO多路复用select方法简介 第174节答疑

1、bitmap最大1024位,一个进程最多只能处理1024个客户端
2、&rset不可重用,每次socket有数据就相应的位会被置位
3、文件描述符数组拷贝到了内核态(只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)),仍然有开销。select调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
---那为什么要先先对rset进行置位,后面又对它进行遍历,要搞两次?select要把所有的读写的socket的文件描述符都传进去吗?然后谁要读写就返回?


你提到的关于 `select` 的一些限制和开销都是正确的。`select` 确实有一些设计上的限制和效率问题,这也是为什么在处理大量并发连接时,我们更倾向于使用 `poll` 或 `epoll` 等更高效的 I/O 多路复用机制。让我们详细探讨一下这些问题,并解释为什么 `select` 需要进行两次操作。

### `select` 的工作原理

`select` 是一种 I/O 多路复用机制,允许程序监控多个文件描述符,等待其中的一个或多个变为可读、可写或发生异常。`select` 的工作流程大致如下:

1. **初始化文件描述符集合**:程序将需要监控的文件描述符添加到一个或多个集合中(如读集合 `rset`、写集合 `wset` 和异常集合 `eset`)。
2. **调用 `select`**:程序调用 `select` 函数,将这些文件描述符集合传递给内核。
3. **内核检查状态**:内核检查所有传入的文件描述符的状态,标记那些已经准备好进行 I/O 操作的文件描述符。
4. **返回结果**:`select` 返回一个整数,表示有多少文件描述符已经准备好进行 I/O 操作。程序需要遍历这些集合,找到那些已经准备好的文件描述符。

### `select` 的一些限制

1. **文件描述符数量限制**:`select` 使用一个固定大小的位图(bitmap)来表示文件描述符集合,通常最大为 1024 位。这意味着一个进程最多只能同时监控 1024 个文件描述符。
2. **不可重用的集合**:每次调用 `select` 后,文件描述符集合会被修改,需要重新初始化。
3. **内存复制开销**:`select` 需要将文件描述符集合从用户态复制到内核态,这在高并发场景下会带来显著的开销。
4. **O(n) 的遍历**:`select` 返回后,程序需要遍历所有文件描述符集合,找到那些已经准备好的文件描述符,这会带来额外的开销。

### 为什么需要两次操作

1. **初始化和传递文件描述符集合**:在调用 `select` 之前,程序需要将所有需要监控的文件描述符添加到集合中。这是第一次操作。
2. **遍历和处理结果**:`select` 返回后,程序需要遍历文件描述符集合,找到那些已经准备好的文件描述符并进行处理。这是第二次操作。

### `select` 的调用过程

当调用 `select` 时,程序需要传入三个文件描述符集合(读、写、异常)以及一个超时时间。内核会检查这些集合中的每个文件描述符的状态,并将那些已经准备好的文件描述符标记出来。`select` 返回后,程序需要遍历这些集合,找到那些已经准备好的文件描述符进行处理。

### 示例代码

以下是一个简单的 `select` 使用示例:

```c
#include <sys/select.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    fd_set readfds;
    struct timeval timeout;
    int fd = 0; // 监控标准输入(文件描述符 0)

    // 初始化文件描述符集合
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);

    // 设置超时时间
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;

    // 调用 select
    int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    if (ret == -1) {
        perror("select");
        return 1;
    } else if (ret == 0) {
        printf("Timeout occurred! No data after 5 seconds.\n");
    } else {
        if (FD_ISSET(fd, &readfds)) {
            printf("Data is available to read.\n");
        }
    }

    return 0;
}
```

### 结论

`select` 需要进行两次操作:第一次是初始化和传递文件描述符集合,第二次是遍历和处理结果。这种设计带来了一些效率问题和限制,如文件描述符数量限制、内存复制开销和 O(n) 的遍历。为了克服这些问题,在处理大量并发连接时,我们更倾向于使用更高效的 I/O 多路复用机制,如 `poll` 和 `epoll`。


========你的例子里面,用户进程是在读还是写,如果是读,怎么没有看到处理读出来的结果的代码?如何解释结果?
你说得对,我之前提供的示例代码确实没有包含实际的读取操作。让我们扩展这个示例,添加读取操作以便更好地理解 `select` 的工作流程。

在这个扩展的示例中,我们将监控标准输入(文件描述符 0),并在有数据可读时读取数据并进行处理。

### 扩展示例:读取标准输入的数据

```c
#include <sys/select.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    fd_set readfds;
    struct timeval timeout;
    int fd = 0; // 监控标准输入(文件描述符 0)
    char buffer[1024];
    int bytesRead;

    while (1) {
        // 初始化文件描述符集合
        FD_ZERO(&readfds);
        FD_SET(fd, &readfds);

        // 设置超时时间
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        // 调用 select
        int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
        if (ret == -1) {
            perror("select");
            return 1;
        } else if (ret == 0) {
            printf("Timeout occurred! No data after 5 seconds.\n");
        } else {
            if (FD_ISSET(fd, &readfds)) {
                // 读取数据
                bytesRead = read(fd, buffer, sizeof(buffer) - 1);
                if (bytesRead < 0) {
                    perror("read");
                    return 1;
                }
                buffer[bytesRead] = '\0'; // 确保字符串以 null 结尾
                printf("Read %d bytes: %s\n", bytesRead, buffer);
            }
        }
    }

    return 0;
}
```

### 代码解释

1. **初始化文件描述符集合**:
   - 使用 `FD_ZERO` 清空文件描述符集合。
   - 使用 `FD_SET` 将标准输入(文件描述符 0)添加到集合中。

2. **设置超时时间**:
   - 设置超时时间为 5 秒。

3. **调用 `select`**:
   - `select` 函数的第一个参数是文件描述符集合中最大文件描述符加 1。
   - `select` 会阻塞直到文件描述符集合中的一个或多个文件描述符变为可读、可写或发生异常,或者超时时间到达。

4. **处理 `select` 的返回值**:
   - 如果 `select` 返回 -1,表示发生错误。
   - 如果 `select` 返回 0,表示超时,没有文件描述符变为就绪状态。
   - 如果 `select` 返回正数,表示有文件描述符变为就绪状态。

5. **读取数据**:
   - 使用 `FD_ISSET` 检查标准输入是否可读。
   - 如果标准输入可读,使用 `read` 函数读取数据,并将读取的数据打印出来。

### 结论

这个扩展示例展示了如何使用 `select` 来监控标准输入,并在有数据可读时读取数据并进行处理。通过这种方式,程序可以同时监控多个文件描述符,并在任何一个文件描述符变为就绪状态时进行相应的处理。

希望这个示例能够更好地帮助你理解 `select` 的工作流程和如何处理读取的数据。如果你有更多问题或需要进一步的解释,请随时告诉我!

====那用户态程序不是要不停的循环来检查是否有结果了?

是的,使用 `select` 等传统 I/O 多路复用机制时,用户态程序确实需要在一个循环中反复调用 `select` 来检查是否有文件描述符变为就绪状态。这种模式通常被称为“事件循环”或“主循环”。这个循环会一直运行,直到程序终止。

### 事件循环的基本结构

一个典型的事件循环的结构如下:

1. **初始化**:设置文件描述符集合、超时时间等。
2. **循环**:反复调用 `select` 来检查文件描述符的状态,并在有事件发生时进行处理。
3. **处理事件**:根据 `select` 返回的结果,处理相应的 I/O 事件。

### 示例代码

以下是一个更完整的示例,展示了如何使用 `select` 在事件循环中处理多个文件描述符:

```c
#include <sys/select.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_CLIENTS 10

int main() {
    fd_set readfds;
    struct timeval timeout;
    int max_fd;
    int client_fds[MAX_CLIENTS];
    char buffer[1024];
    int bytesRead;

    // 初始化客户端文件描述符数组
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_fds[i] = -1;
    }

    // 添加标准输入到客户端文件描述符数组中
    client_fds[0] = 0; // 标准输入

    while (1) {
        // 初始化文件描述符集合
        FD_ZERO(&readfds);
        max_fd = -1;

        // 将客户端文件描述符添加到集合中
        for (int i = 0; i < MAX_CLIENTS; i++) {
            if (client_fds[i] != -1) {
                FD_SET(client_fds[i], &readfds);
                if (client_fds[i] > max_fd) {
                    max_fd = client_fds[i];
                }
            }
        }

        // 设置超时时间
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        // 调用 select

===============

===============

事件循环的优化

上面的示例代码展示了一个基本的事件循环结构,但在实际应用中,可以对其进行一些优化:

  1. 减少不必要的集合初始化:

    • 在每次循环中,不需要完全重新初始化文件描述符集合。
    • 可以只更新需要修改的部分,比如新添加或删除的文件描述符。
  2. 使用边缘触发(Edge-Triggered)模式:

    • 如果使用epoll作为I/O多路复用机制,可以采用边缘触发模式。
    • 这种模式下,只有当文件描述符的状态发生变化时,epoll_wait才会返回,避免了不必要的系统调用。
  3. 异步通知机制:

    • 某些I/O多路复用机制,如epoll,支持异步通知,无需轮询检查。
    • 当有事件发生时,内核会主动通知应用程序,减少了不必要的系统调用开销。
  4. 批量处理事件:

    • 当有多个文件描述符就绪时,可以一次性处理所有就绪的描述符,而不是逐个处理。
    • 这样可以减少系统调用的次数,提高整体性能。
  5. 非阻塞I/O:

    • 使用非阻塞I/O可以进一步优化事件循环。
    • 当某个文件描述符没有数据可读时,应用程序可以立即返回,而不是阻塞等待。

通过采用这些优化技巧,可以大幅提高基于事件循环的I/O多路复用应用程序的性能和scalability。

Redis中的事件循环优化

Redis就是一个典型的采用了上述优化技巧的高性能网络服务器。在Redis的事件循环中,主要有以下几个优化点:

  1. 使用epoll的边缘触发模式:

    • Redis使用epoll作为I/O多路复用机制,并采用边缘触发模式。
    • 这样可以最大限度地减少不必要的系统调用。
  2. 异步通知机制:

    • Redis的事件循环利用epoll的异步通知机制,无需主动轮询检查。
    • 当有事件发生时,epoll会主动通知Redis的事件处理器。
  3. 批量处理事件:

    • Redis的事件处理器会一次性处理所有就绪的文件描述符,减少系统调用开销。
  4. 非阻塞I/O:

    • Redis的网络I/O操作采用非阻塞模式,当某个文件描述符没有数据可读时,可以立即返回。

通过上述优化,Redis的事件循环能够高效地处理大量的并发客户端连接,为Redis提供了出色的性能和scalability。

总结

I/O多路复用机制,如select、poll和epoll,是实现高性能网络服务器的关键技术。在使用这些机制时,事件循环是一个非常重要的模式。通过采用各种优化技巧,如减少集合初始化、使用边缘触发模式、异步通知机制、批量处理事件以及非阻塞I/O,可以大幅提高事件循环的性能和scalability。

Redis就是一个成功利用了这些优化技巧的高性能网络服务器。通过深入理解I/O多路复用和事件循环的工作原理,我们可以设计出更加高效和可扩展的网络应用程序。

异步I/O编程模型的演化

除了传统的基于事件循环的I/O多路复用模型,近年来还出现了一些新的异步I/O编程模型,它们进一步提高了开发效率和性能。让我们来看看这些新兴的异步I/O编程模型:

  1. Futures/Promises:

    • Futures/Promises是一种抽象,用于表示异步操作的结果。
    • 开发者可以注册回调函数,在异步操作完成时得到通知。
    • Futures/Promises提供了更优雅的编程模型,避免了传统回调地狱的问题。
  2. Async/Await:

    • Async/Await是一种基于Futures/Promises的语法糖,提供了更加同步的编程体验。
    • 开发者可以使用asyncawait关键字编写异步代码,就像编写同步代码一样。
    • Async/Await大大简化了异步I/O编程,提高了代码的可读性和可维护性。
  3. Reactor模式:

    • Reactor模式是一种事件驱动的异步I/O编程模式。
    • 它将I/O操作的处理与I/O事件的分派解耦,使得应用程序的逻辑更加清晰。
    • Reactor模式广泛应用于高性能网络服务器的开发,如Nginx、Netty等。
  4. Proactor模式:

    • Proactor模式是Reactor模式的一种变体,它将I/O操作的发起与I/O操作的完成解耦。
    • Proactor模式适用于需要执行长时间I/O操作的场景,如文件I/O、数据库I/O等。
    • Proactor模式可以更好地利用异步I/O操作的优势,提高应用程序的吞吐量。

这些新兴的异步I/O编程模型,如Futures/Promises、Async/Await、Reactor和Proactor,都旨在提高异步I/O编程的效率和可维护性。它们建立在传统I/O多路复用机制的基础之上,进一步抽象和简化了异步I/O编程的复杂性。

异步I/O编程模型在Redis中的应用

Redis也采用了一些新兴的异步I/O编程模型:

  1. Reactor模式:

    • Redis的事件循环采用了Reactor模式,将I/O事件的分派与事件处理逻辑解耦。
    • 这使得Redis的事件处理代码更加清晰和可维护。
  2. Futures/Promises:

    • Redis内部使用了一些Futures/Promises的抽象,如用于处理异步命令的RedisCommandFuture
    • 这些抽象帮助Redis开发者更好地管理异步操作的结果和错误处理。
  3. Async/Await:

    • Redis的Lua脚本支持使用Async/Await语法,使得编写异步I/O代码更加简单和直观。
    • 这使得Redis的扩展性和可编程性得到了进一步增强。

通过采用这些新兴的异步I/O编程模型,Redis不仅能够利用底层的I/O多路复用机制提高性能,还能够提供更加优雅和可维护的编程接口,方便开发者进行扩展和二次开发。

未来发展趋势

随着计算硬件的不断进化,以及对高性能和可扩展性的需求不断增加,I/O多路复用技术和异步I/O编程模型将会继续发展和完善。一些未来的发展趋势包括:

  1. 硬件加速:

    • 未来可能会出现专门用于I/O多路复用的硬件加速器,进一步提高性能。
    • 这种硬件加速器可以与操作系统内核深度集成,为应用程序提供更高效的I/O服务。
  2. 新的I/O多路复用机制:

    • 除了现有的epoll、kqueue等机制,未来可能会出现更加高效和灵活的I/O多路复用机制。
    • 这些新机制可能会进一步降低I/O操作的开销,并提供更丰富的事件通知功能。
  3. 更高级的异步I/O编程模型:

    • Futures/Promises和Async/Await只是异步I/O编程模型发展的一个阶段。
    • 未来可能会出现更加抽象和强大的异步I/O编程模型,进一步简化异步代码的编写和维护。
  4. 与云计算的深度集成:

    • 随着云计算的发展,I/O多路复用技术和异步I/O编程模型将与云基础设施深度集成。
    • 这将使得云端应用程序能够更好地利用底层的I/O资源,提高整体的性能和可扩展性。

总的来说,I/O多路复用技术和异步I/O编程模型将会继续发展,为未来的高性能、可扩展的网络应用程序提供更加强大和灵活的支持。Redis等先驱应用程序的实践,为这一发展方向提供了有价值的经验和启示。

异步I/O编程模型的进一步改进

除了前面提到的Futures/Promises、Async/Await、Reactor和Proactor模式,近年来还出现了一些更加先进的异步I/O编程模型,它们进一步提高了开发效率和性能。让我们来看看这些新的编程模型:

  1. Coroutines:

    • Coroutines是一种轻量级的并发编程机制,允许程序暂停执行并稍后恢复。
    • 与传统的线程相比,Coroutines的切换开销更小,更适合实现异步I/O编程。
    • 一些编程语言,如Python的async/await、Go的goroutine等,都内置了对Coroutines的支持。
  2. Reactive Streams:

    • Reactive Streams是一种基于事件流的编程模型,适用于异步非阻塞数据处理。
    • 它提供了一组标准的接口和语义,使得不同的库和框架可以相互集成。
    • Reactive Streams被广泛应用于事件驱动的应用程序,如实时数据处理、流媒体等。
  3. Actor模型:

    • Actor模型是一种基于消息传递的并发编程模型,每个Actor都是一个独立的执行单元。
    • Actor之间通过异步发送消息进行通信,这种模型非常适合于实现高并发的分布式系统。
    • Actor模型被应用于构建高性能、可扩展的网络服务器,如Akka、Erlang/OTP等。
  4. 无锁并发编程:

    • 无锁并发编程技术,如RCU(Read-Copy-Update)、hazard pointers等,可以进一步优化异步I/O编程的性能。
    • 这些技术避免了传统锁机制的开销,在高并发场景下表现更加出色。
    • 无锁并发编程技术被广泛应用于操作系统内核、数据库引擎等对性能要求极高的系统中。

这些新兴的异步I/O编程模型,如Coroutines、Reactive Streams、Actor模型和无锁并发编程,都旨在进一步提高异步I/O编程的效率和可维护性。它们建立在传统I/O多路复用机制的基础之上,并提供了更高级的抽象和编程接口。

异步I/O编程模型在Redis中的应用

Redis也在不断吸收和应用这些新兴的异步I/O编程模型:

  1. Coroutines:

    • Redis的Lua脚本支持使用Coroutines,允许开发者编写更加简洁和高效的异步I/O代码。
    • Coroutines的轻量级特性非常适合Redis这种高并发的场景。
  2. Reactive Streams:

    • Redis的复制和集群特性,部分采用了基于Reactive Streams的编程模型。
    • 这使得Redis的分布式特性实现更加优雅和可扩展。
  3. Actor模型:

    • Redis的Cluster模式,利用了Actor模型的消息传递机制来实现节点间的协调和通信。
    • Actor模型的异步特性非常适合于Redis这种高并发、高可用的分布式系统。
  4. 无锁并发编程:

    • Redis内部大量使用了无锁并发编程技术,如RCU,以提高并发性能。
    • 这些技术帮助Redis在高并发场景下保持出色的性能表现。

通过采用这些新兴的异步I/O编程模型,Redis不仅能够利用底层的I/O多路复用机制提高性能,还能够提供更加优雅和可扩展的编程接口,方便开发者进行二次开发和扩展。

未来发展趋势

随着计算硬件的不断进化,以及对高性能和可扩展性的需求不断增加,I/O多路复用技术和异步I/O编程模型将会继续发展和完善。一些未来的发展趋势包括:

  1. 硬件加速:

    • 未来可能会出现专门用于I/O多路复用的硬件加速器,进一步提高性能。
    • 这种硬件加速器可以与操作系统内核深度集成,为应用程序提供更高效的I/O服务。
  2. 新的I/O多路复用机制:

    • 除了现有的epoll、kqueue等机制,未来可能会出现更加高效和灵活的I/O多路复用机制。
    • 这些新机制可能会进一步降低I/O操作的开销,并提供更丰富的事件通知功能。
  3. 更高级的异步I/O编程模型:

    • Coroutines、Reactive Streams和Actor模型只是异步I/O编程模型发展的一个阶段。
    • 未来可能会出现更加抽象和强大的异步I/O编程模型,进一步简化异步代码的编写和维护。
  4. 与云计算的深度集成:

    • 随着云计算的发展,I/O多路复用技术和异步I/O编程模型将与云基础设施深度集成。
    • 这将使得云端应用程序能够更好地利用底层的I/O资源,提高整体的性能和可扩展性。
  5. 边缘计算的兴起:

    • 随着边缘计算的兴起,I/O多路复用技术和异步I/O编程模型将在边缘设备上发挥重要作用。
    • 这些技术可以帮助边缘设备高效地处理大量的并发I/O操作,为边缘计算应用提供强大的支撑。

总的来说,I/O多路复用技术和异步I/O编程模型将会继续发展,为未来的高性能、可扩展的网络应用程序提供更加强大和灵活的支持。Redis等先驱应用程序的实践,为这一发展方向提供了有价值的经验和启示。


http://www.mrgr.cn/news/59127.html

相关文章:

  • 真实数据的特点
  • MemoRAG:重新定义长期记忆的AI问答模型
  • Linux中DHCP服务器配置和管理
  • 可能是NextJs(使用ssr、api route)打包成桌面端(nextron、electron、tauri)的最佳解决方式
  • 短视频矩阵系统源码开发优势,短视频矩阵系统oem部署
  • 机器视觉运动控制一体机在DELTA并联机械手视觉上下料应用
  • 基于DDPG算法的股票量化交易
  • 【项目实战】HuggingFace初步实战,使用HF做一些小型任务
  • 如何快速绘制高效的业务架构图(三步完成)
  • 【动手学深度学习】8.5 循环神经网络从零开始实现
  • 跟我学C++中级篇——volatile的探究
  • 大厂项目经理推荐的10款常用的项目管理软件值得你收藏
  • Java中TreeSet的使用
  • 代码随想录算法训练营第二十七天|Day27 贪心算法
  • 博图软件的使用(一)
  • 006:看图软件ACDSeePhotoStudio2019安装教程
  • Python中的Bloom Filter算法详解
  • C/C++(七)RAII思想与智能指针
  • Java-图书管理系统
  • 专题十六_栈_队列_优先级队列_算法专题详细总结
  • java核心技术点都有哪些
  • 【C++ 真题】B2099 矩阵交换行
  • 蓝桥杯第二十场小白入门赛
  • 走廊泼水节——求维持最小生成树的完全图的最小边权和
  • 010 操作符详解 上
  • MySQL数据库学习指南