多路io
1. select
- 最大监听 1024 个 fd(内核宏限制)
- 每次调用都要把整个 fd 集合从用户态拷贝到内核态
- 内核通过遍历所有 fd 找就绪的
- 水平触发(LT)
- 可移植性最好(几乎所有系统支持)
返回值:
>0:就绪的 fd 总数
0:超时
-1:出错(errno)
使用步骤:
定义 fd_set
FD_ZERO 清空
FD_SET 把需要监听的 fd 加进去
调用 select
用 FD_ISSET 遍历判断哪个就绪
需知道的套配宏
cpp
FD_ZERO(&set); // 清空集合
FD_SET(fd, &set); // 把 fd 加入监听
FD_CLR(fd, &set); // 从集合删除 fd
FD_ISSET(fd, &set); // 判断 fd 是否就绪
cpp
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_ZERO(fd_set *);
FD_SET(int fd, fd_set *);
FD_ISSET(int fd, fd_set *);
cpp
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
int main() {
fd_set rfds;
struct timeval tv;
int ret;
while (1) {
// 1. 清空集合
FD_ZERO(&rfds);
// 2. 添加要监听的 fd
FD_SET(0, &rfds);
tv.tv_sec = 1;
tv.tv_usec = 0;
// 3. 监听:最大fd+1,读集合,超时
ret = select(1, &rfds, NULL, NULL, &tv);
if (ret == -1) {
perror("select");
break;
} else if (ret == 0) {
printf("timeout...\n");
continue;
}
// 4. 判断是否就绪
if (FD_ISSET(0, &rfds)) {
char buf[128];
read(0, buf, 127);
printf("select 读到: %s", buf);
}
}
return 0;
}
2. poll
- 没有 1024 数量限制
- 原理和 select 几乎一样:仍遍历、仍全量拷贝
- 只是数据结构换成链表,去掉上限
- 水平触发(LT)
返回值:
>0:就绪 fd 数量
0:超时
-1:错误
使用步骤:
定义 struct pollfd 数组
填入 fd 和 events
调用 poll
遍历数组,判断 revents & POLLIN 等
cpp
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd;
short events; // POLLIN/POLLOUT
short revents; // 内核返回
};
cpp
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
int main() {
struct pollfd fds[1];
fds[0].fd = 0;
fds[0].events = POLLIN; // 监听可读
while (1) {
// 监听 1 个 fd,超时 1000ms
int ret = poll(fds, 1, 1000);
if (ret == -1) {
perror("poll");
break;
} else if (ret == 0) {
printf("timeout...\n");
continue;
}
// 判断返回事件
if (fds[0].revents & POLLIN) {
char buf[128];
read(0, buf, 127);
printf("poll 读到: %s", buf);
}
}
return 0;
}
3. epoll(高性能王者)
- Linux 特有
- 三个 API:
epoll_create/epoll_ctl/epoll_wait - 内核使用红黑树 管理 fd,只在添加 / 删除时拷贝一次
- 用回调机制 ,只返回就绪的 fd,不用遍历
- 支持 LT(水平) + ET(边缘) 两种触发方式
- 高并发下性能碾压 select/poll
返回值:
>0:就绪 fd 个数
0:超时
-1:错误
使用步骤:
epoll_create 创建句柄
构建 epoll_event,调用 epoll_ctl(ADD) 加入 fd
循环 epoll_wait 等待事件
直接遍历返回的就绪数组,不用全扫
cpp
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);
cpp
#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>
int main() {
int epfd = epoll_create(1);
struct epoll_event ev, events[1];
ev.events = EPOLLIN;
ev.data.fd = 0;
// 添加监听
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev);
while (1) {
// 等待事件,最多返回 1 个,超时 1000ms
int n = epoll_wait(epfd, events, 1, 1000);
if (n == -1) {
perror("epoll_wait");
break;
} else if (n == 0) {
printf("timeout...\n");
continue;
}
if (events[0].data.fd == 0) {
char buf[128];
read(0, buf, 127);
printf("epoll 读到: %s", buf);
}
}
return 0;
}
4、性能对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 监听上限 | 1024 | 无 | 无 |
| 遍历方式 | 遍历全部 fd | 遍历全部 fd | 只返回就绪 fd |
| 数据拷贝 | 每次都全量拷贝 | 每次都全量拷贝 | 仅增删时拷贝 |
| 效率 | O(n) | O(n) | O(1) |
| 平台 | 全平台 | 全平台 | Linux 专属 |
| 触发方式 | LT | LT | LT + ET |