跳转到内容

C 语言多路复用

来自代码酷

模板:Note

C语言多路复用[编辑 | 编辑源代码]

多路复用(Multiplexing)是网络编程中一种高效处理多个I/O流的机制,允许单个进程/线程同时监控多个文件描述符(如套接字)。在C语言中,主要通过selectpollepoll三种系统调用实现。

核心概念[编辑 | 编辑源代码]

多路复用解决的核心问题是:如何避免为每个连接创建独立线程/进程带来的资源消耗。其数学模型可表示为: 效率=有效事件数系统调用次数

典型应用场景包括:

  • 聊天服务器
  • 实时数据采集系统
  • HTTP代理服务器

工作原理对比[编辑 | 编辑源代码]

flowchart TD A[阻塞I/O] -->|单线程阻塞| B(低效) C[多线程I/O] -->|线程切换开销| D(高内存占用) E[多路复用] -->|事件驱动| F(高效低耗)

select系统调用[编辑 | 编辑源代码]

最古老的多路复用实现,通过位掩码监控文件描述符集合。

函数原型[编辑 | 编辑源代码]

#include <sys/select.h>

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

示例:监控标准输入[编辑 | 编辑源代码]

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

int main() {
    fd_set readfds;
    struct timeval timeout;
    
    while(1) {
        FD_ZERO(&readfds);
        FD_SET(0, &readfds); // 监控stdin
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        int ret = select(1, &readfds, NULL, NULL, &timeout);
        if (ret == -1) {
            perror("select");
        } else if (ret) {
            if(FD_ISSET(0, &readfds)) {
                printf("Data available on stdin\n");
                char buf[256];
                fgets(buf, sizeof(buf), stdin);
                printf("You typed: %s", buf);
            }
        } else {
            printf("Timeout occurred\n");
        }
    }
    return 0;
}

输出示例:

(等待5秒)
Timeout occurred
(用户输入"hello"后回车)
Data available on stdin
You typed: hello

select的局限性[编辑 | 编辑源代码]

  • 文件描述符数量限制(通常1024)
  • 每次调用需重新设置fd_set
  • 线性扫描所有描述符

poll系统调用[编辑 | 编辑源代码]

改进select的设计,使用链表结构避免数量限制。

函数原型[编辑 | 编辑源代码]

#include <poll.h>

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

结构体定义[编辑 | 编辑源代码]

struct pollfd {
    int   fd;         // 文件描述符
    short events;     // 监控的事件
    short revents;    // 实际发生的事件
};

epoll系统调用[编辑 | 编辑源代码]

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);

性能对比表[编辑 | 编辑源代码]

方法 时间复杂度 最大fd数 触发方式
select O(n) FD_SETSIZE 水平触发
poll O(n) 无限制 水平触发
epoll O(1) 系统限制 水平/边缘触发

实际案例:简易聊天服务器[编辑 | 编辑源代码]

以下展示使用epoll的服务器核心逻辑:

#define MAX_EVENTS 10

struct epoll_event ev, events[MAX_EVENTS];
int epoll_fd = epoll_create1(0);

// 添加监听socket到epoll
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ev);

while(1) {
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    for(int i = 0; i < nfds; i++) {
        if(events[i].data.fd == listen_sock) {
            // 处理新连接
            int conn_sock = accept(listen_sock, NULL, NULL);
            ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
            ev.data.fd = conn_sock;
            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_sock, &ev);
        } else {
            // 处理客户端数据
            char buf[1024];
            int len = read(events[i].data.fd, buf, sizeof(buf));
            if(len > 0) {
                // 广播给所有客户端
                for(每个已连接客户端) {
                    write(client_fd, buf, len);
                }
            }
        }
    }
}

触发模式详解[编辑 | 编辑源代码]

  • 水平触发(LT):只要文件描述符就绪就会持续通知
  • 边缘触发(ET):仅当状态变化时通知一次

stateDiagram-v2 [*] --> 未就绪 未就绪 --> 就绪: 数据到达(ET/LT) 就绪 --> 未就绪: 数据读完(仅ET) 就绪 --> 就绪: 仍有数据(LT持续通知)

最佳实践建议[编辑 | 编辑源代码]

1. 小规模连接(<1000):poll/select更便携 2. 大规模高并发:优先选择epoll 3. 边缘触发需配合非阻塞I/O使用 4. 注意处理EAGAIN/EWOULDBLOCK错误

页面模块:Message box/ambox.css没有内容。

扩展思考[编辑 | 编辑源代码]

  • 与多线程模型的性能对比
  • 在不同操作系统上的移植性考虑(kqueue, IOCP)
  • 与异步I/O(AIO)的关系与区别