APUE笔记之14章_高级I/O

第14章 高级I/O

非阻塞I/O

10.5节曾将系统调用分成2类:“低速”系统调用 和 其他。

低速系统调用是指可能使进程永远阻塞的一类系统调用,如:

  • 读管道、终端设备、网络设备的数据并不存在,读操作可能使调用者永远阻塞;
  • 如果数据不能被相同的文件类型立即接受(如管道中无空间、网络流控制),写操作可能会使调用者永远阻塞;
  • 以只写模式打开FIFO,在没有其他进程已经用读模式打开该FIFO时,也要等待;
  • 对已经加上强制性记录锁的文件进行读写;
  • 某些ioctl操作
  • 某些进程间的通信函数

虽然磁盘文件的读写暂时阻塞调用者,但是并不能将与磁盘I/O相关的系统调用视为“低速”。

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

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

  1. 如果调用 open 获得描述符,则可指定 O_NONBLOCK 标志(见3.3节)。 
  2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK 文件状态标志(见3.14节)

POSIX.1要求,对于一个非阻塞的描述符如果无数据可读,则read返回-1,errno被设置为EAGAIN. 

使用非阻塞I/O,可以这样设计程序:

程序一直发起write调用直到写完数据,但是只有少数的write调用真正输出了数据,其余的都只返回了错误。这种形式的循环称为轮询,在多用户系统上用它会浪费CPU时间。

可以将应用程序设计成使用多线程的,从而避免使用非阻塞I/O。如若我们能在其他线程中继续进行,则可以允许单个线程在I/O调用中阻塞。这种方法有时能简化应用程序的设计;但是,线程间同步的开销有时却可能增加复杂性,也许会导致得不偿失的结果。

记录锁

当2个进程同时写一个文件,后果会如何呢?大多数UNIX系统中,文件的最后状态取决于写该文件的最后一个进程。

进程有时需要确保它正在单独写一个文件。于是,商用UNIX系统提供了记录锁机制。

记录锁(record locking)的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件。 一个更适合的术语可能是字节范围锁(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。

早期的伯克利版本的UNIX只支持flock函数,但该函数只能对整个文件加锁,而不能对文件的一部分加锁。

POSIX.1标准的基础是fcntl方法。

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

对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW. 第三个参数(我们将调用flockptr)是一个指向flock结构的指针。

struct flock {
    short l_type;   /* F_RDLCK, F_WRLCK, F_UNLCK */
    short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start;  /* offset in bytes, relative or l_whence */
    off_t l_len;    /* length, in bytes; 0 means lock to EOF */
    pid_t l_pid;    /* returned with F_GETLK */
};

flock结构包括:

  • 锁类型: 共享锁(读锁)、独占性锁(写锁)、解锁
  • 起始字节偏移量
  • 字节长度
  • 进程ID(由F_GETLK返回,表示已有该进程将阻塞当前进程获取锁)

如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换已有锁。因此,若一进程在某文件的16~32 字节区间有一把写锁,然后又试图在 16~32 字节区间加一把读锁,那么该请求将成功执行,原来的写锁会被替换为读锁。(注:这里是同一个进程加锁,后锁替换前锁

加读锁时,该描述符必须是读打开。加写锁时,该描述符必须是写打开。

fcntl函数的3个命令:

  • F_GETLK: 判断由 flockptr 所描述的锁是否会被另外一把锁排斥(阻塞)。如果存在一把锁,它阻止创建由 flockptr 所描述的锁,则该现有锁的信息将重写 flockptr 指向的信息。如果不存在这种情况,则将 l_type 设置为 F_UNLCK,而其他信息保持不变。
  • F_SETLK: 设置由 flockptr 所描述的锁。也可以用来清除由flockptr指定的锁(l_type为F_UNLCK)。
  • F_SETLKW: 这个命令是F_SETLK的阻塞版本(命令中的W代表wait)。如果所请求的读锁或写锁因为别的进程已加锁而不能被授予,那么调用进程会被置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。

应当了解,用F_GETLK测试能否建立一把锁,然后用F_SETLK或F_SETLKW企图建立那把锁,这两者不是一个原子操作。因此不能保证在这两次fcntl调用之间不会有另一个进程插入并建立一把相同的锁。如果不希望在等待锁变为可用时产生阻塞,就必须处理由F_SETLK返回的可能的出错。

在设置或释放文件上的一把锁时,系统按要求组合或分裂相邻区: 

  • 100-199字节有一把锁时,需解锁第150字节
  • 内核将持有2把锁:100-149字节 和 151-199字节
  • 又对第150字节加锁
  • 则内核又将只持有一把锁:100-199字节

锁的隐含继承和释放

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

1. 锁与进程和文件两者相关联: 

   1.1 当一个进程终止时,它所建立的锁全部释放;

   1.2 当一个文件描述符关闭了,该进程通过这一描述符所引用的文件上的任何一把锁都会释放(这些锁都是该进程设置的)。

         这就意味着,如果执行下列4步:

fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = dup(fd1);  // or, fd2 = open(pathname, ...); 
close(fd2);

         则在close(fd2)以后,fd1上设置的锁也被释放。如果将dup改成open,效果也是一样。

2. 由fork产生的子进程不继承父进程设置的锁

3. 在执行exec后,新程序可以继续原执行程序的锁。但是注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec的一部分关闭该文件描述符的时候,将释放相应文件的所有锁。

建议性锁和强制性锁

建议性锁不能阻止对数据库文件有写权限的任何其他进程写这个数据库文件。

强制性锁会让内核检查每一个open、read和write,验证调用进程是否违背了正在访问的文件上的某一把锁。

强制性锁也被称为强迫方式锁(enforcement-mode locking)。 

在Linux中,如果用户想要使用强制性锁,则需要在各个文件系统基础上用mount命令的-o mand选项来打开该机制。

Linux中可以使用 strace 命令查看一个进程的系统调用的跟踪信息。

I/O多路转接

对于有多个文件描述符需要读的情况,有以下几种方案:

  • 不能用一个进程处理,否则一旦一个文件描述符的读阻塞了,所有其他的文件描述符即使准备好了也无法读;
  • 可以使用 轮询+非阻塞读 的方式,但这样太浪费CPU时间,因为大多数时候是没有数据可读的;
  • 使用异步I/O技术。利用这种技术,进程告诉内核:当描述符准备好了,用一个信号通知该进程。但这个技术有2个问题:一是可移植性不好;二是受到特殊设备的限制,即只有特殊设备该机制才有效。
  • 一种较好的技术是使用I/O多路转接。

I/O多路转接(I/O multiplexing)技术

  • 首先,构造一张感兴趣的描述符列表;
  • 然后,调用一个函数,直到这些描述符中至少一个已准备好进行I/O时,该函数才返回

poll、pselect和select这3个函数使我们能够执行I/O多路转接。在从这些函数返回时,进程会被告知哪些描述符已准备好可以进行I/O. 

select函数

传给select的参数告诉内核: 

  • 关心的所有描述符
  • 对于每个描述符我们所关心的条件(读、写、异常条件)
  • 愿意等待多长时间(永远等待、等待一个固定的时间、或者根本不等待)。

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

  • 已准备好的描述符的总数量
  • 对于读、写或异常这3个条件中的每一个,哪些描述符已准备好
#include <sys/select.h>

// -1, 出错; 0, 没有描述符准备好; >0, 准备好的描述符的数量
int select(int maxfdp1, 
           fd_set *restrict readfds, 
           fd_set *restrict writefds, 
           fd_set *restrict exceptfds, 
           struct timeval *restrict tvptr); 

返回值: -1, 出错; 0, 没有描述符准备好; >0, 准备好的描述符的数量

readfds, writefds, exceptfds 这是3个类似于数组的结构,里面的元素是0或1,代表对应的文件描述符准备好没有。

所以,要使用一些宏来测试一个fd是否准备好等工作,如FD_ISSET, FD_CLR, FD_SET, FD_ZERO

pselect函数

#include <sys/select.h>

// -1, 出错; 0, 没有描述符准备好; >0, 准备好的描述符的数量
int select(int maxfdp1,   
           fd_set *restrict readfds, 
           fd_set *restrict writefds, 
           fd_set *restrict exceptfds,  
           const struct timespec *restrict tsptr, 
           const sigset_t *restrict sigmask); 

返回值: -1, 出错; 0, 没有描述符准备好; >0, 准备好的描述符的数量

pselect函数是select函数的变体,它与select函数的区别有以下几点:

  • select的超时值用timeval结构指定,而pselect的超时值用timespec结构指定;
  • pselect的超时值被声明为const,这保证了pselect不会改变此值;
  • pselect 可使用可选信号屏蔽字。若sigmask为NULL,那么和select相同;否则,sigmask指向一个信号屏蔽字。在返回时,恢复以前的信号屏蔽字。

poll函数

#include <poll.h>

int poll(struct pollfd fdarray[], 
         nfds_t nfds, 
         int timeout);

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

fdarray数组中的元素数由nfds指定。

与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 
};

注意,poll没有更改events成员。这与select不同,select修改其参数以指示哪一个描述符已准备好了。

异步I/O 

信号机制提供了一种以异步形式通知某种事件已经发生的方法。

但是这些形式的异步I/O是受限制的:

  • 它们并不能用在所有的文件类型上;
  • 而且只能使用一个信号; 如果要对一个以上的描述符进行异步I/O,那么在进程接收到这个信号时不知道应该对应于哪一个描述符。

后略(POSIX异步I/O)

函数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

writev 函数从缓冲区中聚集输出数据的顺序是:iov[0]、iov[1]直至 iov[iovcnt-1]。writev返回输出的字节总数,通常应等于所有缓冲区长度之和。

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

函数readn和writen

管道、FIFO以及某些设备(特别是终端和网络)有下列两种性质: 

  • 一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样。这不是一个错误,应当继续读该设备。
  • 一次write操作的返回值也可能少于指定输出的字节数。这可能是由某个因素造成的,例如,内核输出缓冲区变满。这也不是错误,应当继续写余下的数据。通常,只有非阻塞描述符,或捕捉到一个信号时,才发生这种write的中途返回。
ssize_t readn(int fd, void *buf, size_t nbytes);
ssize_t writen(int fd, void *buf, size_t nbytes);

返回值:读或写的字节数;若出错,返回-1

这2个函数是自己实现的,见图14-24. 大致实现如下:

ssize_t readn(int fd, void *buf, size_t n) 
{ 
    ...
    nleft = n;
    while (nleft > 0) {
        nread = read(fd, ptr, nleft);
        if (nread < 0) { ...  }  // handle error here
        else if (nread == 0) { break; }  // EOF
        nleft -= nread; 
        ptr += nread;
    }
    return n-nleft;
}

ssize_t writen(int fd, const void *ptr, size_t n)
{
    nleft = n;
    while (nleft > 0) {
        nwritten = write(fd, ptr, nleft); 
        if (nwritten < 0) { ... } // handle error here
        else if (nwritten == 0) { break; }  // Done 
        nleft -= nwritten;
        ptr += nwritten; 
    }
    return n-nleft;
}

存储映射I/O 

存储映射I/O(memory-mapped 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 

  • addr参数用于指定映射存储区的起始地址。通常将其设置为0,表示由系统选择该映射区的起始地址
  • fd参数: 指定要被映射文件的描述符。在文件映射到地址空间之前,必须先打开该文件。
  • len参数: 映射的字节数
  • off参数: 要映射字节在文件中的起始偏移量(有关off值的一些限制将在后面说明)。
  • prot参数:指定了映射存储区的保护要求(见图14-25)
  • flag参数:影响映射存储区的多种属性。MAP_SHARED,相当于对文件write;MAP_PRIVATE,创建映射文件的一个私有副本,于是任何改动只会改变副本而不会改变原文件。

映射文件的起始偏移量受系统虚拟存储页长度的限制。那么如果映射区的长度不是页长的整数倍时,会怎么样呢?假定文件长为 12 字节,系统页长为 512 字节,则系统通常提供 512字节的映射区,其中后500字节被设置为0。可以修改后面的这500字节,但任何变动都不会在文件中反映出来。于是,不能用mmap将数据添加到文件中。我们必须先加长该文件。 (见图14-27程序)

如果共享映射中的页已修改,那么可以调用 msync 将该页冲洗到被映射的文件中。msync函数类似于fsync(见3.13节),但作用于存储映射区。

何时解除映射存储区的映射:

  • 进程终止时会解除
  • 调用 munmap 函数也会解除

注意,关闭映射存储区的文件描述符并不解除映射区。 

#include<sys/mman.h>

int munmap(void *addr, size_t len);

调用munmap并不会使得映射区的内容写到磁盘文件上。需调用msync,或者等待内核虚拟存储算法自动进行。

与mmap和memcpy相比,read和write执行了更多的系统调用,并做了更多的复制。read将数据从内缓冲区中复制到应用缓冲区;write将数据从应用缓冲区复制到内核缓冲区。而mmap和memcpy则直接将数据从映射到地址空间的一个内核缓冲区复制到另一个内核缓冲区。

(完)

发布了169 篇原创文章 · 获赞 332 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/nirendao/article/details/88360802