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多路复用能够同时监视多个客户端的连接请求和数据传输,而不必为每个客户端创建一个独立的线程或进程。
应用场景:
1
select()系统调用
select() 是一种执行 I/O 多路复用操作的系统调用,可以让程序同时监视多个文件描述符的状态变化,从而实现高效的 I/O 操作。
调用 select() 会阻塞进程,直到某个文件描述符变为就绪状态(可以读或写)。
其函数原型如下所示:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数详解:
在 select() 函数中,readfds、writefds 和 exceptfds 都是指向 fd_set 类型的指针,它们代表文件描述符集合。
fd_set 数据类型内部实现是一个位掩码,用于存储多个文件描述符,但用户无需了解其具体实现细节。
Linux 提供了四个宏来操作这些文件描述符集合:
例如:
fd_set fset;
FD_ZERO(&fset); // 初始化集合
FD_SET(3, &fset); // 添加文件描述符3
FD_SET(4, &fset); // 添加文件描述符4
FD_SET(5, &fset); // 添加文件描述符5
返回值详解:
使用 select() 的注意事项:
以下是一个简单的示例,展示如何使用 select() 来检测标准输入的可读状态:
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() 在很多场景下都很有用,但也有其局限性:
2
poll()系统调用
poll()系统调用提供了一种执行I/O多路复用的方式,与select()类似,但在接口和用法上有所不同。
poll()使用一个struct pollfd类型的数组来监视文件描述符的就绪状态。
它的原型如下所示:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数解释:
pollfd结构体定义如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
events和revents字段支持多种标志,以下列出常见的标志及其说明:
这些标志可以通过位或操作组合,例如events = POLLIN | POLLOUT,表示同时监视可读和可写事件。
下面的示例展示了如何使用poll()来监视文件描述符的可读事件:
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()的优点和局限:
注意事项:
3
epoll系统调用
epoll是一种高效的I/O多路复用机制,专为处理大量文件描述符而设计,是poll和select的改进版本。
epoll的主要优点在于其对大规模并发连接的性能支持和事件通知的效率提升。
epoll在Linux内核2.5.44版本后引入,并且只在Linux系统上可用。
epoll采用事件驱动模型,它由三个主要的系统调用组成:
3.1、epoll_create / epoll_create1系统调用
这些函数用于创建一个新的epoll实例。
int epoll_create(int size);
参数:size参数是一个提示值,用于指定初始的文件描述符个数,但实际上它已经被废弃,不再有实际意义。
int epoll_create1(int flags);
参数:flags参数通常可以为0或EPOLL_CLOEXEC,后者设置文件描述符的close-on-exec标志。
成功时,这些函数返回一个新的epoll文件描述符,失败时返回-1。
3.2、epoll_ctl系统调用
用于管理epoll实例中的文件描述符。函数原型如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
struct epoll_event定义如下:
struct epoll_event {
uint32_t events; /* 需要监听的事件 */
epoll_data_t data; /* 关联的用户数据 */
};
参数说明:
3.3、epoll_wait系统调用
用于等待文件描述符的事件。函数原型如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
返回值为触发的事件数量,-1表示发生错误。
下面展示了一个使用epoll监视标准输入的基本例子:
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的优势:
适用场景:
总结来说,epoll比select和poll更高效,适合大规模I/O并发的应用场景,且提供灵活的事件控制能力。