高级 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); // 重新设置标志
- int 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) */
…
};
- struct flock {
-
-
-
与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()函数
原文地址:https://blog.csdn.net/weixin_62434750/article/details/140469288
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!