[单刷 APUE 系列] 第十四章——高级 I/O

非阻塞I/O

在最前面,咱们讲过IO分红带缓冲的IO和不带缓冲的IO,可是实际上,这个区别并非很大,由于缓冲区并无影响到实际的读写。咱们知道,系统调用实际上分红两种,高速的系统调用和低速的系统调用,换句话说,低速的调用会致使系统永久性阻塞,可是须要注意的是,并非磁盘IO都是低速调用。好比open、read、write函数,若是这些操做不能完成就会马上出错返回,并不会致使系统阻塞。在前面的时候咱们也学到过,若是在open的时刻,指定O_NONBLOCK,或者在一个已打开的文件描述符上调用fcntl函数,附加上O_NONBLOCK参数。实际上虽然指定了参数,可是在某些状况下颇有可能丢失信息。在大量传输信息的时候容易出现系统调用大量失败的状况。javascript

记录锁

在不少状况下,咱们须要面对多方一块儿操做文件的状况,这就是一个典型的资源竞争冲突,为了保证文件的正确读写,Unix系统提供了文件记录锁的机制,也就是上文中提到过的文件记录锁。为了提供这个功能,各个系统都自行实现了API,其中,POSIX1.x标准规定的是fcntl方法,而BSD系列则是规定flock方法,SystemV在fcntl方法的基础上构建了lockf函数java

fcntl函数

int fcntl(int fildes, int cmd, ...);

The commands available for advisory record locking are as follows:

F_GETLK    Get the first lock that blocks the lock description pointed to by the third argument, arg, taken as a pointer to a struct flock (see above).  The information retrieved overwrites the information passed to fcntl in the flock structure.  If no lock is found that would prevent this lock from being created, the structure is left unchanged by this function call except for the lock type which is set to F_UNLCK. F_SETLK Set or clear a file segment lock according to the lock description pointed to by the third argument, arg, taken as a pointer to a struct flock (see above). F_SETLK is used to establish shared (or read) locks (F_RDLCK) or exclusive (or write) locks, (F_WRLCK), as well as remove either type of lock (F_UNLCK). If a shared or exclusive lock cannot be set, fcntl returns immediately with EAGAIN. F_SETLKW This command is the same as F_SETLK except that if a shared or exclusive lock is blocked by other locks, the process waits until the request can be satisfied. If a signal that is to be caught is received while fcntl is waiting for a region, the fcntl will be interrupted if the signal han-dler has not specified the SA_RESTART (see sigaction(2)).复制代码

前面也介绍过这个函数,不过此次会讲解记录锁的内容,对于记录所来讲,cmd参数是F_GETLKF_SETLK或者FSETLKW,第三个参数是一个纸箱flock结构体的指针数据库

struct flock {
    off_t       l_start;    /* starting offset */
    off_t       l_len;      /* len = 0 means until end of file */
    pid_t       l_pid;      /* lock owner */
    short       l_type;     /* lock type: read/write, etc. */
    short       l_whence;   /* type of l_start */
};复制代码

基本上也不用讲解了,注释早已说明一切。这个结构体就是经过指定文件区域和锁的类型等参数锁定文件。不过须要注意的是,l_type其实是取值SEEK_SETSEEK_CUR、或SEEK_END。而且上面提到的类型只有两种:共享读锁和独占写锁,实际上就是读写锁。编程

  • F_GETLK参数判断flockptr参数所描述的锁是否会被另外一把锁排斥
  • F_SETLK参数设置由flockptr所描述的锁
  • F_SETLKW这是F_SETLK的阻塞版本

很容易想到,在开发中确定是先用F_GETLK参数测试是否能创建一把锁,然后使用F_SETLK或者F_SETLKW创建锁,可是这二者并非原子操做,前面已经讲过,非原子操做很容易致使操做冲突。
在设置释放锁的时候,内核是根据字节数维持锁的范围的,也就是说,实际上内核只是维护了一个flock结构体的链表,而后每次的锁更改都会致使链表被遍历而且合并。
对于记录锁的自动继承和释放有3条规则:数组

  1. 锁和进程、文件相关联,换言之,一个进程结束的时候,全部的锁所有释放,这其实是exit函数作清理的,第二就是文件描述符关闭的时候,该文件全部的锁都会关闭
  2. fork产生的子进程不继承父进程的锁。由于锁是用于限制多个进程读写同一个文件的,若是fork能继承锁,那就起不到约束做用了
  3. 执行exec后,新程序继承原执行程序的锁,可是close_on_exec则会不同。

其实锁对数据库这种大量读写IO的程序才是最有用的,因此基本上锁就能够直接考虑数据库的环境,若是数据库的客户端库使用的是同一套锁机制,那就能保证文件的共享访问,可是建议性锁没法保证其余有权限存取数据库文件的进程读写此文件。而强制性锁则会让进程检查每个open、read和write函数,验证调用进程是否违背了正在访问文件的锁,这就是强制性锁和建议性锁的区别。网络

IO多路转接

前面谈到过,对于内核来讲,IO只有两种方式:阻塞和非阻塞,阻塞IO会致使CPU等待IO从而浪费等待时间,因此系统提供了非阻塞IO,可是非阻塞IO带来的问题就是完整IO没有完成,为了获取完整的数据,应用程序须要重复调用IO操做来确认是否完成,也就是轮询。
当从一个文件描述符读,而后又写到另外一个描述符时,一般会写出如下代码数据结构

while ((n = read(STDIN_FILENO, buf, BUFSIZE)) > 0)
    if (write(STDOUT_FILENO, buf, n) != n)
        err_sys("write error");复制代码

这种循环获取的形式就是轮询,很是简单,可是消耗了CPU资源,而且若是须要有更高的要求,好比必须从两个文件描述符读取。
典型的应用就是网络守护进程,例如Nginx和Telnet,这里直接拿原著中的Telnet讲解,telnet因为存在两个输入两个输出,因此不能使用阻塞式的IO函数,开发者的第一反应,应该是fork函数,使用两个进程,每一个进程都负责一条读写通道,可是这就须要进程同步,而多线程编程也一样是这样的问题。
另外一个方法就是使用一个进程,可是使用非阻塞IO读取数据。其基本思想很简单,两个描述符都读取,可是一直处于循环,每次循环都查询一次两个文件描述符,若是没有就马上返回不阻塞,这种循环就是典型的轮询,这是种很是常见的技术,实际上倒是很是浪费CPU资源的技术,因此目前,基本开发以及不能也不推荐了。
还有几种技术就是异步IO,这种技术实质上就是相似通知,当描述符准备完毕后,进程通知内核,可是实际上目前原生API并不能作到移植,因此,目前大部分的开发,包括Node.js等在内的网络服务,基本都是使用第三方或者本身实现线程池。不过,目前Linux系统已经有了名为AIO的原生异步IO。
如今目前大部分的使用方式就是IO多路转接,系统构造一张链表,里面存储全部的文件描述符,而后调用函数侦听,知道其中一个已经准备完毕的时候返回。poll、pselect和select三个函数就是这样执行的。多线程

select和pselect函数

这连个函数是POSIX规定的app

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, const struct timespec *restrict timeout, const sigset_t *restrict sigmask);复制代码

第一个参数nfds的意思就是“最大文件描述符编号值+1”,由于文件描述符都是从0开始的,从后面readfds、writefds、errorfds中找出最大描述符编号值并+1就是这个参数的值,中间三个参数是指向描述符集的指针,使用fd_set数据结构表示,实际上有下列五个函数异步

void FD_CLR(fd, fd_set *fdset);
void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy);
void FD_ISSET(fd, fd_set *fdset);
void FD_SET(fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);复制代码

是否是发现比原著多了一个FD_COPY函数,实际上就是复制用的,可有可无。最后一个参数就是制定愿意等待的时间长度,使用timeval结构体,也就是能够指定秒和微妙单位。

  1. timeout == NULL,永远等待
  2. timeout->tv_sec == 0 && timeout->tv_usec == 0,不等待
  3. timeout->tv_sec != 0 || timeout->tv_usec != 0,等待指定时间

select实际上和描述符自己阻塞无关,它只是简化了咱们监听一堆文件描述符的繁琐操做,除了select之外,上面还有一个select的变体pselect,pselect和select很像,可是select得超时值用timeval结构体定义,pselect使用timespec结构,pselect可以使用可选信号屏蔽字,若是sigmask为null,则二者同样,可是sigmask指向屏蔽字的时候,将以原子操做形式安装屏蔽字。

poll函数

除了select之外,你们应该还见过poll函数

int poll(struct pollfd fds[], nfds_t nfds, int timeout);复制代码

看起来poll函数相对于select更加简洁易懂,select函数对三种类型都指定了参数用于构造描述符集,可是poll函数使用的则是pollfd结构体数组,pollfd结构体以下

struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
};复制代码

nfds参数指定了fds数组的大小,从上面的注释中应该也看得出来结构体到底是怎么构造的,events是咱们关心fd的事件,而revents则是内核设置,返回的时候用于说明每一个描述符发生了哪些事件。

The event bitmasks in events and revents have the following bits:

     POLLERR        An exceptional condition has occurred on the device or socket.  This flag is output only, and ignored if present in the input events bitmask.

     POLLHUP        The device or socket has been disconnected.  This flag is output only, and ignored if present in the input events bitmask.  Note that POLLHUP
                    and POLLOUT are mutually exclusive and should never be present in the revents bitmask at the same time.

     POLLIN         Data other than high priority data may be read without blocking.  This is equivalent to ( POLLRDNORM | POLLRDBAND ).

     POLLNVAL       The file descriptor is not open.  This flag is output only, and ignored if present in the input events bitmask.

     POLLOUT        Normal data may be written without blocking.  This is equivalent to POLLWRNORM.

     POLLPRI        High priority data may be read without blocking.

     POLLRDBAND     Priority data may be read without blocking.

     POLLRDNORM     Normal data may be read without blocking.

     POLLWRBAND     Priority data may be written without blocking.

     POLLWRNORM     Normal data may be written without blocking.复制代码

上面是两个参数可取的值,每一个系统实现可能存在误差,因此须要自行尝试。

异步I/O

前面讲过,非阻塞IO带来的就是轮询,前面内容包括前面的章节整合一下,能够概括出如下主流轮询技术:

  1. read,最原始,性能最低的一种,重复检查IO状态来完成完整数据的读取,也就是前面一小节的开头代码
  2. select,在read基础上改进的方案,经过对文件描述符上的事件状态判断
  3. poll,使用链表做为文件描述符的存储方式,和select相似
  4. epoll,目前Linux下最高效的IO事件通知机制,进入轮询时候若是没有检查到IO事件就会休眠,直到事件将其唤醒
  5. queue,和epoll相似,不过是FreeBSD下的

虽然轮询知足了非阻塞IO获取完整数据的需求,可是依旧是同步的,也须要花费CPU用于便利文件描述符或者休眠等待事件发生。因此就有了异步IO,目前据笔者所知,只有Linux下有AIO技术算是真正原生提供的API。
可是,实际上,是有模拟方式的,信号机构提供了异步形式通知事件发生的方法,使用一个信号通知进程,可是,因为信号是有限的,若是使用一个信号,则进程不知道是哪一个文件描述符发生的事件,若是用多个信号,文件描述符的数量可能远远超出信号的数量。
实际上,最容易想到的办法就是多线程。让部分线程进行阻塞IO或者非阻塞IO加轮询技术来完成数据获取,让另外一个线程进行计算,然后经过线程间通讯将IO获得的数据进行传递,就能轻松实现异步IO。

SystemV异步IO

SystemV中异步IO是归属给STREAMS系统的,他只能用于STREAMS设备和管道,异步IO信号是SIGPOLL。实际上因为这种机制自己的限制,目前已经找不到Unix环境会去采用它了,因此这里也不须要再讲解了。

BSD异步IO

对于BSD系列的系统来讲,异步IO信号是SIGIO和SIGURG信号的组合,SIGIO是通用异步IO的信号,SIGURG则是通知网络链接的数据已经到达。

POSIX异步IO

POSIX标准对不一样类型文件异步IO提供了可移植的模型,异步IO使用AIO控制块来描述IO操做。

struct aiocb {
        int             aio_fildes;             /* File descriptor */
        off_t           aio_offset;             /* File offset */
        volatile void   *aio_buf;               /* Location of buffer */
        size_t          aio_nbytes;             /* Length of transfer */
        int             aio_reqprio;            /* Request priority offset */
        struct sigevent aio_sigevent;           /* Signal number and value */
        int             aio_lio_opcode;         /* Operation to be performed */
};复制代码

上面是苹果系统下的AIO控制块实现,实际上和POSIX规定几乎同样,它是继承于FreeBSD3.0的AIO实现,
从上面能够看出,每一个字段究竟的意义,aio_fildes就是文件描述符,读写操做从aio_offset指定的偏移量位置开始,对于读操做,会将数据复制到aio_buf的缓冲区内,对于写操做,会从这个缓冲区写入磁盘,aio_nbytes字段指定了读写的字节数。
除了上面4个字段之外,aio_reqprio就是异步IO请求的顺序,aio_sigevent就是IO事件完成后如何通知,而aio_lio_opcode就是执行的操做。

struct sigevent {
        int                             sigev_notify;                           /* Notification type */
        int                             sigev_signo;                            /* Signal number */
        union sigval    sigev_value;                            /* Signal value */
        void                    (*sigev_notify_function)(union sigval);   /* Notification function */
        pthread_attr_t  *sigev_notify_attributes;       /* Notification attributes */
};复制代码

sigevent结构体是归属于signal信号机制模型中的数据结构,其中sigev_notify字段是通知类型

  • SIGEV_NONE 不通知进程
  • SIGEV_SIGNAL 异步IO完成后,产生sigev_signo指定的信号,
  • SIGEV_THREAD 异步请求完成后,由sigev_notify_function指定的函数被调用
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);复制代码

在异步IO以前须要先初始化AIO控制块,当函数返回成功时候,异步IO请求就已经被放在了等待处理队列中。这些返回值与实际IO擦作的结果没有任何关系,若是想要强制全部等待中的异步操做不等待直接写入存储,则调用aio_fsync函数
固然,好像aio_fsync函数并非很是普遍,因此在使用的时候记得运行时检查。
为了获取一个异步读写的完成状态,能够调用aio_error函数

int aio_error(const struct aiocb *aiocbp);复制代码

返回以下:

  1. 返回值为0,异步操做成功,使用aio_return函数得到返回值
  2. 返回值为-1,对aio_error操做失败
  3. 返回值为EINPROGRESS,读写操做仍处于等待状态
ssize_t aio_return(struct aiocb *aiocbp);复制代码

记住在aio_error检查已经成功以前,不要调用aio_return函数,并且须要小心每一个异步操做只能调用一次aio_return函数。
若是在其余操做完成以后,异步操做还未完成,那可使用

int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout);复制代码

aio_suspend函数会阻塞当前进程直到操做完成,通常状况下不多会使用。
若是咱们想要取消已经处于进行中的异步操做,可使用以下函数

int aio_cancel(int fildes, struct aiocb *aiocbp);复制代码

这个函数会返回4个返回值:

  1. AIO_ALLDONE,全部操做已经完成
  2. AIO_CANCELED,全部操做已经取消
  3. AIO_NOtCANCELED,至少有一个请求没有取消
  4. -1,函数自己失败

除了上述函数之外,还有一个函数也被包含在异步请求函数中,可是实际上不多见到,因此这里就很少作讲解。

readv和writev函数

ssize_t readv(int d, const struct iovec *iov, int iovcnt);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);复制代码

这两个函数用于在一次读写中读写多个非连续的缓冲区,也就是说能够将传统的多个函数读写调用压缩到一个,这连个函数第二个参数就是一个指向iovec结构体的指针,其实是一个指向数组的指针

struct iovec {
    char   *iov_base;  /* Base address. */
    size_t iov_len;    /* Length. */
};复制代码

第三个参数就是数组的长度。iov数组中的元素最大值就是IOV_MAX。

存储映射IO

存储映射IO能将一个磁盘文件映射到存储空间中的一个缓冲区上,因而,当从缓冲区中读取数据的时候,就等同于读取文件。Unix系统提供了此类函数

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);复制代码

addr指定映射存储区的起始地址。一般为0,也就是系统自动分配区域。fd参数指定被映射文件的文件描述符,也就表明必须先打开这个文件。prot参数指定了映射存储区的保护要求以下:
|prot|说明|
|----|---|
|PROT_READ|存储区可读|
|PROT_WRITE|存储区可写|
|PROT_EXEC|存储区可执行|
|PROT_NONE|存储区不可访问|
固然,这个参数的指定必然是基于文件描述符的打开方式的,很容易明白,由于存储映射IO技术本质上仍是基于文件描述符的,因此不可能绕过文件描述符的限制读写。
flag参数影响映射存储区的多种属性,以下就是可选值:

MAP_ANONYMOUS     Synonym for MAP_ANON.

     MAP_ANON          Map anonymous memory not associated with any specific file.  The offset argument is ignored.  Mac OS X specific: the file descriptor used
                       for creating MAP_ANON regions can be used to pass some Mach VM flags, and can be specified as -1 if no such flags are associated with the
                       region.  Mach VM flags are defined in <mach/vm_statistics.h> and the ones that currently apply to mmap are:

                       VM_FLAGS_PURGABLE   to create Mach purgable (i.e. volatile) memory

                       VM_MAKE_TAG(tag)    to associate an 8-bit tag with the region
                       <mach/vm_statistics.h> defines some preset tags (with a VM_MEMORY_ prefix).  Users are encouraged to use tags between 240 and 255.  Tags
                       are used by tools such as vmmap(1) to help identify specific memory regions.

                       VM_FLAGS_SUPERPAGE_SIZE_*     to use superpages for the allocation.  See <mach/vm_statistics.h> for supported architectures and sizes (or
                       use VM_FLAGS_SUPERPAGE_SIZE_ANY to have the kernel choose a size).  The specified size must be divisible by the superpage size (except for
                       VM_FLAGS_SUPERPAGE_SIZE_ANY), and if you use MAP_FIXED, the specified address must be properly aligned. If the system cannot satisfy the
                       request with superpages, the call will fail. Note that currently, superpages are always wired and not inherited by children of the process.

     MAP_FILE          Mapped from a regular file.  (This is the default mapping type, and need not be specified.)

     MAP_FIXED         Do not permit the system to select a different address than the one specified.  If the specified address cannot be used, mmap() will fail.
                       If MAP_FIXED is specified, addr must be a multiple of the pagesize.  If a MAP_FIXED request is successful, the mapping established by
                       mmap() replaces any previous mappings for the process' pages in the range from addr to addr + len.  Use of this option is discouraged.

     MAP_HASSEMAPHORE  Notify the kernel that the region may contain semaphores and that special handling may be necessary.

          MAP_PRIVATE       Modifications are private (copy-on-write).

     MAP_SHARED        Modifications are shared.

     MAP_NOCACHE       Pages in this mapping are not retained in the kernel's memory cache.  If the system runs low on memory, pages in MAP_NOCACHE mappings will
                       be among the first to be reclaimed.  This flag is intended for mappings that have little locality and provides a hint to the kernel that
                       pages in this mapping are unlikely to be needed again in the near future.复制代码

这就不讲解了,原著上已经讲解的足够清楚了。
调用mprotect能够更改现有映射的权限

int mprotect(void *addr, size_t len, int prot);复制代码

也就是一个修改映射区域权限的函数,当页已经修改完毕,能够调用msync函数冲洗到被映射的文件中。

int msync(void *addr, size_t len, int flags);复制代码

基本就和fsync函数差很少,也很少说了,基本上都在Unix手册上
当进程终止的以后,天然会自动解除存储区的映射,或者能够调用munmap函数解除

int munmap(void *addr, size_t len);复制代码

munmap函数删除了指定地址的映射,若是继续对其进行读写会致使无效内存引用。而且这个函数不会冲洗缓冲区内容到文件,因此须要当心使用。

相关文章
相关标签/搜索