嵌入式学习Day34
多路 I/O 的概念
多路 I/O(Multiplexed I/O)是一种高效管理多个输入/输出流的技术,允许单个线程或进程同时监控多个文件描述符(如套接字、管道等),并在它们就绪时进行读写操作。核心目标是避免为每个 I/O 操作创建独立线程或进程的开销,提升系统资源利用率。
IO模型:
1、阻塞IO
2、非阻塞IO EAGAIN 忙等待 errno
3、信号驱动IO SIGIO 用的相对少(了解)
4、并行模型 进程,线程
5, IO多路复用 select、poll、epoll
阻塞IO ===》最常用 默认设置。
非阻塞IO ===》在阻塞IO的基础上调整其为不再阻塞等待。在程序执行阶段调整文件的执行方式为非阻塞:===》fcntl() ===>动态调整文件的阻塞属性。
信号驱动io :文件描述符需要追加 O_ASYNC 标志。设备有io事件可以执行时,内核发送SIGIO信号。
并发:进程、线程。
select 函数概述(轮询)
select 是一种多路 I/O 复用机制,允许程序监视多个文件描述符的状态(可读、可写、异常),直到一个或多个文件描述符就绪。它是 Unix/Linux 系统中的系统调用,常用于处理非阻塞 I/O。
select 函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds: 监视的文件描述符最大值加 1。
- readfds: 监视可读的文件描述符集合。
- writefds: 监视可写的文件描述符集合。
- exceptfds: 监视异常的文件描述符集合。
- timeout: 超时时间。
文件描述符集合操作
select 使用 fd_set
结构体管理文件描述符集合,常用操作宏如下:
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加文件描述符到集合
FD_CLR(int fd, fd_set *set); // 从集合中移除文件描述符
FD_ISSET(int fd, fd_set *set); // 检查文件描述符是否在集合中
select 的使用步骤
1.初始化文件描述符集合
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
2.设置超时时间(可选)
struct timeval timeout;
timeout.tv_sec = 5; // 5 秒
timeout.tv_usec = 0;
3.调用 select
int ret = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (ret == -1) {perror("select error");
} else if (ret == 0) {printf("Timeout\n");
} else {if (FD_ISSET(sockfd, &read_fds)) {// 处理可读事件}
}
select 的优缺点
优点
- 跨平台支持(Unix/Linux/Windows)。
- 超时机制灵活。
缺点
select的调用一般要注意几点:
① readfds等是指针结果参数,会被函数修改,所以一般会另外定义一个allread_fdset,保持全部要监听读的句柄,将它的拷贝传递给select函数,返回可读的句柄集合,类型fdset支持赋值运算符=;
② 要注意计算nfds,当新增监听句柄时比较容易修改,当减少监听句柄时较麻烦些,如果要精确修改需要遍历或者采用最大堆等数据结构维护这个句柄集,以方便的找到第二大的句柄,或者干脆在减少监听句柄时不管nfds;
③ timeout如果为NULL表示阻塞等,如果timeout指向的时间为0,表示非阻塞,否则表示select的超时时间;
④ select返回-1表示错误,返回0表示超时时间到没有监听到的事件发生,返回正数表示监听到的所有事件数(包括可读,可 写,异常),通常在处理事件时 会利用这个返回值来提高效率,避免不必要的事件触发检查。(比如总共只有一个事件,已经在可读集合中处理了一个事件,则可写和异常就没必要再去遍历句柄集 判断是否发生事件了);
⑤ Linux的实现中select返回时会将timeout修改为剩余时间,所以重复使用timeout需要注意。
select的缺点在于:
① 由于描述符集合set的限制,每个set最多只能监听FD_SETSIZE(在Linux上是1024)个句柄(不同机器可能不一样);
② 返回的可读集合是个fdset类型,需要对所有的监听读句柄一一进行FD_ISSET的测试来判断是否可读;
③ nfds的存在就是为了解决select的效率问题(select遍历nfds个文件描述符,判断每个描述符是否是自己关心的,对关心的描述符判断是否发生事件)。但是解决不彻底,比如如果只监听0和1000两个句柄,select需要遍历1001个句柄来检查事件。
select 适用场景
- 需要监视少量文件描述符。
- 需要跨平台兼容性。
- 超时精确控制要求不高。
epoll函数概述(主动上报)
epoll
是Linux系统中用于高效处理大量文件描述符(FD)的I/O多路复用机制,属于select
/poll
的增强版。它通过事件驱动的方式监控多个文件描述符的状态变化,适合高并发场景。
epoll的核心函数
epoll
主要涉及三个系统调用:
1. epoll_create
创建epoll
实例,返回一个文件描述符(epoll FD)。
int epoll_create(int size); // size仅作历史兼容,现代内核忽略
2. epoll_ctl
向epoll
实例注册、修改或删除监控的文件描述符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op
参数:EPOLL_CTL_ADD
:注册新的FD。EPOLL_CTL_MOD
:修改已注册的FD。EPOLL_CTL_DEL
:删除FD。
3. epoll_wait
等待事件触发,返回就绪的事件列表。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events
:输出参数,存储就绪事件。timeout
:超时时间(毫秒),-1
表示阻塞,0
表示非阻塞。
事件类型(epoll_event
)
struct epoll_event {uint32_t events; // 监听的事件类型(位掩码)epoll_data_t data; // 用户数据(通常包含fd)
};
- 常见事件标志:
EPOLLIN
:数据可读。EPOLLOUT
:数据可写。EPOLLET
:启用边缘触发(ET)模式(默认是水平触发LT)。
触发模式
- 水平触发(LT):只要FD处于就绪状态,
epoll_wait
会持续通知。 - 边缘触发(ET):仅当FD状态变化时通知一次,需非阻塞读写直至
EAGAIN
。
性能优势
- 时间复杂度:
select
/poll
:O(n),需遍历所有FD。epoll
:O(1),仅返回就绪的FD。
- 适用场景:
- 连接数多但活跃度低的场景(如Web服务器)。
注意事项
- 使用ET模式时,必须将FD设置为非阻塞(
O_NONBLOCK
)。 epoll
仅适用于Linux系统,其他系统需使用kqueue
(BSD)或IOCP
(Windows)。