• 第十四章:高级I/O


    14.1:引言

    本章内容包括非阻塞I/O、记录锁、系统V流机制、I/O多路转接(select和poll函数)、readv和writev函数以及存储映射I/O(mmap),这些都称为高级I/O。

    14.2:非阻塞I/O

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

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

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

    (2)对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。

     实例 14-1:长的非阻塞write

    14.3:记录锁

    记录锁(record lock)的功能是:当一个进程正在修改或读文件的某以部分时,它可以阻止其他进程修改同以文件区。

    1. fcntl记录锁

    #include <fcntl.h>
    int fcntl(int filedes, int cmd, .../* struct flock *flockpty */);
    // 返回值:若成功则依赖于cmd,若失败则返回-1

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

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

    对flock结果说明如下:

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

    要加锁或解锁区域的起始偏移量。这由l_start和l_whence决定

    区域的字节长度,由l_len表示。

    具有能阻塞当前进程的锁,其持有进程的进程ID放在l_pid中(仅由F_GETLK返回)

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

    l_start是相对偏移量,l_whence则决定了相对偏移量的起点。l_whence的可选值是SEEK_SET、SEEK_CUR、SEEK_END。

    如若l_len是0,则表示锁的区域从其起点(由l_start和l_whence决定)直至最大可能偏移量为止,也就是不管文件添加多少数据,它们都在锁的范围之内。

    为了锁整个文件,我们设置l_start和l_whence,使锁的起点位于文件开始处,并且说明长度(l_len)为0。(有多种方法可以指定文件开始处,但最常见的方法是设置l_start为0,l_whence为SEEK_SET。)

    锁的兼容性

    共享锁和独占锁的基本规则是:多个进程在一个给定的字节可以有一把共享的读锁,但是在给定的字节上,只能有一个进程有一把独占的写锁。进一步而言,如果在给定的字节上有一把或多把读锁,则不能在该字节上加写锁;同样,如果再给定的字节上有一个写锁,则不能在字节上加读锁。

    上述规则适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间有一把锁,后来该进程又企图在该文件区间上加另外一把锁,则新锁将替换老锁。

    加读锁时,该文件必须是读打开;加写锁时,该文件必须是写打开。

    以下说明fcntl函数的三种命令:

    F_GETLK:判断由flockptr所描述的锁是否被另一把锁所排斥。如果一把排斥flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在一把排斥的锁,则只将l_type设置为F_UNLCK,flockptr指向的结构的其他信息不变。该函数的作用就是测试由flockptr所指向的锁能不能加到执行文件区间。

    F_SETLK:设置由flockptr描述的锁。如果要建立一把读锁或写锁,按上述兼容性规则,如果不能创建该锁,则fcntl出错返回,此时errno设置为EAGAIN。此命令也用于清除锁,把l_type设置为F_UNLCK。

    F_SETLKW:这是F_SETLK的阻塞版本(W表示wait)。如果当前所请求的区间的某一部分,另一个进程已有一把锁,而按兼容性规则由flockptr描述的锁无法创建,则进程休眠。如果请求的锁已可用,或者休眠由信号中断,则该进程被唤醒。

    注意:

    用F_GETLK测试一把锁,然后用F_SETLK或者F_SETLKW来设置锁,这两步不是一个原子操作。因此不能保证在使用F_SETLK时,测试的结果依然不变。

    2. 锁的隐含继承和释放

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

    (1)锁与进程和文件两方面有关。这有两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显了,任何时候关闭一个描述符时,则进程通过这一描述符可以引用的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。

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

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

    3.实例 在文件整体上加锁

    我们了解到,守护进程可以利用一把锁来保证只有守护进程的唯一副本在运行。下面函数实现了这种机制。

    #include <unistd.h>
    #include <fcntl.h>
    
    int lockfile(int fd)
    {
        struct flock fl;
        
        fl.l_type = F_WRLCK;
        fl.l_start = 0;
        fl.l_whence = SEEK_SET;
        fl.l_len = 0;
        return fcntl(fd, F_SETLK, &fl);
    }

    还有另一种方法,是使用write_lock来实现:

    #define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)

    4.在文件尾端加锁

    在接近文件尾端加锁或解锁一定要小心。

    如下代码:

    write_lock(fd, 0, SEEK_END, 0);
    write(fd, buf, 1);
    un_lock(fd, 0, SEEK_END);
    write(fd, buf, 1);

    该代码所做的可能不是你所期望的。首先,它得到一把写锁,它从当前文件尾端起,包括以后可能添加到文件中的所有字节。然后,它在文件尾端添加了一个字节,该字节将被加锁。随后,解锁,但是刚才增加的那个字节将仍旧被锁着。最后,第二个写,这次写入的一个字节是不被加锁的。

    所以,这段代码执行完之后,该文件中倒数第二个字节是被锁着的。跟我们的预期差别很远吧。

    5.建议性锁和强制性锁

    14.4:STREAMS

    不太理解STREAMS机制、STREAMS设备是什么意思。

    14.5:I/O多路转接

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

    while ((n = read(STDIN_FILENO, buf, BUFSIZE)) > 0)
    {
        if (write(STDOUT_FILENO, buf, n) != n)
        {
            perror("write error!");
        }
    }

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

     处理这种问题的一个方法是PPC或者TPC。即一个连接一个进程、一个连接一个线程,但是这样做也会增加进程间通信和线程间同步的复杂度。

    还有一种方法是使用非阻塞I/O(nonblocking I/O)

    基本方法是将两个输入描述符设置为非阻塞的,对第一个描述符发出read操作,如果有数据则处理,如果没有数据,则read立即返回。然后对第二个描述符做同样的操作。在此之后等待若干秒,循环上述操作。这种方式成为轮询。

    这种方式的不足之处是浪费CPU。

    还有一种技术称之为异步I/O(asynchronous I/O)

    其基本思想是进程告诉内核,当一个描述符已准备好可以进行I/O时,用一个信号通知它。这种技术有两个问题。第一,并非所有系统都支持。其次,这种信号对每个进程而言只有一个,在接到该信号时进程无法判断是哪一个描述符已准备好可以进行I/O。为了确定是哪一个,仍需将这两个描述符都设置为非阻塞的,并顺序试执行I/O。

    一种比较好的技术是使用I/O多路转接(I/O multiplexing)

    先构造一张有关描述符的列表,然后调用一个函数,直到该描述符列表中的一个已准备好I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O。

    poll、select、pselect这三个函数可以让我们实现I/O多路转接。

    14.5.1:select、pselect函数

    在所有依从POSIX的平台上,select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:

    我们所关心的描述符。

    对每个描述符,我们所关心的状态。(是否读一个给定描述符,是否写一个给定描述符,是否关心一个描述符的异常状态)

    愿意等待多久。

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

    已准备好I/O的描述符个数。

    对于读、写、异常这三个状态中的每一个,哪些描述符已准备好。

    使用这些信息就可以调用相应的I/O函数,并且确定这些I/O函数不会阻塞。

    #include <sys/select.h>
    int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);
    // 返回值:准备就绪的描述符数,若超时返回0,若出错返回-1

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

    struct timeval
    {
        long tv_sec; // seconds
        long tv_usec; // microseconds
    };

    有三种情况:

    tvptr=NULL 永远等待。

    tvptr->tv_sec==0 && tvptr->tv_usec=0 完全不等待。

    tvptr->tv_sec!=0 || tvptr->tv_usec != 0 等待指定时间。若超时,则返回0。

    POSIX允许实现中修改timeval的值,所以在select返回后,你不能指望该结构保持之前的值。在Linux 2.4.22中,若在该指定时间尚未超时就返回,那么就将用余下的时间值更新该结构。注意与poll函数中对应参数做对比。

    中间三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。

    对fd_set类型可以进行的处理是:分配一个这种类型的变量;将这种类型的一个变量赋值给同类型的另一个变量;或对于这种类型的变量使用下列四个函数中的一个:

    #include <sys/select.h>
    int FD_ISSET(int fd, fd_set *fdset); // 返回值:若fd在描述符集中则返回非0,否则返回0
    void FD_CLR(int fd, fd_set *fdset);
    void FD_SET(int fd, fd_set *fdset);
    void FD_ZERO(fd_sete *fdset);

    调用FD_ZERO将一个指定fd_set变量的所有位置为0;调用FD_SET设置一个fd_set变量的指定位;调用FD_CLR清除一个fd_set变量的指定位;然后调用FD_ISSET测试fd_set变量的指定位是否设置。

    select函数的中间三个参数的任意一个或全部都可以为NULL。如果三个都是NULL,则select提供一个高精度的计时器。

    select函数的第一位参数maxfdp1的意思是“最大描述符加1”。

    select有三个可能的返回值:

    返回-1表示出错。

    返回0表示没有描述符准备好。

    返回正值表示已经准备好的描述符数。该值是三个描述符集中已准备好的描述符的和。

    14.5.2:poll函数

    poll函数类似于select,但其程序员接口不同。

    #include <poll.h>
    int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
    // 返回值:准备就绪的描述符数,若超时返回0,若出错返回-1

     与select不同,poll不是为每个状态构造一个描述符集,而是构造一个poll结构数组,每个数组元素指定一个描述符编号及其所关心的状态。

    struct pollfd
    {
        int fd;            // file descriptor to check
        short events;     // events of interest on fd
        short revents;     // events that occurred on fd
    };

    fdarray的个数由nfds参数指定。

    应将events成员设置成以下值。通过这些值,告诉内核对该描述符我们关心哪些状态。返回时,内核设置revents成员,以说明对于该描述符已发生了什么事件。(注意,poll没有更改events成员,这与select不同,select修改其参数以指示哪一个描述符已准备好了。)

    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    标志名    输入致events    从reevents得到结果    说明

    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    POLLIN    *          *            不阻塞的可读高优先级外的数据

    POLLRDNORM  *          *            不阻塞的可读普通数据

    POLLRDBAND *          *            不阻塞的可读非0优先级波段数据

    POLLPRI     *          *            不阻塞的可读高优先级数据

    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    POLLOUT   *          *            不阻塞的可写普通数据

    POLLWRNORM *            *            与POLLOUT相同

    POLLWRBAND *           *            不阻塞的可写非0优先级波段数据

    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    POLLERR               *            已出错

    POLLHUP              *            已挂断

    POLLNVAL              *            描述符不引用一个打开文件

    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 

    前四行测试可读性,中间三行测试可写性,最后三行测试异常状态。最后三行是由内核在返回时设置的,即使在events中没有设置这三个值,如果有异常情况发生,也会在reevents中返回它们。

    poll的最后一个参数表示我们愿意等待多长时间。与select类似,有三种情况:

    timeout == -1 永远等待。捕捉信号返回,则poll返回-1,errno设置为EINTR。

    timeout == 0 不等待。

    timeout > 0 等待timeout毫秒。超时返回0

     应当理解文件结束和挂断的区别。如果正从终端输入数据,并键入文件结束符,POLLIN被打开,于是就可读文件结束指示(read 0)。POLLHUP在revents中没有打开。如果正读调制解调器,电话线已挂断,则在revents中将接到POLLHUP通知。

    与select一样,不论描述符是否阻塞,都不影响poll是否阻塞。

    select与poll的可中断性

    在接到信号后,select和poll都不自动重启。

    14.6:异步I/O

     使用上一节的select、poll可以实现异步形式的通知。关于描述符的状态,系统并不主动告诉我们,需要我们主动查询(调用select或poll)。信号机构提供一种以异步形式通知某种事件已发生的方法。

    但是异步I/O的一个限制是每个进程只有一个信号。如果要对几个描述符进行异步I/O,那么在进程收到该信号时并不知道信号对应于哪一个描述符。

    14.7:readv和writev函数

    readv和writev函数用于在一次函数调用中读取、写入多个缓冲区。也称为散布读、聚集写。

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

    14.8:readn和writen函数

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

    (1)一次read返回的数据可能少于要求的数据。

    (2)一次write返回的个数也可能小于要写入的数据长度。

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

    #include "apue.h"
    ssize_t readn(int filedes, void *buf, size_t nbytes);
    ssize_t writen(int filedes, void *buf, size_t nbytes);
    // 两个函数返回值:已读写字节数,若出错返回-1

    注意:这两个函数并非任何标准,而是apue这本书中作者写出来的,方便以后使用。

    14.9:存储映射I/O

    存储映射I/O使一个磁盘文件和存储空间中的一个缓冲区相映射,于是当从缓冲区中读取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动写入文件。

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

    #include <sys/mmap.h>
    void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
    // 返回值:若成功则返回映射区的起始地址,若出错则返回MAP_FAILED

    addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由操作系统选择该映射区的起始地址。此函数的返回地址是该映射区的起始地址。

    filedes指定要被映射问价的描述符。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。off是要映射字节在文件中的起始偏移量。

    prot参数说明对映射存储区的保护要求。

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

    flag参数影响映射存储区的多种属性

    MAP_FIXED 返回值必须等于addr。因为这不利于可移植性,所以不建议使用此标志。如果未指定此标志,而addr非0,则内核只把addr视为一种建议,但是不保证会使用该起始地址。
    MAP_SHARED 这一标志说明了本进程对映射存储区所进行的存储操作配置。此标志指定存储操作修改映射文件,也就是说,存储操作相当于对该文件的write操作。必须指定本标志或下一个标志,但不能同时指定。
    MAP_PRIVATE 本标志说明,对映射区的存储操作导致创建该映射文件一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原始文件。

    调用mprotect可以更改一个现存的映射存储区的权限:

    #include <sys/mman.h>
    int mprotect(void *addr, size_t len, int prot); // 返回值:若成功返回0,若出错则返回-1

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

    #include <sys/mman.h>
    int msync(void *addr, size_t len, int flag); //返回值:若成功则返回0,若出错则返回-1

    如果映射是私有的,那么不修改被映射的文件。flags参数使我们对如何冲洗存储区有某种程度的控制。我们可以指定MS_ASYNC标志以简化被写页的调度。如果我们希望在返回之前等待写操作完成,则可以指定MS_SYNC标志。一定要指定MS_ASYNC和MS_SYNC中的一个。

    进程终止时,或调用了munmap函数之后,存储映射区就被自动解除映射。关闭文件描述符filedes并不解除映射区。

    #include <sys/mman.h>
    int munmap(caddr_t addr, size_t len); // 返回值:若成功则返回0,若出错则返回-1

    munmap不会影响被映射对象,调用munmap不会将映射存储区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。

    在解除了映射后,对于MAP_PRIVATE存储区的修改被丢弃。

    实例 14-12:用存储映射I/O复制一个文件。

    14.10:小结

    本章说明了很多高级I/O功能。

    非阻塞I/O--发一个I/O操作,不使其阻塞。

    记录锁

    系统V流机制

    I/O多路转接--select、poll函数

    readv和writev函数

    存储映射I/O(mmap)

  • 相关阅读:
    1.23学习总结:文件流
    vue-router重写push方法,解决相同路径跳转报错,解决点击菜单栏打开外部链接
    手把手教Electron+vue,打包vue项目,打包成桌面程序。
    后台获取的map集合封装json
    VUE同级组件之前方法调用
    字节跳动今日头条-抖音小程序序html富文本显示解决办法
    别总写代码,这130个网站比涨工资都重要
    vue 组件之间的自定义方法互相调用
    swiper轮播图出现疯狂抖动(小程序)
    vue通过地址下载文件
  • 原文地址:https://www.cnblogs.com/lit10050528/p/4464863.html
Copyright © 2020-2023  润新知