UNIX环境高级编程(14-高级I/O)
本章主要介绍几种高级I/O功能,主要有非阻塞I/O、记录锁、I/O多路转接、异步I/O、readv/writev函数和存储映射I/O。
非阻塞I/O
某些系统调用可能会使进程永远阻塞,一般称其为低速系统调用。而使用非阻塞I/O,可以使open
、read
和write
这类I/O操作不会阻塞,如果不能完成这些操作时,会立即出错返回。
有两种方法将其指定为非阻塞I/O:
-
调用
open
时指定O_NONBLOCK
标志。 -
通过
fcntl
函数打开O_NONBLOCK
文件状态标志。#include <fcntl.h> // Returns: depends on cmd if OK (see following), −1 on error int fcntl(int fd, int cmd, ... /* int arg */ );
记录锁
记录锁的主要功能是阻止多个进程同时修改文件的某一文件区。记录锁可以对整个文件加锁,也可以只针对文件的一部分进行加锁。
锁的类型
主要有共享读锁和独占性写锁这两种。
加读/写锁时,文件描述符必须是读/写打开。
任意多个进程在给定的字节上可以有一把共享的读锁,但是只能有一个进程有一把独占写锁。如果已经有一把或多把读锁,则不能再加上写锁;如果已经有一把写锁,则不能再对它加任何读锁。
对于同一个进程而言,如果尝试在同一个文件区间再加一把锁,无论之前是哪种类型的锁,新的锁都会覆盖旧的锁。
fcntl记录锁
记录锁也是通过fcntl
函数进行操作的,其cmd参数可选项为F_GETLK
,F_SETLK
或 F_SETLKW
。第三个参数是一个指向flock结构的指针flockptr
,用于描述锁。
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 l_whence */
off_t l_len; /* length, in bytes; 0 means lock to EOF */
pid_t l_pid; /* returned with F_GETLK */
};
l_pid
变量返回的是持有锁的进程的pid。
注意:
-
锁可以在文件尾端或者越过尾端处开始,但是不能在起始位置之前开始。
-
将起始偏移量指向文件起始处(如l_whence=SEEK_SET,l_start=0),且l_len设置为0,即可对整个文件加锁。
在设置或释放文件上的一把锁时,系统按要求组合或分裂相邻区。
一块大的加锁区域,解锁其中的一部分,系统会自动将剩余加锁区域分裂为两个加锁区域,并各自维护一把锁;如果对两块加锁区域的中间未加锁部分加锁,则3个相邻区域会合并成一个加锁区域。如上图14.4所示,100-199间解锁150,则分成两块区域;之后重新加锁150,则又会变为上半部分的状态。
加锁和解锁
上面提到的3个命令对应于3种加解锁方式,具体如下:
- F_GETLK:判断是否会被其他锁阻塞。如果flockptr描述的锁被阻塞,则现有锁的信息会重写flockptr指向的内容;如果没有被阻塞,则将
l_type
设置为F_UNLCK
,其余flockptr指向的信息不变。 - F_SETLK:设置flockptr所描述的锁。如果尝试获得读锁/写锁,但是系统无法给这把锁,那么会立即出错返回,并将
errno
设置为EACCES
或EAGAIN
。如果将类型设置为F_UNLCK
,那么此命令会清除flockptr指定的锁。 - F_SETLKW:F_SETLK的阻塞版本。不能获取锁的时候,进程会被休眠,直到锁可用或者被信号唤醒。
继承与释放
-
锁与进程和文件两者相关联。即(a)当一个进程终止时,它建立的锁全部释放;(b)关闭一个描述符时,引用的文件上的该进程的所有锁都会释放(无论该文件是否还有其他的描述符)。
如图14.8所示,当父进程关闭fd1、2或3中任意一个时,与之关联的锁都会释放。系统会逐个检查lockf链表中的各项,释放调用进程持有的锁。
-
由fork产生的子进程不继承父进程所设置的锁。
-
在执行exec后,新程序可以继承原程序的锁。
I/O多路转接
对于需要同时对多个文件进行操作的场景,比如从两个描述符中读取数据并全部存入另一个文件中,无法通过阻塞读(read)来读取这两个描述符,因为当一个描述符被读操作阻塞时,另一个描述符可能有数据可以读取。
通过I/O多路转接技术,可以构建一张描述符表,调用一个函数,直到列表中的一个描述符准备好后该函数返回。omv-confdbadm populate
select
#include <sys/select.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int select(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict tvptr);
-
maxfdp1
指定搜索的最大描述符,该值应该是3个描述符集中的最大值+1。 -
readfds
,writefds
和exceptfds
是指向描述符集的指针,分别表示我们关心的可读、可写或处于异常状态的描述符集合。// Returns: nonzero if fd is in set, 0 otherwise int FD_ISSET(int fd, fd_set *fdset); void FD_CLR(int fd, fd_set *fdset); void FD_SET(int fd, fd_set *fdset); void FD_ZERO(fd_set *fdset);
描述符集支持以上4中操作,声明一个描述符集后,必须首先使用
FD_ZERO
将其置为0,之后再通过SET和CLR函数设置各个描述符位。 -
tvptr
为等待时间(该值在返回时可能被改变)。- 设置为NULL表示永远等待。捕捉到信号(函数返回-1且errno设置为EINTR)或有描述符准备好后才返回。
- 时间设置为0则表示不等待,测试完所有描述符后立即返回。
- 时间不为0,则等待对应的时间。超时(返回0)或有描述符准备好后即返回,另外也会被信号打断。
-
该函数的返回值>0则表示有描述符已经准备好了,此返回值是3个描述符集中准备好的描述符之和,因此,如果描述符集中有相同的描述符,则该描述符会被多次计数。描述符集中仍旧打开的位是准备好的描述符,可以通过
FD_ISSET
来测试。
当3个描述符集都设置为NULL时,select就变成了一个延时函数。
另外还有一个变体函数pselect
。
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int pselect(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
const struct timespec *restrict tsptr,
const sigset_t *restrict sigmask);
与select
函数主要有以下不同:
- 等待时间使用的数据结构不同。
- 超时时间不会被改变。
- 多了一个信号屏蔽字
sigmask
。当不为NULL时,调用pselect
函数会原子地安装该信号屏蔽字,在返回时复原。
poll
#include <poll.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
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 */
};
使用pollfd结构的数组代替了select
函数中的3个描述符集。nfds
即为数组中的元素个数。其中,events
的可选值见图14.17,可以选择多个;返回时,revents
说明了描述符发生的事件。
timeout
指定等待时间,单位是毫秒。
异步I/O
本节主要讨论POSIX中的异步I/O接口。
AIO控制块
异步接口使用AIO控制块来描述I/O操作,其主要结构如下:
struct aiocb {
int aio_fildes; /* file descriptor */
off_t aio_offset; /* file offset for I/O */
volatile void *aio_buf; /* buffer for I/O */
size_t aio_nbytes; /* number of bytes to transfer */
int aio_reqprio; /* priority */
struct sigevent aio_sigevent; /* signal information */
int aio_lio_opcode; /* operation for list I/O */
};
其中,aio_buf
作为读写操作的缓冲区,在操作完成前必须始终有效且不能复用。
如果文件打开方式为追加模式O_APPEND,向其写入数据时,偏移量aio_offset
会被忽略。
aio_lio_opcode
指定该操作是读(LIO_READ)、写(LIO_WRITE)还是空(LIO_NOP)操作,该参数仅在基于列表的异步I/O操作lio_listio时有效。
aio_sigevent
结构如下,它表示在I/O事件完成后,如何通知程序:
struct sigevent {
int sigev_notify; /* 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
控制通知类型,有如下3中取值:
- SIGEV_NONE: 不通知进程。
- SIGEV_SIGNAL:产生
sigev_signo
指定的信号。如果程序捕获该信号,并设置SA_SIGINFO标志(通过sigaction设置),那么信号处理程序得到的siginfo结构中的si_value被设置为sigev_value
。 - SIGEV_THREADS:调用
sigev_notify_function
指定的函数,且传入的参数为sigev_value
。默认情况下该函数通过一个单独的分离线程执行,除非sigev_notify_attributes
设置了线程参数。
接口函数
#include <aio.h>
// Both return: 0 if OK, −1 on error
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
读写函数返回时,异步I/O请求被放入等待处理队列,返回值与读写操作的结果无关。
// Returns: 0 if OK, −1 on error
int aio_fsync(int op, struct aiocb *aiocb);
如果希望等待中的异步操作不等待而直接写入,可以调用aio_fsync函数,同样的,该函数也仅仅是发送一个请求,而不会等待操作结束。
op
参数设定为O_DSYNC,则执行起来与fdatasync类似;如果设置为O_SYNC,则与fsync类似。
int aio_error(const struct aiocb *aiocb);
获取异步读/写或同步操作的完成状态,返回值有以下4种情况:
- 0:异步操作成功完成。
- -1:函数出错,可以通过errno查看出错信息。
- EINPROGRESS:操作仍在等待。
- 其他:相关的异步操作失败返回的错误码。
ssize_t aio_return(const struct aiocb *aiocb);
获取异步操作的返回值,如果上面的aio_error返回0时,可以调用该函数查看异步操作的返回值。函数返回-1表示出错,会设置errno;其余情况为异步操作的结果。
注意:
异步操作完成前不要调用该函数,并且对每个异步操作仅调用一次该函数。因为调用该函数后,操作系统就可以释放掉包含了I/O操作返回值的信息。
// Returns: 0 if OK, −1 on error
int aio_suspend(const struct aiocb *const list[], int nent,
const struct timespec *timeout);
阻塞进程等待异步操作完成。
list
参数是指向SIO控制块数组的指针,nent
为数组的条目数。timeout
设置为NULL可以不设时间限制。
如果被信号中断,则返回-1且errno设置为EINTR;如果超时则返回-1且errno设置为EAGAIN。任何一个操作完成都会使该函数返回0。
int aio_cancel(int fd, struct aiocb *aiocb);
取消异步操作。
fd
为未完成操作的文件的文件描述符。aiocb
为文件上的某个指定的异步操作,如果设置为NULL,则会取消文件上所有未完成的异步操作。该函数无法保证能够取消正在进程中的操作。
返回值:
- AIO_ALLDONE:所有操作在取消前就已经完成。
- AIO_CANCELED:所要求的操作已被取消。
- AIO_NOTCANCELED:至少一个请求的操作没有被取消。
- -1:调用失败,错误码在errno中。
对被取消的操作调用aio_error
会返回错误ECANCELED。
int lio_listio(int mode, struct aiocb *restrict const list[restrict],
int nent, struct sigevent *restrict sigev);
该函数提交一系列由一个AIO控制块列表描述的I/O请求。
mode
参数决定该函数是否是异步的。如果被设定为LIO_WAIT
,那么函数将在列表中的所有操作完成后返回;如果设定为LIO_NOWAIT
,那么函数将在I/O请求入队后返回,并在所有操作结束后,按照sigev
的设定被异步地通知(无需通知则设为NULL)。sigev
通知不同于AIO控制块本身的通知,它是额外的,且只会在所有操作完成后才会发送。
readv和writev
这两个函数用于在一次函数调用中读、写多个非连续缓冲区,也称之为散布读和聚集写。
#include <sys/uio.h>
// Both return: number of bytes read or written, −1 on error
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
writev
按照iov[0]、iov[1]直至iov[iovcnt-1]的顺序输出数据,且返回输出的总字节数。
readv
则将读入的数据按照上面的顺序依次存入各个缓冲区,返回读到的总字节数。如果遇到文件尾端,则返回0。
存储映射I/O
该技术将一个磁盘文件映射到存储空间的一个缓冲区上,从缓冲区读写数据就相当于向文件读写数据。可以在不使用read/write函数的情况下执行I/O。
映射与解除
#include <sys/mman.h>
// Returns: starting address of mapped region if OK, MAP_FAILED on error
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off );
addr
指定映射存储区的起始地址。设置为0则由系统自动分配。
prot
参数指定映射存储区的保护要求,如下表所示:
prot | 说明 |
---|---|
PROT_READ | 映射区可读 |
PROT_WRITE | 映射区可写 |
PROT_EXEC | 映射区可执行 |
PROT_NONE | 映射区不可访问 |
表中前三项可以任意组合(按位或),但是保护要求不能超过文件本身的访问权限。
flag
参数指定了映射区的各类属性:
-
MAP_FIXED:返回值必须等于
addr
。即要求内核必须将存储区的起始地址设置为addr
,如果没有此标志且addr
非0,内核仅将addr
的值视为一种建议。 -
MAP_SHARED:表示存储操作会修改映射文件,即相当于调用write。
-
MAP_PRIVATE:表示存储操作会创建改映射文件的副本,所有的存储操作不会修改真实文件。
注意:
off
和addr
的值一般要求是虚拟系统存储页长度的倍数。
对于一些映射区不是页长整数倍的情况,系统会分配更多的映射区以满足此要求。如文件长为12字节,页长512字节,则系统会提供512字节的映射区。可以修改后面500字节的内容,但是不会作用到原文件上。
// Returns: 0 if OK, −1 on error
int munmap(void *addr, size_t len);
进程终止或者调用munmap
都会解除映射区。但是关闭文件描述符并不会解除映射,并且调用munmap
也不会使映射区的内容写到磁盘文件上。
其他
// Returns: 0 if OK, −1 on error
int mprotect(void *addr, size_t len, int prot);
该函数可以更改一个现有映射的权限。
对于通过MAP_SHARED方式进行的映射,所作的修改不会立即写回到文件中。
// Returns: 0 if OK, −1 on error
int msync(void *addr, size_t len, int flags);
改函数将修改的页冲洗到文件中去。
如果将flags
参数指定为MS_ASYNC,则仅仅是请求一个写入操作;如果指定为MS_SYNC,那么在返回之前会等待写操作完成。这两个选项必选其一。
另外,还可以指定MS_INVALIDATE,来告诉操作系统丢弃与底层存储器没有同步的页。