【IO模型】select、poll、epoll的区别

五种IO模型

I/O模型是操作系统中用于管理输入输出操作的机制。不同的I/O模型在处理I/O操作时,资源利用效率和响应速度各有不同。主要的I/O模型包括阻塞I/O非阻塞I/OI/O复用信号驱动I/O异步I/O

阻塞IO

在阻塞I/O模型中,系统调用(如read或recv)在等待I/O操作完成时,整个进程会被阻塞,直到数据被成功读入或写入。
阻塞状态下,进程不会做任何其他工作,CPU资源被挂起

特点:

  • 简单易用,代码编写和理解都较为直观
  • 效率低下,因为进程在等待I/O操作完成时无法执行其他任务,资源利用率不高。

非阻塞IO

非阻塞I/O模型使系统调用立即返回,而不管I/O操作是否完成。如果数据还没有准备好,系统调用会返回一个错误,进程可以在稍后再次尝试该操作。非阻塞I/O允许进程在等待I/O完成的同时执行其他任务,提高了CPU的利用率。
特点:

  • 提高了进程的并发性资源利用率,因为进程在等待I/O时可以执行其他操作。
  • 需要反复轮询检查 I/O状态,这种忙轮询(busy-waiting)可能导致CPU资源的浪费,特别是在I/O操作频繁或数据未就绪时。

信号驱动IO

在信号驱动I/O模型中,进程可以请求操作系统在I/O就绪时发出信号进程不必一直等待I/O操作的完成,而是继续执行其他任务,当I/O就绪时,操作系统通过发送信号通知进程进行处理
这减少了进程对I/O操作的等待时间,但需要处理信号。

特点:

  • 避免了忙轮询,提高了CPU利用率,同时进程可以处理其他任务。
  • 信号处理比较复杂,信号处理程序(Signal Handler)的设计和调试也相对困难。

IO复用

IO复用又叫多路复用模型,通过使用select、poll、epoll等系统调用,允许一个进程同时监控多个描述符(文件描述符)。一旦某个IO描述符就绪,进程就可以对其进行IO操作。

在这里插入图片描述
特点:

  • 允许单个进程同时处理多个I/O操作,资源开销较低,适合处理大量并发连接的场景
  • 对每次I/O操作都需要遍历所有的I/O描述符,尤其是在使用select或poll时,当描述符数量较多时,性能可能下降。

异步IO

异步I/O模型允许进程发起I/O操作后立即返回,而I/O操作在后台完成。操作系统会在I/O操作完成后,通知进程(通过回调函数、信号或状态变化)。
与其他模型不同的是,异步I/O模型中,I/O操作的完成不再由进程主动检查,而是由操作系统来管理和通知

特点:

  • 最高效的IO模型,进程完全不受IO操作的影响,可以处理其他业务,这也是为什么是异步的
  • 比较复杂,涉及异步回调或事件处理机制的设计,调试和维护较为困难

应用场景:高性能应用程序,比如高并发服务器,数据库系统等。

下面详细讲IO复用模型

IO复用的原理

select

函数原型

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中:

  • nfds:需要监控的文件描述符的范围,即监控的文件描述符集合的最大值+1
  • readfds:指向一个文件描述符集合(位图)的指针,用于表示哪些文件描述符可读
  • writefds:指向一个文件描述符集合的指针,用于表示哪些文件描述符可写
  • exceptfds:同样指向文件描述符集合,用于表示哪些文件描述符出现异常
  • timeout是一个指向timeval结构的指针,用于指向select调用的超时时间,其结构如下:
struct timeval {
    long tv_sec;  /* 秒 */
    long tv_usec; /* 微秒 */
};
  • 设置超时时间,可以将 timeout 设置为 NULL 以无限期等待,或者将 timeout 的 tv_sec 和 tv_usec 设置为 0 以立即返回(不等待)。

  • select函数返回值为就绪的文件描述符的数量,0表示超时,-1表示出错。

使用样例:

#include <sys/select.h>
#include <unistd.h> // for close
#include <stdio.h>
#include <sys/time.h> // for timeval
#include <string.h> // for memset

int main() {
    fd_set readfds;
    struct timeval tv;
    int retval;
    int fd = 0; // 通常是标准输入(stdin)

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

    // 设置超时时间
    tv.tv_sec = 5; // 5秒超时
    tv.tv_usec = 0; // 0微秒

    retval = select(fd + 1, &readfds, NULL, NULL, &tv);
    if (retval == -1) {
        perror("select()");
    } else if (retval) {
        printf("Data is available now.\n");
        // 使用 FD_ISSET 检查 fd 是否在集合中
        if (FD_ISSET(fd, &readfds)) {
            char buffer[1024];
            int n = read(fd, buffer, sizeof(buffer));
            buffer[n] = '\0';
            printf("Read: %s", buffer);
        }
    } else {
        printf("No data within five seconds.\n");
    }

    return 0;
}

select原理及缺点

select函数通过将所有的文件描述符集合传递给内核,然后内核检查这些描述符的状态,如果有描述符就绪,select返回。

select的主要问题在于它的调用效率较低,因为每次调用都需要遍历所有的文件描述符集合,并且select本身有文件描述符数量的限制(通常为1024或2048)

poll

函数原型

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • struct pollfd结构体定义如下:
struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 需要监视的事件 */
    short revents;  /* 实际发生的事件 */
};
  • events是需要监听的事件的掩码,可以是以下值的组合:

    • POLLIN:有数据可读。
    • POLLOUT:可以写入数据。
  • revents 是由 poll 填充的实际发生的事件。poll 返回时,你可以检查 revents 以确定哪些事件发生。

  • fds 是一个指向 pollfd 结构体数组的指针。每个 pollfd 结构体描述一个文件描述符和操作系统需要关心该文件描述符的事件,以及以及就绪的事件。

  • nfds 是 fds 数组的大小,即数组中包含的 pollfd 结构体的数量

  • timeout 以毫秒为单位指定等待事件发生的时间:

    • 如果 timeout 为负值,poll 将一直阻塞,直到有一个文件描述符的事件就绪。
    • 如果 timeout 为 0,poll 将立即返回,不等待任何事件。
  • 返回就绪的文件描述符的数量,如果超时返回0,出错返回-1。

使用样例:

#include <poll.h>
#include <unistd.h> // for close
#include <stdio.h>
#include <string.h> // for memset

int main() {
    struct pollfd fds[1];
    int timeout_msecs = 5000; // 5秒超时
    int ret;

    fds[0].fd = 0; // 通常是标准输入(stdin)
    fds[0].events = POLLIN; // 关注可读事件

    ret = poll(fds, 1, timeout_msecs);

    if (ret == -1) {
        perror("poll()");
    } else if (ret == 0) {
        printf("No data within five seconds.\n");
    } else {
        if (fds[0].revents & POLLIN) {
            printf("Data is available now.\n");
            char buffer[1024];
            int n = read(0, buffer, sizeof(buffer));
            buffer[n] = '\0';
            printf("Read: %s", buffer);
        }
    }

    return 0;
}

poll的原理及其缺点

poll与select类似,但它使用一个pollfd结构体数组来管理文件描述符和事件,避免了select中的文件描述符数量限制问题。poll也会遍历整个文件描述符数组,因此在描述符数量多时性能不佳。

epoll

epoll是Linux特有的I/O多路复用接口,分为三个主要操作函数:epoll_createepoll_ctlepoll_wait

epoll_create

创建一个epoll实例

int epoll_create(int size);
  • size:指定监听的文件描述符的最大数量
  • 返回一个epoll实例的文件描述符

epoll_ctl

控制epoll实例,注册、修改、删除监听的文件描述符

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:由epoll_create返回的epoll实例的文件描述符
  • op:操作类型,可以是
    • EPOLL_CTL_ADD(添加)
    • EPOLL_CTL_MOD(修改)
    • EPOLL_CTL_DEL(删除)
  • fd:需要监控的文件描述符
  • event 是一个指向 epoll_event 结构体的指针,用于描述你对 fd 的监视需求。其结构定义如下:
struct epoll_event {
    uint32_t events; /* epoll 事件 */
    epoll_data_t data; /* 用户数据 */
};
  • events 是一个事件掩码,通常是以下值的组合:
    • EPOLLIN:有数据可读
    • EPOLLOUT:可以写入数据
    • EPOLLET:启用边缘触发
  • data 是一个联合体(union),允许你存储与文件描述符相关的用户数据。通常存文件描述符。

epoll_wait

获取就绪事件

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • pfd 是由 epoll_create 返回的 epoll 实例的文件描述符
  • events 是一个指向 epoll_event 结构体数组的指针,用于存储发生的事件(输出型参数)。
  • maxevents 是你准备处理的最大事件数,也就是 events 数组的大小
  • timeout 以毫秒为单位指定等待事件发生的时间:负数表示一直阻塞,直到有事件就绪。0表示无论是否有事件发生都会立刻返回。

epoll的原理

在这里插入图片描述

epoll的底层会创建一颗红黑树,是epoll_create函数的结果。epoll_create返回的文件描述符中对应的struct file结构体中有一个指针指向eventpoll 结构体,这个结构体指向一颗红黑树以及一个双链表

更具体的,每当使用 epoll_ctl 添加或修改一个文件描述符时,epoll 内部会将该文件描述符的各种信息创建一个结构体epitem对象里表示一个事件,插入到一个红黑树中。也就是说, epoll_ctl 函数的参数op实际上指的是红黑树节点的添加删除和修改红黑树的节点就是epitem对象。

这种设计使得 epoll 在处理大量文件描述符时,即使文件描述符数量非常大,增删改操作的效率仍然保持较高(红黑树的特性)

  • 每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是 lgn,其中 n 为树的高度).
  • 所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,一个文件描述符的状态发生变化(如可读、可写等),内核会触发一个回调函数,将该文件描述符标记为就绪并将其加入到就绪队列中
  • 这个就绪队列本质就是一个双链表rdllist
  • 所以,调用epoll_wait函数其实就是在这个就绪队列里面获取节点,如果rdllist不为空就说明有关心的事件就绪了,同时就将事件拷贝到参数指针struct epoll_event *events中带给用户。

水平触发和边缘触发

水平触发又叫LT工作模式,边缘触发又叫ET模式。
这两种工作模式在处理就绪的文件描述符就绪事件的行为不同。

  • 水平触发:只要文件描述符的状态满足条件(如可读、可写),epoll_wait 就会一直返回该文件描述符的事件。这意味着如果文件描述符可读,epoll_wait 会持续通知应用程序,直到应用程序读取了所有可用的数据为止。epoll的默认工作模式就是水平触发。
  • 边缘触发:只有在文件描述符的状态从未就绪变为就绪时,epoll_wait 才会通知应用程序。这意味着如果应用程序没有处理完事件,而文件描述符状态没有变化,epoll_wait 不会再次返回该事件。所以,该模式下处理就绪事件一次就必须全部处理完,否则就会丢失事件

总结:

  • select和poll只支持LT工作模式,而epoll俩种都支持
  • 边缘触发更高效,避免了重复处理事件,但需要更谨慎的设计和实现
  • 水平触发可能会产生不必要的重复处理,通知效率低

epoll的优点

  • 接口使用方便,三个函数使用简单
  • 关心的文件描述符没有数量限制
  • 通过事件回调、红黑树、双链表,避免了遍历获得就绪事件提高了效率·。
  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值