高级 IO-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

高级 I/O

非阻塞 I/O

简介

  • 阻塞是指进程进入休眠状态并交出 CPU 控制权, wait()、pause()、sleep() 等函数都会导致阻塞

  • 阻塞式 I/O 是指对文件的 I/O 操作(读写操作)是阻塞的,非阻塞式 I/O 则是非阻塞的

  • 对于某些文件类型(如管道文件、网络设备文件和字符设备文件),进行读操作时,如果数据未准备好,读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这是阻塞式 I/O 的一种表现

  • 非阻塞式 I/O 中,即使没有数据可读,读操作也不会阻塞,而是会立即返回错误

  • 普通文件的读写操作不会阻塞,read() 或 write() 一定会在有限时间内返回,因此普通文件的 I/O 操作是非阻塞的

  • 对于某些文件类型(如管道文件、设备文件等),它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O 操作

阻塞 I/O 与非阻塞 I/O 读文件

  • 在调用 open() 函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open() 调用成功后,后续的 I/O 操作将以非阻塞式方式进行

  • 如果未指定 O_NONBLOCK 标志,则默认使用阻塞式 I/O 进行操作

  • 对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其没有影响,普通文件的读写操作不会阻塞,总是以非阻塞的方式进行 I/O 操作

  • 使用 od 命令读取设备文件

    • sudo od -x /dev/input/event3

    • 需要添加 sudo,在 Ubuntu 系统下,普通用户是无法对设备文件进行读取或写入操作

阻塞 I/O 的优点与缺点

  • 当对文件进行读取操作时,如果文件当前无数据可读

    • 阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞

    • 非阻塞 I/O 不会挂起应用程序,而是立即返回

  • 非阻塞 I/O 的处理方式

    • 要么一直轮训等待,直到数据可读

    • 要么直接放弃读取操作

  • 阻塞式 I/O 的优点

    • 能够提升 CPU 的处理效率

    • 当自身条件不满足时,进入阻塞状态,交出 CPU 资源,让 CPU 资源给其他进程使用

  • 非阻塞式 I/O 的缺点

    • 应用程序会不断地去轮训,导致占用非常高的 CPU 使用率

使用非阻塞 I/O 实现并发读取

  • 非阻塞式方式同时读取鼠标和键盘为例

    • 打开鼠标设备文件并设置为非阻塞模式:

      • 使用 open() 函数打开鼠标设备文件,并指定 O_NONBLOCK 标志
    • 将标准输入(键盘)设置为非阻塞模式

      • 使用 fcntl() 函数获取标准输入的当前标志,然后添加 O_NONBLOCK 标志,并重新设置标志
    • 使用非阻塞 I/O 同时读取鼠标和键盘

      • 在一个无限循环中,分别读取鼠标和键盘的数据
  • 虽然这种方法解决了阻塞问题,但由于使用了轮询方式,会导致程序的 CPU 占用率特别高,对系统产生较大的副作用

何为 I/O 多路复用

I/O 多路复用机制

  • 允许监控多个文件描述符,一旦某个文件描述符可执行I/O操作,即通知应用程序进行读写

技术目的

  • 解决并发I/O场景中进程或线程因特定I/O系统调用而阻塞的问题,实现非阻塞I/O

应用场景

  • 适用于需要同时处理多个I/O源的并发式非阻塞I/O,如同时读取鼠标和键盘

系统调用

  • 使用select()和poll()两个功能相似的系统调用来执行I/O多路复用,它们在细节上略有不同

特征

  • I/O多路复用具有外部阻塞式、内部监视多路I/O的特征

I/O 多路复用

select()函数介绍

  • 用于执行 I/O 多路复用操作,阻塞直到指定的文件描述符就绪 (可读或可写)

  • #include <sys/select.h>

    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    • nfds:文件描述符的最大值加1

    • readfds、writefds、exceptfds:分别用于监控文件描述符集合的读、写和异常状态的 fd_set 类型指针

      • fd_set 操作:通过四个宏 FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO() 来操作文件描述符集合

        • FD_ZERO():初始化文件描述符集合为空

        • FD_SET():添加文件描述符到集合

        • FD_CLR():从集合移除文件描述符

        • FD_ISSET():检查文件描述符是否在集合中

    • timeout:控制 select() 阻塞的最大时间,可设置为 NULL 以永久阻塞,或指定时间以限制阻塞

    • 返回值 -1:表示发生错误,并设置 errno

      • 错误码

        • EBADF:文件描述符非法

        • EINTR:被信号中断

        • EINVAL:参数非法

        • ENOMEM:内存不足

    • 返回值 0:表示在文件描述符就绪前 select() 已超时,此时 readfds、writefds 和 exceptfds 集合被清空

    • 返回正整数:表示有一个或多个文件描述符就绪,返回值是就绪的文件描述符数量。需要通过 FD_ISSET() 检查具体哪个文件描述符就绪。如果同一文件描述符在多个集合中都就绪,会被多次计数

  • select() 行为:函数阻塞直至至少一个监控的文件描述符就绪,或被信号中断,或超时

    • 文件描述符就绪:readfds、writefds 或 exceptfds 指定的文件描述符集合中至少有一个就绪

    • 信号中断:调用被信号处理函数中断

    • 超时:timeout 指定的时间上限已超时

  • 使用注意事项

    • 每次调用 select() 后,需要重新初始化并设置 readfds、writefds、exceptfds,因为 select() 返回后,这些集合会被修改以反映哪些文件描述符是就绪的

poll()函数介绍

  • 系统调用 poll()与 select()函数接口差异

    • select():使用三个 fd_set 集合来指定关心的文件描述符

    • poll():使用 struct pollfd 类型的数组来指定文件描述符及其关心的条件

  • #include <poll.h>

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

    • fds:指向 struct pollfd 数组的指针

      • struct pollfd 结构体

      • struct pollfd {

        int fd; /* file descriptor /
        short events; /
        requested events /
        short revents; /
        returned events */

        };

        • fd:文件描述符

        • events:请求的事件

        • revents:返回的事件

          • poll 的 events 和 revents 标志
    • nfds:数组元素个数

    • timeout:控制阻塞行为,可设置为 -1(无限阻塞)、0(非阻塞)或大于 0(指定阻塞时间)

    • 返回值

      • -1:表示错误

      • 0:表示超时

      • 正整数:表示有文件描述符就绪,返回值是 fds 数组中 revents 不为 0 的元素数量

  • 事件设置

    • events:初始化以指定关心的事件

    • revents:由 poll() 函数设置,返回实际发生的事件

小结

  • 必要性 of I/O 操作

    • 当使用 select() 或 poll() 检测到文件描述符就绪(可读或可写)时,必须执行相应的 I/O 操作,以清除这种就绪状态
  • 持续状态问题

    • 如果不进行 I/O 操作来清除文件描述符的就绪状态,这个状态将持续存在
  • 影响后续调用

    • 由于就绪状态的持续存在,下一次调用 select() 或 poll() 时,如果文件描述符保持就绪,函数将直接返回,可能导致程序错误判断或无法正确处理实际数据
  • 实际应用举例

    • 在 select() 使用中,通过 FD_ISSET() 宏判断文件描述符是否可执行 I/O 操作,然后执行必要的读写操作来清除状态

    • 在 poll() 使用中,同样需要在检测到文件描述符就绪后,执行适当的 I/O 操作以清除就绪状态

异步 IO

I/O 多路复用 vs. 异步 I/O

  • I/O 多路复用

    • 通过 select() 或 poll() 系统调用主动查询文件描述符的 I/O 可执行状态
  • 异步 I/O

    • 进程请求内核在文件描述符可以执行 I/O 时发送信号,允许进程执行其他任务,直到接收到信号

异步 I/O 实现步骤

  • 启用非阻塞 I/O:设置文件描述符的 O_NONBLOCK 标志

  • 启用异步 I/O:设置 O_ASYNC 标志

  • 设置接收进程:通过 fcntl() 设置异步 I/O 事件的接收进程(通常为调用进程的进程 ID)

  • 注册信号处理函数:为 SIGIO 信号注册处理函数,用于执行就绪的 I/O 操作

信号驱动 I/O

  • 异步 I/O 常被称为信号驱动 I/O,因为它依赖信号来通知进程执行 I/O 操作

O_ASYNC 标志

  • 功能:使能文件描述符的异步 I/O 事件

  • 设置方法:无法在 open() 调用时直接设置,但可以通过 fcntl() 添加 O_ASYNC 标志

具体操作

  • 获取和设置标志

    • int flag;
      flag = fcntl(0, F_GETFL); // 获取当前的标志
      flag |= O_ASYNC; // 添加 O_ASYNC 标志
      fcntl(fd, F_SETFL, flag); // 重新设置标志
  • 设置异步 I/O 的所有者

    • fcntl(fd, F_SETOWN, getpid()); // 设置接收进程的 PID

注册信号处理函数

  • 通过 signal() 或 sigaction() 为 SIGIO 注册处理函数,以便在接收到信号时执行 I/O 操作

优化异步 I/O

背景

  • 异步 I/O vs select()/poll()

    • 在需要检查大量文件描述符(如数千个)的应用程序中,异步 I/O 比 select() 和 poll() 提供更好的性能

    • 异步 I/O 通过内核记住要检查的文件描述符,并在这些描述符上可以执行 I/O 操作时发送信号,避免了频繁检查

    • select() 或 poll() 通过轮询方式检查多个文件描述符,消耗大量的 CPU 资源

  • 使用推荐

    • 当需要检查的文件描述符数量较多时,建议使用异步 I/O 或 epoll

    • 当需要检查的文件描述符数量较少时,select() 或 poll() 是不错的选择

  • 异步 I/O 存在的问题

    • 非排队信号:默认的异步 I/O 通知信号 SIGIO 是非排队信号,不支持信号排队机制

    • 事件不明确:异步 I/O 并未告知应用程序文件描述符上发生的具体事件,如是否可读或可写,是否发生异常等

  • 需要对异步 I/O 优化,对信号阻塞丢失和事件不明确的问题

使用实时信号替换默认信号 SIGIO

  • 替换默认信号

    • 默认的异步 I/O 通知信号 SIGIO 是一个非实时信号,可以通过设置使用实时信号来替换 SIGIO
  • 设置实时信号

    • 通过 fcntl() 函数,将操作命令 cmd 参数设置为 F_SETSIG,第三个参数 arg 指定一个实时信号编号,例如 SIGRTMIN

    • fcntl(fd, F_SETSIG, SIGRTMIN);

      • 将 SIGRTMIN 实时信号指定为文件描述符 fd 的异步 I/O 通知信号,代替默认的 SIGIO 信号
  • 恢复默认信号

    • 如果将 fcntl() 函数的第三个参数 arg 设置为 0,将会重新指定 SIGIO 信号作为异步 I/O 通知信号,即回到默认状态

使用 sigaction()函数注册信号处理函数

  • 注册信号处理函数

    • 使用 sigaction 函数来为实时信号注册信号处理函数

    • 在 sa_flags 参数中指定 SA_SIGINFO,表示使用 sa_sigaction(而非 sa_handler)指向的函数作为信号处理函数

  • 信号处理函数功能

    • sa_sigaction 指向的函数能接收更多参数,比如 siginfo_t 指针,提供对触发信号的详细信息访问

    • 这使得信号处理函数能够根据信号的具体信息来执行更精确的操作

  • siginfo_t 结构体

    • 当实时信号被触发时,内核构建一个 siginfo_t 类型的对象并传递给信号处理函数

    • siginfo_t 包含关键信息如

      • si_signo:触发信号的编号

      • si_fd:发生异步 I/O 事件的文件描述符

      • si_code:指示文件描述符 si_fd 上发生的具体事件(例如读就绪、写就绪或异常事件)

      • si_band:与 poll() 系统调用的返回字段 revents 相对应的位掩码

      • si_code 和 si_band 的可能值

  • 利用 siginfo_t

    • 在信号处理函数中,可以通过检查 siginfo_t 结构体的 si_code 和 si_band 字段来确定发生的具体事件,并据此执行相应的 I/O 操作

存储映射 I/O

简介

  • 定义与机制

    • 存储映射 I/O(memory-mapped I/O)是一种将文件映射到进程地址空间的内存区域的技术
  • 读写操作简化

    • 通过内存映射,读取内存中的数据相当于读取文件数据,写入内存中的数据则直接写入文件
  • 避免传统 I/O 函数

    • 使用存储映射 I/O可以执行 I/O 操作,而无需直接调用 read() 和 write() 函数

mmap()和 munmap()函数

  • mmap() 用于将文件映射到进程的地址空间

    • mmap() 函数功能和用法

      • #include <sys/mman.h>

        void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

      • 映射起始地址 addr

        • 建议设置为 NULL,由系统自动选择映射起始地址
      • 映射长度 length

        • 定义映射区域的大小,单位是字节
      • 保护模式 prot

        • PROT_EXEC:映射区可执行

        • PROT_READ:映射区可读

        • PROT_WRITE:映射区可写

        • PROT_NONE:映射区不可访问

      • 映射属性 flags

        • 如 MAP_SHARED(修改映射区会写回文件,可共享)、MAP_PRIVATE(修改不影响原文件,不可共享)等

        • 可通过 man 手册进行查看

        • 通常情况下,参数 flags 中只指定了 MAP_SHARED

      • 文件描述符 fd

      • 映射偏移量 offset

      • 返回值

        • 成功返回值

          • mmap() 函数成功时返回映射区的起始地址
        • 错误返回值

          • 如果 mmap() 函数失败,返回 (void *)-1

          • 通常使用 MAP_FAILED 宏来表示这个错误返回值

        • 错误信息

          • 发生错误时,系统会设置 errno 变量,以提供具体的错误原因
    • 映射区的起始地址和大小

      • 地址和偏移量 offset 通常需为系统页大小的整数倍

      • 映射区大小 length 不需为页大小整数倍,但实际映射区会按页大小对齐

    • 与映射区相关的两个信号

      • 如果映射区访问权限与文件原始权限不符,会触发 SIGSEGV 信号(如只读映射区尝试写入)

      • 如果访问的映射区部分不存在(如文件被截断),会触发 SIGBUS 信号

  • munmap() 函数

    • 用于解除映射关系,释放之前通过 mmap() 映射的内存区域

    • 提供映射区的起始地址和长度来指定解除哪部分的映射

    • #include <sys/mman.h>

      int munmap(void *addr, size_t length);

      • addr:解除映射的起始地址,需为系统页大小的整数倍

      • length:解除映射的区域大小,也需为系统页大小的整数倍

    • 自动解除映射

      • 进程终止时会自动解除映射,即使未显式调用 munmap()

      • 关闭文件(close())不会解除映射

    • 通常将 munmap() 的 addr 参数设置为 mmap() 的返回值,length 参数设置为 mmap() 的 length 参数值,以解除整个映射区域

mprotect()函数

  • mprotect() 系统调用用于更改现有映射区的保护要求

  • #include <sys/mman.h>

    int mprotect(void *addr, size_t len, int prot);

    • prot:指定映射区的新保护要求,取值与 mmap() 的 prot 参数相同

      • PROT_EXEC:映射区可执行

      • PROT_READ:映射区可读

      • PROT_WRITE:映射区可写

      • PROT_NONE:映射区不可访问

    • addr:更改保护要求的映射区起始地址,必须是系统页大小的整数倍

    • len:指定地址范围的大小

    • 返回值

      • 调用成功时返回 0

      • 调用失败时返回 -1 并设置 errno 以指示错误原因

msync()函数

  • read()和write()系统调用和磁盘访问

    • read() 和 write() 不会直接访问磁盘,而是在用户空间缓冲区和内核缓冲区之间复制数据

    • 数据最终会在后续某时刻被内核写入磁盘

  • 数据的同步问题

    • 使用 write() 写入数据不会立即写入磁盘,而是先缓存于内核缓冲区中,导致操作与实际磁盘写入不同步
  • 文件映射区的数据同步

    • 同 write() 类似,文件映射区的数据修改也不会立即刷新至磁盘

    • 可以使用 msync() 函数来同步映射区的数据至磁盘

  • #include <sys/mman.h>

    int msync(void *addr, size_t length, int flags);

    • addr 和 length 指定同步内存区域的起始地址和大小,必须与系统页大小对齐

    • flags 参数可选 MS_ASYNC(异步同步)、MS_SYNC(同步等待写入完成)、MS_INVALIDATE(更新其他映射)

  • 映射解除与数据持久化

    • munmap() 解除映射不会自动将数据写入磁盘

    • MAP_SHARED 标志指定的映射区,数据会自动更新至磁盘

    • MAP_PRIVATE 标志指定的映射区,解除映射时所做的修改会被丢弃

普通 I/O 与存储映射 I/O 比较

  • 普通 I/O 方式的缺点

    • 使用 read() 和 write() 函数实现文件读写,涉及多层函数调用和数据在不同缓存间的转移,效率较低

    • 对于小数据量操作,普通 I/O 方式仍然方便且影响不大

  • 存储映射 I/O 的优点

    • 通过内存共享实现文件操作,减少了数据复制操作,提高了效率

    • 直接在应用层内存区域操作映射区,避免了频繁的系统调用

  • 存储映射 I/O 的共享特性

    • 文件直接映射到应用层内存区域,实现应用层与内核层的直接数据交互,视为共享内存
  • 存储映射 I/O 的不足

    • 映射文件大小固定,且必须为系统页大小的整数倍,可能导致内存浪费

    • 适用于大数据量操作,小数据量操作不如普通 I/O 方便

  • 存储映射 I/O 的应用场景

    • 主要用于处理大量数据,如视频图像处理,特别是 Framebuffer 编程(LCD 编程)

文件锁

背景

  • 文件编辑冲突

    • 在Linux系统中,当多个进程同时编辑同一文件时,文件的最终状态取决于最后一个写入的进程,这可能导致数据混乱和内容不一致
  • 文件锁机制

    • 为了防止这种冲突,Linux提供了文件锁机制,确保在某一时间段内只有一个进程可以对文件进行I/O操作,从而保护文件数据的正确性
  • 文件锁的类型

    • 建议性锁:是一种协议,要求程序在访问文件前先上锁,但并不强制阻止未上锁的访问,需要所有程序共同遵守

    • 强制性锁:强制要求在文件被锁定时,其他进程无法访问,内核会检查每个I/O操作以验证文件锁的拥有者

  • 文件锁的实现

    • 通过调用flock()、fcntl()和lockf()函数,可以在Linux系统中实现文件锁,以管理对共享文件资源的访问

flock()函数加锁

  • flock() 是 Linux 系统中用于在文件上设置建议性锁的系统调用

  • 功能是控制文件的并发访问,可以帮助防止多个进程同时写入同一个文件时发生数据损坏

  • #include <sys/file.h>

    int flock(int fd, int operation);

    • fd:文件描述符,指定需要加锁的文件

    • operation:指定操作方式,可以是以下几种

      • LOCK_SH:设置共享锁,允许多个进程同时持有

      • LOCK_EX:设置排他锁,一次只能由一个进程持有

      • LOCK_UN:用于释放文件的锁定状态

      • LOCK_NB:用于非阻塞模式,可以与 LOCK_SH 或 LOCK_EX 结合使用,使 flock() 在无法立即获得锁时不会阻塞,而是立即返回错误

      • 返回值:

        成功时返回 0,失败时返回 -1 并设置 errno

  • 行为规则

    • 在同一进程中,多次对同一文件描述符调用 flock() 不会导致死锁。新的锁请求将替代旧的锁

    • 文件在关闭时将自动解锁。如果进程结束,其所有锁也将被释放

    • 进程不能解锁另一个进程持有的锁

    • 由 fork() 创建的子进程不会继承父进程的锁

  • 关于文件描述符的复制

    • 如果一个文件描述符被复制(如使用 dup() 或 fcntl()),新的文件描述符也将引用同一个锁

    • 使用任何一个文件描述符解锁都将释放锁,但如果不显式调用解锁操作,则必须关闭所有相关的文件描述符才能自动释放锁

fcntl()函数加锁

  • fcntl()是一个多功能文件描述符管理工具,通过不同的cmd操作命令实现不同的功能

    • #include <unistd.h>

      #include <fcntl.h>

      int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

      • 与锁相关的命令有F_SETLK、F_SETLKW、F_GETLK

        • F_SETLK:尝试加锁,如果失败立即返回

        • F_SETLKW:阻塞加锁,直到锁可用

        • F_GETLK:测试是否能加锁,返回阻塞锁的信息

      • struct flock结构体:用于描述文件锁的细节,包括锁的类型、起始位置、长度和阻塞进程的PID等

        • struct flock {
          ...
          short l_type; /* Type of lock: F_RDLCK,F_WRLCK, F_UNLCK /
          short l_whence; /
          How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END /
          off_t l_start; /
          Starting offset for lock /
          off_t l_len; /
          Number of bytes to lock /
          pid_t l_pid; /
          PID of process blocking our lock(set by F_GETLK and F_OFD_GETLK) */
          ...
          };
  • 与flock()的区别

    • flock()只能对整个文件加锁/解锁,fcntl()可以对文件的某个区域加锁/解锁

    • flock()仅支持建议性锁,fcntl()支持建议性锁和强制性锁

  • 锁的类型

    • F_RDLCK:共享读锁

    • F_WRLCK:独占写锁

    • F_UNLCK:解锁

    • 不同类型锁彼此之间的兼容性

  • 锁区域规则

    • 锁区域可以从文件末尾开始或越过末尾,但不能在起始位置之前

    • l_len为0表示从起始位置到文件末尾的动态锁区

    • 要锁整个文件,可以将起始位置设置为文件起点,l_len为0

  • 规则

    • 关闭文件时自动解锁

    • 进程不能解锁另一个进程持有的锁

    • fork()创建的子进程不会继承父进程的锁

    • 复制的文件描述符(如dup())共享同一个锁

  • 强制性锁

    • 一般不建议使用,需要设置文件的Set-Group-ID位和禁用组用户执行权限。系统支持时,未获取到锁的进程对文件的读写操作会失败

lockf()函数加锁

  • lockf() 是一个库函数,用于文件锁定操作

  • lockf() 的内部实现基于 fcntl() 系统调用

  • 因此,lockf() 可以被视为对 fcntl() 锁功能的一种封装,提供了更简洁的接口

小结

非阻塞 I/O:进程向文件发起 I/O 操作,使其不会被阻塞

I/O 多路复用:select()和 poll()函数

异步 I/O:当文件描述符上可以执行 I/O 操作时,内核会向进程发送信号通知它

存储映射 I/O:mmap()函数

文件锁:flock()、fcntl()以及 lockf()函数

相关推荐
并不会35 分钟前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
龙鸣丿40 分钟前
Linux基础学习笔记
linux·笔记·学习
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
Nu11PointerException3 小时前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
2401_850410833 小时前
文件系统和日志管理
linux·运维·服务器
XMYX-04 小时前
使用 SSH 蜜罐提升安全性和记录攻击活动
linux·ssh
二十雨辰6 小时前
[linux]docker基础
linux·运维·docker
@小博的博客6 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
饮浊酒6 小时前
Linux操作系统 ------(3.文本编译器Vim)
linux·vim