嵌入式Linux C笔记——高级IO

目录

非阻塞IO

[I/O 多路复用](#I/O 多路复用)

I/O多路复用的背景

I/O多路复用的基本原理

select函数

select()的API原型:

使用select()的示例代码:

select()的局限性:

poll函数

poll()的API原型:

poll()的示例代码:

epoll函数

epoll的工作原理:

epoll的API

epoll()的示例代码:

异步IO

Linux中的异步I/O实现

基于信号的异步I/O

使用信号进行异步I/O的步骤:

代码示例(基于信号的异步I/O):

基于libaio的异步I/O

主要API:

代码示例(基于libaio的异步I/O):

基于io_uring的异步I/O

io_uring的基本原理:

主要API:

代码示例(基于io_uring的异步I/O):


非阻塞IO

非阻塞IO:

非阻塞IO(Non-blocking IO)是指当进程发起IO操作时,即使数据还没有准备好,操作会立即返回,而不会阻塞进程的执行。如果数据尚未准备好,操作会返回错误或特定的状态,告知进程数据尚不可用。

这里我们只需要设置一下文件描述符的属性即可。

  1. fcntl():用于设置文件描述符的属性,其中可以通过F_SETFL标志设置文件描述符为非阻塞模式:O_NONBLOCK

    • 原型:

      复制代码
      int fcntl(int fd, int cmd, ... /* arg */ );
    • 参数说明: fd:文件描述符。 cmd:要执行的命令,F_SETFL表示设置文件状态标志。 arg:设置的标志,通常使用O_NONBLOCK来启用非阻塞模式。

    • 返回值: 成功时返回文件描述符的状态,失败时返回-1,并设置errno

    设置非阻塞模式后,当进行read()write()操作时,如果数据不可用,系统不会阻塞当前进程,而是返回-1,并将errno设置为EAGAIN

  2. read():当文件描述符处于非阻塞模式时,如果没有数据可读,read()会立即返回-1,并设置errnoEAGAIN,而不会阻塞进程。

    • 原型:

      复制代码
      ssize_t read(int fd, void *buf, size_t count);
    • 返回值: 如果没有数据可读,返回-1,errno设置为EAGAIN。如果成功读取数据,则返回实际读取的字节数。

  3. write():同样地,当文件描述符处于非阻塞模式时,如果无法立即完成写入操作,write()会返回-1,并设置errnoEAGAIN

    • 原型:

      复制代码
      ssize_t write(int fd, const void *buf, size_t count);
    • 返回值: 如果无法立即写入数据,返回-1,errno设置为EAGAIN。如果成功写入数据,则返回实际写入的字节数。

复制代码
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
​
int main() {
    int fd = open("file.txt", O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        return 1;
    }
​
    char buffer[100];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        if (errno == EAGAIN) {
            printf("No data available.\n");
        } else {
            perror("read");
        }
    } else {
        write(STDOUT_FILENO, buffer, bytesRead);
    }
​
    close(fd);
    return 0;
}
​

I/O 多路复用

I/O 多路复用(I/O multiplexing)是操作系统提供的一种机制,允许一个线程或进程通过一个或多个接口同时处理多个I/O流。这种机制特别适合于需要同时处理大量I/O操作的场景,例如高并发的网络服务器。Linux中的I/O多路复用通常使用select()poll()epoll()等系统调用来实现。

本文将详细介绍Linux I/O多路复用的概念、API、使用方法以及在实际开发中的应用,帮助开发者更好地理解和运用这些机制。

I/O多路复用的背景

在传统的I/O模型中,一个进程通常只能同时进行一个I/O操作。当进行I/O操作时,进程需要等待数据的到来,这会导致进程阻塞。为了避免因为等待I/O操作而浪费CPU时间,操作系统提供了I/O多路复用机制,使得进程或线程能够同时监听多个I/O流,从而减少阻塞,提高程序的效率。

I/O多路复用机制在网络编程中尤为重要,尤其是在处理大量并发连接时。如果为每个连接创建一个线程或进程,资源开销非常大,且会导致上下文切换带来较大的性能损失。而使用I/O多路复用,则可以通过单个进程或线程来处理多个I/O操作,从而显著提升性能。

I/O多路复用的基本原理

I/O多路复用的核心思想是通过一个调用来同时监听多个I/O事件(如读、写等),当其中某个事件发生时,程序可以立即处理相关I/O。常见的I/O多路复用方法有以下几种:

  1. select:最早的多路复用机制,使用一个固定大小的文件描述符集来监听多个I/O事件。

  2. poll :是select()的改进版本,支持动态大小的文件描述符集,但性能依然有限。

  3. epoll:Linux特有的高效I/O多路复用机制,具有较好的扩展性和性能,尤其在高并发场景下表现突出。

select函数

select()是最早实现的I/O多路复用接口,它能够监听多个文件描述符上的读写事件。当某个文件描述符准备好进行I/O操作时,select()返回,并通知应用程序进行处理。

select()的API原型:
复制代码
#include <sys/select.h>
#include <unistd.h>
​
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 参数说明

    • nfds:需要监视的文件描述符的数量。通常是文件描述符的最大值加1。

    • readfds:需要检测是否可以读取的文件描述符集合。

    • writefds:需要检测是否可以写入的文件描述符集合。

    • exceptfds:需要检测是否有异常的文件描述符集合。

    • timeout:等待时间,单位为秒。如果超时则返回,若为NULL则表示一直阻塞,直到事件发生。

  • 返回值

    • 返回文件描述符的数量,表示哪些文件描述符准备好进行I/O操作。

    • 失败时返回-1,并设置errno

使用select()的示例代码:
复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
​
int main() {
    fd_set readfds;
    struct timeval timeout;
    int fd = open("test.txt", O_RDONLY);
    
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
​
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;
​
    int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    if (ret == -1) {
        perror("select");
        close(fd);
        return 1;
    }
​
    if (ret == 0) {
        printf("Timeout occurred!\n");
    } else {
        if (FD_ISSET(fd, &readfds)) {
            printf("File is ready for reading\n");
        }
    }
​
    close(fd);
    return 0;
}

此示例代码演示了如何使用select()来监听文件是否可读,并设置了一个5秒的超时时间。select()会检查文件是否可读,并在超时或事件发生时返回。

select()的局限性:
  • 性能问题select()在每次调用时都需要重新构建文件描述符集合,这对于大量文件描述符时效率较低。

  • 最大文件描述符限制select()的文件描述符集合是固定大小的,通常为1024,这使得它不适合用于高并发应用。

poll函数

poll()是对select()的改进,解决了select()的一些限制,如文件描述符数量限制问题。poll()使用一个动态的pollfd结构数组来表示需要监视的文件描述符,适应性更强。

poll()的API原型:
复制代码
#include <poll.h>
​
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 参数说明

    • fds:指向pollfd结构数组的指针,每个pollfd结构表示一个需要监听的文件描述符。

    • nfds:表示fds数组的元素数量。

    • timeout:等待的超时时间,单位为毫秒。如果设置为-1,则表示永久阻塞。

  • 返回值

    • 返回准备好进行I/O操作的文件描述符数量。

    • 超时返回0,错误返回-1。

poll()的示例代码:
复制代码
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <fcntl.h>
​
int main() {
    struct pollfd pfds[1];
    int fd = open("test.txt", O_RDONLY);
​
    pfds[0].fd = fd;
    pfds[0].events = POLLIN;
​
    int ret = poll(pfds, 1, 5000);  // 5秒超时
    if (ret == -1) {
        perror("poll");
        close(fd);
        return 1;
    }
​
    if (ret == 0) {
        printf("Timeout occurred!\n");
    } else {
        if (pfds[0].revents & POLLIN) {
            printf("File is ready for reading\n");
        }
    }
​
    close(fd);
    return 0;
}

poll()的优势在于,它没有select()那样的文件描述符数量限制,并且可以更灵活地监听不同类型的事件(如读、写、异常)。

epoll函数

epoll是Linux特有的高效I/O多路复用机制,解决了select()poll()在高并发场景下的性能瓶颈。epoll使用事件驱动的方式,可以避免文件描述符集合的拷贝和遍历,从而提高性能,尤其适合处理大量并发连接的网络应用。

epoll的工作原理:
  1. 创建epoll实例 :通过epoll_create()epoll_create1()函数创建一个epoll实例。

  2. 注册事件 :使用epoll_ctl()注册需要监听的文件描述符及其对应的事件。

  3. 等待事件 :使用epoll_wait()等待并获取发生的事件。

epoll的API
  1. epoll_create():创建一个epoll实例。

    复制代码
    int epoll_create(int size);

    size参数被忽略,通常传递128即可。

  2. epoll_create1():创建epoll实例,并可指定额外的标志,如EPOLL_CLOEXEC

    复制代码
    int epoll_create1(int flags);
  3. epoll_ctl():添加、删除或修改文件描述符的事件。

    复制代码
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  4. epoll_wait():等待并返回准备好事件的文件描述符。

    复制代码
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll()的示例代码:
复制代码
#include <stdio.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>

#define MAX_EVENTS 10

int main() {
    int epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create");
        return 1;
    }

    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = STDIN_FILENO;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
        perror("epoll_ctl");
        close(epfd);
        return 1;
    }

    struct epoll_event events[MAX_EVENTS];
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        close(epfd);
        return 1;
    }

    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == STDIN_FILENO) {
            printf("Input ready for reading\n");
        }
    }

    close(epfd);
    return 0;
}

epoll的优势在于:

  • 高性能 :尤其在大量并发连接的情况下,epoll避免了每次都遍历所有文件描述符。

  • 支持边缘触发epoll支持边缘触发(Edge Triggered)和水平触发(Level Triggered)模式。边缘触发模式下,只有当文件描述符状态发生变化时才会通知应用程序,从而减少不必要的调用。

异步IO

异步I/O的基本原理是:当应用程序发起I/O操作时,它并不等待操作完成,而是立即返回。操作系统会在I/O操作完成时通过信号、回调函数或者其他方式通知进程,进程再去检查结果或进行下一步操作。这样,进程可以在等待I/O操作的同时做其他事情,提高了CPU资源的利用率。

常见的异步I/O工作流程如下:

  1. 发起异步I/O请求:应用程序调用异步I/O接口,发起I/O请求。

  2. 操作完成后通知:操作系统在I/O操作完成后,通过信号、回调等方式通知应用程序。

  3. 应用程序继续处理:应用程序根据通知的结果进行下一步操作。

Linux中的异步I/O实现

Linux提供了两种主要的异步I/O机制:

  1. 基于信号的异步I/O

    • 使用SIGIO信号来通知进程某个I/O操作完成。进程需要设置自己的信号处理器,并通过fcntl()ioctl()系统调用来启用异步I/O。
  2. 基于libaio的异步I/O

    • libaio是一个用户空间库,提供了异步I/O的高级接口。它使用内核提供的异步I/O支持,允许应用程序提交多个I/O请求,并在I/O完成时获取结果。
  3. 基于io_uring的异步I/O(推荐)

    • io_uring是Linux内核5.1引入的新型异步I/O接口,比传统的AIO接口效率更高,支持零拷贝、批量提交和高效的事件通知。

基于信号的异步I/O

使用信号进行异步I/O的核心是通过SIGIO信号和fcntl()系统调用进行设置。应用程序可以为一个文件描述符注册SIGIO信号,当文件描述符准备好进行I/O操作时,内核会发送SIGIO信号给进程。进程接收到信号后,可以继续执行后续操作。

使用信号进行异步I/O的步骤:
  1. 设置文件描述符的异步I/O标志 : 使用fcntl()系统调用设置文件描述符为异步I/O模式。

  2. 捕捉SIGIO信号 : 进程需要设置信号处理程序来捕获SIGIO信号。

  3. 在信号处理程序中进行I/O操作: 在信号处理程序中检查I/O是否完成,如果完成则进行后续操作。

代码示例(基于信号的异步I/O):
复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>

void sigio_handler(int signo) {
    printf("Received SIGIO signal, performing I/O operation\n");
    // 此处可以执行I/O操作
}

int main() {
    // 设置SIGIO信号处理函数
    signal(SIGIO, sigio_handler);
    
    // 打开文件并设置文件描述符为异步I/O模式
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    
    // 设置文件描述符为异步I/O
    if (fcntl(fd, F_SETFL, O_ASYNC) == -1) {
        perror("fcntl");
        close(fd);
        return 1;
    }

    // 将文件描述符的异步通知发送到当前进程
    if (fcntl(fd, F_SETOWN, getpid()) == -1) {
        perror("fcntl");
        close(fd);
        return 1;
    }
    
    // 进入循环,等待信号
    while (1) {
        pause();  // 等待SIGIO信号
    }

    close(fd);
    return 0;
}

在这个示例中,SIGIO信号会在文件描述符可读时发送到进程,进程接收到信号后在信号处理函数中执行I/O操作。

基于libaio的异步I/O

libaio库是Linux提供的一个用户空间库,它通过内核提供的AIO支持来实现异步I/O。与基于信号的异步I/O不同,libaio提供了一组系统调用接口,允许应用程序发起多个异步I/O请求,并在所有请求完成后一次性处理结果。libaio通过提交I/O请求并返回一个io_context对象来管理I/O操作。

主要API
  1. io_setup():创建AIO上下文。

  2. io_submit():提交异步I/O请求。

  3. io_getevents():获取已完成的异步I/O请求。

  4. io_destroy():销毁AIO上下文。

代码示例 (基于libaio的异步I/O):
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <libaio.h>

#define BUFFER_SIZE 1024

int main() {
    io_context_t ctx;
    struct iocb iocb[1];
    struct io_event ev;
    char buffer[BUFFER_SIZE];
    int fd = open("test.txt", O_RDONLY);
    
    if (fd == -1) {
        perror("open");
        return 1;
    }
    
    // 初始化AIO上下文
    if (io_setup(128, &ctx) < 0) {
        perror("io_setup");
        close(fd);
        return 1;
    }
    
    // 准备异步I/O操作
    memset(&iocb, 0, sizeof(iocb));
    io_prep_pread(&iocb[0], fd, buffer, BUFFER_SIZE, 0);  // 读取文件到缓冲区
    iocb[0].data = buffer;

    // 提交异步I/O操作
    if (io_submit(ctx, 1, &iocb) < 0) {
        perror("io_submit");
        io_destroy(ctx);
        close(fd);
        return 1;
    }
    
    // 等待I/O操作完成
    int ret = io_getevents(ctx, 1, 1, &ev, NULL);
    if (ret < 0) {
        perror("io_getevents");
        io_destroy(ctx);
        close(fd);
        return 1;
    }

    printf("Read %ld bytes: %s\n", ev.res, buffer);

    // 销毁AIO上下文并关闭文件
    io_destroy(ctx);
    close(fd);
    return 0;
}

这个示例使用libaio的异步I/O机制,向文件读取数据。程序通过io_submit()提交读取操作,io_getevents()用来等待I/O操作完成。

基于io_uring的异步I/O

io_uring是Linux内核5.1版本引入的一种新的异步I/O机制,目的是提供比libaio更高效的I/O处理方式。io_uring通过使用环形缓冲区来管理I/O请求和结果,避免了传统AIO模型中的上下文切换和系统调用开销。

io_uring的基本原理
  1. 提交环(Submission Queue, SQ):应用程序将I/O请求提交到提交环,提交的I/O请求包含了操作所需的数据和参数。

  2. 完成环(Completion Queue, CQ):I/O操作完成后,内核将结果写入完成环,应用程序可以读取这些结果。

主要API
  • io_uring_setup():设置io_uring实例。

  • io_uring_enter():提交和等待I/O操作的完成。

  • io_uring_register():注册I/O操作所需的资源。

  • io_uring_cqe():获取完成事件。

代码示例 (基于io_uring的异步I/O):
复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>

#define QUEUE_SIZE 64

int main() {
    struct io_uring ring;
    struct io_uring_cqe *cqe;
    struct io_uring_sqe *sqe;
    char buffer[1024];
    int fd = open("test.txt", O_RDONLY);

    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 初始化io_uring
    io_uring_queue_init(QUEUE_SIZE, &ring, 0);

    // 准备异步读取请求
    sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);

    // 提交请求
    io_uring_submit(&ring);

    // 等待完成
    io_uring_wait_cqe(&ring, &cqe);
    if (cqe->res < 0) {
        perror("read failed");
    } else {
        printf("Read %d bytes: %s\n", cqe->res, buffer);
    }

    // 清理
    io_uring_cqe_seen(&ring, cqe);
    io_uring_queue_exit(&ring);
    close(fd);
    return 0;
}

在这个示例中,io_uring用于异步读取文件,程序提交读取请求并等待完成。

相关推荐
疯狂飙车的蜗牛42 分钟前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
远游客07133 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<3 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟3 小时前
centos-stream9系统安装docker
linux·docker·centos
XH华3 小时前
初识C语言之二维数组(下)
c语言·算法
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
冷眼看人间恩怨4 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
cominglately6 小时前
centos单机部署seata
linux·运维·centos
魏 无羡6 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse6 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos