1. 非阻塞IO
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O进行操作
1.1 O_NONBLOCK
open函数的flag参数指定O_NONBLOCK表示使用非阻塞方式进行IO操作。open函数默认的是阻塞式IO。
1.2 阻塞式IO的优缺点
阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率!
2. IO多路复用
I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。
2.1 select()
调用 select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
fd_set 是一个文件描述符的集合体,其操作如下:
#include <sys/select.h> void FD_CLR(int fd, fd_set *set); // 初始化集合为空 int FD_ISSET(int fd, fd_set *set); // 判断文件描述符是否在集合内 void FD_SET(int fd, fd_set *set); // 添加文件描述符 void FD_ZERO(fd_set *set); // 移除文件描述符
readfds是用来检测是否可读的文件描述符的集合
readfds是用来检测是否可写的文件描述符的集合
readfds是用来检测是否发生异常情况的文件描述符的集合
timeout: 是一个timeval类型的结构体,用于设置阻塞的时长。
NULL: 表示一直阻塞,直到某一个或多个文件描述符成为就绪态
0: timeval两个成员变量都为0表示不阻塞
select()函数内部会修改 readfds、writefds、exceptfds 这些集合,当 select()函数返回时,它们包含的就是已处于就绪态的文件描述符集合了(所以循环利用这些集合时是需要重新初始化的,timeout也需要重新配置)
返回值: -1表示有错误;0表示超时;正整数表示有一个或多个文件描述符达到就绪态
2.2 poll()
和select类似,在 poll()函数中,需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
fds: 结构体struct pollfd类型的数组
nfds: fds数组的大小
timeout: 单位毫秒。-1 表示一直阻塞;0表示不阻塞
-1表示有错误;0表示超时;返回一个正整数表示有一个或多个文件描述符处于就绪态了,返回值表示 fds 数组中返回的 revents变量不为 0 的 struct pollfd 对象的数量。
pollfd的events和revents
3. 异步IO
异步IO是结合信号使用的,所以异步IO通常也称为信号驱动IO
3.1 编程步骤
3.1.1 通过指定O_NONBLOCK标志使能非阻塞IO
3.1.2 通过指定O_ASYNC标志使能异步IO
在调用 open()时无法通过指定 O_ASYNC 标志来使能异步 I/O,但可以使用 fcntl()函数添加 O_ASYNC 标志使能异步 I/O
int flag; flag = fcntl(0, F_GETFL); //先获取原来的 flag flag |= O_ASYNC; //将 O_ASYNC 标志添加到 flag fcntl(fd, F_SETFL, flag); //重新设置 flag
3.1.3 设置异步IO事件的接收进程
fcntl(fd, F_SETOWN, getpid());
3.1.4 注册信号处理函数。默认该信号是SIGIO
使用singal()或sigaction()都可
3.2 缺陷
默认的异步IO通知信号SIGIO是非排队信号,可能造成信号的丢失
无法得知发生了什么事件。
3.3 优化异步IO
3.3.1 替换默认信号
使用实时信号替换默认信号(实时信号是支持排队的)
#define _GNU_SOURCE // 这个宏必须要定义,不然编译会报错 fcntl(fd, F_SETSIG, SIGRTMIN);
3.3.2 使用sigaction()函数
配置sa_flags魏SA_SIGINFO,即使用sigaction结构体的第二个成员作为处理函数。
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
注册函数参数中包括siginfo_t指针,当触发信号时该对象有内核构建。我们只需要读取他的结构体成员就可以判断发生了什么事件了
si_signo: 引发处理函数的的信号
si_fd: 发生异步IO事件的文件描述符
si_code: 发生了什么事件
是一个位掩码,其中包含的值与系统调用 poll()中返回的 revents 字段中的值相同。如下表,si_code 中可能出现的值与 si_band 中的位掩码有着一一对应关系。
4. 存储映射IO
它能够将文件映射到进程地址空间的一块内存区域中。映射好了后,我们就读写这块内存实际上就是读写的文件。该功能通常用来处理图像数据的,如摄像头图像的采集,LCD的驱动等等。对于小文件,使用前面介绍的read,write,库函数这些反而好些。
4.1 mmap()
建立映射关系
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr: 用户指定映射到内存区域的起始地址。通常将其设置为 NULL,这表示由系统选择该映射区的起始地址,这是最常见的设置方式;如果参数 addr 不为 NULL,则表示由自己指定映射区的起始地址,此函数的返回值是该映射区的起始地址
length:指定映射区域的长度。系统会自动按页的大小对齐。所以实际上映射时所开辟的空间是系统页的倍数。而多余length的内存设置为0,但和文件并没有映射关系。
offset: 文件偏移量。
prot:指定了映射区的保护要求(可用或运算符指定多个)
PROT_EXEC: 映射区可执行
PROT_READ: 映射区可读
PROT_WRITE: 映射区可写
PROT_NONE: 映射区不可访问
flags: 可影响映射区的多种属性
MAP_SHARED:此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
MAP_PRIVATE:此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-on-write),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。
MAP_FIXED:在未指定该标志的情况下,如果参数 addr 不等于 NULL,表示由调用者自己指定映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的值作为映射区的起始地址;如果指定了 MAP_FIXED 标志,则表示要求必须使用参数 addr 指定的值作为起始地址,如果使用指定值无法成功建立映射时,则放弃!通常,不建议使用此标志,因为这不利于移植。
4.2 munmap()
解除映射,需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close()关闭文件时并不会解除映射
#include <sys/mman.h> int munmap(void *addr, size_t length);
4.3 mprotect()
该函数可以更改一个现有映射区的保护要求
#include <sys/mman.h> int munmap(void *addr, size_t length);
4.4 msync()
和fsync函数类似,msync是作用于映射区的。
#include <sys/mman.h> int msync(void *addr, size_t length, int flags);
flags:
MS_ASYNC:以异步方式进行同步操作。调用 msync()函数之后,并不会等待数据完全写入磁盘之后才返回
MS_SYNC:以同步方式进行同步操作。调用 msync()函数之后,需等待数据全部写入磁盘之后才返回
MS_INVALIDATE:是一个可选标志,请求使同一文件的其它映射无效(以便可以用刚写入的新值更新它们)
5. 文件锁
5.1 flock()
该函数可以对文件进行加锁或解锁,需要注意的是,同一个文件不会同时具有共享锁和互斥锁。
#include <sys/file.h> int flock(int fd, int operation);
operation:
LOCK_SH: 在 fd 引用的文件上放置一把共享锁。所谓共享,指的便是多个进程可以拥有对同一个文件的共享锁,该共享锁可被多个进程同时拥有
LOCK_EX: 在 fd 引用的文件上放置一把排它锁(或叫互斥锁)。所谓互斥,指的便是互斥锁只能同时被一个进程所拥有
LOCK_UN: 解除文件锁定状态,解锁、释放锁
LOCK_NB: 表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、直到其它进程释放锁为止
关于flock的几条规则:
同一进程对文件多次加锁不会导致死锁。新锁或替代旧锁
文件关闭的时候,会自动解锁
一个进程不可以对另一个进程持有的文件锁进行解锁
由fork()创建的子进程不会继承父进程创建的锁
当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以。
5.2 fcntl()
该函数也可用户对文件加锁
flock()和fcntl()区别:
flock()仅仅支持对整个文件进行加锁/解锁; 而fcntl()可对某个区域进行加锁/解锁
flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ ); struct flock { ... short l_type; /* ,可以设置为 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) */ ... };
cmd:
F_SETLK: 对文件添加由flockptr指向的struct flock对象所描述的锁
F_SETLKW: 该命令是F_SETLK的阻塞版本。
锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。
若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始,到文件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的,这意味着不管向该文件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。
如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置,并且指定参数 l_len 等于 0。
如果一个进程对文件的某个区域已经上了一把锁,后来该进程又试图在该区域再加一把锁,那么通常新加的锁将替换旧的锁
共享读锁(F_RDLCK)和互斥写锁(F_WRLCK)的兼容关系
几条规则:
文件关闭的时候,会自动解锁
一个进程不可以对另一个进程所持有的文件锁进行解锁
有fork()创建的子进程不会继承父进程所创建的锁
当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以