Linux/UNIX高级I/O
高级I/O
非阻塞IO
非阻塞I/O使我们可以调用open、read和write这样的I/O操作,并使这些操作不会永久阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。
对于一个给定的描述符有两种方法对其指定非阻塞I/O:
1) 如果调用open获得描述符,则可指定O_NONBLOCK标志
2) 对于已打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。
记录锁
记录锁的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。锁定的是文件中的一个区域。
记录锁函数fcntl:
#include<unistd.h>
#include<fcntl.h>
int fcntl(intfd, int cmd, ... /* arg */ );
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。其中第三个参数arg是一个flock结果指针,其结构组成如下:
struct flock {
...
short l_type; /* Type of lock: 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
(F_GETLKonly) */
...
};
flock结果说明如下:
所希望的所类型:由l_type决定, 可为F_RDLCK(共享锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)。
要加锁或解锁区域的起始字节偏移量,这由l_whence和l_start决定。
区域字节:由l_len决定。若l_len为0,则表示锁的区域从起点开始直至最大可能偏移量为止。
具有能阻塞当前进程的锁,其持有进程的ID存放在l_pid中(由F_GETLK情况下返回)
共享读锁和独占写锁:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程独用一把锁。
共享读锁和独占写锁概念是针对不同进程提出锁的请求,不适用于但个进程提出多个锁请求。当一个进程对一个文件区间已经有了一把锁,后来该进程有企图在同一文件区间再加上一把锁,那么新锁将替换老锁。
加读锁时,该描述符必须是读打开;加写锁时,必须是写的打开。
以下说明fcntl函数的三种命令:
F_GETLK:判断由arg中所描述的锁是否会被另外一把锁排斥(阻塞)。如果存在一把锁,他阻止创建由arg所描述的锁,则把该现存锁的信息写到arg指向的结构中。如果不存这种情况,则除了l_type设置为F_UNLCK之外,arg所指向结构中的其他信息保持不变。
F_SETLK:设置由arg所描述的锁。如果试图建立一把读锁或写锁,但实际情况不允许建立锁(比如已经有写锁),则fcntl出错返回。
F_SETLKW:它是F_SETLK的阻塞版本。
锁的隐含继承和释放:
关于记录锁的自动继承和释放有三条规则:
1. 锁与进程文件两方面有关:第一点很明显,当一个进程终止时,它所建立的锁全部释放;第二点指的是任何时候关闭一个描述符时,则该进程通过这一文件描述符引用的文件上的任何一把锁都被释放。
2. 由fork产生的子进程不能继承父进程所设置锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于继承过来的任一描述符,子进程需要调用fcntl才能获得它自己的锁。这与锁的作用相一致,锁的作用是阻止多个进程同时写同一个文件。如果子进程继承父进程的锁,则父子进程就可以同时写同一个文件。
3. 在执行exec后,新程序可以继承原执行程序的锁。除非队以文件描述符设置了close-on-exec标志,那么执行exec时,将释放锁。
锁的数据结构实现
考虑一个进程,它执行下列语句:
fd1= open(pathname, … );
write_lock(fd1,0, SEEK_SET, 1);
if((pid== fork()) > 0){
fd2= dup(fd1);
fd3= open(pathname, …);
}else if (pid ==0){
read_lock(fd1,1,SEEK_SET,1)
}
其中write_lock和read_lock调用fcntl的加锁实现。
下图显示了父子进程暂停后的数据结构情况:
有了记录锁之后,在原来的数据结构上增加了lockf结构,他们由i节点开始相互连接起。注意,每个lock结构说明了一个给定进程的一个加锁区域(有偏移量和长度定义)。图中显示了两个lockf结构,一个是由父进程调用write_lock形成的,另一个则由子进程调用read_lock形成的。每一个结构都包含了相应的进程ID。在父进程中,关闭fd1、fd2和fd3中任何一个都将释放由父进程设置的写锁。
建议性锁和强制性锁
建议性锁是这样规定的:每个使用上锁文件的进程都要检查是否有锁存在,当然还得尊重已有的锁。内核和系统总体上都坚持不使用建议性锁,它们依靠程序员遵守这个规定。(Linux默认是采用建议性锁)
强制性锁是由内核执行的。当文件被上锁来进行写入操作时,在锁定该文件的进程释放该锁之前,内核会阻止任何对该文件的读或写访问,每次读或写访问都得检查锁是否存在。
例子:
例1,我有几个进程(不一定有亲缘关系)都通过fctnl机制来操作文件,这个就叫一致的方法。
但是,如果同时,又有个流氓进程,管它3721,冲上去,open, write。这时候那几个进程fcntl对这种方式无能为力,这样就叫不一致。文件最后的状态就不定了。正因为这种锁约束不了其它的访问方式,所以叫建议行锁。强制性锁需要内核支持的,对read, write, open都会检查锁。
例2,所谓建议性锁就是假定人们都会遵守某些规则去干一件事。例如,人与车看到红灯都会停,而看到绿灯才会继续走,我们可以称红绿等为建议锁。但这只是一种规则而已,你并不防止某些人强闯红灯。而强制性锁是你想闯红灯也闯不了。
STREAMS
I/O多路转接
I/O多路转接:先构造一张有关描述符的列表,然后调用一个函数,知道这些描述符中的一个已准备好进行I/O时,该函数返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O。
poll、pselect和select这三个函数使我们能够执行I/O多路转接。
select和pselect函数
#include<sys/select.h>
int select(intnfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, structtimeval *timeout);
select函数使我们可以执行I/O多路转接。
最后一个参数,它指定愿意等待的时间:
structtimeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
由三种情况:
timeout== NULL:永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到的一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR。
tvptr->tv_sec== 0 && tvptr->tv_usec == 0:完全不等待。测试所有指定的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。
tvptr->tv_sec!= 0 || tvptr->tv_usec != 0:等待指定的秒数和微妙数。当指定的描述符之一已准备好,或当指定的时间值已超过时立即返回。如果在超时时还没有一个描述符准备好,则返回值是0。与第一种情况一样,这种等待可被捕捉到的信号中断。
中间的三个参数readfds,writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。对fd_set数据类型而进行的处理是:分配一个这种类型的变量;将这种类型的变量赋予同类型的另一个变量;或对于这种类型的变量使用下列四个函数的一个。
#include <sys/types.h>
void FD_CLR(int fd, fd_set *set); //将一个指定位清除
int FD_ISSET(int fd, fd_set*set); //测试一指定为是否设置。若fd存在,返回非0
//值;否则返回0。
void FD_SET(int fd, fd_set *set); //设置一个fd_set变量的指定位
void FD_ZERO(fd_set *set); //讲一个指定的fd_set变量的所有位设置为0
测试程序如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/select.h> int main() { fd_set rfds; struct timeval tv; int retval; char buf[1024]; for(;;) { FD_ZERO(&rfds); FD_SET(STDIN_FILENO, &rfds); /* Wait up to five seconds. */ tv.tv_sec = 5; tv.tv_usec = 0; retval = select(1, &rfds, NULL, NULL, &tv); /* Don‘t rely on the value of tv now! */ if (retval) { printf("Data is availablenow.\n"); if(FD_ISSET(STDIN_FILENO, &rfds)) { read(STDIN_FILENO,buf,1024); printf("Read buf is:%s\n",buf); } } else printf("No data within five seconds.\n"); } exit(0); }
执行结果
hello
Data is available now.
Read buf is: hello
No data within five seconds.
No data within five seconds.
world
Data is available now.
Read buf is: world
No data within five seconds.
pselect与select功能相同,但其超时值用timespec结构(秒和纳秒)指定。可选择信号屏蔽字。超时值被声明为const。
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds,fd_set *writefds,fd_set *exceptfds,
const struct timespec*timeout,const sigset_t *sigmask);
poll函数
poll函数类似于select,但是其程序接口不同。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds,int timeout);
与select函数不同,poll不是为每个状态构造个描述符集,而是构造一个pollfd结果数组。结构定义如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fds数组中元素个数由nfds指定。
timeout == -1:永远等待
timeout == 0:不等待
timeout > 0:等待timeout毫秒
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);
这两个函数的第二个参数是指向iovec结果数组的一个指针:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
iovec结构第一个元素指向缓冲区起始地址,第二个元素指定长度。iovec数组元素个数由iovcnt指定。
writev以顺序iovec[0],iovec[1], iovec[2]从缓冲区中聚集输出数据,返回总的输出字节数。
readv同理。
存储映射I/O
存储映射I/O使磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动写入文件。这样就可以在不使用read和write的情况下执行I/O。
为实现这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数时实现的。munmap可以去除映射关系。
#include<sys/mman.h>
void *mmap(void*addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void*addr, size_t length);
其中addr参数用于指定映射存储区的起始地址。通常设置为0,表示由系统选择该映射区的起始地址。此函数的返回地址是该映射区的其实地址。
fd指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。off是要映射字节在文件中的起始偏移量。
prot参数说明对映射区的保护要求。可以为PROT_NONE,或者PROT_READ、PROT_WRITE、PROT_EXEC任意组合的按位或。对指定映射存储区的保护要求不能超过文件open模式访问权限。
flag:可设为
MAP_FIXED:返回值必须等于addr
MAP_SHARED:这一标志说明本进程对映射区所进行的存储操纵的配置。指定存储操作修改映射文件。
MAP_PRIVATE:本标志说明,对映射区的存储操作导致创建该映射文件的一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原文件。
调用mprotect可以更改一个现存映射存储区的权限。
#include<sys/mman.h>
intmprotect(const void *addr, size_t len, int prot);
如果共享存储映射区中的页已被修改,可以调用msync将该页冲洗到被映射的文件中。
#include<sys/mman.h>
int msync(void*addr, size_t length, int flags);
如下程序是使用mmap的例子:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/stat.h> int main(int argc, char *argv[]) { int fdin, fdout; void *src, *dst; struct stat statbuf; if (argc != 3) printf("usage: %s <fromfile> <tofile>", argv[0]); if ((fdin = open(argv[1], O_RDONLY)) < 0) printf("can‘t open %s for reading", argv[1]); if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) printf("can‘t creat %s for writing", argv[2]); if (fstat(fdin,&statbuf) < 0) /* need size ofinput file */ printf("fstat error"); /*set size of output file */ if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1) printf("lseek error"); if (write(fdout, "", 1) != 1) printf("write error"); if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED) printf("mmap error for input"); if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED) printf("mmap error for output"); memcpy(dst, src, statbuf.st_size); /* does the file copy */ exit(0); }
该程序完成了类似cp的功能。