【APUE】Chapter14 Advanced I/O

14.1 Introduction

  这一章介绍的内容主要有nonblocking I/O, record locking, I/O multiplexing, asynchronous I/O, the readv and writev, memory-mapped I/O

  这一章是后面章节的基础,也就是说先当成基础记着,在后面的实操应用章节再去体会。

 

14.2 Nonblocking I/O

  "blocking"主要针对slow system call,含义是“the slow system calls are those that can block forever”。

  1. 什么情况能出现slow system calls:像read write open ioctl IPC(interprocess communication functions)都可能酝酿出slow system call

  2. 书上马上又提了“system calls related to disk I/O are not considered slow”,只要跟I/O相关的,都不算slow。这个slow是跟分配系统CPU资源的速度比起来。

  3. 如果nonblocking I/O操作没有成功,则直接返回,并且errno中保存着错误的值。

  4. 可以人工把read write这样的操作改成nonblocking的,方法是通过设置file descriptor的flag达成目标

  基于上述1、2、3、4,书上给出了下面的例子(我稍加改造):

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

  代码执行结果如下:

  1. 输入输出都是regular file

  

  2. 输入是regular file, 输出是terminal(stderr重定向到err文件)

  

  上面这段代码做的事情就是先从stdin读一个500000byte的字符,然后以nonblocking的方式不断写入stdout中。

  (1)对于1的情况,输入输出都是regular file的,记得书中说过“disk I/O都算不上slow system call”因此,nonblocking不会有啥影响

  (2)对于2的情况,输入虽然是regular file,但是输出变成了terminal;而且在write之前还人为设定成nonblocking的了(即不等着执行完就返回),就造成影响了。可以看到,errno=11,意思是资源暂时不沟通,不能满足你的nonblocking I/O的请求,只能写入一部分数据。

  如果不要nonblocking这个flag(屏蔽掉line15和line29),还是执行如下命令:

  

  查看err文件的结果如下:

  

  可以看到,不人为设定nonblocking,则不会出现这种问题。从这个小节可以看出来,书写的还是非常有逻辑的,每个example与之前提到的一些内容都是有关联的。

 

14.3 Record Locking

  这种record locking目的是针对多个process访问同一个文件时候的同步问题。实现record locking有多重方式,书上介绍的是通过fcntl函数来实现的方式。

  1. 回顾fcntl函数

  int fcntl(int fd, int cmd, ... /* struct flock *flockptr */)

  在record locking这个背景下:

  参数fd代表file descriptor,具体关联到这个process打开的某个file

  参数cmd决定了操作的模式:

    F_GETLK : 判断fd关联的file是否被上锁,如果上锁了flockptr中就存放住锁的信息

    F_SETLK : 将flockptr定制的锁,加到fd所关联的file上

    F_SETLKW: 与F_SETLK类似,多的一个'W'代表wait,即这是一个blocking函数。如果暂时不能上锁,就sleep等着,直到可以加锁或者这个process收到某个signal

  参数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_type :代表锁的方式,是读锁,写锁,还是解锁

  l_whence:锁作用范围的设定模式,SET,CUR,或END

  l_len:锁作用范围的长度

  l_start: 锁作用范围的起始位置

  l_pid: 如果F_GETLK模式下,没有加锁成功,l_pid返回当前占用这个file的process的id号

  这些不同的锁之间互相遵循规则如下:

  

  这些rule作用的条件是:不同的process争夺相同的file的lock权。如果同一个process对lock多次请求,后一次的lock就会顶掉前一次的lock。

  并且,要想获得read lock,fd参数必须是以read的模式打开;如果要想获得write lock,fd参数必须是以write的模式打开

  2. 一个deadlock的例子

  书上给了一段代码,大意是有个file一共2byte。parent process对0 byte加锁,child process对1 byte加锁;等待对方都执行完之后,parent再请求对1 byte的锁,child再请求对0 byte的锁。这样就互相锁住了。代码如下:

 1 #include "apue.h"
 2 #include <fcntl.h>
 3 
 4 static void lockabyte(const char *name, int fd, off_t offset)
 5 {
 6     if (writew_lock(fd, offset, SEEK_SET,1)<0) { 
 7         err_sys("%s: writew_lock error", name); 
 8     } 
 9     printf("%s: got the lock, byte %lld\n", name, (long long)offset);
10 }
11 
12 int main()
13 {
14     int fd;
15     pid_t pid;
16 
17     /*create a file and write two bytes to it*/
18     fd = creat("tmplock", FILE_MODE);
19     write(fd, "ab",2);
20 
21     TELL_WAIT();
22     if ((pid = fork())<0) { 
23         err_sys("fork error"); 
24     } 
25     else if (pid==0) { 
26         lockabyte("child", fd, 0);
27         TELL_PARENT(getppid());
28         WAIT_PARENT();
29         lockabyte("parent", fd, 1);
30     } 
31     else { 
32         lockabyte("parent",fd,1);
33         TELL_CHILD(pid);
34         WAIT_CHILD();
35         lockabyte("parent", fd, 0);
36     } 
37     exit(0);
38 }

  代码执行结果如下:

  

  按理说应该出现死锁的,但是系统自动识别了这样可能的死锁情况,并避免了死锁的出现。是哪里避免了死锁的出现呢?

  是fcntl函数,它再执行的期间,会检查是否出现死锁的情况。具体man中的解释如下:

  

  函数中writew_lock中调用了fcntl,并且传入的参数正式F_SETLKW(具体可以参考书上P489)

  因此,fcntl这个函数在这样的背景下开始检查是否出现deadlock情况。并且,最终选择让其中一个process获得锁的控制权(但是具体让哪个process获得控制权,得看系统实现

  3. lock的在process之间的继承

    (1)lock的一侧是process,另一侧是file。当process结束时候,这个process所有加在这个file上的lock都被release了。另一点,当fd关闭了,该process所有加在fd上的lock都关闭了。

      见如下两个情况:

      情况a.      

fd1 = open(pathname,...);
read_lock(fd1,..._;
fd2 = dup(fd1);
close(fd2);

      情况b.

fd1 = open(pathname,...);
read_lock(fd1, ...);
fd2 = open(pathname, ...);
close(fd2);

    情况a、b的最终结果都是加在pathanme上的lock都随着fd2的close动作而被release了。

  (2)lock永远不会跟着fork这样的操作继承到child process中。原因也比较直观,比如parent process对file有个write lock,如果child process也继承了这个wirte lock,逻辑就说不清混乱了。

  (3)lock可以随着exec这样的操作继承下来(但是,如果fd的close-on-exec的flag被打开了,lock也不能传下去)。原因也比较直观,因为exec完全取代了当前正在执行的program,但是还沿用之前的pid,因此还要保存调用exec时候的context,这样逻辑上也说的通。

  (4)总之,lock是不能在不同的process之间继承,可选择性的在exec这样的场景下执行。

  为了更形象的说明问题,书上给了一段代码并配上了一张图:

  代码:

fd1 = open(pathname,...);
write_lock(fd1, 0, SEEK_SET, 1); /*parent write lock byte 0*/
if ((pid=fork())>0)
{
     fd2 = dup(fd1);
     fd3 = open(pathname,...);  
}
else if (pid==0)
{
     read_lock(fd1, 1, SEEK_SET, 1); /*child read locks byte 1*/
}
pause();

  图:

  

  (1)如果parent process中把fd1 fd2 fd3任意关一个,则parent给file加上的wirte lock就被release了。

  (2)此时child process中的fd1已经是独立的fd1了,因此加在file上的锁不会随着parent process中fd1 fd2 fd3的关闭而被release。

  4. Locks at End of File

    在file尾巴上加锁是一个稍微特殊一些的情况

    书上给了个例子:

1 writew_lock(fd, 0, SEEK_END, 0);
2 write(fd, buf, 1);
3 un_lock(fd, 0, SEEK_END);
4 write(fd, buf, 1);

    1. 把fd关联的file在尾部加锁,意思是从尾巴开始,onwards方向锁上

    2. 往file末尾写一个byte

    3. 把加在文件尾部的锁解开

    4. 再写一个byte

    上述操作造成的结果就是,文件倒数第二个byte还是处于被lock的状态,具体如下图:

    

    在自己使用时,一定要注意加锁解锁作用范围的控制,避免这种漏掉一个情况。到时候真是哭都不知道找谁去。

  5. Advisory versus Mandatory Locking

    这一节与系统file system的具体implementation关系比较紧密,不同系统实现差别还是比较大的。

    一开始看的也是云里雾里,直到看到了这篇blog(http://www.thegeekstuff.com/2012/04/linux-file-locking-types/),解答了我很多的疑惑。

    下面说一下我个人的理解。

    首选需要再思考一个问题:两个不同的process如果都想操作同一个文件(比如,write操作),凭什么保证某一个时刻只有一个process在真正write这个file?

    (1)方式一:凭自觉,大家都守一样的规矩(Advisory Locking)

    借用“4.lock在process之间集成”中的那张图来说明,最关键的一点,就是凭的就是某个process(假设这个process具有写权限)在真的执行write之前,要先去访问file的v-node中的lockf pointer,看是否有其他的lock正在占用这个file。简单说,就是所有访问某个file的process都守一样的规矩:write之前必须先获得该文件的write lock占有权;而能不能获得占有权,还得去访问lockf pointer指向的lock list,去挨个查看。

    上面说的这种方式一中的所有process,对于某个file来说,叫cooperating processes:即,大家都遵守一样的规矩,“虽说都有钥匙(写权限),但是也先排队(请求写锁),再开门(占有写锁)”。

    (2)方式二:强制让大家都守规矩 (Mandatory Locking)

    方式一只能针对都守规矩的process;如果其他类型的process,有写权限,但是不知道要守这个规矩,直接就执行write操作了,怎么办?那就强制让大家都守规矩,不论哪个process来访问这个file,都得强制去先去访问v-node中的lockf pointer,即“先敲门排队,再开门”

    这种强制守规矩措施分为两个层次:

    层次一:在system级别,必须开启这样mandatory locking的功能(让file system这个大管家具备这样的技能

    层次二:在file级别,让system对哪个file采用mandatory locking策略了,需要对这个file特殊处理(即把这个file在system管家那里挂号

    通过这样的方式,就保证了不管是哪个process来访问该file,system都会强迫这个process守规矩,获得相应的锁权才能执行执行相应操作。

    

    个人觉得上面的原理理解是最关键的。再给出一个实操的例子:

    (1)我使用的系统信息如下:

    

    (2)首先需要用mount命令(这个命令与挂载文件系统有关,需要root权限),实现层次一system级别的设置:

    

    (3)编辑书上figure14.12的代码如下:

 1 #include "apue.h"
 2 #include <errno.h>
 3 #include <fcntl.h>
 4 #include <sys/wait.h>
 5 
 6 int main(int argc, char *argv[])
 7 {
 8     int fd;
 9     pid_t pid;
10     char buf[5];
11     struct stat statbuf;
12     
13     /*创建一个文件, 往里写几个字符*/
14     fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);
15     write(fd, "abcdef", 6);
16 
17     /*设置set-group-id的bit位, 关闭group-execute的bit位*/
18     fstat(fd, &statbuf);
19     fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID);
20 
21     /*apue的自定义函数*/
22     TELL_WAIT();
23     
24     if ((pid = fork())<0) { 
25         err_sys("fork error"); 
26     } 
27     else if (pid>0) { /*父进程 加一个write锁 从头开始加到尾*/
28         if (write_lock(fd, 0, SEEK_SET, 0)<0) { 
29             err_sys("write_lock error"); 
30         }
31         TELL_CHILD(pid); /*告知child process执行到这里了*/
32         if (waitpid(pid, NULL, 0)<0) { 
33             err_sys("waipid error"); 
34         } 
35     } 
36     else { 
37         WAIT_PARENT(); /*子进程 等着父进程中TELL_CHILD函数发信号 此时父进程已经给这个文件上了一个write lock了*/
38         set_fl(fd, O_NONBLOCK); /*将fd设为非阻塞的*/
39 
40         if (read_lock(fd, 0, SEEK_SET, 0) != -1) { /*子进程中加read锁 从头加到尾*/
41             err_sys("child: read_lock succeeded"); 
42         } 
43         printf("read_lock of already-locked region returns %d means %s\n",errno,strerror(errno)); /*通过errno验证子进程加read锁是否成功*/
44         if (lseek(fd, 0, SEEK_SET)==-1) { /*将fd移动到beginning的位置*/
45             err_sys("lseek error"); 
46         } 
47         if (read(fd, buf, 2)<0) { /*从文件中尝试读俩byte 如果能读成功就证明mandatory locking没用 反之则证明起作用了*/
48             err_ret("read failed (mandatory locking works)"); 
49         } 
50         else { 
51             printf("read OK (no mandatory locking), buf = %2.2s\n", buf); 
52         } 
53     } 
54     exit(0);
55 }

    执行结果如下:

    

    上述带代码是为了检验mandatory locking是否起作用的:

      (1)创建一个file, 设置set-group-id bit,关闭group-executable bit,实现层次二file级别的设置

      (2)fork

      (3)parent process给file加一个写锁,并等着child process执行完的状态;child process一定等着parent process给file加完锁了才往下进行

      (4)child process首先将从parent process继承来的file descriptor改成nonblock的,随后尝试给fd加read lock。此时parent process的write lock正占着这个file, child process的read lock是加不上去的。这个属于验证了advisory locking功能的范畴。由于之前fd设定为nonbocking的,因此child process不会一直等着可以获得read lock,而是返回-1(证明没锁成功),并将errno设为11(资源被别人占着呢,得不到锁)

      (5)接着child process执行read操作(这属于典型的不守规矩的,没成功获得read lock的权限,还要硬read),结果就是mandatory locking起作用了,没让child process去完成read的操作。

    这样,通过上面的例子,就完整理解了了advisory locking和mandatory locking的关系。

    如果在系统层面没有mandatory locking的功能,结果是怎样呢?

    我回到原先的文件夹下,再执行操作:

    

    可以发现虽然child process的read lock还是加不上(advisory locking有效),但是却可以不守规矩地越过parent的write lock,直接从tmplock文件中读(mandatory locking失效)。

 

14.4 I/O Multiplexing

  这里主要说,如果一个process中涉及到多个input和多个output,该如何处理。

  以telnet command为例说明(P500)

  telnet command是user terminal和telnetd daemon的连接节点:即从user terminal读数据,写到telnetd daemon中;又从telnetd daemon读数据,写到user terminal中。因此,telnet command相当于有两个input和两个output。

  其实,好几次实际编程中,都遇到过这样的问题,这个I/O Multiplexing说的就是这个问题。

  作者的思路非常明确,先列举了几种可能的替代的方案来解决telnet command这个问题。

  其他方案一:fork分出parent process和child process;但是parent和child谁先结束谁后结束的问题需要考虑,麻烦。

  其他方案二:multi threads多线程,要考虑同步的问题,麻烦。

  其他方案三:polling轮询方式,大量的CPU时间浪费了,低效。

  其他方案四:asynchoronous I/O异步I/O,可移植性差,signal作用有限

  所以,引出来了I/O Multiplexing这个比较好的解决方法。用一个函数来批量的搞定需要处理的各种file descriptors。

  1. select函数

  int select(int maxfdp1fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict expectfds, struct timeval *restrict tvptr)

  按照颜色区分select的三类参数:

  (1)tvptr:“specifies how long we want to wait in terms of seconds and microseconds” 具体分为,不等,无限等,有限等三种情况,可以精确到微秒。这里有个情况需要说明,一种是select中管理的fd有ready的了,另一种是等待的tvptr到点儿了,这两种情况select都会返回。

  (2)readfds, writefds, exceptfds:这三参数都是指向file description set的指针,有个专门的数据结构就是fd_set。这三参数形式上类似,以readfds为例,指向的fd_set中“one bit per possible descriptor”,一个bit与一个file descriptor相对应。调用select的时候,这三个参数里面各个bits对应的fd正是select函数管理的对象。具体看下图:

  

  既然这三个参数中每个bit关联上一个fd,那么这种关联是咋建立起来的呢?

  书上的思路非常连贯,马上给出了如下的函数:

  int FD_ISSET(int fd, fd_set *fdset)

  int FD_CLR(int fd, fd_set *fdset)

  int FD_SET(int fd, fd_set *fdset)

  int FD_ZERO(fd_set *fdset)

  上面几个函数,望文生义即可。参照下面的用法:

fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(STDIN_FILENO, &rset);

if (FD_ISSET(fd, &rset))
{
    ...
}

  来来来,分析下上面的代码:

  (a)清零那个就是清零,类似初始化字符串;虽说把这块内存划给你了,让你随便用,但是之前这上面有什么可不一定,所以要清零

  (b)FD_SET(STDIN_FILENO, &rset)这句话有点儿说道。之前不是说一个bit跟fd关联么。这个得回顾一下之前学到的file descriptor的知识,STDIN_FILENO就是0。即,fd_set中监控数值为0的file descriptor,即标准输入。

    这里还得多说一句,0、1、2在file descriptor的背景下已经被定死了,就是代表标准输入,标准输出,标准出错输出。也就是说,rset中0、1、2三个位置早就是给stdin stdout stderr留好的了。如果这个时候process中开启了一个新的fd,那么这个fd按照一般的最小递增原则,会被分配成3(详情得回顾FILE IO这章内容);如果想让select管理值为3的fd,就调用FD_SET就OK了。这样就理解了,其实是fd_set的bit与fd的“”是一一对应的。

  这样就带来一个问题了:select通过fd_set来获得需要他去管理的fd的时候,应该是通过遍历bit的方式来查看哪些bit对应的fd需要去管理。从0开始往后遍历,“”到哪里是头?于是,这就引出了第三个参数。

  (3)maxfdp1:“maximum file descriptor plus 1”,其实看完上面的分析,也就知道这个函数的意义了:

     类似 for (int bit=0; bit<maxfdp1; bit++),这个maxfdp1就起到这个作用。

     只不过,有三个fd_set,maxfdp1的取值要取三个fd_set里面相关的最大的fd值+1。

  下面分析select函数的返回值:

  (1)-1:出错了

  (2)0:表示no descriptors are ready。所有fd_set都清零。

  (3)positive value :返回已经ready的fd总数;同一个fd,既有read的ready,又有write的ready,在返回值的时候算两个。并且,这种情况下所有fd_set中只留下ready的bit。

  通过上面的分析,可以得出结论,调用一次select只能管一次的fd_set管理。

  最后再说,说等着fd_set中的fd状态是ready,这个ready到底是啥意思呢?

  (1)对于fd的read和write来说,各自的操作都不被block就算ready了

  (2)对于fd的except来说,真的出现异常就算是ready了

  2. poll Function

  int poll(struct pollfd fdarray[], nfds_t nfds, int timeout)

  分析三个参数:

  fdarray:  

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

  fd表示fd;events表示监听fd哪些相关的事件;revents表示fd哪些事件发生了。就OK。

  这时候,如果既想监听一个fd的read又想监听该fd的write,就该在fdarray中用两个elements来表示。通过这个体会一下select和poll的参数设计思路,类比数据库设计思路,select更像是列表,而poll更像是用行表。

 nfds:类比select的maxfdp1参数

 timeout : 类比select的tvptr参数

   

  

14.5 Asynchronous I/O

  异步IO比较复杂,也有可能各种问题,但是现实中还是有大量其应用的场景。

  POSIX标准给了一套异步I/O的使用套路。

  1. 介绍了AIO  control blocks这样的数据结构,来设定异步I/O的各种属性

struct aiocb{
   int aio_fildes; /*file descriptor*/
   off_t aio_offest; /*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*/    
};

  各种含义都在里面了,有几点书上提醒要注意的:

  (1)aio_reqprio这种优先级只是建议性的,并不是强制性的;最终谁排前面后面,主要由系统算法决定

  (2)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 signal); /* notify funciton*/
    ptrhead_attr_t *sigev_notify_attributes; /* notify attrs*/
};

  主要根据sigev_notify,分为三种处理方式:

    a. SIGEV_NONE : 啥都不干

    b. SIGEV_SIGNAL :产生sigev_signo信号,并且把sigev_value传给设定的signal handler函数

    c. SIGEV_THREAD:用detached thread的方式去处理signal

  2. aio_read aio_write 异步读写函数

    int aio_read(struct aiocb *aiocb);

    int aio_write(struct aiocb *aiocb);

    就是异步读写,提交上去就OK了,不用阻塞等着。

    注意,参数传入的是指向aiocb的一个结构体指针。在异步IO操作的过程中,要保证传入的aiocb的结构体不能被改动了,因为异步IO操作会一直用到这个结构体的内容;直到异步IO执行完成了,才允许修改。异步IO如何判断执行完成了,下面会说。

  3. aio_error 和 aio_return 函数

    int aio_error(const struct aiocb *aiocb);

    int aio_return(const struct aiocb *aiocb);

    这俩函数搭伙使用 判断异步IO执行到啥状态了。

    (1)aio_error函数的四个返回值 {0, -1, EINPROGRESS, else}表示不同的状态(P513有具体的解释

    (2)aio_return函数根据aiocb对应的write read fsync不同操作,返回相应的结果(比如write,就返回写入了多少个byte)

       关于aio_return有两点要注意:

        a. 慎用这个函数,一旦用了这个函数,传入的aiocb参数占用的memory就可以被操作系统拿去干其他的事情了

        b. aio_return返回成功了,也不意味着内容持久化到磁盘上了,只能说明任务都成功提交到queue中了

    这俩函数针对一个异步IO的状态的。实际中很可能一堆aio等着去判断状态,就可以用下面的函数。

  4. aig_suspend函数

    int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout)

    这个函数应用的场景就是:有一堆异步IO都在执行中,如果有其中有一个异步IO完成了,就需要进行响应处理。

    这是一个阻塞函数,三种情况可以让这个函数解除阻塞:

    (1)调用这个函数所在的process被signal打断了

    (2)超时了,此时errno被设置为EAGAIN

    (3)any of aio完成了

  5. aio_fsync函数

    int aio_fsync(int op, struct aiocb *aiocb)

    这是一个全局催促函数,也是个阻塞函数:

     (1)op标示催促行为类型:可以设为{O_DSYNC, O_SYNC},含义分别是{催数据部分完成,催全部完成}

     (2)aiocb标示催哪个aio

  下面还有两个函数(书上给的代码例子中没有涉及,但是还是记一下看过的内容)

  6. aio_cancel函数

    int aio_cancel(int fd, struct aiocb *aiocb)

    作用是取消对fd的异步操作aiocb。这个函数也不是强制性的,只能是尝试取消,最后能不能取消成功,结果不一定。

  7. lio_listio函数

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

    函数的作用是提交多个异步io请求。

    (1)mode : 是否需要异步,取值{LIO_WAIT, LIO_NOWAIT}。这里我的理解是,这个lio_listio本身可以是通过mode来设定是需要阻塞,等着里面的aio有结果了,还是不需要等着,提交上去就完事儿了。但是list参数中的io必须是aio。

    (2)lsit : 需要提交的aio集合,这时候每个aio中的aio_lio_opcode属性就起作用了,意思是告诉list,我这个aio个体是读、写还是其他的。为什么在list中要告诉,而在单独使用时候,这个属性就没用了呢?因为在单独使用的时候,是通过aio_read aio_write aio_fsync这样具体的函数执行的,意义自然就明确了

    (3)sigev:当list中所有的aio都完成了,就会发出一个sigev信号

  这一部分给出了一个比较综合的例子:用同步IO和异步IO分别实现文件copy的功能,并在copy的同时把每个字符做一个变换

  代码1:

 1 #include "apue.h"
 2 #include <ctype.h>
 3 #include <fcntl.h>
 4 
 5 #define BSZ 4096
 6 
 7 unsigned char buf[BSZ];
 8 
 9 unsigned char translate(unsigned char c)
10 {
11     if (isalpha(c)) { 
12         if (c>='n') { 
13             c -= 13; 
14         }
15         else if (c>='a') { 
16             c += 13; 
17         } 
18         else if (c>='w') { 
19             c -= 13; 
20         } 
21         else { 
22             c += 13; 
23         } 
24     } 
25     return(c);
26 }
27 
28 int main(int argc, char *argv[])
29 {
30     int ifd, ofd, i, n, nw;
31     if (argc != 3) { 
32         err_quit("usage: rot13 infile outfile"); 
33     } 
34     if ((ifd = open(argv[1], O_RDONLY))<0) { 
35         err_sys("can't open %s", argv[1]); 
36     } 
37     if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE))<0) { 
38         err_sys("can't create %s", argv[2]); 
39     } 
40     while ((n = read(ifd, buf, BSZ))>0) { 
41         for ( i=0; i<n; i++)
42            buf[i] = translate(buf[i]);
43         if ((nw = write(ofd, buf, n))!=n) { 
44             if (nw <0) { 
45                 err_sys("write failed"); 
46             } 
47             else { 
48                 err_quit("short write (%d/%d)", nw ,n); 
49             } 
50         }
51     } 
52     fsync(ofd);
53     exit(0);
54 }

  代码2:

  1 #include "apue.h"
  2 #include <ctype.h>
  3 #include <fcntl.h>
  4 #include <aio.h>
  5 #include <errno.h>
  6 
  7 #define BSZ 32768
  8 #define NBUF 8
  9 
 10 enum rwop{
 11     UNUSED = 0,
 12     READ_PENDING = 1,
 13     WRITE_PENDING = 2
 14 };
 15 
 16 struct buf{
 17     enum rwop op;
 18     int last;
 19     struct aiocb aiocb;
 20     unsigned char data[BSZ];
 21 };
 22 
 23 struct buf bufs[BSZ];
 24 
 25 
 26 unsigned char translate(unsigned char c)
 27 {
 28     if (isalpha(c)) { 
 29         if (c>='n') { 
 30             c -= 13; 
 31         }
 32         else if (c>='a') { 
 33             c += 13; 
 34         } 
 35         else if (c>='w') { 
 36             c -= 13; 
 37         } 
 38         else { 
 39             c += 13; 
 40         } 
 41     } 
 42     return(c);
 43 }
 44 
 45 int main(int argc, char *argv[])
 46 {
 47     int ifd, ofd, i, j, n, err, numop;
 48     struct stat sbuf;
 49     const struct aiocb *aiolist[NBUF];
 50     off_t off = 0; /*用于标示input文件读到哪里了*/
 51 
 52     ifd = open(argv[1], O_RDONLY);
 53     ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE);
 54     fstat(ifd, &sbuf); /*获得要读取文件的信息*/
 55 
 56     /*初始化buffers*/
 57     for ( i=0; i<NBUF; i++)
 58     {
 59         bufs[i].op = UNUSED; /*标志该buf是否被用上了*/
 60         bufs[i].aiocb.aio_buf = bufs[i].data; /*给aio指定一个buf空间*/
 61         bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE; /*不响应signal*/
 62         aiolist[i] = NULL; /*aiolist清空*/
 63     }
 64 
 65     numop = 0;
 66     while (1) 
 67     { 
 68         for (i=0; i<NBUF; i++) /*遍历各个bufs*/
 69         {
 70             switch(bufs[i].op)
 71             {
 72                 case UNUSED: /*这个buf目前没被使用*/
 73                     if (off<sbuf.st_size) { /*文件还有内容没有读完*/
 74                         bufs[i].op = READ_PENDING; /*进行状态转换*/
 75                         bufs[i].aiocb.aio_fildes = ifd; /*将第i个buf的file descriptor与ifd关联上*/
 76                         bufs[i].aiocb.aio_offset = off; /*将第i个buf的偏移量设置为off*/
 77                         off += BSZ; /*由于buf一次读BSZ这么多byte, 所以在这里将off向后移动BSZ个位置*/
 78                         if (off >= sbuf.st_size) { /*如果读完了这次之后 已经读完了*/
 79                             bufs[i].last = 1; /*猜测这个last就是标示执行完最后一次read的动作*/
 80                         } 
 81                         bufs[i].aiocb.aio_nbytes = BSZ; /*告诉aiocb就是读了BSZ这么多字符*/
 82                         if (aio_read(&bufs[i].aiocb)<0) { /**这一步真正执行了异步的read操作**/
 83                             err_sys("aio_read faile"); 
 84                         } 
 85                         aiolist[i] = &bufs[i].aiocb; /*告诉第i个buf用上了, 并且是什么也告诉了*/
 86                         numop++; /*正在执行任务的op数量加1*/
 87                     } 
 88                     break;
 89                 case READ_PENDING: /*这个buf正读着呢*/
 90                     if ((err = aio_error(&bufs[i].aiocb))==EINPROGRESS) { /*获取异步read的执行状态 如果正读一半呢, 则继续往下读*/
 91                         continue; 
 92                     } 
 93                     if (err!=0) { /*异步read没执行成功的情况*/
 94                         if (err==-1) { /*aio_error执行失败了*/
 95                             err_sys("aio_error failed"); 
 96                         }
 97                         else { /*异步read操作失败了*/ 
 98                             err_exit(err, "read failed"); 
 99                         } 
100                     } 
101                     if ((n=aio_return(&bufs[i].aiocb))<0) { /*能执行到这 说明异步read可能执行成功了 最起码执行完了 所以在这里用aio_return函数来看一下读进来多少个字符*/ 
102                         err_sys("aio_return failed"); 
103                     } 
104                     if (n!=BSZ && !bufs[i].last  ) { /*没读满BSZ 还不是最后一个 必然是读的过程中读少了 出问题了*/
105                         err_quit("short read (%d/%d)", n ,BSZ); /*没读全 到底读了多少*/
106                     } 
107                     /*能执行到这里 证明已经顺利读进来了 而且读全了*/
108                     for (j=0; j<n; j++) /*执行字符转换*/
109                         bufs[i].data[j] = translate(bufs[i].data[j]);
110                     bufs[i].op = WRITE_PENDING; /*进行角色转换 读 和 转换 都已经完成了 该写了*/
111                     bufs[i].aiocb.aio_fildes = ofd; /*fd换成输出文件的*/
112                     bufs[i].aiocb.aio_nbytes = n;
113                     if (aio_write(&bufs[i].aiocb)<0) { /**执行异步write操作**/
114                         err_sys("aio_write failed"); 
115                     } 
116                     break;
117                 case WRITE_PENDING: /*如果buf正在写状态*/
118                     if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) { /*没写完呢 等着*/
119                         continue; 
120                     } 
121                     if (err!=0) { 
122                         if (err == -1) { /*aio_return本身执行失败*/
123                             err_sys("aio_error failed"); 
124                         }  
125                         else { /*aio_return本身执行成功 但aio_write执行失败*/
126                             err_exit(err, "write failed"); 
127                         } 
128                     } 
129                     if ((n=aio_return(&bufs[i].aiocb))<0) { /*异步write可能执行成功 查看写入了多少byte*/
130                         err_sys("aio_return failed"); 
131                     } 
132                     if (n!=bufs[i].aiocb.aio_nbytes) { /*如果写入byte不等于原来读进来的bytes数*/
133                         err_quit("short write (%d/%d)", n, BSZ); /*看到底写了百分之多少*/
134                     } 
135                     aiolist[i] = NULL; /*现在aiolist[i]这个buf已经完成了他的一次读写操作 被制空*/
136                     bufs[i].op = UNUSED; /*执行状态转换 完成读和写的操作之后 又可以让这个bufs变成可用的*/
137                     numop--; /*少了一个正在执行任务的op*/
138                     break;
139             }
140         }
141         if (numop == 0) { /*没有正在执行读写任务的op了*/
142             if (off >= sbuf.st_size) { /*已经完成全部读的任务了 可以退出while循环了*/
143                 break; 
144             }  
145         } 
146         else { /*等着至少一个op执行完再往下进行 s这样做的策略是: suspend是不耗费cpu的 一旦suspend有结果了 就可以保证上面的for循环至少能命中一个改变状态的op 减少了无谓的占用cpu的轮询次数*/
147             if (aio_suspend(aiolist, NBUF, NULL)<0) { 
148                 err_sys("aio_suspend failed"); 
149             }  
150         } 
151     }
152     bufs[0].aiocb.aio_fildes = ofd; /*这个时候所有的bufs都是可用的 只不过挑第0个buf用一下*/
153     if (aio_fsync(O_SYNC, &bufs[0].aiocb)<0) { /*催促一下所有正在往ofd中异步write操作字符都写进去*/
154         err_sys("aio_fsync failed"); 
155     } 
156     exit(0);
157 }

   两份代码的执行结果如下(首先执行代码1 再执行代码2)

   

   上述的同步IO代码思路比较简单;异步IO的代码用了“有限状态机”的设计思路(以后再体会)。

   同步IO是一个buffer,一套read和write;异步IO用了8个buffer,八套read和write。

   我测试用的文件是500M的文件,上面代码的意义在于体会异步IO的设计思路:复杂但是一些情况可能会好用,先学着。

 

14.6 readv & writev函数

  ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

  ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

  集成化的read和write。

  书上给了这样的例子:一堆文件,一堆buffer,往一个地方写,怎么写?

  主要考虑两种情况:

  (1)把数据都copy到一个buffer里面,尽量少的调用write

      这种情况主要耗时的地方是copy数据,好处是少调用system call(write)

  (2)把各个buffer的信息都推到writev里面,一起提交

      这种情况的主要耗时的地方是要多一些system call(write),好处是少一些copy

  copy 和 system call 这二者之间的tradeoff决定哪种方式的效率高。

 

14.7 readn & writen 函数

  这俩函数是apue自定义的函数,主要功能是:读一次不行就自动多读几次,直到读完为止。后面chapter 20.8会用到。

 

14.8 Memory-Mapped I/O 

  void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off)

  把磁盘文件与一段内存buffer关联起来,目的是可以让系统把对buffer的操作转换为对磁盘文件的操作。

  参数的含义:

  addr:内存中的地址,用于map的起点。如果设成0,则由系统来分配;如果不是0,好像不太好办。

  len :划出来多长的byte用于memory-mapped IO

  fd:跟哪个磁盘文件关联(这个fd对应的磁盘文件必须是open的

  prot:能对内存中留出来map的那块区域的操作权限{PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE}

  off:需要映射的磁盘文件的offset位置,即从哪开始的

  flag:设定memory与磁盘文件的关联模式 {MAP_FIXED, MAX_SHARED, MAX_PRIVATE},即对memory的操作怎么作用于磁盘文件上

  这里还有一个地方需要注意的,addr和off一般都是system's virtual memory page size的整数倍。这个不太好理解,书上给了一个例子:比如disk file的大小是12bytes,而system's page size是512bytes;则一般来说,系统会给分配一个512bytes的mapped region,只不过后500bytes都是0;一旦我们作死去修改后面500bytes的内容,无论怎么作死,mapped region的改变都不会关联到disk file上。

  再看一张整体的示意图,加深印象:

   

  另外,有两个signal与mmap关系比较紧密:

  SIGSEGV 和 SIGBUS

  先单独说一下这两个signal的含义:

    (1)SIGSEGV:指针对应的地址无效,即没有物理内存对应,也无法访问,触发signal

    (2)SIGBUS:指针对应的地址有效,即物理内存存在,但是由于数据格式没有对齐,触发signal

  书上只说了SIGBUS什么情况会出现:

    比如,我们之前map了一个disk file,结果这个disk file的内容被其他process给truncate了;这个时候,我们再去访问memory-mapped region中被truncate掉的那个部分,就会触发SIGBUS信号。后面的示例代码中,我自己增加了这个部分。

  还有一个函数:

  int munmap(void *addr, size_t len)

  作用就是解除memory-mapped region与disk file的关系。第一个参数addr就是mmap函数返回的;第二个就是要接触关联的长度。

  下面看一个综合例子,这个例子功能就是把利用memory-mapped I/O技术实现了文件拷贝。

 1 #include "apue.h"
 2 #include <fcntl.h>
 3 #include <sys/mman.h>
 4 #include <signal.h> 
 5 
 6 #define COPYINCR (1024*1024*1024) /*1 GB*/
 7 
 8 void sig_bus(int signo)
 9 {
10     printf("catch SIGBUS signal\n");
11     fflush(stdin);
12     return;
13 }
14 
15 int main(int argc, char *argv[])
16 {
17     int fdin, fdout;
18     void *src, *dst;
19     size_t copysz;
20     struct stat sbuf;
21     off_t fsz = 0;
22     signal(SIGBUS, sig_bus);
23     //signal(SIGSEGV, sig_segv);
24 
25     fdin = open(argv[1], O_RDONLY);
26     fdout = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE);
27     fstat(fdin, &sbuf);
28     //ftruncate(fdout, sbuf.st_size); /*设定output file 与input file长度一样*/
29 
30     /*通过内存映射的方法 来完成两个大文件的copy*/
31     while (fsz<sbuf.st_size) { 
32         if ((sbuf.st_size - fsz)>COPYINCR) { /*最多控制在1GB*/ 
33             copysz = COPYINCR; 
34         } else { 
35             copysz = sbuf.st_size - fsz; /*不足1GB 就把input文件剩下的都囊括进来*/ 
36         }
37         if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz))==MAP_FAILED) { /*把input文件给映射进去*/
38             err_sys("mmap error for input"); 
39         }
40         if ((dst = mmap(0, copysz, PROT_READ|PROT_WRITE, MAP_SHARED, fdout, fsz)) == MAP_FAILED) { /*把output文件给映射出去*/
41             err_sys("mmap error for output"); 
42         }
43         memcpy(dst, src, copysz); /*内存拷贝*/
44         munmap(src, copysz); /*解除input文件与内存的关联*/
45         munmap(dst, copysz); /*解除output文件与内存的关联*/
46         fsz += copysz;
47     }
48     exit(0);
49 }

    代码执行结果如下:

    

    我们再用系统cp命令执行一遍:

    

    之前还提到过,某种情况下SIGBUS信号可能被出发。现在人为屏蔽掉line28的truncate语句,执行结果如下:

    

    由于o是个新文件长度是0,如果不用truncate命令让o与loadlog_tsinghua.txt的文件长度一样,则执行line40的时候就符合出发SIGBUS的条件了。

    至于实现disk file copy的方式,比较如下两种情况:

    (1)read & write:主要耗时在copy & system call上面

    (2)mmap & memcpy:主要耗时在page fault handling上面

    哪种方法比较好,需要在两种耗时操作上tradeoff,获得合适的结果。

    另,(2)还两个不足:不能处理network和terminal的情况;不能在执行过程中改变文件大小,要不然就SIGBUS或者SIGSEGV信号触发了。

    这个memory-mapped I/O在15.9中会继续提到。

 

14.9 Summary

    这章的内容主要是为后续章节做铺垫了:Nonblocking I/O, Record locking, I/O Multiplexing,Asynchronous I/O, readv & writev, Memory-mapped I/O

    这章的内容中有阻塞与非阻塞、同步与异步,总结一下:

    Nonblocking I/O是说再执行read和write的时候,通过在flag中设置O_NONBLOCKING,不等着真的read或write结束,就直接返回了。(书上P483的figure14.1就是例子)

    Asynchronous I/O是说每个同一个process可以异步执行多个I/O,多input多output(书上P500的figure14.13就是例子)。

    二者总结起来,就是“能说阻塞和非阻塞的,都是同步IO;只有特殊的IO,比如aio才是异步IO”

    哪天糊涂了再去直呼上这个帖子上看看:http://www.zhihu.com/question/19732473

    

    

  

    

posted on 2015-12-02 00:34  承续缘  阅读(847)  评论(1编辑  收藏  举报

导航