《UNIX网络编程卷1:套接字联网API》第6章 IO复用:select和poll函数

发布于:2025-04-06 ⋅ 阅读:(22) ⋅ 点赞:(0)

《UNIX网络编程卷1:套接字联网API》第6章 I/O复用:select和poll函数


6.1 I/O复用的核心价值与适用场景

I/O复用是高并发网络编程的基石,允许单个进程/线程同时监控多个文件描述符(套接字)的状态变化,从而高效处理多个客户端请求。其核心价值在于:

  • 资源高效利用:避免为每个连接创建独立线程/进程的内存与调度开销;
  • 事件驱动模型:仅在数据可读/可写时触发处理逻辑,减少空转;
  • 实时性保障:及时响应多个连接的并发事件。

典型应用场景

  • Web服务器(如Nginx):处理数千并发HTTP连接;
  • 实时通信系统(如IM):同时管理多个客户端的长连接;
  • 嵌入式网关:资源受限设备中处理多路传感器数据。

6.2 select函数深度解析
6.2.1 select函数原型与参数
#include <sys/select.h>

int select(int nfds, 
           fd_set *restrict readfds,
           fd_set *restrict writefds,
           fd_set *restrict exceptfds,
           struct timeval *restrict timeout);

参数详解

  1. nfds:需监控的最大文件描述符+1(优化内核遍历效率);
  2. readfds/writefds/exceptfds:分别监控可读、可写、异常事件的文件描述符集合;
  3. timeout:超时时间(NULL为阻塞,0为非阻塞,>0为限时等待)。

返回值

  • >0:就绪的文件描述符总数;
  • 0:超时无事件;
  • -1:错误(如被信号中断)。
6.2.2 文件描述符集合操作宏
void FD_ZERO(fd_set *set);       // 清空集合
void FD_SET(int fd, fd_set *set);// 添加描述符到集合
void FD_CLR(int fd, fd_set *set);// 从集合移除描述符
int FD_ISSET(int fd, fd_set *set);// 检查描述符是否就绪
6.2.3 select工作流程
  1. 初始化集合:使用FD_ZEROFD_SET设置需监控的描述符;
  2. 调用select:阻塞或等待事件发生;
  3. 遍历检查:通过FD_ISSET轮询所有描述符,处理就绪事件;
  4. 重置集合:select会修改传入的集合,需在每次调用前重新初始化。

代码示例:基于select的TCP服务器框架

fd_set read_set, all_set;
int maxfd = listenfd; // 监听套接字
FD_ZERO(&all_set);
FD_SET(listenfd, &all_set);

for (;;) {
    read_set = all_set; // 每次select调用前重置集合
    int nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);

    if (FD_ISSET(listenfd, &read_set)) { // 新连接到达
        int connfd = Accept(listenfd, NULL, NULL);
        FD_SET(connfd, &all_set);
        maxfd = (connfd > maxfd) ? connfd : maxfd;
    }

    for (int fd = listenfd + 1; fd <= maxfd; fd++) { // 检查客户端连接
        if (FD_ISSET(fd, &read_set)) {
            // 处理客户端请求(如read/write)
        }
    }
}
6.2.4 select的局限性
  1. 文件描述符上限FD_SETSIZE(通常1024)限制最大监控数量;
  2. 线性扫描效率低:每次需遍历所有描述符,时间复杂度O(n);
  3. 内核态内存复制:每次调用需将集合从用户态复制到内核态;
  4. 无法动态扩展:无法在运行中添加/移除描述符,需重新初始化集合。

6.3 poll函数:改进的I/O复用机制
6.3.1 poll函数原型与参数
#include <poll.h>

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

参数解析

  1. fds:指向pollfd结构数组,每个元素描述一个监控的描述符;
  2. nfds:数组长度(即监控的描述符数量);
  3. timeout:超时时间(毫秒,-1为阻塞,0为非阻塞)。

pollfd结构体

struct pollfd {
    int   fd;       // 文件描述符
    short events;   // 监控的事件(POLLIN、POLLOUT等)
    short revents;  // 返回的事件(由内核填充)
};
6.3.2 poll事件标志
  • POLLIN:数据可读(包括TCP连接断开时的EOF);
  • POLLPRI:紧急数据可读(如TCP带外数据);
  • POLLOUT:数据可写;
  • POLLERR:错误发生(自动监控,无需设置);
  • POLLHUP:连接挂起(如对端关闭)。
6.3.3 poll工作流程
  1. 初始化pollfd数组:设置每个元素的fdevents
  2. 调用poll:等待事件发生;
  3. 遍历检查revents:处理就绪的描述符;
  4. 动态维护数组:可动态添加或移除描述符。

代码示例:基于poll的TCP服务器框架

#define MAX_CLIENTS 1024
struct pollfd client_fds[MAX_CLIENTS];
int nfds = 1; // 初始只有监听套接字

client_fds[0].fd = listenfd;
client_fds[0].events = POLLIN;

for (;;) {
    int nready = poll(client_fds, nfds, -1);

    if (client_fds[0].revents & POLLIN) { // 新连接到达
        int connfd = Accept(listenfd, NULL, NULL);
        client_fds[nfds].fd = connfd;
        client_fds[nfds].events = POLLIN;
        nfds++;
    }

    for (int i = 1; i < nfds; i++) { // 遍历客户端连接
        if (client_fds[i].revents & POLLIN) {
            // 处理客户端请求
            if (read返回0) { // 客户端关闭连接
                Close(client_fds[i].fd);
                client_fds[i] = client_fds[nfds-1]; // 数组末尾元素覆盖当前
                nfds--;
                i--; // 重新检查当前位置
            }
        }
    }
}
6.3.4 poll的优势与不足

优势

  • 无描述符数量限制:仅受系统资源约束;
  • 动态管理:可随时添加/移除监控的描述符;
  • 更精细的事件控制:支持更多事件类型(如带外数据)。

不足

  • 仍为线性扫描:时间复杂度O(n);
  • 水平触发模式:未处理事件会持续通知(可能引起忙等)。

6.4 select与poll对比分析
特性 select poll
描述符上限 FD_SETSIZE(通常1024) 仅受系统限制
事件类型 仅读、写、异常 支持更多事件(如POLLPRI)
效率 低(O(n)遍历) 低(O(n)遍历)
内存拷贝 每次调用需复制整个集合 仅传递数组指针
可扩展性 静态集合,需重新初始化 动态数组,可随时修改
跨平台兼容性 所有UNIX系统 多数UNIX系统(Linux、BSD)

选型建议

  • 嵌入式系统:优先poll(动态管理更灵活);
  • 高并发场景:推荐epoll(第7章详解);
  • 跨平台需求:select兼容性更好。

6.5 性能优化与陷阱规避
6.5.1 select性能优化
  1. 减少nfds值:仅传递当前最大描述符+1;
  2. 分离监控集合:将频繁活动的描述符单独监控;
  3. 避免阻塞操作:在事件处理函数中使用非阻塞I/O。
6.5.2 poll常见陷阱
  1. 未重置revents:每次调用poll前需清空revents或重新初始化结构体;
  2. 数组越界:动态添加描述符时需检查数组上限;
  3. 忽略POLLHUP:未处理可能导致死循环。

代码示例:非阻塞read处理

// 设置套接字为非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
    // 处理数据
} else if (n == 0) {
    // 对端关闭连接
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
    // 数据未就绪,继续监控
} else {
    // 其他错误处理
}

6.6 实战:基于select的聊天服务器
6.6.1 功能需求
  • 支持多客户端连接;
  • 客户端消息广播至所有其他客户端;
  • 客户端退出时自动清理资源。
6.6.2 核心代码实现
#include "unp.h"

#define MAX_CLIENTS 1024
int client_fds[MAX_CLIENTS];

void broadcast(int sender_fd, char *msg, int len) {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (client_fds[i] != -1 && client_fds[i] != sender_fd) {
            Writen(client_fds[i], msg, len);
        }
    }
}

int main() {
    int listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    // 绑定与监听(代码同前)

    fd_set all_set, read_set;
    FD_ZERO(&all_set);
    FD_SET(listenfd, &all_set);
    int maxfd = listenfd;

    memset(client_fds, -1, sizeof(client_fds));

    for (;;) {
        read_set = all_set;
        int nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);

        if (FD_ISSET(listenfd, &read_set)) {
            int connfd = Accept(listenfd, NULL, NULL);
            // 将新连接加入数组
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_fds[i] == -1) {
                    client_fds[i] = connfd;
                    FD_SET(connfd, &all_set);
                    maxfd = (connfd > maxfd) ? connfd : maxfd;
                    break;
                }
            }
        }

        for (int i = 0; i < MAX_CLIENTS; i++) {
            int fd = client_fds[i];
            if (fd == -1) continue;

            if (FD_ISSET(fd, &read_set)) {
                char buf[1024];
                ssize_t n = Read(fd, buf, sizeof(buf));
                if (n > 0) {
                    broadcast(fd, buf, n);
                } else {
                    Close(fd);
                    FD_CLR(fd, &all_set);
                    client_fds[i] = -1;
                }
            }
        }
    }
}

6.7 调试工具与性能分析
6.7.1 使用strace跟踪系统调用
strace -e select,poll,read,write ./server

输出示例

select(5, [3 4], NULL, NULL, NULL) = 1 (in [3])
read(3, "hello", 1024)             = 5
6.7.2 监控文件描述符状态
# 查看进程打开的文件描述符
ls -l /proc/<pid>/fd
6.7.3 性能压测工具
# 使用netcat模拟多客户端
for i in {1..1000}; do
    nc 127.0.0.1 9999 &
done

6.8 本章小结与进阶习题

小结:本章深入解析了select和poll的原理、使用场景与优化技巧,通过实战案例展示了高并发服务器的实现方法。

习题

  1. 实现基于poll的聊天服务器,支持昵称注册与私聊功能;
  2. 对比select与poll在1000并发连接下的CPU使用率差异;
  3. 扩展select服务器支持可写事件监控,实现大文件传输。

说明

  1. Linux下更详细select函数内容,可参见博主相关博文Linux下select使用
  2. 嵌入式下如何高效使用select与epoll,可参见博主相关专题博文在嵌入式Linux中实现高并发TCP服务器:从select到epoll的演进与实战
  3. Linux下更详细epoll函数内容,可参见博主相关博文Linux下epoll函数使用详解

付费用户专属资源

  • 完整聊天服务器代码工程(含Makefile);
  • select/poll性能对比测试报告;
  • 扩展阅读:《从select到epoll:Linux I/O模型的演进》。

通过本章学习,读者将掌握I/O复用的核心技术,并能够开发高并发的网络应用。


网站公告

今日签到

点亮在社区的每一天
去签到