嵌入式Linux:I/O多路复用
Linux中的I/O多路复用是指一种同时监控多个文件描述符的机制,允许程序在不阻塞的情况下等待多个I/O事件。
I/O多路复用主要通过select、poll和epoll这三种系统调用来实现,应用程序可以监视多个文件描述符的状态变化,如读、写或异常状态。
多路复用的核心在于通过一个系统调用处理多个I/O请求,减少了进程的切换和阻塞,提高了效率。
在传统的I/O模型中,当进程需要从某个文件描述符(如网络套接字、文件、管道等)读取数据时,通常会进入阻塞状态,直到数据就绪。
这种模型适用于单个I/O操作,但在需要处理多个I/O源时,使用阻塞模式会导致效率低下,因为一个I/O的阻塞会导致整个应用程序被挂起。
I/O多路复用可以避免这种问题,使得应用程序能够同时处理多个I/O事件。
例如,在一个高并发的网络服务器中,I/O多路复用能够同时监视多个客户端的连接请求和数据传输,而不必为每个客户端创建一个独立的线程或进程。
应用场景:
- 高并发服务器:I/O多路复用特别适合于需要同时处理大量连接的服务器,如HTTP服务器、WebSocket服务器等。
- 实时数据处理:在数据流处理(如日志处理、数据采集)应用中,可以通过多路复用来高效地管理多个数据源的输入。
- 图形用户界面:GUI程序中也可以利用多路复用来处理多个用户事件,如键盘输入、鼠标点击和窗口更新。
1
select()系统调用
select() 是一种执行 I/O 多路复用操作的系统调用,可以让程序同时监视多个文件描述符的状态变化,从而实现高效的 I/O 操作。
调用 select() 会阻塞进程,直到某个文件描述符变为就绪状态(可以读或写)。
其函数原型如下所示:
代码语言:javascript代码运行次数:0运行复制int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数详解:
- nfds:该参数为需要监视的文件描述符的范围,通常设置为所有被监视文件描述符的最大值加 1。文件描述符从 0 开始递增,所以需要将最大文件描述符编号加 1。
- readfds:指向文件描述符集合的指针,用于检测是否有文件描述符变为可读状态。如果某个文件描述符可读时,select() 会返回该文件描述符的就绪状态。
- writefds:指向文件描述符集合的指针,用于检测是否有文件描述符变为可写状态。如果某个文件描述符可写时,select() 会返回该文件描述符的就绪状态。
- exceptfds:指向文件描述符集合的指针,用于检测是否有文件描述符出现异常(如带外数据)。不是文件描述符发生错误,而是用于检测某些非常规的情况。
- timeout:用于设定 select() 的阻塞行为。它是一个指向 struct timeval 结构体的指针,包含两个成员:秒和微秒。如果 timeout 被设置为 NULL,select() 将一直阻塞直到有文件描述符变为就绪。如果所有文件描述符都没有就绪,且超过了 timeout 指定的时间上限,select() 将返回 0。
在 select() 函数中,readfds、writefds 和 exceptfds 都是指向 fd_set 类型的指针,它们代表文件描述符集合。
fd_set 数据类型内部实现是一个位掩码,用于存储多个文件描述符,但用户无需了解其具体实现细节。
Linux 提供了四个宏来操作这些文件描述符集合:
- FD_ZERO(fd_set *set):初始化文件描述符集合,将其清空。
- FD_SET(int fd, fd_set *set):将文件描述符 fd 添加到集合中。
- FD_CLR(int fd, fd_set *set):从集合中移除文件描述符 fd。
- FD_ISSET(int fd, fd_set *set):检查文件描述符 fd 是否在集合中,返回 true 表示在集合中。
例如:
代码语言:javascript代码运行次数:0运行复制fd_set fset;
FD_ZERO(&fset); // 初始化集合
FD_SET(3, &fset); // 添加文件描述符3
FD_SET(4, &fset); // 添加文件描述符4
FD_SET(5, &fset); // 添加文件描述符5
返回值详解:
- 返回值为 -1:表示发生错误,并会设置 errno。常见的错误包括:
- EBADF:集合中的某个文件描述符无效。
- EINTR:系统调用被信号中断。
- EINVAL:nfds 参数非法。
- ENOMEM:系统内存不足。
- 返回值为 0:表示在 timeout 指定的时间内没有文件描述符就绪。
- 返回正整数:表示有一个或多个文件描述符就绪。返回值为就绪文件描述符的数量。
使用 select() 的注意事项:
- 文件描述符集合的最大容量限制:fd_set 的容量由常量 FD_SETSIZE 限制。在 Linux 系统中,默认值为 1024。如果需要监视的文件描述符数量超过 1024 个,应该考虑使用 poll() 或 epoll()。
- 重复调用 select() 时,需重新初始化文件描述符集合:在每次调用 select() 之前,必须重新初始化和设置文件描述符集合,否则 select() 的结果可能不正确。
- 处理超时的两种方式:timeout 设为 NULL,表示无限阻塞直到有文件描述符就绪。设置 timeout 指向的 struct timeval 的两个成员变量都为 0,表示非阻塞模式,即立即返回结果。
以下是一个简单的示例,展示如何使用 select() 来检测标准输入的可读状态:
代码语言:javascript代码运行次数:0运行复制int main() {
fd_set readfds;
struct timeval timeout;
int ret;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); // 监视标准输入(文件描述符为0)
timeout.tv_sec = 5; // 超时时间为5秒
timeout.tv_usec = 0;
ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select error");
} else if (ret == 0) {
printf("Timeout: No data within 5 seconds.\n");
} else {
if (FD_ISSET(STDIN_FILENO, &readfds)) {
printf("Data is available on standard input.\n");
}
}
return 0;
}
在这个例子中,select() 会监视标准输入的可读状态,并等待最多 5 秒。如果在此期间有数据可读,则返回成功,否则返回超时。
尽管 select() 在很多场景下都很有用,但也有其局限性:
- 性能问题:当监视大量文件描述符时,select() 的效率较低,因为需要对每个文件描述符进行遍历。
- 文件描述符数量限制:FD_SETSIZE 限制了最多可以监视的文件描述符数量。
- 重复初始化:每次调用都需要重新初始化文件描述符集合。
2
poll()系统调用
poll()系统调用提供了一种执行I/O多路复用的方式,与select()类似,但在接口和用法上有所不同。
poll()使用一个struct pollfd类型的数组来监视文件描述符的就绪状态。
它的原型如下所示:
代码语言:javascript代码运行次数:0运行复制int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数解释:
- fds:指向struct pollfd类型的数组,每个元素表示一个文件描述符和其关注的事件。
- nfds:指定数组中元素的数量。类型nfds_t是无符号整型。
- timeout:用于决定poll()的阻塞行为,单位为毫秒,具体规则如下:
- timeout = -1:一直阻塞,直到一个文件描述符就绪或捕获到信号(类似于select()中timeout为NULL的情形)。
- timeout = 0:非阻塞调用,只检查一次文件描述符的状态。
- timeout > 0:阻塞至多timeout毫秒,若超时则返回。
pollfd结构体定义如下:
代码语言:javascript代码运行次数:0运行复制struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
- fd:文件描述符。设置为负数可以忽略此文件描述符。
- events:表示我们关心的事件类型,使用位掩码的方式。
- revents:由内核设置,表示实际发生的事件。
events和revents字段支持多种标志,以下列出常见的标志及其说明:
这些标志可以通过位或操作组合,例如events = POLLIN | POLLOUT,表示同时监视可读和可写事件。
下面的示例展示了如何使用poll()来监视文件描述符的可读事件:
代码语言:javascript代码运行次数:0运行复制int main() {
struct pollfd fds[1];
fds[0].fd = 0; // 标准输入
fds[0].events = POLLIN; // 监视可读事件
int timeout = 5000; // 超时5秒
int ret = poll(fds, 1, timeout);
if (ret == -1) {
perror("poll");
return 1;
} else if (ret == 0) {
printf("超时,没有数据可读。\n");
} else {
if (fds[0].revents & POLLIN) {
printf("标准输入有数据可读。\n");
}
}
return 0;
}
poll()的优点和局限:
- 优点:poll()可以同时监视大量的文件描述符,数组形式的struct pollfd更容易动态调整文件描述符的集合。
- 局限:与select()类似,poll()的性能在处理大量文件描述符时也会下降,因为它需要线性扫描整个数组来寻找就绪的文件描述符。
注意事项:
- poll()的返回值指示就绪的文件描述符数量,但必须通过检查revents字段来了解具体事件。
- 如果在poll()中将文件描述符设置为负值,则该条目将被忽略,适合在运行时动态调整监视列表。
3
epoll系统调用
epoll是一种高效的I/O多路复用机制,专为处理大量文件描述符而设计,是poll和select的改进版本。
epoll的主要优点在于其对大规模并发连接的性能支持和事件通知的效率提升。
epoll在Linux内核2.5.44版本后引入,并且只在Linux系统上可用。
epoll采用事件驱动模型,它由三个主要的系统调用组成:
- epoll_create或epoll_create1:创建epoll实例。
- epoll_ctl:向epoll实例中添加、删除或修改文件描述符。
- epoll_wait:等待事件的发生,并返回就绪的文件描述符列表。
3.1、epoll_create / epoll_create1系统调用
这些函数用于创建一个新的epoll实例。
代码语言:javascript代码运行次数:0运行复制int epoll_create(int size);
参数:size参数是一个提示值,用于指定初始的文件描述符个数,但实际上它已经被废弃,不再有实际意义。
代码语言:javascript代码运行次数:0运行复制int epoll_create1(int flags);
参数:flags参数通常可以为0或EPOLL_CLOEXEC,后者设置文件描述符的close-on-exec标志。
成功时,这些函数返回一个新的epoll文件描述符,失败时返回-1。
3.2、epoll_ctl系统调用
用于管理epoll实例中的文件描述符。函数原型如下:
代码语言:javascript代码运行次数:0运行复制int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
- epfd:由epoll_create返回的epoll实例文件描述符。
- op:操作类型,可以是以下三个值之一:
- EPOLL_CTL_ADD:添加新的文件描述符到epoll实例。
- EPOLL_CTL_MOD:修改已存在的文件描述符的事件。
- EPOLL_CTL_DEL:从epoll实例中删除文件描述符。
- fd:要操作的文件描述符。
- event:指向epoll_event结构体的指针,用于指定关心的事件。
struct epoll_event定义如下:
struct epoll_event {
uint32_t events; /* 需要监听的事件 */
epoll_data_t data; /* 关联的用户数据 */
};
参数说明:
- events:表示感兴趣的事件类型,可以是以下一个或多个标志的组合:
- EPOLLIN:数据可读。
- EPOLLOUT:数据可写。
- EPOLLERR:错误发生。
- EPOLLET:启用边缘触发(Edge Triggered)模式。只在状态变化时通知,更高效但需要一次性处理所有数据。
- EPOLLONESHOT:事件触发一次后自动移除。
- data:可以是文件描述符或自定义的数据,用于标识事件来源。
3.3、epoll_wait系统调用
用于等待文件描述符的事件。函数原型如下:
代码语言:javascript代码运行次数:0运行复制int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
- epfd:epoll实例文件描述符。
- events:指向epoll_event结构体数组的指针,用于存储就绪的事件。
- maxevents:events数组的大小,表示最多返回的事件个数。
- timeout:指定等待的超时时间,单位为毫秒。
- timeout = -1:无限期等待,直到有事件发生。
- timeout = 0:立即返回,不阻塞。
返回值为触发的事件数量,-1表示发生错误。
下面展示了一个使用epoll监视标准输入的基本例子:
代码语言:javascript代码运行次数:0运行复制#define MAX_EVENTS 5
int main() {
int epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.events = EPOLLIN; // 监视可读事件
event.data.fd = STDIN_FILENO; // 标准输入
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
perror("epoll_ctl: STDIN_FILENO");
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
int timeout = 10000; // 10秒超时
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == STDIN_FILENO) {
printf("标准输入有数据可读。\n");
}
}
close(epfd);
return 0;
}
epoll的优势:
- 性能高:通过内核事件通知机制,避免了遍历整个文件描述符集合。
- 边缘触发支持:能提高高并发情况下的事件处理效率。
适用场景:
- 网络服务器,尤其是高并发的场景。
- 大量I/O操作的多任务系统。
总结来说,epoll比select和poll更高效,适合大规模I/O并发的应用场景,且提供灵活的事件控制能力。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-18,如有侵权请联系 cloudcommunity@tencent 删除嵌入式linux集合事件系统发布者:admin,转转请注明出处:http://www.yc00.com/web/1747612078a4670349.html
评论列表(0条)