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的功能。

相关内容