I/O多路复用技术&select、poll、epoll的区别与使用

一、什么是I/O多路复用技术

I/O多路复用复用的是一个线程或进程来同时监听多个文件描述符,以提高系统的性能和资源利用率。这个线程或进程会等待多个文件描述符中的任何一个变得可读、可写或出现异常,然后执行相应的操作,而不需要为每个文件描述符创建一个独立的线程或进程。这样可以避免多线程或多进程模型的开销,并简化编程模型。多路复用技术允许程序有效地管理多个通信通道,以提高并发性能和资源利用率。它允许应用程序更有效地处理大量并发请求,提高了性能和可伸缩性。

多路复用技术的需要和优点包括:

  1. 高并发处理:在许多网络应用中,需要同时处理大量的连接请求或数据传输,例如Web服务器、聊天应用、实时游戏等。使用多路复用技术,可以更有效地处理这些并发连接,而无需为每个连接创建一个独立的线程或进程。

  2. 节省资源:创建和管理线程或进程需要额外的内存和系统资源,而多路复用技术可以节省这些资源。一个程序可以使用少量线程或进程来处理大量连接,减少了内存和上下文切换的开销。

  3. 提高性能:多路复用技术可以减少轮询的需要,只在有事件发生时才通知应用程序,从而减少了处理开销,提高了性能。

  4. 支持非阻塞I/O:多路复用技术通常与非阻塞I/O一起使用,允许程序在等待数据准备好时继续执行其他任务,而不必一直阻塞等待数据。

  5. 简化程序结构:使用多路复用技术可以简化程序的结构,使代码更清晰易懂,因为不需要处理复杂的线程管理和同步问题。

几种常见的多路复用技术包括selectpollepoll(在Linux系统中),它们各自有不同的实现方式和性能特点,但都达到了管理多个I/O通道的目的。

二、select、poll、epoll的区别

1、通俗理解

当我们想象一个餐馆服务员如何处理多个顾客的点餐请求时,可以更容易理解 selectpollepoll 这三种多路复用机制的工作原理,我们把服务器想成餐厅服务员,桌子想成客户端,菜单想成I/O多路复用技术:

  1. select:就像一个餐馆服务员手里拿着一个大菜单,上面列出了所有桌子的点餐情况。服务员需要不断地询问每个桌子是否需要点菜,这样他可以及时记录下来。但这种方式有个问题,当餐厅桌子很多时,服务员需要不断地询问每个桌子,这可能会浪费一些时间。而且服务员手里的菜单大小有限,不能容纳过多桌子的信息。

  2. poll:与 select 类似,但服务员手里拿着一本更大的菜单,可以记录更多桌子的点餐情况。这减少了服务员询问的次数,提高了效率。但仍然需要遍历整本菜单以检查每个桌子。

  3. epoll:像一个高效的餐馆,服务员不需要拿着菜单询问每个桌子,而是只在桌子上放了一个按钮,当顾客需要点餐时,顾客按下按钮。服务员只需关注按下按钮的桌子,而不需要遍历整个餐厅。这样节省了大量时间和精力,特别适用于大型餐馆,其中桌子数量众多。

2、三者区别

selectpollepoll 都是用于多路复用 I/O 操作的系统调用,它们允许一个进程同时监听多个文件描述符的可读、可写和异常事件,从而提高了 I/O 操作的效率。这些调用通常用于构建高性能的网络服务器或其他需要处理大量并发连接的应用程序。

以下是对 selectpollepoll 的简要介绍和比较:

  1. select

    • select 是最早引入的多路复用机制之一,可在大多数Unix系统上使用。
    • 它使用文件描述符集合来监视多个套接字的状态变化,包括可读、可写和异常。
    • select 的缺点之一是效率较低,因为它需要将所有要监视的文件描述符复制到内核中,同时在每次调用时都需要线性扫描所有文件描述符。
    • select 的最大文件描述符数量通常有限,受到系统限制。
  2. poll

    • poll 是一种替代 select 的多路复用机制,可在大多数Unix系统上使用。
    • select 不同,poll 使用一个 pollfd 数组来存储要监视的文件描述符,每个 pollfd 结构包含文件描述符和所关心的事件。
    • poll 没有 select 的最大文件描述符数量限制,因此它可以处理更多的连接。
    • 尽管 pollselect 更有效率,但它仍然需要线性扫描 pollfd 数组。
  3. epoll

    • epoll 是Linux特有的多路复用机制,通常比 selectpoll 更高效。
    • epoll 使用三个系统调用:epoll_create(创建一个 epoll 对象)、epoll_ctl(添加、修改或删除文件描述符),以及 epoll_wait(等待事件)。
    • epoll 使用内核事件表来存储文件描述符和事件,只有发生事件时才会通知应用程序,因此不需要扫描整个文件描述符集合。
    • epoll 可以处理大量并发连接,因此在高性能服务器应用中广泛使用。

epoll 是最常用的多路复用机制,特别在需要高并发和高性能的网络服务器中。它在Linux系统上提供了更好的性能和可扩展性,因此是构建高性能网络应用的首选。selectpoll 在某些情况下仍然有用,特别是在跨平台开发时,但它们的性能相对较低。选择使用哪种多路复用机制通常取决于应用程序的需求和目标平台。

3、epoll概述

epoll 是 Linux 中用于实现高效多路复用 I/O 的机制,其底层实现是通过内核提供的一系列数据结构和系统调用来完成的。以下是 epoll 底层实现的简要概述:

  1. 数据结构epoll 使用了一些数据结构来管理文件描述符和事件。其中最重要的是:

    • 红黑树(Red-Black Tree):用于存储文件描述符和相应的事件,以便快速查找和管理。
    • 双链表(Doubly Linked List):用于存储准备好的事件,以便快速处理。
  2. 系统调用epoll 使用以下主要的系统调用来实现其功能:

    • epoll_create():创建一个 epoll 实例,返回一个文件描述符,用于标识该实例。
    • epoll_ctl():向 epoll 实例中添加、修改或删除文件描述符,并指定感兴趣的事件类型。
    • epoll_wait():等待就绪的事件发生,一旦有事件发生,将返回就绪的文件描述符列表。
  3. 事件驱动机制epoll 是事件驱动的机制,它会监视文件描述符上的事件,并在事件就绪时通知用户空间。这是通过内核异步地通知用户空间的方式实现的,而不需要用户空间主动轮询。

  4. 水平触发和边缘触发epoll 提供了两种触发模式:

    • 水平触发(Level-Triggered):当文件描述符上有数据可读或可写时,通知用户空间,用户空间可以反复调用 epoll_wait() 以获取就绪的文件描述符列表。这是默认的触发模式,适合处理 I/O 事件。

    • 边缘触发(Edge-Triggered):只有当文件描述符从未就绪变为就绪时,才通知用户空间。用户空间需要处理就绪事件并及时读取或写入数据,否则不会再次通知。这个模式适合处理高速数据流,避免了重复通知。

  5. 非阻塞等待epoll_wait() 支持非阻塞等待,可以设置超时时间,从而避免一直阻塞等待事件的发生。

  6. 支持大量文件描述符epoll 能够有效地处理大量的文件描述符,因为它的内部数据结构经过优化,不会因文件描述符数量增加而导致性能下降。

epoll 底层实现是一个高效的事件管理机制,它通过内核提供的数据结构和系统调用,以及事件驱动的方式,允许用户空间程序高效地处理大量的 I/O 事件,同时减少了不必要的轮询和系统资源的浪费。这使得 epoll 成为了 Linux 下高性能网络编程的重要工具。

4、epoll使用

使用 epoll 的一般步骤如下:

  1. 创建 epoll 实例:首先,你需要使用 epoll_create() 创建一个 epoll 实例,该实例用于管理文件描述符和事件。

  2. 添加文件描述符和关注事件:使用 epoll_ctl() 将你感兴趣的文件描述符添加到 epoll 实例中,并指定你关注的事件类型,如读事件、写事件等。

  3. 调用 epoll_wait():在你的主循环中,使用 epoll_wait() 来等待就绪的事件。这个函数会阻塞等待,直到有事件发生或者超时。

  4. 处理就绪事件:一旦 epoll_wait() 返回,你可以遍历返回的文件描述符列表,检查哪些文件描述符上有事件发生。然后,你可以执行相应的操作,如读取或写入数据。

  5. 重复步骤 3 和 4:通常,你会在一个循环中多次调用 epoll_wait(),以便持续地等待和处理事件。

以下是一个简单的伪代码示例,演示了如何使用 epoll

int epoll_fd = epoll_create(1); // 创建 epoll 实例

// 添加文件描述符和关注的事件
struct epoll_event event;
event.events = EPOLLIN; // 关注读事件
event.data.fd = your_file_descriptor; // 要关注的文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, your_file_descriptor, &event);

while (true) {
    struct epoll_event events[10]; // 存储就绪事件的数组
    int num_events = epoll_wait(epoll_fd, events, 10, -1); // 等待事件发生,可以使用超时参数

    for (int i = 0; i < num_events; i++) {
        int fd = events[i].data.fd;

        if (events[i].events & EPOLLIN) {
            // 可以从文件描述符 fd 读取数据
            // 处理读事件
        }

        // 还可以处理其他事件,如 EPOLLOUT、EPOLLERR 等
    }
}

close(epoll_fd); // 关闭 epoll 实例

这个示例中,我们创建了一个 epoll 实例,将一个文件描述符添加到 epoll 中并关注读事件。然后,在主循环中使用 epoll_wait() 等待事件发生,一旦事件发生,我们就可以根据事件类型执行相应的操作。最后,不要忘记在程序结束时关闭 epoll 实例。请注意,实际代码可能需要更多的错误处理和边界情况处理。

5、Select实现流程

理解select()的底层原理需要深入了解操作系统内核和用户程序之间的交互。下面将详细讲解select()函数的工作流程,包括操作系统的每一步骤:

  1. 用户程序准备

    • 用户程序通过调用select()函数来监视多个文件描述符的状态变化,同时传递一个超时时间。
    • 用户程序创建一个存储文件描述符的集合,通常是使用fd_set数据结构。它可以使用宏操作如FD_SET()来将要监视的文件描述符添加到集合中。(在每次调用 select 之前,用户程序需要遍历所有文件描述符,并使用 FD_SET 将要监视的文件描述符的位设置为 1。当 select 被调用时,它需要遍历整个 fd_set 位图,检查哪些位(文件描述符)发生了变化,这包括了就绪状态的文件描述符和超时状态。)
  2. 用户态到内核态

    • 当用户程序调用select()函数,它进入了内核态,这是因为select()是一个系统调用。
  3. 内核初始化

    • 内核收到select()系统调用后,会进行初始化操作。
    • 内核会检查用户程序传递的文件描述符集合,并将它们与相应的套接字、文件等资源关联起来,以便监视它们的状态变化。
  4. 等待事件

    • 内核会进入等待状态,等待文件描述符集合中的任何一个发生状态变化(可读、可写、出错)或者等待超时到达。
  5. 文件描述符就绪

    • 当有一个或多个文件描述符就绪时,内核会唤醒用户程序,将控制权返回给用户态。
  6. 用户程序响应

    • 用户程序接收到控制权后,可以使用一些工具函数如FD_ISSET()来确定哪些文件描述符就绪了。
    • 用户程序可以进一步处理就绪的文件描述符,进行读取或写入等操作。
  7. 超时处理

    • 如果在超时时间内没有文件描述符就绪,select()会返回0,用户程序可以根据这个信息来执行相应的操作。
  8. 用户态到内核态(再次)

    • 用户程序可能需要再次调用select()来继续等待文件描述符的状态变化,通常需要重新设置文件描述符集合和超时时间。
  9. 用户程序继续执行

    • 用户程序继续执行其他任务,或者再次进入等待状态等待下一次文件描述符状态变化。
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值