阻塞io的局限性
前面三章提到的阻塞IO,非阻塞IO以及多路复用网络IO模型,都没有实现真正意义上的异步非阻塞;
阻塞:阻塞IO会一直阻塞任务直至操作完成。
异步:响应处理和拷贝无需带阻塞的串行完成,时间上是非同步的。
阻塞IO:编程简单,但是必须阻塞,直至数据响应并且拷贝完成;
非阻塞IO:通过设置socket的文件描述符为非阻塞,可以不阻塞等待数据拷贝完成,但是需要持续检查响应并且需要将数据从内核拷贝到应用空间。
多路复用:天然支持海量接入,无需检查响应状态,但是需要将数据从内核拷贝到应用空间。
虽然每一种模型都有各自的优点,但是追求应用极致的性能表现,需要了解对异步和非阻塞支持更好的模型,以追求对各种场景的适应能力。
异步IO
异步IO介绍
异步IO即asynchronous IO ,Linux中的异步IO提供了为aio_read,aio_write等API,但是异步IO不可以用来处理网络IO,只能用于磁盘IO,因为读取需要先获取该文件的fd而不是由内核主动提供。
aio异步读写是在linux内核2.6之后才正式纳入标准。其强大之处在于使用aio_read读写一个文件后,只是向内核发送一个操作指令就返回再也不理睬了,直到内核发送一个信号告诉进程IO完成了,此时数据已经由内核拷贝到了用户指定的空间,可以直接使用该部分内存。整个过程对于调用进程而言完全没有阻塞,同时也是真正的异步。
由于aio系列的读写函数是真正意义上的异步非阻塞io,同时cpu的处理速度远远大于IO的速度。所以即使不能应用于网络IO,当我们恰好有一系列socket文件描述符需要处理时,这个函数的意义也是不言自明的。
api介绍
api声明 | 功能 |
---|---|
int aio_read(struct aiocb *aiocbp) | 异步读取文件。 |
int aio_error(const struct aiocb *aiocbp) | 检查异步I/O操作的错误状态。 |
ssize_t aio_return(struct aiocb *aiocbp) | 检索异步I/O操作的结果。 |
int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout) | 等待一组异步I/O操作完成。 |
int aio_cancel(int fd, struct aiocb *aiocbp) | 取消异步I/O操作。 |
int aio_fsync(int op, struct aiocb *aiocbp) | 异步数据同步 |
可见异步IO依赖struct aiocb结构体,aiocb结构体定义如下:
struct aiocb {
int aio_fildes; // 文件描述符
off_t aio_offset; // 读写文件的偏移量
volatile void *aio_buf; // 缓冲区指针
size_t aio_nbytes; // 读写字节数
int aio_reqprio; // 请求优先级
struct sigevent aio_sigevent; // 异步I/O操作完成后的信号事件
int aio_lio_opcode; // 操作码,如LIO_READ、LIO_WRITE等
int aio_flags; // AIO请求标志
};
编程demo
#include <stdio.h>
#include <stdlib.h>
#include <aio.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int fd, ret;
struct aiocb my_aiocb;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage: %s <filename>\n", argv[0]);
exit(1);
}
memset(&my_aiocb, 0, sizeof(struct aiocb));
my_aiocb.aio_buf = buf;
my_aiocb.aio_fildes = open(argv[1], O_RDONLY);
if (my_aiocb.aio_fildes == -1) {
perror("open");
exit(1);
}
my_aiocb.aio_nbytes = BUF_SIZE;
my_aiocb.aio_offset = 0;
ret = aio_read(&my_aiocb);
if (ret == -1) {
perror("aio_read");
exit(1);
}
while (aio_error(&my_aiocb) == EINPROGRESS);
ssize_t n = aio_return(&my_aiocb);
if (n == -1) {
perror("aio_return");
exit(1);
}
printf("%.*s", (int)n, buf);
ret = close(my_aiocb.aio_fildes);
if (ret == -1) {
perror("close");
exit(1);
}
return 0;
}
该程序以异步方式读取文件,并输出读取的内容。在使用aio_read函数时,需要指定要读取的文件描述符、缓冲区指针、读取的字节数和偏移量。
此外,还需要使用aio_error函数检查异步操作是否完成,使用aio_return函数获取异步操作的结果。最后,关闭文件描述符。
需要注意的是,在使用异步I/O时,必须按照一定的顺序进行操作,否则可能会导致错误。具体可以参考相关文档进行了解。
信号驱动IO
信号驱动IO介绍
首先我们允许套接口进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。
创建套接口并设置其为非阻塞模式。
使用sigaction函数或signal函数设置相应的信号处理程序。
将套接口与信号关联起来,使用fcntl函数设置F_SETOWN标志和FASYNC标志。F_SETOWN标志将所有发送给进程的信号都转发给指定的进程ID,而FASYNC标志则允许套接口被设置为异步通知方式。
等待信号触发。当套接口上有可用数据时,内核将发送SIGIO信号给进程,此时信号处理程序将被调用,并且可以执行相应的操作。
当数据准备好时,进程会收到一个SIGIO信号,此时可以在信号处理函数中调用I/O操作函数处理数据。这种模型的优势在于等待数据报到达(第一阶段)期间,进程可以继续执行,不被阻塞。免去了select的阻塞与轮询,当有活跃套接字时,再由注册的handler 处理。
这个模型的好处是无需进程主动去check活跃的socket,把检查工作交给内核,进程只需要确定合适处理拷贝即可。
信号驱动IO范例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#define BUF_SIZE 1024
static int fd;
static char buffer[BUF_SIZE];
void sigio_handler(int signum)
{
int len = read(fd, buffer, BUF_SIZE);
if (len > 0) {
printf("Received data: %s\n", buffer);
}
}
int main()
{
fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);
if (fd < 0) {
perror("open");
exit(1);
}
fcntl(fd, F_SETOWN, getpid());
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | FASYNC);
signal(SIGIO, sigio_handler);
while (1) {
sleep(1);
}
close(fd);
return 0;
}
在这个例子中,我们使用鼠标设备 /dev/input/mouse0 作为输入设备来演示。首先,我们以非阻塞模式打开设备文件,并设置当前进程为该文件的拥有者(通过 F_SETOWN 和 getpid() 函数)。
然后,我们使用 F_SETFL 和 FASYNC 标志将文件描述符标记为异步通知模式。这意味着当设备上有数据可读时,内核会向当前进程发送 SIGIO 信号。最后,我们注册了一个信号处理程序 sigio_handler,它将读取设备上的数据并打印出来。
在 main() 函数中,我们使用一个无限循环来防止程序退出。当有数据可读时,sigio_handler 会被调用,从而可以及时处理输入数据。
需要注意的是,这个例子仅适用于 Linux 系统,并且需要在编译时链接 -lrt 库。
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下的文章,对c/c++linux课程感兴趣的读者,可以去零声官网查看详细的服务,也欢迎一起蹭免费公开课,共同进步鸭~