1、简介
Linux IO 模型根据实现的功能可以划分为为阻塞 IO、 非阻塞 IO、 信号驱动 IO, IO 多路复用和异
步 IO。 根据等待 IO 的执行结果进行划分, 前四个 IO 模型又被称为同步 IO,如下图:
2、详细介绍
2.1 阻塞IO
在阻塞IO模型中,调用read或write时,如果没有数据可读或写,进程会被挂起,直到数据可用。这是最简单的IO模型。以阻塞读为例: 进程进行 IO 操作时(如 read 操作), 首先会发起一个系统调用, 从而转到内核空间进行处理, 内核空间的数据没有准备就绪时, 进程会被阻塞, 不会继续向下执行, 直到内核空间的数据准备完成后, 数据才会从内核空间拷贝到用户空间, 最后返回用户进程, 由用户空间进行数据的处理, 如下图所示:
示例代码如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
char buffer[100];
int fd = open("file.txt", O_RDONLY);
// 阻塞调用
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read < 0) {
perror("read error");
} else {
printf("Read %zd bytes: %s\n", bytes_read, buffer);
}
close(fd);
return 0;
}
2.2 非阻塞IO
在非阻塞IO模型中,调用read或write时,如果没有数据可读或写,返回-1,errno设置为EAGAIN。进程不会被挂起。和阻塞 IO 模型不同, 非阻塞 IO 进行 IO 操作时, 如果内核数据没有准备好, 内核会立即向进程返回 err, 不会进行阻塞; 如果内核空间数据准备就绪, 内核会立即把数据返回给用户空间的进程, 如下图所示:
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main() {
char buffer[100];
int fd = open("file.txt", O_RDONLY | O_NONBLOCK);
// 非阻塞调用
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read < 0) {
if (errno == EAGAIN) {
printf("No data available\n");
} else {
perror("read error");
}
} else {
printf("Read %zd bytes: %s\n", bytes_read, buffer);
}
close(fd);
return 0;
}
2.3 IO复用
通常情况下使用 select()、 poll()、epoll()函数实现 IO 多路复用。 这里以 select 函数为例进行讲解, 使用时可以对 select 传入多个描述符, 并设置超时时间。 当执行 select 的时候, 系统会发起一个系统调用, 内核会遍历检查传入的描述符是否有事件发生(如可读、 可写事件) 。 如有, 立即返回, 否则进入睡眠状态, 使进程进入阻塞状态, 直到任何一个描述符事件产生后(或者等待超时) 立刻返回。 此时用户空间需要对全部描述符进行遍历, 以确认具体是哪个发生了事件, 这样就能使用一个进程对多个 IO 进行管理, 如下图所示:
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
int main() {
int fd1 = open("file1.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
// 等待文件描述符就绪
int max_fd = (fd1 > fd2) ? fd1 : fd2;
int retval = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (retval == -1) {
perror("select error");
} else if (retval) {
if (FD_ISSET(fd1, &readfds)) {
printf("file1.txt is ready for reading\n");
}
if (FD_ISSET(fd2, &readfds)) {
printf("file2.txt is ready for reading\n");
}
} else {
printf("No file descriptors are ready\n");
}
close(fd1);
close(fd2);
return 0;
}
2.4 信号驱动IO
信号驱动 IO 顾名思义与信号相关。 系统在一些事件发生之后, 会对进程发出特定的信号,而信号与处理函数相绑定, 当信号产生时就会调用绑定的处理函数。 例如在 Linux 系统任务执行的过程中可以按下 ctrl+C 来对任务进行终止, 系统实际上是对该进程发送一个 SIGINT 信号,该信号的默认处理函数就是退出当前程序。具体到 IO 模型上, 可以对 SIGIO 信号注册相应的信号处理函数, 并打开对应描述符的信号驱动。 每当有 IO 数据产生时, 系统就会发送一个 SIGIO 信号, 进而调用相应的信号处理函数,从而在这个处理函数中对数据进行读取, 如下图 所示:
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
void handler(int sig) {
printf("Data is available to read!\n");
}
int main() {
signal(SIGIO, handler);
int fd = open("file.txt", O_RDONLY | O_NONBLOCK);
fcntl(fd, F_SETFL, O_ASYNC); // 设置异步IO
// 允许进程接收SIGIO信号
fcntl(fd, F_SETOWN, getpid());
// 主循环,保持进程运行
while (1) {
pause(); // 等待信号
}
close(fd);
return 0;
}
2.5 异步IO
在异步IO模型中,进程提交IO请求后可以继续执行,IO操作在后台完成,完成后操作系统会通知进程。aio_read 函数常常用于异步 IO, 当进程使用 aio_read 读取数据时, 如果数据尚未准备就绪就立即返回, 不会阻塞。 若数据准备就绪就会把数据从内核空间拷贝到用户空间的缓冲区中,然后执行定义好的回调函数对接收到的数据进行处理。
示例代码:
#include <stdio.h>
#include <aio.h>
#include <fcntl.h>
#include <string.h>
int main() {
struct aiocb cb;
char buffer[100];
int fd = open("file.txt", O_RDONLY);
memset(&cb, 0, sizeof(struct aiocb));
cb.aio_fildes = fd;
cb.aio_buf = buffer;
cb.aio_nbytes = sizeof(buffer);
// 提交异步读取
if (aio_read(&cb) == -1) {
perror("aio_read error");
}
// 检查IO是否完成
while (aio_error(&cb) == EINPROGRESS) {
// 可以执行其他操作
printf("Doing other work...\n");
sleep(1);
}
// 获取结果
ssize_t bytes_read = aio_return(&cb);
printf("Read %zd bytes: %s\n", bytes_read, buffer);
close(fd);
return 0;
}
3、总结
Linux中的五种IO模型主要包括阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO。
- 阻塞IO:调用read或write时,如果没有数据可读或写,进程会被挂起,直到操作完成。这种模型简单易用,但在高并发场景下效率低。
- 非阻塞IO:在调用read或write时,如果没有数据可读或写,返回-1,errno设置为EAGAIN。这样,进程可以继续执行其他任务,但需要轮询或管理状态。
- IO复用:使用select、poll或epoll等系统调用,可以监视多个文件描述符,等待其中一个或多个就绪。这种方法适合处理大量并发连接,提高了效率。
- 信号驱动IO:通过设置信号处理程序,进程在文件描述符就绪时会收到信号。这种模型减少了轮询,但处理信号的复杂性增加。
- 异步IO:提交IO请求后,进程可以继续执行,IO操作在后台完成。当完成后,操作系统会通知进程。这种方式能有效利用CPU资源,但实现较复杂。
这些模型各有优缺点,选择时需根据具体应用场景进行权衡。
参考资料:《itop-3568开发板驱动开发指南v2.0》