- 非阻塞 IO:调用时不会阻塞调用方,而是立即返回,并将 errno 设置为
EAGIN
- 将 fd 指定为非阻塞 IO 的方法
- 使用
O_NONBLOCK
标志打开 - 通过
fcntl
打开
- 使用
- 轮询:多次循环尝试同一个 IO 动作,如果失败不阻塞
- 将 fd 指定为非阻塞 IO 的方法
- 记录锁(byte-range locking):保证一个进程单独写一个文件的部分数据,对文件中的部分区域加锁
int fctnl(int fd, int cmd, ... /* struct flock *flockptr */);
- cmd 为
F_GETLK
/F_SETLK
/F_SETLKW
- F_GETLK 判断由 flockptr 描述的锁是否会被另外一把锁排斥,即判断当前锁能不能加上,如果能加上保持 flock 结构不变,否则设置为排斥当前锁的信息
- F_SETLK 设置有 flockptr 描述的锁,如果该出差会立即返回,errno 为 EACCESS 或 EAGAIN
- F_SETLKW F_SETLK 的阻塞版本,如果加锁失败则调用方被阻塞,如过锁可用,则被唤醒
- flock 结构
1
2
3
4
5
6
7struct flock {
short l_type; /* F_RDLCK, F_WRLCK, FUNLCK */
short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* offset in bytes, relative to l_whence */
off_t l_len; /* length, in bytes; 0 menas lock to EOF */
pid_t l_pid; /* pid of which process acquired the lock, returned with F_GETLK */
}; - 设置或释放文件上的锁时,系统按要求合并或分裂相邻区,如下图
- 锁的隐含继承和释放
- 锁与进程和文件两者相关联
- 进程终止时,它锁简历的锁全部释放
- 无论一个 fd 何时关闭,改进程通过这个 fd 引用的文件上的任何一把锁都会释放
- 同一个文件打开多次,close 一次锁全部释放
- 如果fd1 通过 dup 复制出 fd2,close 任意一个 fd,都会释放文件上的锁
- 子进程不会继承父进程的文件锁,只会继承 fd,如果子进程需要加锁,需要另外通过 fcntl 加锁
- 执行 exec 后,新程序继承原程序的锁(同一个进程,pid 并没有发生变化)
- 锁与进程和文件两者相关联
- 在文件尾端上加锁时要非常小心,因为大多数实现根据 l_whence 和 l_start 计算出的绝对偏移量,如下图
- 建议性锁 & 强制性锁
- 建议性锁:使用相同的库函数或者实现方式才生效的加锁实现(例如其他有写权限的进程没用就可以在有锁的时候继续写)
- 强制性锁:OS kernel 保证锁实现有效
- cmd 为
IO 多路复用
- 动机:一个进程处理多个 fd,轮询感兴趣的 fd
- 工作流程
graph LR start((start)) --> A[构造感兴趣 fd 列表] A --> B[注册 A 中的 fd 列表] B --> C[检查任一 fd 就绪] C --> D[处理就绪 fd, 进行 IO] D --> B
int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr)
告诉内核关心哪些 fd 的那些状态(可读、可写、异常),返回已就绪的 fd 总数量、哪些 fd 已就绪- 参数
- tvptr 超时时间,分下列几种情况
tvptr == NULL
永远等待,除非被信号中断tvptr->tv_sec == 0 && tvptr->tv_usec == 0
不等待,立即返回tvptr->tv_sec != 0 || tvptr->tv_usec != 0
等待指定时长,到时间或有 fd 就绪则返回
- readfds/writefds/writefds: fd 集合指针,每个 fd 维护一个 bit
- maxfdp1:
max fd + 1
,最大的 fd 编号 + 1,最大值通常为 1024
- tvptr 超时时间,分下列几种情况
- 返回值
-1
:出错,例如没有 fd ready 是收到信号0
:没有 fd 就绪> 0
:就绪 fd 数量,如果一个 fd 读写都就绪,则会被计数两次
- 参数
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout)
类似 select,用来检查感兴趣的 fd 是否就绪1
2
3
4
5struct pollfd {
int fd; /* file sescriptor to check, or < 0 to ingore */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};- select vs poll
- select fd 数量一般有上限限制,而 poll 没有
- select 会修改传入的 fd_set,而 poll 不会修改 events,因此 select 每次调用前都需要重新设置 fd_set,二 poll 不需要重新设置 events
- select 和 poll 都不受 fd 是否阻塞影响
- select 和 poll 都会被信号中断
异步 IO
- POSIX AIO 的问题
- 每个异步操作有 3 处可能产生错误的地方
- 操作提交
- 操作本身的结果
- 决定异步操作状态的函数
- 涉及大量的额外设置和处理规则
- 错误恢复很难
- 每个异步操作有 3 处可能产生错误的地方
- AIO 的 io 请求加入操作系统 IO 队列就返回成功,在 IO 操作完成之前需要保证缓冲区稳定
散布读(scatter read) & 聚集写(gather write)
readv
从一个 fd 中将数据读到多个 buffer 中,先填满一个 buffer 再填下一个writev
将多个 buffer 的数据写入到一个 fd 中- readv/writev 需要指定每个 buffer 的起始地址和长度
- 好处:能够减少系统调用次数
readn & writen
- 背景:管道、FIFO、网络设备有下面两种性质,导致需要多次调用 read/write
- 一次 read 操作返回的数据少于要求的数据
- 一次 write 操作的返回值少于指定输出字节数
存储映射(memory-mapped) IO
- Memory-mapped IO:将磁盘文件存储空间的一个缓冲区上,通过读写内存 buffer 的方式读写文件,而不用使用 read/write,映射区域位于堆栈之间
- 映射区域保护要求,不能超过文件 open 模式访问权限
- PROT_READ 可读
- PROT_WRITE 可写
- PROT_EXEC 可执行
- PROY_NONE 不可访问
- 存储区映射方式
- MAP_FIXED 返回值必须等于 addr 参数
- MAP_SHARED 指定存储操作修改映射文件,即存储操作相等于对该文件的 write,由内核觉得何时写回脏页
- MAP_PRIVATE 对存储区的操作导致创建该映射文件的一个副本,后续的引用都引用该副本
- 示意图
- mmap 时的 addr 和 offset 需要和虚拟存储页长度对齐(即是page size 的整数倍)
- 如 page size 是 512B,映射 100B 的文件,也会提供 512B 的映射区
- 操作映射文件长度之外的内存区域不会反映在文件上,而是需要先增加文件长度
- mmap 相关的信号
SIGSEGV
试图写只读的映射区SIGBUS
试图访问已截断的映射区
- mmap 与子进程
- fork 出的子进程继承父进程的存储映射区
- exec 切换执行程序后,不继承存储映射区