C 语言多路复用
外观
C语言多路复用[编辑 | 编辑源代码]
多路复用(Multiplexing)是网络编程中一种高效处理多个I/O流的机制,允许单个进程/线程同时监控多个文件描述符(如套接字)。在C语言中,主要通过select、poll和epoll三种系统调用实现。
核心概念[编辑 | 编辑源代码]
多路复用解决的核心问题是:如何避免为每个连接创建独立线程/进程带来的资源消耗。其数学模型可表示为:
典型应用场景包括:
- 聊天服务器
- 实时数据采集系统
- HTTP代理服务器
工作原理对比[编辑 | 编辑源代码]
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):仅当状态变化时通知一次
最佳实践建议[编辑 | 编辑源代码]
1. 小规模连接(<1000):poll/select更便携 2. 大规模高并发:优先选择epoll 3. 边缘触发需配合非阻塞I/O使用 4. 注意处理EAGAIN/EWOULDBLOCK错误
页面模块:Message box/ambox.css没有内容。
多路复用不解决所有问题,CPU密集型任务仍需配合多线程/进程 |
扩展思考[编辑 | 编辑源代码]
- 与多线程模型的性能对比
- 在不同操作系统上的移植性考虑(kqueue, IOCP)
- 与异步I/O(AIO)的关系与区别