IO多路复用
IO多路转接
机制 | 监听方式 | 是否遍历所有 fd | 最大监听数 |
---|---|---|---|
select | fd_set 位图 |
每次遍历所有 fd,效率低 | 1024 (受 FD_SETSIZE 限制) |
epoll | 事件驱动 | 只遍历发生事件的 fd,效率高 | 无固定限制 |
while((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
if(write(STDOUT_FILENO, buf, n) != n)
err_sys(“wrtite error”);
需求:从两个描述符读, 可能会阻塞在一个读IO上,导致另外的描述符也不能读。也不知道到底哪个输入会得到数据
IO多路转接:构造一个感兴趣的描述符表,调用一个函数,直到其中一个描述符已经准备好,该函数才返回。 poll , select, pselect , 返回后 进程会得知哪些描述符已经准备好。 然后就可以正确调用IO read , write
Select
函数原型
1 | int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
参数 | 说明 |
---|---|
nfds | 表示三个描述符集最大值加1. |
readfds, writefds | 表示感兴趣的 读写。 每个描述符为一位。 |
exceptfds | 监听异常事件(如 OOB 数据) |
timeout | 超时控制,决定 select 阻塞时间,NULL 表示无限等待 |
返回值 | -1表示出错,0表示描述符都没有准备好(读集合中读不会阻塞,写集合写不会阻塞),大于0为准备好的描述符个数 |
Fd_set操作
文件描述符集合,存储需要监听的文件描述符。
宏 | 作用 |
---|---|
FD_ZERO(&set) | 清空 fd_set 集合 |
FD_SET(fd, &set) | 将 fd 添加到 fd_set |
FD_CLR(fd, &set) | 从 fd_set 中移除 fd |
FD_ISSET(fd, &set) | 检查 fd 是否在 fd_set 中(即是否准备好) |
使用示例
1 | #include <stdio.h> |
要点
- select 会修改 fd_set,需要每次重新设置。(因为会清除未发生变化的描述符)
- timeout每次也要重新设置
- 最多能监听102个描述符
epoll
高效的特点:
- 事件驱动:注册的fd,不会每次遍历所有fd。而是只关心发生变化的fd,epoll_wait也只返回有事件的fd,而不是select需要遍历
- 支持边缘触发ETL模式:减少重复通知
用法:
-
创建epoll实例:epoll_create1
1
2
3
4
5
6
7
8
int epfd = epoll_create1(0); // 推荐使用 epoll_create1
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
} -
添加、修改、删除
fd
:epoll_ctl()1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd: epoll实例
- op:操作
- EPOLL_CTL_ADD → 添加 fd
- EPOLL_CTL_MOD → 修改 fd监听事件
- EPOLL_CTL_DEL → 删除fd
- fd:要监听的文件描述符
- event:要监听的epoll事件
epoll_event:表示一个fd上的事件
存储监听的事件类型和与之关联的fd
1
2
3
4
5
6struct epoll_event {
uint32_t events; // 事件类型(EPOLLIN、EPOLLOUT、EPOLLERR、EPOLLET)
epoll_data_t data; // 存储用户数据(通常是文件描述符)
};事件类型:读、写、错误、ET模式
ET模式:只在fd发生状态改变时候通知触发。默认是LT模式
- LT:只要fd可读/可写,每次wait都会返回该fd
- ET:只在状态改变时候通知。
1
2
3
4
5
6
7
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;- ptr常指向一个数据结构体,当fd不够满足需求适合使用
- fd存储描述符
示例:给fd注册监听可读事件。
1
2
3
4
5
6
7
8struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = socket_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event) == -1) {
perror("epoll_ctl: EPOLL_CTL_ADD");
exit(EXIT_FAILURE);
} -
等待事件
1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数 作用 epfd
:epoll
实例的文件描述符。events
:用于存储触发的事件。 maxevents
:最多返回的事件数量。 timeout: -1
→ 阻塞等待0
→ 立即返回>0
→ 等待超时时间(毫秒)返回值 > 0
:触发的事件数量,events
数组里存有n
个就绪的fd
;0
:超时,无事件发生;-1
:错误,errno
设定错误类型。示例:wait返回有事件的fd的event
比如监听了 fd 1,3,4,5 。 其中1,4有事件,那么nfds返回2. events[0].data.fd=1, events[1].data.fd=2。即返回有事件fd的event,填充进events数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14struct epoll_event events[10]; // 存放触发的事件
int nfds = epoll_wait(epfd, events, 10, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
printf("fd %d 可读\n", events[i].data.fd);
}
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 张彦东的博客!