Linux网络IO模型浅析(四)信号驱动IO与异步IO

阻塞io的局限性

前面三章提到的阻塞IO非阻塞IO以及多路复用网络IO模型,都没有实现真正意义上的异步非阻塞

阻塞:阻塞IO会一直阻塞任务直至操作完成。
异步:响应处理和拷贝无需带阻塞的串行完成,时间上是非同步的。

阻塞IO:编程简单,但是必须阻塞,直至数据响应并且拷贝完成;
非阻塞IO:通过设置socket的文件描述符为非阻塞,可以不阻塞等待数据拷贝完成,但是需要持续检查响应并且需要将数据从内核拷贝到应用空间。
多路复用:天然支持海量接入,无需检查响应状态,但是需要将数据从内核拷贝到应用空间。

虽然每一种模型都有各自的优点,但是追求应用极致的性能表现,需要了解对异步和非阻塞支持更好的模型,以追求对各种场景的适应能力。

异步IO

异步IO介绍

异步IO即asynchronous IO ,Linux中的异步IO提供了为aio_readaio_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课程感兴趣的读者,可以去零声官网查看详细的服务,也欢迎一起蹭免费公开课,共同进步鸭~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫大魔宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值