Linux应用:select、poll
什么是阻塞IO和非阻塞IO
阻塞 IO
在阻塞 I/O 模型中,当应用程序调用一个 I/O 操作(如读取文件、从网络套接字接收数据等)时,该应用程序会被阻塞,直到 I/O 操作完成。例如,一个进程调用read系统调用从磁盘读取数据,在数据从磁盘传输到内核缓冲区并复制到用户空间之前,进程会一直处于等待状态,无法执行其他任务。这种模型简单直接,但在等待 I/O 完成的期间,进程资源被浪费,无法进行其他有用的工作。
非阻塞 IO
非阻塞 I/O 允许应用程序在发起 I/O 操作后,立即返回,而不是等待操作完成。应用程序可以继续执行其他任务,然后通过轮询(不断检查 I/O 操作是否完成)或者其他方式(如事件通知)来确定 I/O 操作的状态。例如,当一个套接字设置为非阻塞模式后,调用recv函数读取数据,如果此时没有数据可读,recv函数会立即返回一个错误码(如EWOULDBLOCK),而不会阻塞进程。应用程序可以在返回后执行其他计算任务,然后再次尝试读取数据。非阻塞 I/O 提高了程序的并发处理能力,但需要更多的编程复杂性来管理 I/O 操作的状态和轮询逻辑。
O_NONBLOCK和fcntl
O_NONBLOCK
O_NONBLOCK是一个标志位,用于在打开文件或创建套接字等 I/O 操作时设置非阻塞模式。当一个文件描述符被设置为O_NONBLOCK模式后,对该文件描述符的 I/O 操作(如read、write等)将不会阻塞进程。如果 I/O 操作不能立即完成,系统调用会立即返回,通常返回一个表示资源暂时不可用的错误码(如EWOULDBLOCK)。
fcntl
fcntl函数是一个用于操作文件描述符的通用函数。它可以用于改变已打开文件的属性,其中包括设置或清除O_NONBLOCK标志位。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <cerrno>
int main() {
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
close(fd);
exit(EXIT_FAILURE);
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL");
close(fd);
exit(EXIT_FAILURE);
}
// 现在fd处于非阻塞模式
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("非阻塞读取:资源暂时不可用\n");
} else {
perror("read");
}
} else if (n > 0) {
printf("读取到 %zd 字节: %.*s\n", n, (int)n, buffer);
} else {
printf("没有数据可读\n");
}
close(fd);
return 0;
}
打开一个名为 test.txt 的文件,接着把该文件描述符设置成非阻塞模式,最后关闭文件描述符。
select
select是一种 I/O 多路复用技术,它允许一个进程同时监控多个文件描述符的可读、可写和异常事件。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将一直阻塞,直到有事件发生;如果设置为 0,select将立即返回,不等待任何事件。
在调用select函数时,进程会阻塞,直到以下情况之一发生:
监控的文件描述符集合中有一个或多个文件描述符发生了可读、可写或异常事件。
超时时间到达。
接收到一个信号并中断select调用。
当select返回后,应用程序需要检查各个文件描述符集合,以确定哪些文件描述符发生了相应的事件,然后对这些文件描述符进行相应的 I/O 操作。select的优点是跨平台性好,几乎所有的操作系统都支持;缺点是文件描述符集合的大小有限制(通常为 1024),并且每次调用select都需要将文件描述符集合从用户空间复制到内核空间,效率较低。
查看鼠标:
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <cerrno>
#define BUFFER_SIZE 1024
int main() {
fd_set readfds;
struct timeval timeout;
char buffer[BUFFER_SIZE];
int mouse_fd;
// 打开鼠标设备文件
mouse_fd = open("/dev/input/mouse0", O_RDONLY );
if (mouse_fd == -1) {
perror("Failed to open mouse device");
return 1;
}
while (1) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 将标准输入和鼠标设备添加到集合中
FD_SET(STDIN_FILENO, &readfds);
FD_SET(mouse_fd, &readfds);
// 设置超时时间
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 找出最大的文件描述符
int max_fd = (mouse_fd > STDIN_FILENO)? mouse_fd : STDIN_FILENO;
// 调用 select 函数
int activity = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
if (activity < 0) {
perror("select error");
close(mouse_fd);
return 1;
} else if (activity == 0) {
printf("No input received within 5 seconds.\n");
} else {
if (FD_ISSET(STDIN_FILENO, &readfds)) {
// 读取标准输入
ssize_t bytes = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("You entered: %s", buffer);
}
}
if (FD_ISSET(mouse_fd, &readfds)) {
struct input_event ev;
ssize_t bytes = read(mouse_fd, buffer, BUFFER_SIZE);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("You entered mouse: %s", buffer);
}
if (bytes == -1 && errno != EAGAIN) { // 处理错误情况,除了 EAGAIN(非阻塞下无数据)
perror("read from mouse device error");
}
}
}
}
// 关闭鼠标设备文件
close(mouse_fd);
return 0;
}
stdlib.h:标准库,包含通用工具函数。
sys/select.h:提供select函数,用于多路复用输入输出。
unistd.h:包含了许多 UNIX 标准的系统调用,如read、write、close等。
fcntl.h:用于文件控制,如open函数。
linux/input.h:提供 Linux 系统下输入设备相关的结构体和常量。
cerrno:用于处理错误码。
struct timeval timeout:定义一个时间结构体,用于设置select函数的超时时间。
char buffer[BUFFER_SIZE]:定义一个缓冲区,用于存储从标准输入或鼠标设备读取的数据。
int mouse_fd:定义一个整数变量,用于存储鼠标设备文件的文件描述符。
使用open函数以只读模式打开鼠标设备文件/dev/input/mouse0。若打开失败,open函数会返回 -1,此时使用perror输出错误信息并返回 1 表示程序异常退出。
FD_ZERO(&readfds):清空文件描述符集合readfds。
FD_SET(STDIN_FILENO, &readfds)和FD_SET(mouse_fd, &readfds):将标准输入和鼠标设备的文件描述符添加到集合中。
timeout.tv_sec = 5和timeout.tv_usec = 0:设置超时时间为 5 秒。
int max_fd = (mouse_fd > STDIN_FILENO)? mouse_fd : STDIN_FILENO:找出最大的文件描述符,作为select函数的第一个参数。
int activity = select(max_fd + 1, &readfds, NULL, NULL, &timeout):调用select函数监控文件描述符集合,若有文件描述符就绪或超时,select函数会返回。
activity < 0:若select函数返回 -1,表示发生错误,使用perror输出错误信息,关闭鼠标设备文件并返回 1 表示程序异常退出。
activity == 0:若select函数返回 0,表示超时,输出提示信息。
activity > 0:若select函数返回大于 0 的值,表示有文件描述符就绪。
FD_ISSET(STDIN_FILENO, &readfds):检查标准输入是否就绪,若就绪则使用read函数读取数据,并输出到屏幕。
FD_ISSET(mouse_fd, &readfds):检查鼠标设备是否就绪,若就绪则使用read函数读取数据,并输出到屏幕。同时,处理读取错误情况。
poll
poll也是一种 I/O 多路复用技术,与select类似,但在一些方面有所改进。poll函数的原型如下
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:一个指向pollfd结构体数组的指针,每个pollfd结构体包含一个文件描述符、需要监控的事件(如POLLIN表示可读,POLLOUT表示可写)以及事件发生的情况。
nfds:fds数组中元素的个数。
timeout:等待事件发生的超时时间,单位为毫秒。如果设置为 - 1,poll将一直阻塞;如果设置为 0,poll将立即返回。
poll与select相比,主要有以下优点:
没有文件描述符数量的限制,因为它通过数组来管理文件描述符,而不是像select那样使用固定大小的文件描述符集合。
在事件通知方面,poll的实现更高效,不需要像select那样每次都重新设置文件描述符集合。当poll返回后,应用程序可以直接检查pollfd结构体数组中每个元素的事件发生情况,然后对相应的文件描述符进行 I/O 操作。
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <errno.h>
#define BUFFER_SIZE 1024
int main() {
struct pollfd fds[2];
char buffer[BUFFER_SIZE];
int mouse_fd;
// 打开鼠标设备文件
mouse_fd = open("/dev/input/mouse0", O_RDONLY);
if (mouse_fd == -1) {
perror("Failed to open mouse device");
return 1;
}
// 设置 pollfd 结构体
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = mouse_fd;
fds[1].events = POLLIN;
while (1) {
// 调用 poll 函数
int activity = poll(fds, 2, 5000); // 超时时间为 5 秒
if (activity < 0) {
perror("poll error");
close(mouse_fd);
return 1;
} else if (activity == 0) {
printf("No input received within 5 seconds.\n");
} else {
if (fds[0].revents & POLLIN) {
// 读取标准输入
ssize_t bytes = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("You entered: %s", buffer);
}
}
if (fds[1].revents & POLLIN) {
struct input_event ev;
ssize_t bytes = read(mouse_fd, buffer, BUFFER_SIZE - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("You entered: %s", buffer);
}
if (bytes == -1 && errno != EAGAIN) { // 处理错误情况,除了 EAGAIN(非阻塞下无数据)
perror("read from mouse device error");
}
}
}
}
// 关闭鼠标设备文件
close(mouse_fd);
return 0;
}
epoll
创建epoll实例:使用epoll_create或epoll_create1函数创建一个epoll实例,这会在内核中创建一个epoll专用的数据结构,用于存储要监控的文件描述符及其相关信息。
注册文件描述符:利用epoll_ctl函数向epoll实例中添加、修改或删除要监控的文件描述符,并指定需要监控的事件类型,如可读(EPOLLIN)、可写(EPOLLOUT)等。当文件描述符对应的 I/O 设备上有相应事件发生时,内核会将其记录下来。
等待事件发生:调用epoll_wait函数阻塞等待,直到有注册的文件描述符上发生了指定的事件,或者达到超时时间。该函数返回时会返回就绪的文件描述符数量,并将这些就绪文件描述符的信息填充到用户提供的事件数组中,用户程序可以据此进行相应的处理。
相关函数
epoll_create(int size):创建一个epoll实例,返回一个文件描述符,用于后续的epoll操作。参数size在早期版本中用于提示内核分配多大的内存空间,但现在已被忽略,不过仍需传入一个大于 0 的值。
epoll_create1(int flags):功能与epoll_create类似,但更灵活。flags参数可以传入特定的标志位,如EPOLL_CLOEXEC,表示在执行exec系列函数时自动关闭该epoll文件描述符。若传入 0,则与epoll_create功能相同。
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是一个指向struct epoll_event结构体的指针,用于指定要监控的事件类型和关联的数据。
epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout):等待epoll实例中就绪的事件。epfd是epoll实例的文件描述符;events是一个数组,用于存储就绪的事件信息;maxevents指定events数组的最大元素个数;timeout是超时时间,单位为毫秒,若为 -1 则表示无限等待,直到有事件发生。
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <errno.h>
#define BUFFER_SIZE 1024
#define MAX_EVENTS 10
int main() {
int epoll_fd, mouse_fd;
struct epoll_event ev, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
// 打开鼠标设备文件
mouse_fd = open("/dev/input/mouse0", O_RDONLY);
if (mouse_fd == -1) {
perror("Failed to open mouse device");
return 1;
}
// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(mouse_fd);
return 1;
}
// 添加标准输入到 epoll 实例
ev.events = EPOLLIN;
ev.data.fd = STDIN_FILENO;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1) {
perror("epoll_ctl: STDIN_FILENO");
close(epoll_fd);
close(mouse_fd);
return 1;
}
// 添加鼠标设备到 epoll 实例
ev.events = EPOLLIN;
ev.data.fd = mouse_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, mouse_fd, &ev) == -1) {
perror("epoll_ctl: mouse_fd");
close(epoll_fd);
close(mouse_fd);
return 1;
}
while (1) {
// 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, 5000);
if (nfds == -1) {
perror("epoll_wait");
close(epoll_fd);
close(mouse_fd);
return 1;
} else if (nfds == 0) {
printf("No input received within 5 seconds.\n");
} else {
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == STDIN_FILENO) {
// 读取标准输入
ssize_t bytes = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("You entered: %s", buffer);
}
} else if (events[i].data.fd == mouse_fd) {
struct input_event ev;
ssize_t bytes = read(mouse_fd,buffer, BUFFER_SIZE - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("You entered: %s", buffer);
}
if (bytes == -1 && errno != EAGAIN) { // 处理错误情况,除了 EAGAIN(非阻塞下无数据)
perror("read from mouse device error");
}
}
}
}
}
// 关闭文件描述符
close(epoll_fd);
close(mouse_fd);
return 0;
}