第十四章 高级I/O
14.2 非阻塞I/O
非阻塞I/O使我们可以发出open、read和write这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。
对于一个给定的描述符,有两种为其指定非阻塞I/O的方法:
- 如果调用open获得描述符,则可指定O_NONBLOCK标志
- 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志
14.3 记录锁
记录锁(字节范围锁)的功能是:当地一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。它可以锁定文件中一个区域或是整个文件。
fcntl记录锁
#include <fcntl.h>
int fcntl(int fd, int cmd, .../* struct flock *flockptr*/);
//返回值:若成功,依赖于cmd;否则,返回-1
struct flock {
short l_type; /*F_RDLCK, F_WRLCK, or F_UNLCK*/
short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END */
off_ t l_start; /* offset in bytes, relative to 1_whence */
off_t l_1en; /* length, in bytes; 0 means lock to EOF */
pid_t l pid; /* returned with F GETLK /
};
fcntl函数的三种命令:
- F_GETLK:判断由flockptr所描述的锁是否会被另外一把锁所排斥(阻塞),如果存在一把锁,它阻止创建由flockptr所描述的锁,则该现有锁的信息将重写fockptr指向的信息。如果不存在这种情况,将l_type设置为F_UNLCK。
- F_SETLK:设置由flockptr所描述的锁。如果我们试图获得一把加粗样式读锁或写锁,而兼容性规则阻止系统给我们这把锁,那么fcntl会立即出错返回,此时errno置为EACCES或EAGAIN。此命令也用来清除由flockptr指定的锁(l_type为F_UNLCK)
- F_SETLKW:这个命令是F_SETLK的阻塞版本
flockptr是一个指向flock结构的指针。flock结构包含了:
- l_type:所希望的锁类型,F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)
- l_start和l_whence:加锁或解锁区域的起始字节偏移量
- l_len:区域字节长度_len
- l_pid:持有锁的进程ID
注意:
- 锁可以在当前文件尾端处开始或者越过尾端处开始,但是不能在文件起始位置之前开始。
- 如若l_len为0,则表示锁的范围可以扩展到最大可能偏移量。
共享读锁与独占性写锁:如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何读锁。
如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换已有锁。
如果加读锁的请求来得很频繁,使得该文件区间始终存在一把或几把读锁,那么欲加写锁的进程就将等待很长时间。
死锁:检测到死锁时,内核必须选择一个进程接收出错返回。
锁的隐含继承和释放
- 无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放
- 由fork产生的子进程不继承父进程所设置的锁
- 在执行exec后,新程序可以继承原执行程序的锁
当对文件的一部分加锁时,内核将指定的偏移量变换成绝对文件偏移量。
建议性锁和强制性锁
考虑数据库访问例程库。如果该库中所有函数都以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集为合作进程。不使用数据库访问例程库协同一致的方法来访问数据库的进程是非合作进程。
如果这些函数是唯一地用来访问数据库的函数,那么它们使用建议性锁是可行的。
建议性锁并不能阻止对数据库文件有写权限的任何其他进程写这个数据库文件。
强制性锁会让内核检查每一个open、read和write,验证调用进程是否违背了正在访问的文件上的某一把锁。
在linux中要使用强制性锁,则需要在各个文件系统基础上用mount命令的-o mand选项来打开该机制。
sudo mount -o remount,mand /dev/mmcblk0p3 /
对一个特定文件打开其设置组ID位、关闭其组执行位便开启了对该文件的强制性锁机制
当read或write是非阻塞描述符时,若不能获得锁则返回错误,errno设置为EAGAIN。
若open中标志位指定为O_TRUNC或O_CREAT,则不论是否制定O_NOBLOCK,欲打开的文件具有强制性锁,open都立即出错返回,errno设置为EAGAIN。
14.4 I/O多路转接
14.4.1 函数select和pselect
select函数使我们可以执行I/O多路转接。
#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);
//返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1
tvptr参数指定等待的时间长度,单位为秒和微秒
- tvptr==NULL:永远等待。若捕捉到一个信号,则select返回−1,errno设置为EINTR。
- tvptr−>tvsec==0&&tvptr−>tvusec==0:不等待,测试所有指定的描述符并立即返回。
- tvptr−>tvsec≠0||tvptr−>tvusc≠0:等待指定的秒数和微秒数。
若在超时时间尚未到期时,select就返回,那么Linux3.2.0将用剩余时间值更新该结构。
readfds、writefds、exceptfds是指向描述符集的指针,说明了我们关心的可读、可写或处于异常条件的描述符集合。
对于fd_set的操作函数:
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset); //测试描述符集中的一个指定位是否已打开
//返回值:若 fd 在描述符集中,返回非 0 值;否则,返回 0
void FD_CLR(int fd, fd_set *fdset); //清除fdset中指定位
void FD_SET(int fd, fd_set *fdset); //开启fdset中的指定位
void FD_ZERO(fd_set *fdset); //将一个fd_set变量的所有位设置为
在声明了一个描述符集之后,必须用FDZERO将这个描述符集置为0,然后在其中设置我们关心的各个描述符的位。
select中的三个描述符集指针都可以为NULL,表示对相应条件并不关心。如果三个指针都是NULL,则select提供了比sleep更精确的定时器。
select第一个参数maxfdp1的意思是最大文件描述符值加1。也可以使用FD_SERSIZE,他指定最大描述符数。
select返回值:
- 返回值−1表示出错。如在所指定的描述符中一个都没准备好时捕捉到一个信号。
- 返回值0表示超时的时候没有描述符准备好。
- 一个正返回值说明了已经准备好的描述符数,该值是3个描述符集中已准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中会对其计两次数。
对于“准备好”的含义要作一些更具体的说明。
- 若对读集中的一个描述符进行的read操作不会阻塞,则认为此描述符是准备好的。
- 若对写集中的一个描述符进行的write操作不会阻塞,则认为此描述符是准备好的。
- 若对异常条件集中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。异常条件包括:在网络连接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件。
- 对于读、写和异常条件,普通文件的文件描述符总是返回准备好。
pselectt函数:
#include <sys/select.h>
int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, const struct timespec *restrict tsptr, const sif=gset_t *restrict sigmask);
//返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1
pselect函数与select函数的区别:
- pselect超时值由timespec结构指定,提供更精准的超时时间。
- pselect超时值被声明为const,保证了调用pselect不会改变此值。
- pselect可使用信号屏蔽字,调用pselect时以原子操作的方式安装该信号屏蔽字,返回时恢复以前的信号屏蔽字。
14.4.2 函数poll
poll与select类似
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
//返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1
使用poll函数时构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。
struct pollfd {
int fd; // file descriptor to check, or < 0 to ignore
short events; // events of interest on fd
short revents; // events that occurred on fd
};
fdarray数组中的元素数由nfds指定。
events告诉内核我们关心的是每个描述符的哪些事件。
revents成员由内核设置,用于说明每个描述符发生了哪些事件。
每个数组元素的events和revents成员的取值如下:
POLLIN:可以不阻塞地读高优先级数据以外的数据(等效于POLLRDNORM | POLLRDBAND)
POLLRDNORM:可以不阻塞地读普通数据
POLLRDBAND:可以不阻塞地读优先级数据
POLLPRI:可以不阻塞地读高优先级数据
POLLOUT:可以不阻塞地写普通数据
POLLWRNORM:与POLLOUT相同
POLLWRBAND:可以不阻塞地写优先级数据
POLLERR:已出错
POLLHUP:已挂断
POLLNVAL:描述符没有引用一个打开文件
最后三行时由内核在返回时设置的。
poll的最后一个参数指定的是我们愿意等待多长时间。
- timeout == -1:永远等待
- timeout == 0:不等待
- timeout > 0:等待timeout毫秒
当一个描述符被挂断(POLLHUP)后,就不能再写该描述符,但是有可能仍然可以从该描述符读取到数据。
在接到一信号时都不重启动poll和select,即便使用了SA_RESTART标志也是如此。
14.5 异步I/O
AIO控制块:`
struct aiocbt {
int aio_fildes; /* file descriptor */
off_t aio_offset; /* file offset for I/O */
volatile void *aio_buf; /* buffer for I/O */
sizet aio_nbytes; /* number of bytes to transfer */
int aio_reqprio; /* priority */
struct sigevent aio_sigevent;/* signal information */
intnaio_lioopcode; /* operation for list I/O */
};
aio_sigevent字段控制,在I/O事件完成后,如何通知应用程序。
struct sigevent {
int sigevnotify; /* notify type */
int sigev_signo; /* signal number */
union sigval sigev_value; /* notify argument */
void (*sigev_notify_function)(union sigval); /* notify function */
pthread attr_t *sigev_notify_attributes; /* notify attrs */
};
sigev_notify取值:
- SIGEV_NONE:异步I/O请求完成后,不通知进程。
- SIGEV_SIGNAL:异步I/O请求完成后,产生由sigev_signo字段指定的信号。
- SIGEV_THREAD:当异步VO请求完成时,由sigev_notify_function字段指定的函数被调用。sigev_value 字段被传入作为它的唯一参数。除非sigev_notify_attributes字段被设定为pthread属性结构的地址,且该结构指定了一个另外的线程属性,否则该函数将在分离状态下的一个单独的线程中执行。
在进行异步I/O之前需要先初始化AIO控制块,调用aio_read函数来进行异步读操作,或调用aio_write函数来进行异步写操作
#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
//返回值:若成功,返回 0;若出错,返回 -1
要想强制所有等待中的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制块并调用aio_fsync函数:
#include <aio.h>
int aiu_fsync(int op, struct aiocb *aiocb);
//返回值:若成功,返回 0;若出错,返回 -1
AIO控制块中的aio_fildes字段指定了其异步写操作被同步的文件。如果op参数设定为O_DSYNC,那么操作执行起来就会像调用了fdatasync一样。否则,如果op参数设定为O_SYNC,那么操作执行起来就会像调用了fsync一样。
为获取一个异步读、写或者同步操作的完成状态,需要调用aio_error函数
#include <aio.h>
int aio_error(const struct aiocb *aiocb);
//返回值:异步操作成功返回 0;函数调用失败返回 -1;异步读、写或同步操作仍在等待返回 EINPROGRESS
若返回值不是上面的情况,则其他任何返回值是相关的异步操作失败返回的错误码。
可调用aio_return函数来获取异步操作的返回值,获取返回值前必须确认操作已完成
#include <aio.h>
ssize_t aio_return(const struct aiocb *aiocb);
//返回值:函数调用失败,返回 -1,并设置 errno
直到异步操作完成之前,都需要小心不要调用aio_return函数。
需要小心对每个异步操作只调用一次aio_return。
若果返回值不是-1,则返回的时异步操作的结果。
如果在完成了所有事务时,还有异步操作未完成,可以调用aio_suspend函数来阻塞进程,直到操作完成
#include <aio.h>
int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout);
//返回值:若有任何I/O操作完成,返回 0;若出错,返回 -1
返回值:
- 如果我们被一个信号中断,它将会返回-1,并将errno设置为EINTR。
- 如果在没有任何I/O操作完成的情况下,阻塞的时间超过了函数中可选的timeout参数所指定的时间限制,那么aio_suspend将返回-1,并将errno设置为EAGAIN。
- 如果有任何I/O操作完成,返回0。
list参数是一个指向AIO控制块数组的指针,nent参数表明了数组中的条目数。数组中的空指针会被跳过,其他条目都必须指向已用于初始化异步、I/O操作的AIO控制块。
若还有不想再完成的等待中的异步I/O操作时,可尝试使用aio_cancel函数取消它们
#include <aio.h>
int aio_cancel(int fd, struct aiocb *aiocb);
//若 aiocb 为NULL,系统尝试取消所有该文件上未完成的异步 I/O 操作;其他情况下,系统将尝试取消由 AIO 控制块描述的单个异步 I/O 操作
fd参数指定了那个未完成的异步I/O操作的文件描述符。
如果aiocb参数为NULL,系统将会尝试取消所有该文件上未完成的异步I/O操作。其他情况下,系统将尝试取消由AIO控制块描述的单个异步I/O操作。
aio.cancel函数返回值。
AIO_ALLDONE:所有操作在尝试取消它们之前已经完成。
AIO_CANCELED:所有要求的操作已被取消。
AIO_NOTCANCELED:至少有一个要求的操作没有被取消。
-1:对aio_cance1的调用失败,错误码将被存储在errno中。
如果异步I/O操作被成功取消,对相应的AIO控制块调用aio_error函数将会返回错误ECANCELED。
aio_listio函数提交一系列由一个AIO控制块列表描述的I/O请求
#include <aio.h>
int lio_listio(int mode, struct aiocb *restrict const list[restrict], int nent, struct sigevent *retrict sigev);
//返回值:若成功,返回 0;若出错,返回 -1
- mode参数决定了I/O是否真的是异步的。如果该参数被设定为LIO_WAIT,lio_listio函数将在所有由列表指定的I/O操作完成后返回。在这种情况下,sigev参数将被忽略。如果mode参数被设定为LIO_NOWAIT,lio_listio函数将在I/O请求入队后立即返回。进程将在所有1/O操作完成后,按照sigev参数指定的,被异步地通知。如果不想被通知,可以把sigev设定为NULL。
- 每个AIO控制块本身也可能启用了在各自操作完成时的异步通知。被sigev参数指定的异步通知是在此之外另加的,并且只会在所有的I/O操作完成后发送。
- list参数指向AIO控制块列表,该列表指定了要运行的I/O操作的。nent参数指定了数组中的元素个数。AIO控制块列表可以包含NULL指针,这些条目将被忽略。
- 在每一个AIO控制块中,aio_lio_opcode 字段指定了该操作是一个读操作(LIO_READ)、写操作(LIO_WRITE),还是将被忽略的空操作(LIO_NOP)。读操作会按照对应的AIO控制块被传给了aio_read函数来处理。写操作会按照对应的AIO控制块被传给了aio_write函数来处理。
14.6 函数readv和writev
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovent);
ssize_t writev(int fd, const struct iovec *iov, int iovent);
//返回值:已读或已写的字节数;若出错,返回 -1
struct iovec {
void *iov_base; // starting address of buffer
size_t iov_len; // size of buffer
};
iov数组中的元素数由iovcnt指定。
14.8 存储映射I/O
存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上。
应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
//返回值:若成功,返回映射区的起始地址;若出错,返回 MAP_FAILED
- addr参数用于指定映射存储区的起始地址,通常设置为0,表示由系统选择该映射区的起始地址。
- fd参数是指定要被映射文件的描述符。
- len是映射的字节数,
- off是要映射字节在文件中的起始偏移量。
(addr和off的值通常被要求是系统虚拟存储页长度的倍数) - prot参数指定了映射存储区的保护要求:
PROT_READ|:映射区可读
PROT_WRITE:映射区可写
PROT_EXEC:映射区可执行
PROT_NONE:映射区不可访问 - flag参数影响映射存储区的多种属性。
MAP_FIXED:返回值必须等于addr。
MAP_SHARE:对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE:对映射区域的写入操作会产生一个映射文件的复制,对此区域作的任何修改都不会写回原来的文件内容。
对指定映射存储区的保护要求不能超过文件open模式访问权限。
不能用mmap将数据添加到文件中。
子进程能通过 fork 继承存储映射区。
调用mprotect函数可以更改一个现有映射的权限
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
//返回值:若成功,返回 0;若出错,返回 -1
地址参数addr的值必须是系统页长的倍数。
如果修改的页是通过MAP_SHARED标志映射到地址空间的,那么修改并不会立即写回到文件中。
如果共享映射中的页已经修改,那么可以调用msync将页冲洗到被映射的文件中
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
//返回值:若成功,返回 0;若出错,返回 -1
flags参数使我们对如何冲洗存储区有某种程度的控制。可以指定MS_ASYNC标志来简单地调试要写的页。如果希望在返回之前等待写操作完成,则可指定MS_SYNC标志。一定要指定MS_ASYNC和MS_SYNC中的一个。
当进程终止时,会自动解除存储映射区的映射,或者直接调用munmap函数也可以解除映射区
#include <sys/mman.h>
int munmap(void *addr, size_t len);
//返回值:若成功,返回 0;若出错,返回 -1
调用munmap并不会使映射区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,会在我们将数据写到存储映射区后的某个时刻,按内核虚拟存储算法自动进行。在存储区解除映射后,对MAP_PRIVATE存储区的修改会被丢弃。