zoukankan      html  css  js  c++  java
  • apue学习笔记(第十四章 高级I/O)

    本章涵盖了从多概念和函数:非阻塞I/O、记录锁、I/O多路转换、异步I/O、readv和writev函数以及存储映射I/O

    非阻塞I/O

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

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

    1.如果调用open获得描述符,则可制定O_NONBLOCK标志(第三章)

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

    下面程序时一个非阻塞I/O的实例

     1 #include "apue.h"
     2 #include <errno.h>
     3 #include <fcntl.h>
     4 
     5 char    buf[500000];
     6 
     7 int
     8 main(void)
     9 {
    10     int        ntowrite, nwrite;
    11     char    *ptr;
    12 
    13     ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
    14     fprintf(stderr, "read %d bytes
    ", ntowrite);
    15 
    16     set_fl(STDOUT_FILENO, O_NONBLOCK);    /* set nonblocking */
    17 
    18     ptr = buf;
    19     while (ntowrite > 0) {
    20         errno = 0;
    21         nwrite = write(STDOUT_FILENO, ptr, ntowrite);
    22         fprintf(stderr, "nwrite = %d, errno = %d
    ", nwrite, errno);
    23 
    24         if (nwrite > 0) {
    25             ptr += nwrite;
    26             ntowrite -= nwrite;
    27         }
    28     }
    29 
    30     clr_fl(STDOUT_FILENO, O_NONBLOCK);    /* clear nonblocking */
    31 
    32     exit(0);
    33 }
    View Code

    记录锁

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

    fcntl记录锁

    #include <fcntl.h>
    int fcntl(int fd,int cmd,.../* struct flock *flockptr */);

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

     struct flock {
             ...
             short l_type;    /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
             short l_whence;  /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
             off_t l_start;   /* Starting offset for lock */
             off_t l_len;     /* Number of bytes to lock */
             pid_t l_pid;     /* PID of process blocking our lock(F_GETLK only) */
             ...
    };

    对flock结构的说明如下:

    1.所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁)

    2.要加锁或解锁区域的起始字节偏移量(l_start和l_whence)

    3.区域的字节长度(l_len)

    4.进程的ID(l_pid)持有的锁能阻塞当前进程(仅由F_GETLK返回)

    5.如若l_len为0,则表示锁的返回可以扩展到最大可能偏移量

    上面提到了两种类型的锁:F_RDLCK和F_WRLCK。下面演示在多个进程中的其兼容性规则

    在单个进程中情况就不一样:如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么心锁将替换已有锁。

    下面说明fcntl函数的3种命令

    F_GETLK  判断由flockptr所描述的锁是否会被另外一把锁排斥。如果存在这样的锁,则讲该锁的信息重写到flockptr指向的信息中,如果不存在,则将l_type设置为F_UNLCK

    F_SETLK  设置由flockptr锁描述的锁。如果该操作不符合兼容性规则,则函数立即出错返回,并将errno设置为EACCES或EAGAIN

    F_SETLKW 这个命令是F_SETLK的阻塞版本(W代表wait)。如果该操作不符合兼容性规则,则调用进程被置为休眠,知道请求创建的锁已经可用,才将该进程唤醒

    对fcntl记录锁函数的封装

     1 int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len )
     2 { 
     3     struct flock lock; 
     4     lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */ 
     5     lock.l_start = offset; /* byte offset, relative to l_whence */ 
     6     lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */ 
     7     lock.l_len = len; /* #bytes (0 means to EOF) */ 
     8     return( fcntl(fd, cmd, lock) ); 
     9 }
    10 
    11 #define read_lock(fd,offset,whence,len)
    12 lock_reg(fd,F_SETLK,F_RDLCK,offset,whence,len) 
    13 #define needw_lock(fd,offset,whence,len) 
    14 lock_reg(fd,F_SETLKW,F_RDLCK,offset,whence,len) 
    15 #define write_lock(fd,offset,whence,len)
    16 lock_reg(fd,F_SETLK,F_WRLCK,offset,whence,len) 
    17 #define writew_lock(fd,offset,whence,len) lock_reg(fd,F_SETLKW,F_WRLCK,offset,whence,len) 
    18 #define un_lock(fd,offset,whence,len) lock_reg(fd,F_SETLK,F_UNLCK,offset,whence,len)

    锁的隐含继承和释放有3条规则

    1.记录锁和文件两者相关联:(a)当一个进程终止时,它所建立的锁全部释放;(b)无论一个描述符何时关闭,该进程通过这一描述符引用的文件的任何一把锁都会释放

       如果执行下列4步:

       fd1=open(pathname,...);

       read_lock(fd1,...);

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

       close(fd2);

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

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

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

    在文件尾端加锁

    考虑下列代码序列:

    writew_lcok(fd,0,SEEK_END,0);

    write(fd,0,SEEK_END,0);

    un_lock(fd,0,SEEK_END);

    write(fd,buf,1);

    这段代码序列造成的文件锁状态如下图所示

    可见,在对相对于文件尾端的字节范围加锁或解锁时需要特别小心。

    建议性锁和强制性锁

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

    如果一个进程试图读或写一个强制性锁起作用的文件,有以下8种情况

    如果欲打开的文件具有强制性记录锁,而且open调用中的标志指定为O_TRUNC或O_CREAT,则open立即出错返回,errno设置为EAGAIN。

    下面程序可以用来测试系统是否支持强制性锁

     1 #include "apue.h"
     2 #include <errno.h>
     3 #include <fcntl.h>
     4 #include <sys/wait.h>
     5 
     6 int
     7 main(int argc, char *argv[])
     8 {
     9     int                fd;
    10     pid_t            pid;
    11     char            buf[5];
    12     struct stat        statbuf;
    13 
    14     if (argc != 2) {
    15         fprintf(stderr, "usage: %s filename
    ", argv[0]);
    16         exit(1);
    17     }
    18     if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
    19         err_sys("open error");
    20     if (write(fd, "abcdef", 6) != 6)
    21         err_sys("write error");
    22 
    23     /* turn on set-group-ID and turn off group-execute */
    24     if (fstat(fd, &statbuf) < 0)
    25         err_sys("fstat error");
    26     if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
    27         err_sys("fchmod error");
    28 
    29     TELL_WAIT();
    30 
    31     if ((pid = fork()) < 0) {
    32         err_sys("fork error");
    33     } else if (pid > 0) {    /* parent */
    34         /* write lock entire file */
    35         if (write_lock(fd, 0, SEEK_SET, 0) < 0)
    36             err_sys("write_lock error");
    37 
    38         TELL_CHILD(pid);
    39 
    40         if (waitpid(pid, NULL, 0) < 0)
    41             err_sys("waitpid error");
    42     } else {                /* child */
    43         WAIT_PARENT();        /* wait for parent to set lock */
    44 
    45         set_fl(fd, O_NONBLOCK);
    46 
    47         /* first let's see what error we get if region is locked */
    48         if (read_lock(fd, 0, SEEK_SET, 0) != -1)    /* no wait */
    49             err_sys("child: read_lock succeeded");
    50         printf("read_lock of already-locked region returns %d
    ",
    51           errno);
    52 
    53         /* now try to read the mandatory locked file */
    54         if (lseek(fd, 0, SEEK_SET) == -1)
    55             err_sys("lseek error");
    56         if (read(fd, buf, 2) < 0)
    57             err_ret("read failed (mandatory locking works)");
    58         else
    59             printf("read OK (no mandatory locking), buf = %2.2s
    ",
    60               buf);
    61     }
    62     exit(0);
    63 }
    View Code

    I/O多路转换

    函数select和pselect

    #include <sys/select.h>
    int select(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict exceptfds,struct timeval *restrict tvptr);

    参数tvptr指定愿意等待的时间,有下面3种情况

    1.tvptr==NULL 永远等待,直到所指定的描述符中的一个已经准备好或捕捉到一个信号返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR

    2.tvptr->tv_sec==0 && tvptr->tv_usec==0 不等待,测试所有指定的描述符并立即返回

    3.tvptr->tv_sec!=0 || tvptr->tv_usec!=0  等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回

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

    对于描述符集fd_set结构,提供了下面4个函数

    #include <sys/select.h>
    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);

    select第一个参数maxfdp1的意思是“最大文件描述符编号值加1”,在上面3个描述符集中找到最大描述符编号值,然后加1就是第一个参数值。

    select有3个可能的返回值

    1.返回值-1表示出错。如果在所指定的描述符一个都没准备好时捕捉到一个信号,则返回-1

    2.返回0表示没有描述符准备好,指定的时间就过了。

    3.一个正返回值说明了已经准备好的描述符数,在这种情况下,3个描述符集中依旧打开的位对应于已准备好的描述符。

    POSIX.1也定义了一个select的变体,称为pselect

    #include <sys/select.h>
    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);

    除了下列几点外,pselect与select相同

    1.超时值使用的结构不一样,pselect使用的是timespec结构

    2.pselect的超时值被声明为const,保证了调用pselect不会改变此值

    3.pselect可使用可选信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。

    函数poll

    #include <poll.h>
    int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);

    与select不同,poll构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对描述符感兴趣的条件。

    struct pollfd {
            int   fd;         /* file descriptor */
            short events;     /* requested events */
            short revents;    /* returned events */
    };

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

    结构中的events成员应设置为下图所示值的一个或几个

    后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。

    poll最后一个参数指定的是我们愿意等待多久时间(毫秒值)

    异步I/O

    使用一个信号(System V是SIGPOLL,在BSD中是SIGIO)通知进程,对某个描述符锁关心的某个时间已经发生。

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

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

     struct iovec {
            void  *iov_base;    /* Starting address */
            size_t iov_len;     /* Number of bytes to transfer */
     };

    下图显示了iovec结构

    iov数组中元素数由iovcnt指定

    writev函数从缓冲区中聚集输出数据的顺序是:iov[0]、iov[1]知道iov[iovcnt-1]

    readv函数则将读入的数据按上述同样顺序散布到缓冲区中

    函数readn和writen

    通常,在读、写一个管道、网络设备或终端时,需要考虑一下特性:

    1.一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样。这不是一个错误,应当继续读该设备。

    2.一次write操作的返回值也可能少于指定输出的字符数。

    下面两个函数的功能分别是读、写指定的N字节数据,并处理返回值可能少于要求值的情况。

    #include "apue.h"
    ssize_t readn(int fd,void *buf,size_t nbytes);
    ssize_t writen(int fd,void *buf,size_t nbytes);

    下面是这两个函数的实现

    readn函数

     1 #include "apue.h"
     2 
     3 ssize_t             /* Read "n" bytes from a descriptor  */
     4 readn(int fd, void *ptr, size_t n)
     5 {
     6     size_t        nleft;
     7     ssize_t        nread;
     8 
     9     nleft = n;
    10     while (nleft > 0) {
    11         if ((nread = read(fd, ptr, nleft)) < 0) {
    12             if (nleft == n)
    13                 return(-1); /* error, return -1 */
    14             else
    15                 break;      /* error, return amount read so far */
    16         } else if (nread == 0) {
    17             break;          /* EOF */
    18         }
    19         nleft -= nread;
    20         ptr   += nread;
    21     }
    22     return(n - nleft);      /* return >= 0 */
    23 }
    View Code

    writen函数

     1 #include "apue.h"
     2 
     3 ssize_t             /* Write "n" bytes to a descriptor  */
     4 writen(int fd, const void *ptr, size_t n)
     5 {
     6     size_t        nleft;
     7     ssize_t        nwritten;
     8 
     9     nleft = n;
    10     while (nleft > 0) {
    11         if ((nwritten = write(fd, ptr, nleft)) < 0) {
    12             if (nleft == n)
    13                 return(-1); /* error, return -1 */
    14             else
    15                 break;      /* error, return amount written so far */
    16         } else if (nwritten == 0) {
    17             break;
    18         }
    19         nleft -= nwritten;
    20         ptr   += nwritten;
    21     }
    22     return(n - nleft);      /* return >= 0 */
    23 }
    View Code

    存储映射I/O

    存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上。于是,当从缓冲区取数据时,就相当于读文件中的相应字节。

    与此类似,将数据存入缓冲区,相应字节就自动写入文件。

  • 相关阅读:
    测试方案
    测试需求
    软件测试知识点总结
    自动化测试
    测试——缺陷报告包括那些内容,由什么组成
    测试——缺陷的类型
    软件测试工具简介
    测试工程师
    剑指offer系列21--二叉搜索树的后续遍历序列
    剑指offer系列20--从上到下打印二叉树
  • 原文地址:https://www.cnblogs.com/runnyu/p/4645754.html
Copyright © 2011-2022 走看看