前言
select、poll 和 epoll 是 Linux 下用于多路复用 I/O(Input/Output)的系统调用,它们用于监视多个文件描述符,以查看哪个文件描述符上有可读、可写或发生了异常的事件。
1、select
select 是最早的多路复用 I/O 机制之一。它允许你监控多个文件描述符,以检测哪些文件描述符有事件发生。它通过修改一个位掩码来表示每个文件描述符的状态。
- 使用方法
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
fd_set read_fds;
int max_fd;
struct timeval timeout;
// 设置文件描述符集合
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
max_fd = STDIN_FILENO;
// 设置超时时间
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select");
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout occurred!\n");
} else {
if (FD_ISSET(STDIN_FILENO, &read_fds)) {
printf("Data is available on stdin.\n");
}
}
return 0;
}
- select的几个宏
/*
用于将文件描述符添加到文件描述符集合中
fd:要添加的文件描述符。
fdset:要修改的文件描述符集合。
*/
void FD_SET(int fd, fd_set *fdset);
/*
用于从文件描述符集合中删除指定的文件描述符
fd:要删除的文件描述符。
fdset:要修改的文件描述符集合。
*/
void FD_CLR(int fd, fd_set *fdset);
/*
用于检查文件描述符集合中是否包含指定的文件描述符
fd:要检查的文件描述符。
fdset:要检查的文件描述符集合。
*/
int FD_ISSET(int fd, fd_set *fdset);
/*
用于清空文件描述符集合
fdset:要清空的文件描述符集合。
*/
void FD_ZERO(fd_set *fdset);
- select函数的五个参数含义
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的最大文件描述符值加1。
readfds: 监控可读事件的文件描述符集合。
writefds: 监控可写事件的文件描述符集合。
exceptfds: 监控异常事件的文件描述符集合。
timeout: 超时时间,指定 select 需要等待的时间。如果为 NULL,则 select 会一直等待直到有事件发生。
- 优缺点分析
(1)优点
简单: select 的接口简单易用,广泛支持。
兼容性: 几乎所有 UNIX-like 操作系统都支持 select。
(2)缺点
文件描述符限制: 默认情况下,select 的文件描述符数量限制为 1024(可以通过重新编译内核或修改宏 FD_SETSIZE 增加,但不建议)。
性能瓶颈: 每次调用 select 都需要将文件描述符集合从用户态复制到内核态,这在文件描述符数量很大时可能会带来性能问题。
线性扫描: select 需要线性扫描文件描述符集合,效率较低。
2、poll
poll 是 select 的改进版,功能和 select 类似,但解决了一些 select 的局限性。poll 使用一个结构体数组来表示文件描述符及其相关的事件,而不是使用位掩码。
- 使用方法
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct pollfd fds;
int timeout = 5000; // 超时时间:5秒
fds.fd = STDIN_FILENO;
fds.events = POLLIN;
fds.revents = 0;
int ret = poll(&fds, 1, timeout);
if (ret == -1) {
perror("poll");
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout occurred!\n");
} else {
if (fds.revents & POLLIN) {
printf("Data is available on stdin.\n");
}
}
return 0;
}
- poll函数的参数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds: pollfd 结构体数组,每个结构体表示一个文件描述符及其事件。
nfds: pollfd 结构体数组的大小。
timeout: 超时时间,单位是毫秒。如果设置为 -1,poll 将无限期等待直到有事件发生;如果设置为 0,poll 将立即返回。
- pollfd结构体
struct pollfd {
int fd; // 文件描述符
short events; // 监控的事件类型
short revents; // 实际发生的事件
};
- 事件常量
POLLIN: 文件描述符可读(例如,有数据可读或连接可接受)。
POLLOUT: 文件描述符可写(例如,缓冲区有足够空间)。
POLLERR: 文件描述符发生错误。
POLLHUP: 文件描述符挂起(通常用于检测连接的关闭)。
POLLNVAL: 文件描述符无效(例如,关闭了的文件描述符)。
- 优缺点分析
(1)优点
文件描述符数量无上限: 没有像 select 那样的文件描述符数量限制。
灵活性: 可以同时监控多个文件描述符及其事件。
(2)缺点
性能瓶颈: 每次调用 poll 时都需要遍历 pollfd 数组,这在监控大量文件描述符时会带来性能开销。
3、epoll
epoll 是 Linux 专有的 I/O 多路复用机制,是对 select 和 poll 的进一步改进,特别适合处理大量并发连接。
- 使用方法
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_EVENTS 10
int main() {
int epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 假设fd是我们想要监控的文件描述符
int fd = /* ... */;
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 监听读事件,并设置为边缘触发模式
event.data.fd = fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
// 处理读事件
char buffer[512];
ssize_t count = read(events[i].data.fd, buffer, sizeof(buffer));
if (count == -1) {
perror("read");
exit(EXIT_FAILURE);
} else if (count == 0) {
// EOF
close(events[i].data.fd);
} else {
// 处理接收到的数据
printf("Read %zd bytes: %.*s\n", count, (int)count, buffer);
}
}
}
}
close(epfd);
return 0;
}
- epoll的几个函数
epoll_create1(int flags): 创建一个 epoll 实例。flags 参数可以是 0 或 EPOLL_CLOEXEC,它指示在 exec 系列系统调用时关闭文件描述符。
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event): 控制 epoll 实例,用于添加、修改或删除监控的文件描述符。
epfd: epoll 实例的文件描述符。
op: 操作类型,可以是 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或 EPOLL_CTL_DEL(删除)。
fd: 需要监控的文件描述符。
event: 指向 epoll_event 结构体的指针,该结构体包含了事件类型和关联的文件描述符。
epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout): 等待事件的发生。
epfd: epoll 实例的文件描述符。
events: 指向一个 epoll_event 数组的指针,内核将把发生事件的文件描述符信息填充到该数组中。
maxevents: events 数组的大小,即本次可以处理的最大事件数。
timeout: 等待超时的时间,以毫秒为单位。-1 表示无限等待,0 表示立即返回。
- struct epoll_event 结构体
struct epoll_event {
uint32_t events; /* epoll 事件类型 */
epoll_data_t data; /* 用户数据 */
};
events:
类型:uint32_t
用途: 此成员用于指定需要监听的事件类型以及当事件发生时要报告的事件类型。它可以是以下标志的一个或多个的组合(通过位或操作 | 来组合):
EPOLLIN: 表示对应的文件描述符上有数据可读(对于套接字来说,表示有数据到达)。
EPOLLOUT: 表示对应的文件描述符可以写入数据(对于套接字来说,表示可以发送数据)。
EPOLLRDHUP: 表示对方关闭了连接,或者半关闭。
EPOLLPRI: 表示对应的文件描述符有紧急数据可读(带外数据)。
EPOLLERR: 表示对应的文件描述符发生了错误。
EPOLLHUP: 表示对应的文件描述符挂起。
EPOLLET: 表示事件为边缘触发(Edge Triggered)。
EPOLLONESHOT: 表示事件只监听一次,当处理完这个事件后,需要重新添加到 epoll 实例中才能再次监听。
EPOLLEXCLUSIVE: 防止多个 epoll 实例同时监听相同的文件描述符时发生竞争。
data:
类型:epoll_data_t
用途: 用于存储与事件关联的用户数据。这个数据可以是一个文件描述符、一个指针,或者是一个 uint32_t 或 uint64_t 类型的值。它用于在事件发生时快速找到对应的上下文。
epoll_data_t 是一个联合体(union),其定义如下:
typedef union epoll_data {
void *ptr; /* 用户自定义指针 */
int fd; /* 文件描述符 */
uint32_t u32; /* 32位整型数据 */
uint64_t u64; /* 64位整型数据 */
} epoll_data_t;
ptr: 一个指向任意类型的指针,通常用于将自定义数据与事件关联起来。
fd: 一个文件描述符,通常用于标识哪个文件描述符触发了事件。
u32: 一个 32 位无符号整数。
u64: 一个 64 位无符号整数。
- 优缺点分析
(1)优点
支持大量文件描述符: epoll 可以处理的文件描述符数量几乎不受限制,而 select 通常只能处理 1024 个文件描述符(通过 FD_SETSIZE 限制)。对于大规模网络应用程序,epoll 的优势尤为明显。
事件通知机制: epoll 使用事件通知而不是轮询。它只会在文件描述符上有事件发生时通知应用程序,而不是像 select 和 poll 那样每次都要遍历所有文件描述符。
边缘触发与水平触发:epoll 支持两种触发模式:
水平触发(Level Triggered, LT): 这是 epoll 的默认模式,类似于 select 和 poll。只要文件描述符上有事件未被处理,epoll_wait 就会一直返回这些事件。
边缘触发(Edge Triggered, ET): 这种模式下,只有当文件描述符的状态发生变化(例如有新的数据到达)时,epoll_wait 才会返回事件。这种模式通常需要非阻塞 I/O 操作,以避免因为事件未被及时处理而导致的死锁。
高效性: epoll 在内核中使用红黑树和双向链表来管理事件,这使得在大量文件描述符的情况下,事件的添加、删除和等待操作都非常高效。
(2)缺点
epoll 只能在 Linux 系统上使用,不是跨平台的解决方案。
对于某些 I/O 密集型任务,尽管 epoll 性能优异,但开发和调试复杂度较高,尤其是在边缘触发模式下。
4、总结
select 适合处理少量文件描述符和需要跨平台支持的场景。它简单易用,但有文件描述符数量限制,并且在文件描述符数量多时性能较差。
poll 适合需要监控大量文件描述符的场景。它没有文件描述符数量限制,但在大量文件描述符时性能依然较差。
epoll 非常适合用于构建高性能的网络服务器,尤其是在需要处理大量并发连接的情况下。例如,像 Nginx、Redis 这样的高性能服务器程序,都使用了 epoll 来实现 I/O 多路复用。