非阻塞I/O
低速系统调用的是可能会使进程永远阻塞的一类系统调用。
1. 如果某些文件类型(读管道、终端设备和网络设备)的数据并不存在,读操作可能会使调用者永远阻塞
2. 如果数据不能被相同的文件类型立即接受(管道中无空间、网络流控制)。写操作可能会使调用者永远阻塞
3. 在某种条件发生之前打开某些文件类型可能会发生阻塞
4. 对已经加上强制性记录锁的文件进行写
5. 某些ioctl操作
6. 某些进程间通信函数
非阻塞I/O使我们可以发出open,read和write这样的I/O操作,并使这些操作永远不会阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞
对于一个给定的描述符,有两种为其指定非阻塞I/O的方法
1. 如果调用open获得描述符,则可指定O_NONBLOCK标志
2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志
记录锁
对于有些应用程序,如数据库,进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用UNIX系统提供了记录锁机制。
记录锁的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。一个更适合的术语可能是字节范围锁,因为它锁定的只是文件中的一个区域
fcntl记录锁
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*struct flock *flockptr*/);
//返回值:若成功,依赖于cmd,否则,返回-1
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数是一个指向flock结构的指针
struct flock{
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
- 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)
- 要加锁或解锁区域的起始字节偏移量(l_start 和 l_whence)
- 区域的字节长度( l_len )
- 进程的ID( l_pid)持有的锁能阻塞当前进程(仅由 F_GETLK 返回)
对于加锁或解锁区域的说明还要注意下列几项规则
1. 指定区域起始偏移量的两个元素与lseek函数中最后两个参数类似。l_whence可选用的值是SEEK_SET、SEEK_CUR和SEEK_END
2. 锁可以在当前文件尾端处开始或者越过尾端处开始,但不能在文件起始位置之前开始
3. 若l_len为0,则表示锁的范围可以扩展到最大可能偏移量。这以为着不管向该文件追加写了多少数据,它们都是可以处于锁的范围内,而且起始位置可以是文件中的任意一个位置
4. 为了对整个文件加锁,我们设置l_start和l_whence指向文件的起始位置,并且指定长度为0。
上面所提到的两种锁的使用基本规则是:任意多个进程在一个给定的字节上可以有一把共享的锁,但是在一个给定字节上只能有一个进程有一把独占写锁。如果在一个给定字节上已经有一把或多把读锁,则不能再该字节上再加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何读锁。
cmd的3中命令:
命令 | 描述 |
---|---|
F_GETLK | 判断有flockptr所描述的锁是否会被另一把锁排斥。如果存在一把锁,它阻止创建由flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变 |
F_SETLK | 设置有flockptr所描述的锁,如果师门试图获得一把读锁或写锁,而兼容性规则阻止系统给我们这把锁,那么fcntl会立即出错返回,此时errno设置为EACCES或EAGAIN此命令也用来清除有flockptr指定的锁 |
F_SETLKW | 这个命令是F_SETLK的阻塞版本。如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程会置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒 |
#include "apue.h"
#include <fcntl.h>
int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock;
lock.l_type = type;
lock.l_start = offset;
lock.l_whence = whence;
lock.l_len = len;
return(fcntl(fd, cmd, &lock));
}
锁的隐含继承和释放
规则:
1. 锁与进程和文件两者关联。这有两重含义:一、当一个进程终止时,它所建立的锁全部释放;二、无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放。
2. 由fork产生的子进程不继承父进程锁设置的锁。这个约束是有道理的,因为锁的作用是阻止多个进程同时写同一个文件。如果子进程通过fork继承父进程的锁,则父进程和子进程就可以写同一个文件。
3. 在执行exec后,新程序可以继承原执行程序的锁。但注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁
在文件尾端加锁
在对相对于文件尾端的字节范围加锁或解锁时需要特别小心。大多数实现按照l_whence的SEEK_CUR或SEEK_END值,用l_start以及文件当前位置或当前长度得到绝对文件偏移量。但是,常常需要相对于文件的当前长度指定一把锁,但又不能调用fstat来得到当前文件长度,因为我们在该文件上没有锁。
建议性锁和强制性锁
考虑数据库访问例程库。如果该库中所有函数都以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集合为合作进程。如果这些函数是唯一地用来访问数据库的函数,那么他们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库有写权限的任何其他进程写这个数据库文件。不使用数据库访问例程库协同一致的方法来访问数据库的进程是非合作进程
强制性锁会让内核检查每一个open、read和write,验证调用进程是否违背了正在访问的文件上的某一把锁。强制性锁有时也称为强迫方式锁
对一个特定文件打开其设置组ID位、关闭其组执行位便开启了对该文件的强制性机制。因为当组执行位关闭时,设置组ID位不再有意义。
I/O多路转接
当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中使用阻塞I/O:
while((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
if(write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");
但是如果要从两个描述符读,这种情况可能阻塞。所以为了处理这种情况需要另一种不同的技术
可以使用多进程或者多线程,还可以使用非阻塞,但是这些方法都有不足之处。
还有一种技术称为异步I/O(asynchronous I/O)。当描述符准备好可以进行I/O时,用一个信号通知它。这种技术有两个问题。首先,可移植性的问题。第二,这种信号对每个进程而言只有1个
一种较好的技术就是使用I/O多路转接。为了使用这种技术,先构造一张我们感兴趣的描述符的列表,然后调用一个函数,知道这些描述符中的一个已准备好进行I/O时,该函数才返回。poll、pselect和select这3个函数使我们能够执行I/O多路转接。
函数select和pselect
函数告诉内核:
1. 我们所关心的描述符
2. 对于每个描述符我们所关心的条件
3. 愿意等待多长时间
4. 已准备好的描述符的总数量
5. 对于读、写或异常这3个条件中的每一个,哪些描述符已准备好
#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict excepyfds
, struct timeval *restrict tvptr);
//返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1
tvptr | 描述 |
---|---|
NULL | 永远等待 |
等于0 | 根本不等待 |
不等于0 | 等待指定秒数和微妙数 |
中间3个参数是指向描述符集的指针。这3个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个fd_set数据类型中。
对于fd_set数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用下列4个函数中的一个。
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset);//检测是否打开
//返回值:若fd在描述符集中,返回非0值;否则,返回0
void FD_CLR(int fd, fd_set *fdset)//清空一位
void FD_SET(int fd, fd_set *fdset);//设置一位
void FD_ZERO(fd_set *fdset);//清0
select的中间3个参数中的任意一个可以是空指针,这表示对相应条件并不关心。如果所有3个指针都是NULL,则select提供了比sleep更精确的定时器
第一个参数就是表示要搜寻的最大描述符编号值加1。
返回值
1. 返回值-1表示出错
2. 返回值0表示没有描述符准备好
3. 一个正返回值说明了已经准备好的描述符数。
pselect是一个select的变体,最后一个参数增加了可使用可选信号屏蔽字
函数poll
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_y nfds, int timeout);
//返回值:准备就绪的描述符数目:若超时,返回0;若出错,返回-1
struct pollfd{
int fd;
short events;
short revents;
};
fdarrary数组中的元素数由nfds指定
标志名 | events | revents | 说明 |
---|---|---|---|
POLLIN | 1 | 1 | 可以不阻塞的读高优先级数据以外的数据 |
POLLRDNORM | 1 | 1 | 可以不阻塞的读不通数据 |
POLLRDBAND | 1 | 1 | 可以不阻塞的读优秀先级数据 |
POLLPRI | 1 | 1 | 可以不阻塞的读高优先级数据 |
POLLOUT | 1 | 1 | 可以不阻塞的写普通数据 |
POLLWRNORM | 1 | 1 | 与POLLOUT相同 |
POLLWRBAND | 1 | 1 | 可以不阻塞的优先级写 |
POLLERR | 1 | 已出错 | |
POLLHUP | 1 | 已挂断 | |
POLLNVAL | 1 | 描述符没有引用一个打开文件 |
当一个描述符被挂断后,就不能再写该描述符,但是仍有可能从该描述符读到数据
timeout | 说明 |
---|---|
-1 | 永远等待 |
0 | 不等待 |
大于0 | 等待timeout毫秒 |
异步I/O
struct aiocb{
int aio_fildes;//文件描述符
off_t aio_offset//偏移量
volatile void *aio_buf;//指定开始地址
size_t aio_nbytes;//字节数
int aio_reqprio;//异步I/O请求提示顺序
struct sigevent aio_sigevent;//字段控制
int aio_lio_opcode;//控制通知类型
};
struct sigevent{
int sigev_notify;
int sigev_signo;
union sigval sigev_value;
void (*sigev_notify_function)(union sigval);
pthread_attr_t *sigev_notify_attributes;
};
sigev_notify字段控制通知的类型。
SIGEV_NONE 异步I/O请求完成后,不通知进程
SIGEV_SIGNAL 异步I/O请求完成后,产生由sigev_signo字段指定的信号。
SIGEV_THREAD 当异步I/O请求完成时,由sigev_notify_function字段指定的函数被调用。
异步读写操作
#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
//两个函数的返回值:若成功,返回0;若出错,返回-1
要想强制所有等待中的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制块并调用aio_fsync函数
#include <aio.h>
int aio_fsync(int op, struct aiocb *aiocb);
//返回值:若成功,返回0;若出错,返回-1
如果op参数设定为O_DSYNC,那么操作执行起来就会像调用了fdatasync一样。否则,如果op参数设定为O_SYNC,那么操作执行起来就会调用了fsnyc一样。
为了获得一个异步读、写或者同步操作的完成状态,需要调用aio_error函数
#include <aio.h>
int aio_error(const struct aiocb *aiocb);
//返回值:见下
返回值 | 说明 |
---|---|
0 | 异步操作成功完成。需要调用aio_return函数获取操作返回值 |
-1 | 对aio_error的调用失败。这种情况下,errno会告诉我们为什么 |
EINPROGRESS | 异步读、写或同步操作仍在等待 |
其他情况 | 其他任何返回值是相关的异步操作失败返回的错误码 |
如果异步操作成功,可以调用aio_return函数来获取异步操作的返回值
#include <aio.h>
ssize_t aio_return(const struct aiocb *aiocb);
直到异步操作完成之前,都需要小心不要调用aio_return函数。操作完成之前的结果是未定义的。还需要小心对每个异步操作只调用一次aio_return。一旦调用了该函数,操作系统就可以释放掉包含了I/O操作返回值的记录
如果aio_return函数本身失败,会返回-1,并设置errno。其他情况下,它将返回异步操作的结果,即返回read、write或者fsync在被成功调用时可能返回的结果。
如果在完成了所有事务时,还有异步操作未完成时,可以调用aio_suspend函数来阻塞进程,直到操作完成。
#include <aio.h>
int aio_suspend(const struct aiocb *const list[], int nent,
const struct timespec *timeout);
//返回值:若成功,返回0;若出错,返回-1
当还有我们不想再完成的等待中的异步I/O操作时,可以尝试使用aio_cancel函数来取消它们
#include <aio.h>
int aio_cancel(int fd, struct aiocb *aiocb);
如果aiocb参数为NULL,系统将会尝试取消所有该文件上未完成的异步I/O操作。
返回值 | 说明 |
---|---|
AIO_ALLDONE | 所有操作在尝试取消它们之前已经完成 |
AIO_CANCELED | 所有要求的操作已被取消 |
AIO_NOTCANCELED | 至少有一个要求的操作没有被取消 |
-1 | 对aio_cancel的调用失败。错误码将被存储在errno中 |
#include <aio.h>
int lio_listio(int mode, struct aiocb *restrict const list
[restrict], int nent, struct sigvent *restrict sigev);
//返回值:若成功,返回0;若出错,返回-1
函数readv和writev
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读和聚集写
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
//两个函数的返回值:已读或已写的字节数;若出错,返回-1
这两个函数的第二个参数指向iovec结构数组的一个指针:
struct iovec{
void *iov_base;
size_t iov_len;
};
iov数组中的元素数由iovcnt指定,其最大值受限于IOV_MAX。
函数readn和writen
管道、FIFO以及某些设备(特别是终端和网络)有下列两种性质
1. 一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样。这不是个错,应当继续读该设备
2. 一次write操作的返回值可能少于指定输出的字节数。这可能是由某个因素造成的,例如,内核输出 缓冲区变满。这也不是错误,应当继续写余下的数据。
#include "apue.h"
ssize_t readn(int fd, void *buf, size_t nbytes);
ssize_t writen(int fd, void *buf, size_t nbytes);
//两个函数的返回值:读、写的字节数;若出错,返回-1
映射I/O
存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样,就可以在不使用read和write的情况下执行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
调用mprotect可以更改一个现有映射的权限
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
//返回值:若成功,返回0;若出错,返回-1
如果共享映射中的页已修改,那么可以调用msync将该页冲洗到被映射的文件中。msync函数类似于fsync,但作用于存储映射区
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
//返回值:若成功,返回0;若出错,返回-1
当进程终止是,会自动解除存储映射区的映射,或者直接调用munmap函数也可以解除映射区。关闭映射存储区时使用的文件描述符并不接触映射区
#include <sys/mman.h>
int munmap(void *addr, size_t len);
//返回值:若成功,返回0;若出错,返回-1