apue 第14章 高级I/O

Posted 延禾xy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了apue 第14章 高级I/O相关的知识,希望对你有一定的参考价值。

1、引言

2、非阻塞I/O

系统调用分为两类:低速系统调用和其他。
低速系统调用是可以使进程永远阻塞的一类系统调用

  1. 如果某些文件类型(读管道、终端设备和网络设备)的数据并不存在,读操作可能会使调用者永远阻塞
  2. 如果数据不能被相同的文件类型立即接受(管道中无空间、网络流控制)。写操作可能会使调用者永远阻塞
  3. 在某种条件发生之前打开某些文件类型可能会发生阻塞
  4. 对已经加上强制性记录锁的文件进行写
  5. 某些ioctl操作
  6. 某些进程间通信函数

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

对于一个给定的描述符,有两种为其指定非阻塞I/O的方法
1. 如果调用open获得描述符,则可指定O_NONBLOCK标志
2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志

3、记录锁

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

fcntl记录锁

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

cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数是一个指向flock结构的指针

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

对于flock的结构说明如下:

  • 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)
  • 要加锁或解锁区域的起始字节偏移量(l_start 和 l_whence)
  • 区域的字节长度( l_len )
  • 进程的ID( l_pid)持有的锁能阻塞当前进程(仅由 F_GETLK 返回)

关于加锁和解锁区的说明:

  • 指定区域起始偏移量的两个元素与lseek函数中最后两个参数类似。l_whence可选用的值是SEEK_SET、SEEK_CUR和SEEK_END
  • 锁可以在当前文件尾端处开始或者越过尾端处开始,但不能在文件起始位置之前开始
  • 若l_len为0,则表示锁的范围可以扩展到最大可能偏移量。这以为着不管向该文件追加写了多少数据,它们都是可以处于锁的范围内,而且起始位置可以是文件中的任意一个位置
  • 为了对整个文件加锁,我们设置l_start和l_whence指向文件的起始位置,并且指定长度为0。

规则:任意多个进程在一个给定的字节上可以有一把共享的锁,但是在一个给定字节上只能有一个进程有一把独占写锁。

关于cmd命令

  • F_GETLK: 判断有flockptr所描述的锁是否会被另一把锁排斥。如果存在一把锁,它阻止创建由flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变
  • F_SETLK:设置有flockptr所描述的锁,如果试图获得一把读锁或写锁,而兼容性规则阻止系统给我们这把锁,那么fcntl会立即出错返回,此时errno设置为EACCES或EAGAIN此命令也用来清除有flockptr指定的锁
  • F_SETLKW:这个命令是F_SETLK的阻塞版本。如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程会置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;
    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    return (fcntl(fd, cmd, &lock));
}

测试一把锁

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;

    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    if (fcntl(fd, F_GETLK, &lock) < 0)
    {
        perror("fcntl error!");
        exit(EXIT_FAILURE);
    }

    if (lock.l_type == F_UNLCK)
    {
        return 0;
    }

    return (lock.l_pid);
}

读锁和写锁测试

读锁

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

void Perror(const char* s)
{
    perror(s);
    exit(EXIT_FAILURE);
}

pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;

    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    if (fcntl(fd, F_GETLK, &lock) < 0)
    {
            perror("fcntl error!");
            exit(EXIT_FAILURE);
     }

    if (lock.l_type == F_UNLCK)
    {
            return 0;
        }

    return (lock.l_pid);
}

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;
    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    return (fcntl(fd, cmd, &lock));
}

int main()
{
    //创建文件
    int fd = open("./test.tmp",O_RDONLY | O_CREAT | O_EXCL, 0777);
    if (fd == -1)
    {
        printf("file exit!\\n");
        fd = open("./test.tmp", O_RDONLY, 0777);
    }
    else
        printf("create file success.\\n");

    pid_t pid = getpid();
    printf("the proc pid : %d\\n", pid);

    pid_t lockpid = lock_test(fd, F_RDLCK, 0, SEEK_SET, 0);
    if(lockpid == 0)
        printf("check read lockable, ok\\n");
    else
        printf("check read lockable, can't.have write lock, owner pid : %d\\n", lockpid);
    //set read lock
    if (lock_reg(fd, F_SETLK, F_RDLCK, 0,SEEK_SET, 0) < 0)
        printf("set read lock failed\\n");
    else
        printf("set read lock success\\n");

    sleep(60);
    return 0;

}

写锁

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

void Perror(const char* s)
{
    perror(s);
    exit(EXIT_FAILURE);
}

//check lock
pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;

    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    if (fcntl(fd, F_GETLK, &lock) < 0)
    {
            perror("fcntl error!");
            exit(EXIT_FAILURE);
        }

    if (lock.l_type == F_UNLCK)
    {
            return 0;
        }

    return (lock.l_pid);
}

//set lock or free lock
int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;
    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    return (fcntl(fd, cmd, &lock));
}

int main()
{
    int fd = open("./test.tmp", O_WRONLY | O_CREAT | O_EXCL, 0777);
    if (fd == -1)
    {
        printf("file exit \\n");
        fd = open("./test.tmp", O_WRONLY, 077);
    }
    else
        printf("create file success.\\n");

    pid_t pid = getpid();
    printf("the proc pid:%d\\n", pid);

    //check write
    pid_t lockpid = lock_test(fd, F_WRLCK, 0, SEEK_SET, 0);
    if (lockpid == 0)
        printf("check write lockable, ok\\n");
    else
        printf("check write lockable, can't. \\
                have read or write lock,ower pid : %d", lockpid);
    //set write lock
    if (lock_reg(fd, F_SETLK, F_WRLCK, 0, SEEK_SET, 0) < 0)
        printf("set write lock failed\\n");
    else
        printf("set write lock success\\n");
    sleep(60);
    return 0;
}

设置读锁,再设置读锁

ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ ./readflock
create file success.
the proc pid : 31858
check read lockable, ok
set read lock success

ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ ./readflock 
file exit!
the proc pid : 31908
check read lockable, ok
set read lock success

设置读锁载设置写锁

ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ ./readflock 
file exit!
the proc pid : 31973
check read lockable, ok
set read lock success


ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ ./writeflock 
file exit 
the proc pid:32018
check write lockable, can't.                 have read or write lock,ower pid : 31973set write lock failed

锁的隐含继承和释放

  1. 进程终止时,它所建立的锁全部释放。
  2. 无论一个描述符何时关闭,该进程通过这一描述符引用建立的任何一把锁都会释放。
  3. 由fork产生的子进程不继承父进程所设置的锁。
  4. 在执行exec后,新程序可以继承原执行程序的锁。因为执行exec前后还是一个进程。我们 只是改变进程执行的程序,并没有创建新的进程。

在文件尾端加锁

在文件尾端加锁或者解锁时要特别小心,采用SEEK_END和SEEK_CUR是可能不断变化的,所以们最好采用SEEK_SET,使用绝对偏移,否则应时刻记住当前偏移量和文件尾端的位置。

建议性锁和请执行锁(参考:http://www.cppblog.com/mysileng/archive/2012/12/17/196372.html)

1.建议性锁机制是这样规定的:每个使用文件的进程都要主动检查该文件是否有锁存在,当然都是通过具体锁的API,比如fctl记录锁F_GETTLK来主动检查是否有锁存在。如果有锁存在并被排斥,那么就主动保证不再进行接下来的IO操作。如果每一个进程都主动进行检查,并主动保证,那么就说这些进程都以一致性的方法处理锁,(这里的一致性方法就是之前说的两个主动)。但是这种一致性方法依赖于编写进程程序员的素质,也许有的程序员编写的进程程序遵守这个一致性方法,有的不遵守。不遵守的程序员编写的进程程序会怎么做呢?也许会不主动判断这个文件有没有加上文件锁或记录锁,就直接对这个文件进行IO操作。此时这种有破坏性的IO操作会不会成功呢?如果是在建议性锁的机制下,这种破坏性的IO就会成功。因为锁只是建议性存在的,并不强制执行。内核和系统总体上都坚持不使用建议性锁机制,它们依靠程序员遵守这个规定。(Linux默认是采用建议性锁)

2.强制性锁机制是这样规定的: 所有记录或文件锁功能内核执行的。上述提到的破坏性IO操作会被内核禁止。当文件被上锁来进行读写操作时,在锁定该文件的进程释放该锁之前,内核会强制阻止任何对该文件的读或写违规访问,每次读或写访问都得检查锁是否存在。也就是强制性锁机制,让锁变得名副其实,真正达到了锁的效果,而不是像建议性锁机制那样只是个纸老虎。= =!

设置强制性文件锁的方式比较特别:

chmod g+s <filename>
chmod g-x <filename>

这是形象的表示,实际编程中应该通过chmod()函数一步完成。不能对目录、可执行文件设置强制性锁。

4、I/O多路转换

当一个进程有多个输入输出时该如何处理?

  • 直接一个进程阻塞:任何一个I/O读写阻塞,进程无法进行下去,效率低
  • fork多个进程处理:对于操作结束难以处理
  • 在一个进程中使用多线程:要处理同步问题,复杂性太高
  • 是非非阻塞I/O读取数据:采用轮训方式,但是比较浪费CPU时间
  • 异步I/O:当描述符准备好后就用一个信号通知它,但是难于确定是哪个I/O发出的信号

一个比较好的技术是:I/O多路转换。为了使用这种技术,先构造一张我们感兴趣的描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O时,该函数才返回。poll、pselect和select这3个函数使我们能够执行I/O多路转接。

函数select和pselect

select函数告诉内核:

  • 我们所关心的描述符
  • 对于每个描述符我们所关心的条件
  • 愿意等待多长时间

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

  • 已准备好的描述符的总数量
  • 对于读、写或异常这3个条件中的每一个,哪些描述符已准备好
#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds, 
           fd_set *restrict writefds, fd_set *restrict excepyfds
           , struct timeval *restrict tvptr);
//返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1
  • tvptr愿意等待的时间长度
    • NULL。用于等待
    • 0。不等待
    • 不为0。等待制定的时间
  • readfds,writefds,excepyfds:指向描述符集的指针。这3个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个fd_set数据类型中。
  • 对于fd_set数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用下列4个函数中的一个。
#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_set *fdset);//清0

select的中间3个参数中的任意一个可以是空指针,这表示对相应条件并不关心。如果所有3个指针都是NULL,则select提供了比sleep更精确的定时器
* maxfdp1:表示要搜寻的最大描述符编号值加1。
* 返回值 :-1表示出错,0表示没有描述符准备好,正值说明已经准备好的描述符数。

pselect是select的变体,函数形式如下:

#include <sys/select.h>
int pselect(intmaxfdp1,fd_set *restrictreadfds,
    fd_set *restrictwritefds,fd_set *restrictexceptfds,
    const struct timespec *restricttsptr,
    const sigset_t *restrictsigmask);
//返回值:准备就绪的描述符数目,若超时返回0,若出错返回1.

函数poll

poll函数和select类似。

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_y nfds, int timeout);
//返回值:准备就绪的描述符数目:若超时,返回0;若出错,返回-1
  • poll构造一个pollfd数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件
struct pollfd{
    int    fd;//文件描述符
    short  events;//等待的事件
    short  revents;//实际发生了的事件
};
  • 每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。
  • 每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。
  • revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
  • events域中请求的任何事件都可能在revents域中返回
  • 合法的事件如下:

  • POLLIN         有数据可读。

  • POLLRDNORM      有普通数据可读。
  • POLLRDBAND      有优先数据可读。
  • POLLPRI         有紧迫数据可读。
  • POLLOUT       写数据不会导致阻塞。
  • POLLWRNORM      写普通数据不会导致阻塞。
  • POLLWRBAND      写优先数据不会导致阻塞。
  • POLLMSGSIGPOLL     消息可用。

此外,revents域中还可能返回下列事件:

  • POLLER   指定的文件描述符发生错误。
  • POLLHUP   指定的文件描述符挂起事件。
  • POLLNVAL  指定的文件描述符非法。

这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。

timeout参数制定等待的时间:-1,永远等待;0,不等待;>0,等待timeout毫秒数;

异步I/O

[参考:http://blog.jobbole.com/103290/]
同步: 所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

异步:异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

以下主要介绍POSIX异步I/O

在异步非阻塞 I/O 中,我们可以同时发起多个传输操作。这需要每个传输操作都有惟一的上下文,这样我们才能在它们完成时区分到底是哪个传输操作完成了。在 AIO 中,这是一个 aiocb(AIO I/O Control Block)结构。这个结构包含了有关传输的所有信息,包括为数据准备的用户缓冲区。在产生 I/O (称为完成)通知时,aiocb 结构就被用来惟一标识所完成的 I/O 操作。这个 API 的展示显示了如何使用它。

API 函数说明
aio_read请求异步读操作
aio_error检查异步请求的状态
aio_return获得完成的异步请求的返回状态
aio_write请求异步写操作
aio_suspend挂起调用进程,直到一个或多个异步请求已经完成(或失败)
aio_cancel取消异步 I/O 请求
lio_listio发起一系列 I/O 操作

POSIX的异步I/O借口使用AIO控制块来描述I/O操作。aiocb结构体定义了AIO控制块。

struct aiocb{
    int              aio_fildes;//文件描述符
    off_t            aio_offset//偏移量
    volatile void    *aio_buf;//指定开始地址
    size_t           aio_nbytes;//字节数
    int              aio_reqprio;//异步I/O请求提示顺序
    struct sigevent  aio_sigevent;//字段控制
    int              aio_lio_opcode;//控制通知类型
};
  • aio_fields:被打开用来读或写的文件描述符
  • aio_offset : 读或写的偏移量
  • aio_buf:对于读操作,从该缓冲区读取数据。对于写操作,数据从这个缓冲区复制出来
  • aio_nbytes:读或写的字节数
  • aio_reqprio : 异步I/O请求提示顺序
  • aio_sigevent : 字段控制
  • aio_lio_opcode : 控制通知类型

sigevent结构体如下:

struct sigevent{
    int             sigev_notify;
    int             sigev_signo;
    union           sigval sigev_value;
    void            (*sigev_notify_function)(union sigval);
    pthread_attr_t  *sigev_notify_attributes;
};

sigev_notify字段控制通知的类型:

  • SIGEV_NONE 异步I/O请求完成后,不通知进程
  • SIGEV_SIGNAL 异步I/O请求完成后,产生由sigev_signo字段指定的信号。
  • SIGEV_THREAD 当异步I/O请求完成时,由sigev_notify_function字段指定的函数被调用。

在异步I/O之前需要先初始化AIO控制块.读写操作如下:

#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
//两个函数的返回值:若成功,返回0;若出错,返回-1
  • aio_read 函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。
  • aio_read 函数在请求进行排队之后会立即返回。如果执行成功,返回值就为 0;如果出现错误,返回值就为 -1,并设置 errno 的值。
  • aio_write 函数会立即返回,说明请求已经进行排队(成功时返回值为 0,失败时返回值为 -1,并相应地设置 errno)。
//一个使用aio_read进行异步读的例子
#include <aio.h>

...

  int fd, ret;
  struct aiocb my_aiocb;

  fd = open( "file.txt", O_RDONLY );
  if (fd < 0) perror("open");

  /* Zero out the aiocb structure (recommended) */
  bzero( (char *)&my_aiocb, sizeof(struct aiocb) );

  /* Allocate a data buffer for the aiocb request */
  my_aiocb.aio_buf = malloc(BUFSIZE+1);
  if (!my_aiocb.aio_buf) perror("malloc");

  /* Initialize the necessary fields in the aiocb */
  my_aiocb.aio_fildes = fd;
  my_aiocb.aio_nbytes = BUFSIZE;
  my_aiocb.aio_offset = 0;

  ret = aio_read( &my_aiocb );
  if (ret < 0) perror("aio_read");

  while ( aio_error( &my_aiocb ) == EINPROGRESS ) ;

  if ((ret = aio_return( &my_iocb )) > 0) {
    /* got ret bytes on the read */
  } else {
    /* read failed, consult errno */
  }

要想强制所有等待中的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制块并调用aio_fsync函数

#include <aio.h>
int aio_fsync(int op, struct aiocb *aiocb);
//返回值:若成功,返回0;若出错,返回-1
  • op参数为O_DSYNC,那么操作执行起来就会像调用了fdatasync一样。
  • op参数设定为O_SYNC,那么操作执行起来就会调用了fsnyc一样。

为了获得一个异步读、写或者同步操作的完成状态,需要调用aio_error函数

#include <aio.h>
int aio_error(const struct aiocb *aiocb);
  • 返回0:异步操作成功完成。需要调用aio_return函数获取操作返回值
  • 返回-1:对aio_error的调用失败。这种情况下,errno会告诉我们为什么
    *返回EINPROGRESS:异步读、写或同步操作仍在等待
  • 返回其他:其他任何返回值是相关的异步操作失败返回的错误码

如果异步调用成功可以使用aio_return函数获取异步调用的返回值:

#include <aio.h>
ssize_t aio_return(const struct aiocb *aiocb);
  • 如果调用失败,则返回-1,并设置errno
  • 其他情况会返回异步操作的结果。即返回read、write或者fsync在被成功调用时可能返回的结果。

如果在完成了所有事务时,还有异步操作未完成时,可以调用aio_suspend函数来阻塞进程,直到操作完成。

#include <aio.h>
int aio_suspend(const struct aiocb *const list[], int nent,
                const struct timespec *timeout);
//返回值:若成功,返回0;若出错,返回-1
  • aio_suspend 的使用非常简单。我们要提供一个 aiocb 引用列表。如果任何一个完成了,这个调用就会返回 0。否则就会返回 -1,说明发生了错误。
//使用 aio_suspend 函数阻塞异步 I/O
struct aiocb *cblist[MAX_LIST]
/* Clear the list. */
bzero( (char *)cblist, sizeof(cblist) );
/* Load one or more references into the list */
cblist[0] = &my_aiocb;
ret = aio_read( &my_aiocb );
ret = aio_suspend( cblist, MAX_LIST, NULL );

当还有我们不想再完成的等待中的异步I/O操作时,可以尝试使用aio_cancel函数来取消它们

#include <aio.h>
int aio_cancel(int fd, struct aiocb *aiocb);
  • 如果aiocb参数为NULL,系统将会尝试取消所有该文件上未完成的异步I/O操作。
  • 返回AIO_ALLDONE:所有操作在尝试取消它们之前已经完成
  • 返回AIO_CANCELED:所有要求的操作已被取消
  • 返回AIO_NOTCANCELED:至少有一个要求的操作没有被取消
  • 返回-1:对aio_cancel的调用失败。错误码将被存储在errno中

AIO 提供了一种方法使用 lio_listio API 函数同时发起多个传输。这个函数非常重要,因为这意味着我们可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作。从性能的角度来看,这非常重要.
lio_listio API 函数的原型如下:

int lio_listio( int mode, struct aiocb *list[], int nent,
                   struct sigevent *sig );
  • mode 参数可以是 LIO_WAIT 或 LIO_NOWAIT。LIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止。在操作进行排队之后,LIO_NOWAIT 就会返回。
  • list 是一个 aiocb 引用的列表
  • 最大元素的个数是由 nent 定义的
  • sigevent 引用定义了在所有 I/O 操作都完成时产生信号的方法。
//使用 lio_listio 函数发起一系列请求
struct aiocb aiocb1, aiocb2;
struct aiocb *list[MAX_LIST];

...

// Prepare the first aiocb 
aiocb1.aio_fildes = fd;
aiocb1.aio_buf = malloc( BUFSIZE+1 );
aiocb1.aio_nbytes = BUFSIZE;
aiocb1.aio_offset = next_offset;
aiocb1.aio_lio_opcode = LIO_READ;

...

bzero( (char *)list, sizeof(list) );
list[0] = &aiocb1;
list[1] = &aiocb2;

ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );
  • 对于读操作来说,aio_lio_opcode 域的值为 LIO_READ。对于写操作来说,我们要使用 LIO_WRITE,不过 LIO_NOP 对于不执行操作来说也是有效的。

AIO 通知可以使用信号进行异步通知,也可以使用回调函数进行异步通知。

一个使用信号通知的例子:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <aio.h>
#include <fcntl.h>

void AsyncRead(int s, siginfo_t* info, void* context)
{
    struct aiocb* ptr = (struct aiocb*)info->si_value.sival_ptr;
    printf("read = %s\\n", (char*)ptr->aio_buf);
}

int main()
{
    struct aiocb cb;
    int fd;
    char sbuf[100];
    int ret;
    struct sigaction act;

    fd = open("aiotest.txt", O_RDWR, S_IRUSR|S_IWUSR);
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_RESTART | SA_SIGINFO;
    act.sa_sigaction  = AsyncRead;
    sigaction(SIGIO, &act, NULL);

    bzero(&cb, sizeof(cb));
    cb.aio_fildes = fd;
    cb.aio_buf = sbuf;
    cb.aio_nbytes = 100;
    cb.aio_offset = 0;

    cb.aio_sigevent.sigev_value.sival_ptr = &cb;
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    cb.aio_sigevent.sigev_signo = SIGIO;

    ret = aio_read(&cb);
    if (ret == -1)
    {
        perror("aio_read error.");
        exit(EXIT_FAILURE);
    }

    int i = 0;
    while (i != 10)
    {
        printf("%d\\n", i++);
        sleep(3);
    }

    return 0;

}
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ gcc AIONotifyTest.c -lrt 
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ echo test aio > aiotest.txt
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ ls aiotest.txt 
aiotest.txt
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch14AdvancedI_O$ ./a.out 
0
read = test aio
r½. %可能是乱码
1
2
3
4
5

函数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;
    size_t  iov_len;
};
  • iov数组中的元素数由iovcnt指定,其最大值受限于IOV_MAX。
  • writev从缓冲区输出数据的顺序是:iov[0], iov[1],…iov[iovcnt-1]
  • readv则将数据散布到缓冲区。总是先填满一个缓冲区,然后再填写下一个。

iovec结构如下:

函数readn和writen

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

  1. 一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样。这不是个错,应当继续读该设备
  2. 一次write操作的返回值可能少于指定输出的字节数。这可能是由某个因素造成的,例如,内核输出 缓冲区变满。这也不是错误,应当继续写余下的数据。

readn和writen读写制定的N字节的数据,并处理返回值可能小于要求值的情况。

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

存储映射I/O

存储映射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,这表示由系统选择该映射区的起始地址。此函数的返回地址是该映射区的起始地址。
  • filedes指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。
  • len是映射的字节数。
  • off是要映射字节在文件中的起始偏移量。
  • prot参数说明对映射存储区的保护要求(对指定映射存储区的保护要求不能超过文件open模式访问权限)
    • PROT_READ 映射区可读
    • PROT_WRITE 映射区可写
    • PROT_EXEC 映射区可执行
    • PROT_NONE 映射区不可访问
  • flag参数影响映射存储区的多种属性:
    • MAP_FIXED–返回值必须等于addr。因为这不利于可移植性,所以不鼓励使用此标志。如果未指定此标志,而且addr非0,则内核只把addr视为在何处设置映射区的一种建议,但是不保证会使用所要求的地址。将addr指定为0可获得最大可移植性。
    • MAP_SHARED–这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定存储操作修改映射文件,也就是说,存储操作相当于对该文件的write。必须指定本标志或下一个标志(MAP_PRIVATE),但不能同时指定两者。
    • MAP_PRIVATE–本标志说明,对映射区的存储操作导致创建该映射文件的一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原始文件。(此标志的一种用途是用于调试程序,它将程序文件的正文部分映射至一存储区,但允许用户修改其中的指令。任何修改只影响程序文件的副本,而不影响原文件。

存储映射例子:

  • off和addr的值(如果指定了MAP_FIXED)通常应当是系统虚存页长度的倍数.因为off和addr常常指定为0,所以这种要求一般并不重要。
  • 与映射存储区相关的有SIGSEGV和SIGBUS两个信号。信号SIGSEGV通常用于指示进程试图访问对它不可用的存储区。如果进程企图存数据到mmap指定为只读的映射存储区,那么也产生此信号。如果访问映射区的某个部分,而在访问时这一部分实际上已不存在,则产生SIGBUS信号。例如,用文件长度映射了一个文件,但在引用该映射区之前,另一个进程已将该文件截短。此时,如果进程企图访问对应于该文件已截去部分的映射区,则会收到SIGBUS信号。
  • 在调用fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是该地址空间的一部分),但是由于同样的理由,调用exec后的新程序则不继承存储映射区。

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

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
返回值:若成功则返回0,若出错则返回-1
  • prot的许可值与mmap中prot参数一样
  • 地址参数addr的值必须是系统页长的整数倍。

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

#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
返回值:若成功则返回0,若出错则返回-1
  • 如果映射是私有的,那么不修改被映射的文件。
  • 与其他存储映射函数一样,地址必须与页边界对齐。
  • flags参数使我们对如何冲洗存储区有某种程度的控制。我们可以指定MS_ASYNC标志以简化被写页的调度。如果我们希望在返回之前等待写操作完成,则可指定MS_SYNC标志。一定要指定MS_ASYNC和MS_SYNC中的一个。
  • MS_INVALIDATE是一个可选标志,使用它以通知操作系统丢弃与底层存储器没有同步的任何页。若使用了此标志,某些实现将丢弃在指定范围中的所有页,但这并不是所期望的。

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

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

munmap不会影响被映射的对象,也就是说,调用munmap不会使映射区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。在解除了映射后,对于MAP_PRIVATE存储区的修改被丢弃。

参考

[1] http://blog.jobbole.com/103290/
[2] http://www.ibm.com/developerworks/cn/linux/l-async/index.html
[3] http://blog.csdn.net/gotosola/article/details/7412409
[4] http://www.cnblogs.com/nufangrensheng/p/3559664.html
[5] http://www.cppblog.com/mysileng/archive/2012/12/17/196372.html
[6] http://blog.jobbole.com/103290

以上是关于apue 第14章 高级I/O的主要内容,如果未能解决你的问题,请参考以下文章

Apue学习:高级I/O

APUE:高级I/O

第3章 文件I/O_高级文件操作:存储映射

APUE读书笔记-05标准输入输出库(1)

APUE:高级进程间通信

APUE---文件和目录