【C++基本语法】Linux IO多路复用select, poll, epoll特性对比

本篇介绍Linux三种IO复用接口,主要对比其特性,非接口使用和原理解析。

1. 对比

接口优点和使用场景缺点底层实现/原因
select1. 可移植性比epoll好
2.适用于并发场景较少或初学者使用
1. fd限制。监测的文件描述符有上限(1024),可以用FD_SETSIZE改变,但会增加开销
2. 需要频繁在用户态和内核态之间复制fd集合,fd多时效率低
3.每次调用都要遍历整个文件描述符合集
底层用fd数组,每次调用都需要将数组从用户态拷贝到内核态
poll1. 无fd数量限制
1. 内核检查时仍需遍历,并发高时效率低
2. 频繁增删fd时,效率可能不如select
原理与select类似,用链表代替数组
epoll1. 无fd数量限制
2. 仅在fd状态变化时通知应用,减少不必要的检查(O(1))
3. epoll_ctl注册fd后,不需要在每次调用时复制fd集合,内存拷贝开销小
4. 支持水平触发(LT)和边缘触发(ET)

适用于:需要同时处理大量并发连接的场景,如负载均衡器、代理服务器等
缺点说的人很少,缺陷有一篇文章参考Epoll的缺陷底层使用红黑树+双向链表

2. select/poll 运行流程

这篇文章IO多路复用里的动图做的很好(现在它是我的了,hh)
select原理

3. Epoll优越性的原理(参考xiaolincoding)

select/poll 的问题主要是用线性结构去保存fd合集,因此内核检查时都需要遍历,复杂度为O(n),n较大时,效率下降明显。

epoll通过下面两点解决这个问题

  1. 使用红黑树保存进程所有待检测的fd。红黑树增删查的复杂度都是O(logn),所以每次操作时只需要传入待检测的socket,减少数据拷贝。
  2. 使用事件驱动机制,内核里使用一个链表来记录就绪事件。当事件发生时,通过回调将其加入该链表,当用户调用 epoll_wait() 函数时,只会返回有事件发生的fd的个数,无需轮询整个fd合集,极大提高效率。
    epoll原理

4. 边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)

水平触发LT- 只要fd就绪(可读或可写),如果可读,epoll_wait会持续醒来,直到所有数据被读完;(如果socket可写,应用程序写完后应移除EPOLLOUT事件,否则epoll_wait仍会触发)
边缘触发ET- 当发生可读事件时,epoll_wait只苏醒一次,即使应用程序没用从内核读出或者读完数据,后续也不会苏醒,所以要尽量一次读完缓冲区数据。

select, poll 只有LT模式,epoll默认也是水平触发LT,可以切换成ET。

注意点:

  • 使用LT模式,没必要一次读完,后续还会收到通知
  • 使用ET模式,应尽量配合非阻塞IO。因为ET模式下,事件发生后应用会尽量读写数据,如果fd是阻塞的,当读完时,进程会阻塞住。如果fd是非阻塞的,读写完后read和write返回错误EAGIN/EWOULDBLOCK.

5. 示例:用IO多路复用实现监测多个异步连接

#include <vector>
#include <sys/poll.h>

// ...

std::vector<int> clientfds; // 存储多个客户端套接字

// 循环创建多个非阻塞套接字并尝试连接
for (int i = 0; i < num_connections; ++i) {
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1) {
        // 错误处理...
        continue;
    }

    // 设置套接字为非阻塞模式
    // ...

    // 尝试连接服务器
    struct sockaddr_in serveraddr;
    // 初始化 serveraddr
    // ...

    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) {
        if (errno == EINPROGRESS) {
            // 连接正在进行,添加到监测列表
            clientfds.push_back(clientfd);
        } else {
            // 真正的错误,关闭套接字
            close(clientfd);
            // 错误处理...
        }
    }
}

// 为所有非阻塞连接设置pollfd数组
std::vector<pollfd> pollfds;
for (int fd : clientfds) {
    pollfd event;
    event.fd = fd;
    event.events = POLLOUT;
    pollfds.push_back(event);
}

// 设置超时时间
int timeout = 3000;

// 使用poll监测所有连接
int ret = poll(pollfds.data(), pollfds.size(), timeout);

if (ret > 0) {
    for (size_t i = 0; i < pollfds.size(); ++i) {
        if (pollfds[i].revents & POLLOUT) {
            // 检查连接状态
            int err;
            socklen_t len = sizeof(err);
            if (getsockopt(pollfds[i].fd, SOL_SOCKET, SO_ERROR, &err, &len) == 0 && !err) {
                std::cout << "Connection " << pollfds[i].fd << " established successfully." << std::endl;
            } else {
                std::cout << "Connection " << pollfds[i].fd << " failed." << std::endl;
            }
            close(pollfds[i].fd); // 关闭套接字
        } else if (pollfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
            // 发生错误,处理错误
            std::cout << "Connection " << pollfds[i].fd << " error occurred." << std::endl;
            close(pollfds[i].fd); // 关闭套接字
        }
    }
} else if (ret == 0) {
    // 超时
    std::cout << "Connection attempt timed out." << std::endl;
} else {
    // poll出错
    std::cout << "poll failed." << std::endl;
}

// ...

遗留问题

所有人都说poll和select几乎一样。poll的fd合集是链表,它存在于用户空间,调用时究竟是复制了整个fd合集,还是只传递了指针给内核,内核检查的时候不是直接遍历的用户空间的fd合集吗?

参考文档

【1】https://blog.csdn.net/qq_34827674/article/details/115619261
【2】https://blog.csdn.net/v123411739/article/details/124699602?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171405243016800213061387%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=171405243016800213061387&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-124699602-null-null.142v100pc_search_result_base4&utm_term=IO%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8&spm=1018.2226.3001.4187

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值