什么是阻塞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 1024int 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 1024int 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 10int 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;
}