IO多路复用的Select,Poll,Epoll

本文详细讲解了Java后端开发中,Epoll在IO多路复用中的优势,包括与Select和Poll的区别,以及其在高并发场景下的性能提升。重点介绍了Epoll的工作机制、事件驱动模型和边缘触发特性,适合理解并优化并发服务设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


JAVA后端开发知识总结(持续更新…)


IO多路复用的Select,Poll,Epoll



一、基本概念

   I/O多路复用(详细可见《五大I/O模型》)的本质是通过系统内核缓冲I/O数据,让单个进程可以监视多个文件描述符(Socket),一旦某个描述符就绪(读就绪或写就绪),就能够通知用户进程进行相应的读写操作。

  • 文件描述符

   UNIX、Linux中的一个概念。本身是一个非负整数,本质上是一个文件指针数组的索引,指向内核中一个结构体(进程所维护的该进程打开文件的记录表)。应用进程对文件的读写就通过对描述符的读写完成,当它打开一个文件或创建一个文件时,内核向进程返回一个文件描述符。

二、select()

2.1 函数简介

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

select函数参数说明

  • int maxfdp1:待测试的文件描述符个数,它的值是待测试的最大值加1。
  • fd_set *readset 、fd_set *writeset 、fd_set *exceptset:每个参数都是一个数组,存放文件描述符(即文件句柄)。分别指定了要让内核测试异常条件的文件描述符数组。
  • timeval *timeout:内核等待指定文件描述符集合中的任何一个就绪的超时时间

fd_set

  select()提供了一种 fd_set 的核心数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其它文件/命名管道/设备句柄)建立联系。当调用select()时,由内核根据IO状态修改 fd_set 的内容,由此来通知执行了select()的进程哪一个Socket或文件可读。

系统提供了4个宏对描述符集进行操作:

#include <sys/select.h>
#include <sys/time.h>

// 将fd加入set集合
void FD_SET(int fd, fd_set *fdset);
// 将fd从set集合中清除
void FD_CLR(int fd, fd_set *fdset);
// 检测fd是否在set集合中,不在则返回0
void FD_ISSET(int fd, fd_set *fdset);
// 将set清零使集合中不含任何fd
void FD_ZERO(fd_set *fdset);

2.2 select()总结

  从执行过程来看,使用基于select的IO多路复用和同步阻塞IO没有太大的区别,而且多了添加监视socket以及调用select函数的额外操作,按理说效率更低。但是,select()可以让用户可以在一个线程内同时处理多个socket的IO请求,用户可以注册多个感兴趣的socket,然后不断地调用select轮询被激活的socket,即可达到单个线程处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

  但是,select()本身也存在不少问题:

  • 每次调用select,都要把fd_set数组从用户态拷贝到内核态,开销较大。
  • 每次在内核都要遍历传递进来的所有fd_set,当fd_set很大时,会导致效率变低。
  • 系统内核对被监控的fd_set数组大小做了限制,并且是通过宏FD_SETSIZE控制的,大小通常限制为1024:
// 头文件中的定义
#ifndef FD_SETSIZE
#define FD_SETSIZE 1024
#endif
  • 可以在头文件中修改 FD_SETSIZE 来改变select()使用的文件描述符数组的大小,但是必须重新编译内核才能使修改生效。
  • select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符的IO操作,那么之后每次select()时还是会将这些文件描述符通知进程。

三、poll()

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

typedef struct pollfd {
        int fd;                         // 需要被检测或选择的文件描述符
        short events;                   // 对文件描述符fd上感兴趣的事件
        short revents;                  // 文件描述符fd上当前实际发生的事件
} pollfd_t;

poll函数参数说明

  • pollfd *fds:pollfd类型的数组,指向一个结构体数组的第0个元素,用于存放需要检测状态的socket描述符,并且调用poll函数之后fds数组不会被清空。

  • nfds_t nfds:数组 fds 中描述符的总数量。

  • timeout:超时时间。

  • pollfd:表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。

  • events:指定监视fd的事件(输入、输出、错误),是监视该文件描述符的事件掩码,由用户来设置。

  • revents:文件描述符的操作结果事件掩码,内核在调用返回时设置。
    在这里插入图片描述
    总结

  • poll()的机制与select()类似,都是管理多个描述符进行轮询,根据描述符的状态进行处理,但是poll()没有最大文件描述符数量的限制,解决了第三个问题。而且它提供了更多的事件类型,对描述符的复用比select()高。

  • 但是在执行过程中,包含大量文件描述符的数组依然会被整体从用户态复制到内核空间,而且内核也要遍历数组,对效率改善不大。

四、epoll()

  在高并发场景下,对于select()和poll(),进程间上下文切换的时间消耗,以及内核/用户空间大量的内存拷贝、数组轮询等操作消耗,是系统难以承受的。因此,需要一种新的IO多路复用的方法来实现并发服务程序。

  epoll()是基于事件驱动的I/O方式,是Linux内核为处理大批量文件描述符而作了改进的poll,其实现机制与select/poll机制完全不同。epoll()没有描述符个数限制,它使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的拷贝操作只需一次。

  epoll()通过在内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个操作部分。在Linux中,这三个部分对应的函数如下所示:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epoll_create:负责建立一个epoll对象,在epoll文件系统中为这个句柄对象分配资源。参数size表明内核要监听的描述符数量。
  • epoll_ctl:负责向内核的epoll对象中添加要监听的事件类型(文件描述符),已添加的描述符被维护在一颗红黑树上。
  • epoll_wait:负责收集已就绪事件的连接。

eventpoll

当进程调用epoll_create()方法时,Linux内核会创建一个eventpoll结构体:

struct eventpoll{
    ...
    // 红黑树的根节点,树中存储着所有添加到 epoll 中需要被监听的事件
    struct rb_root  rbr;
    // 双链表,存放着通过 epoll_wait() 返回的就绪事件
    struct list_head rdlist;
    ...
};

  每个epoll对象都有一个独立的eventpoll,用于存放通过epoll_ctl()添加进来的事件。这些事件维护在红黑树中,红黑树的插入时间效率是log(n)(n为树的高度)。

  此外,被监听的事件都会与设备驱动程序建立回调关系,每当有被监听的事件就绪,系统注册的回调函数就会被调用,将就绪事件放到rdList中,时间复杂度O(1)。
  当调用epoll_wait()时,无须遍历整个被侦听的描述符集,只需要遍历eventpoll对象中的rdlist双链表中是否有epitem元素即可。然后把就绪事件复制到用户态,同时将事件数量返回给用户。

在epoll中,对于每一个事件,都会建立一个epitem结构体:

struct epitem{
    struct rb_node  rbn;         // 红黑树节点
    struct list_head    rdllink; // 双向链表节点
    struct epoll_filefd  ffd;    // 事件句柄信息
    struct eventpoll *ep;        // 指向所属的eventpoll对象
    struct epoll_event event;    // 期待发生的事件类型
}

  epoll除了提供水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这使得用户空间程序有可能缓存IO状态,减少epoll_wait的调用,提高应用程序效率。

  • 水平触发(LT):默认工作模式,当epoll_wait检测到某描述符事件就绪并通知应用进程时,应用进程可以不立即处理该事件,下次调用epoll_wait时,会再次通知进程。
  • 边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用进程时,应用进程必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。这减少了同一事件的触发次数,使效率更高。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值