2.9 Linux I/O多路复用机制:select、poll、epoll详解

一、select

基本原理:通过轮询方式检查文件描述符集合。

特点

  • 有最大文件描述符数量限制(FD_SETSIZE,通常1024)。
  • 每次调用需要重新设置fd_set,select( )会破坏 fd_set,只保留就绪的 fd,因此必须每次重新设置
  • 线性扫描所有fd,即使只有一个 fd 需要就绪,效率随 fd 数量增加而下降。
  • 每次调用都会全量拷贝 fd_set 两次,一次 用户态 -> 内核态,一次 内核态 -> 用户态。
  • 如果第一遍内核轮询发现没有 fd 就绪会睡眠,当有 fd 就绪会再次唤醒进程,内核会再次轮询一遍,之后返回到用户态。

使用场景:跨平台兼容性要求高,fd 数量少的场景。

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); // 监控标准输入

struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
// -1 表示调用失败;0 表示超时,没有任何文件描述符就绪;>0 表示文件描述符就绪的数量
// 不填时间会无限等待

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>

#define MAX_CLIENTS 30
#define BUFFER_SIZE 1024
#define PORT 8080

int main() {
    int server_fd, new_socket, client_sockets[MAX_CLIENTS];
    fd_set readfds;
    int max_sd, activity, i, valread, sd;
    struct sockaddr_in address;
    char buffer[BUFFER_SIZE] = {0};
    
    // 初始化客户端socket数组
    for (i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = 0;
    }
    
    // 创建服务器socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    
    // 设置socket选项(避免地址占用错误)
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    
    // 绑定socket到端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    
    // 开始监听
    if (listen(server_fd, 5) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    
    printf("Server listening on port %d...\n", PORT);
    
    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&readfds);
        
        // 添加服务器socket到集合
        FD_SET(server_fd, &readfds);
        max_sd = server_fd;
        
        // 添加客户端sockets到集合
        for (i = 0; i < MAX_CLIENTS; i++) {
            sd = client_sockets[i];
            
            if (sd > 0) {
                FD_SET(sd, &readfds);
            }
            
            if (sd > max_sd) {
                max_sd = sd;
            }
        }
        
        // 等待活动(无限等待)
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        
        if ((activity < 0) && (errno != EINTR)) {
            perror("select error");
        }
        
        // 检查是否有新的连接
        if (FD_ISSET(server_fd, &readfds)) {
            if ((new_socket = accept(server_fd, NULL, NULL)) < 0) {
                perror("accept");
                continue;
            }
            
            printf("New connection, socket fd: %d\n", new_socket);
            
            // 添加新socket到客户端数组
            for (i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    printf("Adding to list of sockets as %d\n", i);
                    break;
                }
            }
            
            if (i == MAX_CLIENTS) {
                printf("Too many clients\n");
                close(new_socket);
            }
        }
        
        // 检查客户端socket的活动
        for (i = 0; i < MAX_CLIENTS; i++) {
            sd = client_sockets[i];
            
            if (FD_ISSET(sd, &readfds)) {
                // 读取客户端数据
                if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {
                    // 客户端断开连接
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&address);
                    printf("Client disconnected, ip %s, port %d\n", 
                           inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                    
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    // 回显数据给客户端
                    buffer[valread] = '\0';
                    printf("Received: %s\n", buffer);
                    send(sd, buffer, strlen(buffer), 0);
                }
            }
        }
    }
    
    return 0;
}

二、poll

基本原理:使用 pollfd 结构数组代替 fd_set 位图,没有最大fd数量限制

特点:

  • 没有最大 fd 数量限制(受系统限制);
  • 不需要每次调用重新设置监控集合;
  • 仍然是线性扫描,效率问题依然存在。

使用场景:fd数量较多但不需要高性能的场景。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>

#define MAX_CLIENTS 100
#define BUFFER_SIZE 1024
#define PORT 8080
#define TIMEOUT 30000  // 30秒超时

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 创建pollfd数组
    struct pollfd fds[MAX_CLIENTS + 1];  // +1 给服务器socket
    int nfds = 1;  // 初始只有服务器socket
    int current_size;
    int i, ret;

    // 初始化pollfd数组
    for (i = 0; i < MAX_CLIENTS + 1; i++) {
        fds[i].fd = -1;  // 初始化为无效fd
        fds[i].events = 0;
        fds[i].revents = 0;
    }

    // 创建服务器socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置socket选项(避免地址占用错误)
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // 绑定socket到端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(server_fd, 5) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    // 设置服务器socket
    fds[0].fd = server_fd;
    fds[0].events = POLLIN;  // 监控可读事件

    while (1) {
        // 调用poll等待事件
        ret = poll(fds, nfds, TIMEOUT);

        if (ret < 0) {
            perror("poll error");
            break;
        } else if (ret == 0) {
            printf("Poll timeo

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++/嵌入式开发 秋招面经 文章被收录于专栏

一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。

全部评论

相关推荐

03-03 23:12
已编辑
北京邮电大学 Java
书海为家:我来给一点点小建议,因为毕竟还在学校不像工作几年的老鸟有丰富的项目经验,面试官在面试在校生的时候更关注咱们同学的做事逻辑和思路,所以最好在简历中描述下自己做过项目的完整过程,比如需求怎么来的,你对需求的解读,你想到的解决办法,遇到困难如何找人求助,最终项目做成了什么程度,你从中收获了哪些技能,你有什么感悟。
你的简历改到第几版了
点赞 评论 收藏
分享
刚刷到字节跳动官方发的消息,确实被这波阵仗吓了一跳。在大家还在纠结今年行情是不是又“寒冬”的时候,字节直接甩出了史上规模最大的转正实习计划——ByteIntern。咱们直接看几个最硬的数,别被花里胡哨的宣传词绕晕了。首先是“量大”。全球招7000多人是什么概念?这几乎是把很多中型互联网公司的总人数都给招进来了。最关键的是,这次的资源分配非常精准:研发岗给了4800多个Offer,占比直接超过六成。说白了,字节今年还是要死磕技术,尤其是产品和AI领域,这对于咱们写代码的同学来说,绝对是今年最厚的一块肥肉。其次是大家最关心的“转正率”。官方直接白纸黑字写了:整体转正率超过50%。这意味着只要你进去了,不划水、正常干,每两个人里就有一个能直接拿校招Offer。对于2027届(2026年9月到2027年8月毕业)的同学来说,这不仅是实习,这简直就是通往大厂的快捷通道。不过,我也得泼盆冷水。坑位多,不代表门槛低。字节的实习面试出了名的爱考算法和工程实操,尤其是今年重点倾斜AI方向,如果你简历里有和AI相关的项目,优势还是有的。而且,转正率50%也意味着剩下那50%的人是陪跑的,进去之后的考核压力肯定不小。一句话总结:&nbsp;27届的兄弟们,别犹豫了。今年字节这是铁了心要抢提前批的人才,现在投递就是占坑。与其等到明年秋招去千军万马挤独木桥,不如现在进去先占个工位,把转正名额攥在手里。
喵_coding:别逗了 50%转正率 仔细想想 就是转正与不转正
字节7000实习来了,你...
点赞 评论 收藏
分享
评论
3
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务