UNIX环境高级编程(14-高级I/O)

本章主要介绍几种高级I/O功能,主要有非阻塞I/O、记录锁、I/O多路转接、异步I/O、readv/writev函数和存储映射I/O。

非阻塞I/O

某些系统调用可能会使进程永远阻塞,一般称其为低速系统调用。而使用非阻塞I/O,可以使openreadwrite这类I/O操作不会阻塞,如果不能完成这些操作时,会立即出错返回。

有两种方法将其指定为非阻塞I/O:

  • 调用open时指定O_NONBLOCK标志。

  • 通过fcntl函数打开O_NONBLOCK文件状态标志。

    #include <fcntl.h>
    // Returns: depends on cmd if OK (see following), −1 on error
    int fcntl(int fd, int cmd, ... /* int arg */ );
    

记录锁

记录锁的主要功能是阻止多个进程同时修改文件的某一文件区。记录锁可以对整个文件加锁,也可以只针对文件的一部分进行加锁。

锁的类型

主要有共享读锁独占性写锁这两种。

加读/写锁时,文件描述符必须是读/写打开。

任意多个进程在给定的字节上可以有一把共享的读锁,但是只能有一个进程有一把独占写锁。如果已经有一把或多把读锁,则不能再加上写锁;如果已经有一把写锁,则不能再对它加任何读锁。

Compatibility between different lock types

对于同一个进程而言,如果尝试在同一个文件区间再加一把锁,无论之前是哪种类型的锁,新的锁都会覆盖旧的锁。

fcntl记录锁

记录锁也是通过fcntl函数进行操作的,其cmd参数可选项为F_GETLKF_SETLKF_SETLKW。第三个参数是一个指向flock结构的指针flockptr,用于描述锁。

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

l_pid变量返回的是持有锁的进程的pid。

注意:

  • 锁可以在文件尾端或者越过尾端处开始,但是不能在起始位置之前开始。

  • 将起始偏移量指向文件起始处(如l_whence=SEEK_SET,l_start=0),且l_len设置为0,即可对整个文件加锁。

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

File byte-range lock diagram

一块大的加锁区域,解锁其中的一部分,系统会自动将剩余加锁区域分裂为两个加锁区域,并各自维护一把锁;如果对两块加锁区域的中间未加锁部分加锁,则3个相邻区域会合并成一个加锁区域。如上图14.4所示,100-199间解锁150,则分成两块区域;之后重新加锁150,则又会变为上半部分的状态。

加锁和解锁

上面提到的3个命令对应于3种加解锁方式,具体如下:

  • F_GETLK:判断是否会被其他锁阻塞。如果flockptr描述的锁被阻塞,则现有锁的信息会重写flockptr指向的内容;如果没有被阻塞,则将l_type设置为F_UNLCK,其余flockptr指向的信息不变。
  • F_SETLK:设置flockptr所描述的锁。如果尝试获得读锁/写锁,但是系统无法给这把锁,那么会立即出错返回,并将errno设置为EACCESEAGAIN。如果将类型设置为F_UNLCK,那么此命令会清除flockptr指定的锁。
  • F_SETLKW:F_SETLK的阻塞版本。不能获取锁的时候,进程会被休眠,直到锁可用或者被信号唤醒。

继承与释放

  1. 锁与进程和文件两者相关联。即(a)当一个进程终止时,它建立的锁全部释放;(b)关闭一个描述符时,引用的文件上的该进程的所有锁都会释放(无论该文件是否还有其他的描述符)。

    The FreeBSD data structures for record locking

    如图14.8所示,当父进程关闭fd1、2或3中任意一个时,与之关联的锁都会释放。系统会逐个检查lockf链表中的各项,释放调用进程持有的锁。

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

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

I/O多路转接

对于需要同时对多个文件进行操作的场景,比如从两个描述符中读取数据并全部存入另一个文件中,无法通过阻塞读(read)来读取这两个描述符,因为当一个描述符被读操作阻塞时,另一个描述符可能有数据可以读取。

通过I/O多路转接技术,可以构建一张描述符表,调用一个函数,直到列表中的一个描述符准备好后该函数返回。omv-confdbadm populate

select

#include <sys/select.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int select(int maxfdp1, fd_set *restrict readfds,
         fd_set *restrict writefds, fd_set *restrict exceptfds,
         struct timeval *restrict tvptr);
  • maxfdp1指定搜索的最大描述符,该值应该是3个描述符集中的最大值+1

  • readfdswritefdsexceptfds是指向描述符集的指针,分别表示我们关心的可读、可写或处于异常状态的描述符集合。

    // Returns: nonzero if fd is in set, 0 otherwise
    int FD_ISSET(int fd, fd_set *fdset);
    void FD_CLR(int fd, fd_set *fdset);
    void FD_SET(int fd, fd_set *fdset);
    void FD_ZERO(fd_set *fdset);
    

    描述符集支持以上4中操作,声明一个描述符集后,必须首先使用FD_ZERO将其置为0,之后再通过SET和CLR函数设置各个描述符位。

  • tvptr为等待时间(该值在返回时可能被改变)。

    • 设置为NULL表示永远等待。捕捉到信号(函数返回-1且errno设置为EINTR)或有描述符准备好后才返回。
    • 时间设置为0则表示不等待,测试完所有描述符后立即返回。
    • 时间不为0,则等待对应的时间。超时(返回0)或有描述符准备好后即返回,另外也会被信号打断。
  • 该函数的返回值>0则表示有描述符已经准备好了,此返回值是3个描述符集中准备好的描述符之和,因此,如果描述符集中有相同的描述符,则该描述符会被多次计数。描述符集中仍旧打开的位是准备好的描述符,可以通过FD_ISSET来测试。

当3个描述符集都设置为NULL时,select就变成了一个延时函数。

另外还有一个变体函数pselect

// Returns: count of ready descriptors, 0 on timeout, −1 on error
int pselect(int maxfdp1, fd_set *restrict readfds,
          fd_set *restrict writefds, fd_set *restrict exceptfds,
          const struct timespec *restrict tsptr,
          const sigset_t *restrict sigmask);

select函数主要有以下不同:

  • 等待时间使用的数据结构不同。
  • 超时时间不会被改变。
  • 多了一个信号屏蔽字sigmask。当不为NULL时,调用pselect函数会原子地安装该信号屏蔽字,在返回时复原。

poll

#include <poll.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

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 */
};

使用pollfd结构的数组代替了select函数中的3个描述符集。nfds即为数组中的元素个数。其中,events的可选值见图14.17,可以选择多个;返回时,revents说明了描述符发生的事件。

The events and revents flags for poll

timeout指定等待时间,单位是毫秒

异步I/O

本节主要讨论POSIX中的异步I/O接口。

AIO控制块

异步接口使用AIO控制块来描述I/O操作,其主要结构如下:

struct aiocb {
  int             aio_fildes;     /* file descriptor */
  off_t           aio_offset;     /* file offset for I/O */
  volatile void  *aio_buf;        /* buffer for I/O */
  size_t          aio_nbytes;     /* number of bytes to transfer */
  int             aio_reqprio;    /* priority */
  struct sigevent aio_sigevent;   /* signal information */
  int             aio_lio_opcode; /* operation for list I/O */
};

其中,aio_buf作为读写操作的缓冲区,在操作完成前必须始终有效且不能复用。

如果文件打开方式为追加模式O_APPEND,向其写入数据时,偏移量aio_offset会被忽略。

aio_lio_opcode指定该操作是读(LIO_READ)、写(LIO_WRITE)还是空(LIO_NOP)操作,该参数仅在基于列表的异步I/O操作lio_listio时有效。

aio_sigevent结构如下,它表示在I/O事件完成后,如何通知程序:

struct sigevent {
  int             sigev_notify;                /* notify type */
  int             sigev_signo;                 /* signal number */
  union sigval    sigev_value;                 /* notify argument */
  void (*sigev_notify_function)(union sigval); /* notify function */
  pthread_attr_t *sigev_notify_attributes;     /* notify attrs */
};

sigev_notify控制通知类型,有如下3中取值:

  • SIGEV_NONE: 不通知进程。
  • SIGEV_SIGNAL:产生sigev_signo指定的信号。如果程序捕获该信号,并设置SA_SIGINFO标志(通过sigaction设置),那么信号处理程序得到的siginfo结构中的si_value被设置为sigev_value
  • SIGEV_THREADS:调用sigev_notify_function指定的函数,且传入的参数为sigev_value。默认情况下该函数通过一个单独的分离线程执行,除非sigev_notify_attributes设置了线程参数。

接口函数

#include <aio.h>
// Both return: 0 if OK, −1 on error
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);

读写函数返回时,异步I/O请求被放入等待处理队列,返回值与读写操作的结果无关。

// Returns: 0 if OK, −1 on error
int aio_fsync(int op, struct aiocb *aiocb);

如果希望等待中的异步操作不等待而直接写入,可以调用aio_fsync函数,同样的,该函数也仅仅是发送一个请求,而不会等待操作结束。

op参数设定为O_DSYNC,则执行起来与fdatasync类似;如果设置为O_SYNC,则与fsync类似。

int aio_error(const struct aiocb *aiocb);

获取异步读/写或同步操作的完成状态,返回值有以下4种情况:

  • 0:异步操作成功完成。
  • -1:函数出错,可以通过errno查看出错信息。
  • EINPROGRESS:操作仍在等待。
  • 其他:相关的异步操作失败返回的错误码。
ssize_t aio_return(const struct aiocb *aiocb);

获取异步操作的返回值,如果上面的aio_error返回0时,可以调用该函数查看异步操作的返回值。函数返回-1表示出错,会设置errno;其余情况为异步操作的结果。

注意:

异步操作完成前不要调用该函数,并且对每个异步操作仅调用一次该函数。因为调用该函数后,操作系统就可以释放掉包含了I/O操作返回值的信息。

// Returns: 0 if OK, −1 on error
int aio_suspend(const struct aiocb *const list[], int nent,
              const struct timespec *timeout);

阻塞进程等待异步操作完成。

list参数是指向SIO控制块数组的指针,nent为数组的条目数。timeout设置为NULL可以不设时间限制。

如果被信号中断,则返回-1且errno设置为EINTR;如果超时则返回-1且errno设置为EAGAIN。任何一个操作完成都会使该函数返回0。

int aio_cancel(int fd, struct aiocb *aiocb);

取消异步操作。

fd为未完成操作的文件的文件描述符。aiocb为文件上的某个指定的异步操作,如果设置为NULL,则会取消文件上所有未完成的异步操作。该函数无法保证能够取消正在进程中的操作。

返回值:

  • AIO_ALLDONE:所有操作在取消前就已经完成。
  • AIO_CANCELED:所要求的操作已被取消。
  • AIO_NOTCANCELED:至少一个请求的操作没有被取消。
  • -1:调用失败,错误码在errno中。

对被取消的操作调用aio_error会返回错误ECANCELED。

int lio_listio(int mode, struct aiocb *restrict const list[restrict],
             int nent, struct sigevent *restrict sigev);

该函数提交一系列由一个AIO控制块列表描述的I/O请求。

mode参数决定该函数是否是异步的。如果被设定为LIO_WAIT,那么函数将在列表中的所有操作完成后返回;如果设定为LIO_NOWAIT,那么函数将在I/O请求入队后返回,并在所有操作结束后,按照sigev的设定被异步地通知(无需通知则设为NULL)。sigev通知不同于AIO控制块本身的通知,它是额外的,且只会在所有操作完成后才会发送。

readv和writev

这两个函数用于在一次函数调用中读、写多个非连续缓冲区,也称之为散布读和聚集写。

#include <sys/uio.h>
// Both return: number of bytes read or written, −1 on error
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

struct iovec {
  void  *iov_base; /* starting address of buffer */
  size_t iov_len;  /* size of buffer */
};

writev按照iov[0]、iov[1]直至iov[iovcnt-1]的顺序输出数据,且返回输出的总字节数。

readv则将读入的数据按照上面的顺序依次存入各个缓冲区,返回读到的总字节数。如果遇到文件尾端,则返回0。

存储映射I/O

该技术将一个磁盘文件映射到存储空间的一个缓冲区上,从缓冲区读写数据就相当于向文件读写数据。可以在不使用read/write函数的情况下执行I/O。

映射与解除

#include <sys/mman.h>
// Returns: starting address of mapped region if OK, MAP_FAILED on error
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off );

addr指定映射存储区的起始地址。设置为0则由系统自动分配。

prot参数指定映射存储区的保护要求,如下表所示:

prot 说明
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问

表中前三项可以任意组合(按位或),但是保护要求不能超过文件本身的访问权限。

flag参数指定了映射区的各类属性:

  • MAP_FIXED:返回值必须等于addr。即要求内核必须将存储区的起始地址设置为addr,如果没有此标志且addr非0,内核仅将addr的值视为一种建议。

  • MAP_SHARED:表示存储操作会修改映射文件,即相当于调用write。

  • MAP_PRIVATE:表示存储操作会创建改映射文件的副本,所有的存储操作不会修改真实文件。

注意:

offaddr的值一般要求是虚拟系统存储页长度的倍数。

对于一些映射区不是页长整数倍的情况,系统会分配更多的映射区以满足此要求。如文件长为12字节,页长512字节,则系统会提供512字节的映射区。可以修改后面500字节的内容,但是不会作用到原文件上。

Example of a memory-mapped file

// Returns: 0 if OK, −1 on error
int munmap(void *addr, size_t len);

进程终止或者调用munmap都会解除映射区。但是关闭文件描述符并不会解除映射,并且调用munmap也不会使映射区的内容写到磁盘文件上。

其他

// Returns: 0 if OK, −1 on error
int mprotect(void *addr, size_t len, int prot);

该函数可以更改一个现有映射的权限。

对于通过MAP_SHARED方式进行的映射,所作的修改不会立即写回到文件中。

// Returns: 0 if OK, −1 on error
int msync(void *addr, size_t len, int flags);

改函数将修改的页冲洗到文件中去。

如果将flags参数指定为MS_ASYNC,则仅仅是请求一个写入操作;如果指定为MS_SYNC,那么在返回之前会等待写操作完成。这两个选项必选其一。

另外,还可以指定MS_INVALIDATE,来告诉操作系统丢弃与底层存储器没有同步的页。

posted @ 2021-01-08 10:46  maxiaowei0216  阅读(83)  评论(0编辑  收藏  举报