高级IO

来源:互联网 发布:域名dns劫持怎么解决 编辑:程序博客网 时间:2024/05/29 23:22

1非阻塞IO

前面将IO分成了带缓冲的IO和不带缓冲的IO,这是站在不同的系统调用的角度,针对系统调用是否使用了流缓冲而言的。而阻塞IO和非阻塞IO则是站在更高的、文件打开属性的角度,无论对于哪种系统调用,而看打开的文件是否阻塞而言的。

非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。

对于一个给定的描述符有两种方法对其指定非阻塞I/O:

(1) 如果是调用open以获得该描述符,则可指定O_NONBLOCK标志。

(2) 对于已经打开的一个描述符,则可调用fcntl打开O_NONBLOCK文件状态标志程序3-5中的函数可用来为一个描述符打开任一文件状态标志。

通过程序清单14-1理解非阻塞IO的意义。在此实例中,程序发出了数千个write调用,但是只有2 0个左右是真正输出数据的,其余的则出错返回。这种形式的循环称为轮询,在多用户系统上它浪费了CPU时间。后面介绍的非阻塞描述符的I/O多路转接,这是一种进行这种操作的更加有效的方法。

2  记录锁

2.1 背景

当两个人同时编辑一个文件时,其后果将如何呢?在很多UNIX系统中,该文件的最后状态取决于写该文件的最后一个进程。但是对于有些应用程序,例如数据库,有时进程需要确保它正在单独写一个文件。为了向进程提供这种功能,较新的UNIX系统提供了记录锁机制。

记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。对于UNIX,“记录”这个定语也是误用,因为UNIX内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)。

2.2 fcntl记录锁

前面已经给出了fcntl函数的原型,为了叙说方便,这里再重复一次。

#include <sys/types.h>

#include <unistd.h>

#include <fcnt1.h>

int fcnt1(int filedes,int cmd,.../* struct flock *flockptr * / ) ;

返回:若成功则依赖于cmd(见下),若出错则为- 1

对于记录锁, cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针:

flock结构说明:

• 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)

• 要加锁或解锁的区域的起始地址,由l_start和l_whence两者决定。l_stat是相对位移量(字节),l_whence则决定了相对位移量的起点。这与lseek函数中最后两个参数类似。

• 区域的长度,由l_len表示。

关于加锁和解锁区域的说明还要注意下列各点:

• 该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始或越过该起始位置。

• 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。

• 为了锁整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,l_len说明为0。

上面提到了两种类型的锁:共享读锁( l_type为F_RDLCK)和独占写琐( F_WRLCK)。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上的写锁则只能由一个进程独用。更进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。如下图所示

上面说明的兼容性规则适用于不同进程提出的锁请求,但不使用于单个进程提出的多个锁请求。如果一个进程对一个文件去见已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换老锁。

在设置或释放文件上的一把锁时,系统按需组合或裂开相邻区。例如,若对字节0 ~ 9 9设置一把读锁,然后对字节0 ~ 4 9设置一把写锁,则有两个加锁区: 0 ~ 4 9字节(写锁)及5 0 ~ 9 9(读锁)。又如,若1 0 0 ~ 1 9 9字节是加锁的区,需解锁第1 5 0字节,则内核将维持两把锁,一把用于1 0 0 ~ 1 4 9字节,另一把用于1 5 1 ~ 1 9 9字节。

关于请求、释放、测试锁细节,见apue14.3节。

2.3 锁的继承和释放

关于记录锁的自动继承和释放有三条规则:

(1) 锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。这就意味着如果执行下列四步:

fd1=open(pathname, ...);

read_lock(fd1, ...);

fd2 = dup ( f d 1 ) ;

close ( fd 2 ) ;

则在close(fd2)后,在fd1上设置的锁被释放。如果将dup代换为open,其效果也一样:

fd1=open(pathname, ...);

read_lock(fd1, ...);

fd2=open(pathname, ...);

close ( fd 2 ) ;

(2) 由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。

(3) 在执行exec后,新程序可以继承原执行程序的锁。

先简要地观察4.3 + B S D实现中使用的数据结构,从中可以看到锁是与进程、文件相关联的。考虑一个进程,它执行下列语句(忽略出错返回):

下图显示了父、子进程暂停(执行pause( ))后的数据结构情况。

有了记录锁后,在原来的这些图上新加了flock结构,它们由i节点结构开始相互连接起来。注意,每个flock结构说明了一个给定进程的一个加锁区域。图中显示了两个flock结构,一个是由父进程调用write_lock形成的,另一个则是由子进程调用read_lock形成的。每一个结构都包含了相应进程I D。

在父进程中,关闭fd1、fd2和fd3中的任意一个都将释放由父进程设置的写锁。在关闭这三个描述符中的任意一个时,内核会从该描述符所关连的i节点开始,逐个检查f l o c k连接表中各项,并释放由调用进程持有的各把锁。内核并不清楚也不关心父进程是用哪一个描述符来设置这把锁的。

3  STREAMS

流设备

4 IO多路转接

4.1 意义

当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中使用阻塞I/O:

while ( (n=read(STDIN_FILENO, buf, BUFSIZ) ) > 0)

if (write (STDOUT_FILENO, buf, n) != n)

err_sys (write error) ;

这种形式的阻塞I/O到处可见。但是如果必须读两个描述符又将如何呢?如果仍旧使用阻塞I/O,那么就可能长时间阻塞在一个描述符上,而另一个描述符虽有很多数据却不能得到及时处理。所以为了处理这种情况显然需要另一种不同的技术。

再来概略地观察一个调制解调器拨号程序的工作情况(该程序将在第1 8章中介绍)。该程序读终端(标准输入),将所得数据写到调制解调器上;同时读调制解调器,将所得数据写到终端上(标准输出)。下图显示这种工作情况。

执行这段程序的进程有两个输入,两个输出。如果对这两个输入都使用阻塞read,那么就可能在一个输入上长期阻塞,而另一个输入的数据则被丢失。

解决这个问题有以下几种方法:一、使用多进程(fork子进程);二、使用多线程;三、在一个进程内使用非阻塞IO进行轮询;四、使用异步IO通过信号通知。这些方法都有他们的特点和缺点,详细参见apue14.5节。

一种比较好的技术是使用I/O多路转接(I/O multiplexing)。其基本思想是:先构造一张有关描述符的表,然后调用一个函数,它要到这些描述符中的一个已准备好进行I/O时才返回。在返回时,它告诉进程哪一个描述符已准备好可以进行I/O。

I/O多路转接至今还不是POSIX的组成部分。SVR4和4.3+BSD都提供select函数以执行I/O多路转接。poll函数只由SVR4提供。SVR4实际上用poll实现select。I/O多路转接在4.2+BSD中是用select函数提供的。虽然该函数主要用于终端I/O和网络I/O,但它对其他描述符同样是起作用的。SVR3在增加流机制时增加了poll函数。但在SVR4之前,poll只对流设备起作用。SVR4支持对任一描述符起作用的poll。

4.2 select函数

select函数使我们在SVR4和4.3+BSD之下可以执行I/O多路转接,传向select的参数告诉内核:

(1) 我们所关心的描述符。

(2) 对于每个描述符我们所关心的条件(是否读一个给定的描述符?是否想写一个给定的描述符?是否关心一个描述符的异常条件?)。

(3) 希望等待多长时间(可以永远等待,等待一个固定量时间,或完全不等待)。

从select返回时,内核告诉我们:

(1) 已准备好的描述符的数量。

(2) 哪一个描述符已准备好读、写或异常条件。

使用这种返回值,就可调用相应的I/O函数(一般是read或write),并且确知该函数不会阻塞。

#include <sys/types.h>/* fd_set data type */

#include <sys/time.h> /* struct timeval */

#include <unistd.h> /* function prototype might be here */

int select (int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,

struct timeval *tvptr) ;

返回:准备就绪的描述符数,若超时则为0,若出错则为- 1

先说明最后一个参数,它指定愿意等待的时间。

struct timeval{

long tv_sec; /* seconds */

long tv_usec; /* and microseconds */

};

有三种情况:

• tvptr= =NULL

永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1, errno设置为EINTR。

• tvptr->tvsec= =0 && tvptr->tvusec= =0

完全不等待。测试所有指定的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。

• tvptr->tvsec ! =0 | | tvptr->tvusec! =0

等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时时还没有一个描述符准备好,则返回值是0,(如果系统不提供微秒分辨率,则tvptr->tvusec值取整到最近的支持值。)与第一种情况一样,这种等待可被捕捉到的信号中断。

中间三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。每个描述符集存放在一个fdset数据类型中。这种数据类型的实现可见下图,它为每一可能的描述符保持了一位。

对fg_set数据类型可以进行的处理是:(a)分配一个这种类型的变量, (b)将这种类型的一个变量赋与同类型的另一个变量,或(c)对于这种类型的变量使用下列四个宏:

以下列方式说明了一个描述符集后:

fd_set rset;

int fd;

必须用FD_ZERO清除其所有位:

FD_ZERO (&rset);

然后在其中设置我们关心的各位:

FD_SET (fd,&rset);

FD_SET (STDIN_FILENO,&rset);

从select返回时,用FD_ISSET测试该集中的一个给定位是否仍旧设置:

if (FD_ISSET(fd, &rset)){

. . .

}

select中间三个参数中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。select第一个参数maxfdp的意思是“最大f d加1”。在三个描述符集中找出最高描述符编号值,然后加1,这就是第一个参数值。

例如,若编写下列代码:

fd_set readset, writeset;

FD_ZERO ( & readset) ;

FD_ZERO ( & writeset) ;

FD_SET(0, &readset);

FD_SET(3, &readset);

FD_SET(1, &writeset);

FD_SET(2, &writeset);

select (4, &readset, &writeset, NULL, NULL);

描述符集的情况如下图:

4.3 poll函数

SVR4的poll函数类似于select,但是其调用形式则有所不同。我们将会看到, poll与流系统紧紧相关,虽然在SVR4中,可以对任一描述符都使用该函数。

#include <stropts.h>

#include <poll.h>

int poll(struct pollfd  fdarrar[],  unsigned long  nfds,  int  timeout) ;

返回:准备就绪的描述符数,若超时则为0,若出错则为- 1

与select不同,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说明。

5 readvwritevreadnwriten

5.1 readvwritev

readv和writev函数用于在一个函数调用中读、写多个非连续缓存。有时也将这两个函数称为散布读和聚集写。

#include <sys/types.h>

#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec *iov, int iovcnt) ;

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt) ;

两个函数返回:已读、写的字节数,若出错则为- 1

这两个函数的第二个参数是指向iovec结构数组的一个指针:

struct iovec {

void *iov_base; /* starting address of buffer */

size_t iov_len; /* size of buffer */

} ;

iov数组中的元素数由i o v c n t说明。

下图显示了readv和writev的参数和iovec结构之间的关系。writev以顺序iov[0], iov[1]至iov[iovcnt-1] 从缓存中聚集输出数据。writev返回输出的字节总数,它应等于所有缓存长度之和。

readv则将读入的数据按上述同样顺序散布到缓存中。readv总是先填满一个缓存,然后再填写下一个。readv返回读得的总字节数。如果遇到文件结尾,已无数据可读,则返回0。

5.2 readnwriten

readn和writen的功能是读、写指定的N字节数据,并处理返回值小于要求值的情况。这两个函数只是按需多次调用read和write直至读、写了N字节数据。

6 存储映射IO

存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。

为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。

#include <sys/types.h>

#include <sys/mman.h>

caddr_t mmap(void *addr,size_t len,int prot,int flag, int filedes) ;

返回:若成功则为映射区的起始地址,若出错则为- 1

addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由系统选择该映射区的起始地址。此函数的返回地址是:该映射区的起始地址。filedes指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。off是要映射字节在文件中的起始位移量

在说明其余参数之前,先看一下存储映射文件的基本情况。下图显示了一个存储映射文件。

在此图中,“起始地址”是mmap的返回值。在图中,映射存储区位于堆和栈之间:这属于实现细节,各种实现之间可能不同。

off和addr的值通常应当是系统虚存页长度的倍数。因为映射文件的起动位移量受系统虚存页长度的限制,那么如果映射区的长度不是页长度的整数倍时,将如何呢?假定文件长1 2字节,系统页长为5 1 2字节,则系统通常提供5 1 2字节的映射区,其中后5 0 0字节被设为0。可以修改这5 0 0字节,但任何变动都不会在文件中反映出来。

与映射存储区相关有两个信号: SIGSEGV和SIGBUS。信号SIGSEGV通常用于指示进程试图存取它不能存取的存储区。如果进程企图存数据到用mmap指定为只读的映射存储区,那么也产生此信号。如果存取映射区的某个部分,而在存取时这一部分已不存在,则产生SIGBUS信号。例如,用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到SIGBUS信号。

在fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间中的一部分),但是由于同样的理由,exec后的新程序则不继承此存储映射区。更详细例子参见apue14.9节。

原创粉丝点击