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,书上给出了下面的例子(我稍加改造):

 #include "apue.h"
#include <errno.h>
#include <fcntl.h> char buf[]; int main()
{
int ntowrite, nwrite;
char *ptr; ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
fprintf(stderr, "read %d bytes\n", ntowrite); set_fl(STDOUT_FILENO, O_NONBLOCK); //set nonblocking ptr = buf;
while (ntowrite>)
{
errno = ;
nwrite = write(STDOUT_FILENO, ptr, ntowrite);
fprintf(stderr, "nwrite = %d, errno = %d means %s\n", nwrite, errno, strerror(errno)); if (nwrite>) {
ptr += nwrite;
ntowrite -= nwrite;
}
}
clr_fl(STDOUT_FILENO, O_NONBLOCK); //clear nonblocking
exit();
}

  代码执行结果如下:

  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的锁。这样就互相锁住了。代码如下:

 #include "apue.h"
#include <fcntl.h> static void lockabyte(const char *name, int fd, off_t offset)
{
if (writew_lock(fd, offset, SEEK_SET,)<) {
err_sys("%s: writew_lock error", name);
}
printf("%s: got the lock, byte %lld\n", name, (long long)offset);
} int main()
{
int fd;
pid_t pid; /*create a file and write two bytes to it*/
fd = creat("tmplock", FILE_MODE);
write(fd, "ab",); TELL_WAIT();
if ((pid = fork())<) {
err_sys("fork error");
}
else if (pid==) {
lockabyte("child", fd, );
TELL_PARENT(getppid());
WAIT_PARENT();
lockabyte("parent", fd, );
}
else {
lockabyte("parent",fd,);
TELL_CHILD(pid);
WAIT_CHILD();
lockabyte("parent", fd, );
}
exit();
}

  代码执行结果如下:

  

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

  是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, , SEEK_SET, ); /*parent write lock byte 0*/
if ((pid=fork())>)
{
fd2 = dup(fd1);
fd3 = open(pathname,...);
}
else if (pid==)
{
read_lock(fd1, , SEEK_SET, ); /*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尾巴上加锁是一个稍微特殊一些的情况

    书上给了个例子:

 writew_lock(fd, , SEEK_END, );
write(fd, buf, );
un_lock(fd, , SEEK_END);
write(fd, buf, );

    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的代码如下:

 #include "apue.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h> int main(int argc, char *argv[])
{
int fd;
pid_t pid;
char buf[];
struct stat statbuf; /*创建一个文件, 往里写几个字符*/
fd = open(argv[], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);
write(fd, "abcdef", ); /*设置set-group-id的bit位, 关闭group-execute的bit位*/
fstat(fd, &statbuf);
fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID); /*apue的自定义函数*/
TELL_WAIT(); if ((pid = fork())<) {
err_sys("fork error");
}
else if (pid>) { /*父进程 加一个write锁 从头开始加到尾*/
if (write_lock(fd, , SEEK_SET, )<) {
err_sys("write_lock error");
}
TELL_CHILD(pid); /*告知child process执行到这里了*/
if (waitpid(pid, NULL, )<) {
err_sys("waipid error");
}
}
else {
WAIT_PARENT(); /*子进程 等着父进程中TELL_CHILD函数发信号 此时父进程已经给这个文件上了一个write lock了*/
set_fl(fd, O_NONBLOCK); /*将fd设为非阻塞的*/ if (read_lock(fd, , SEEK_SET, ) != -) { /*子进程中加read锁 从头加到尾*/
err_sys("child: read_lock succeeded");
}
printf("read_lock of already-locked region returns %d means %s\n",errno,strerror(errno)); /*通过errno验证子进程加read锁是否成功*/
if (lseek(fd, , SEEK_SET)==-) { /*将fd移动到beginning的位置*/
err_sys("lseek error");
}
if (read(fd, buf, )<) { /*从文件中尝试读俩byte 如果能读成功就证明mandatory locking没用 反之则证明起作用了*/
err_ret("read failed (mandatory locking works)");
}
else {
printf("read OK (no mandatory locking), buf = %2.2s\n", buf);
}
}
exit();
}

    执行结果如下:

    

    上述带代码是为了检验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:

 #include "apue.h"
#include <ctype.h>
#include <fcntl.h> #define BSZ 4096 unsigned char buf[BSZ]; unsigned char translate(unsigned char c)
{
if (isalpha(c)) {
if (c>='n') {
c -= ;
}
else if (c>='a') {
c += ;
}
else if (c>='w') {
c -= ;
}
else {
c += ;
}
}
return(c);
} int main(int argc, char *argv[])
{
int ifd, ofd, i, n, nw;
if (argc != ) {
err_quit("usage: rot13 infile outfile");
}
if ((ifd = open(argv[], O_RDONLY))<) {
err_sys("can't open %s", argv[]);
}
if ((ofd = open(argv[], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE))<) {
err_sys("can't create %s", argv[]);
}
while ((n = read(ifd, buf, BSZ))>) {
for ( i=; i<n; i++)
buf[i] = translate(buf[i]);
if ((nw = write(ofd, buf, n))!=n) {
if (nw <) {
err_sys("write failed");
}
else {
err_quit("short write (%d/%d)", nw ,n);
}
}
}
fsync(ofd);
exit();
}

  代码2:

 #include "apue.h"
#include <ctype.h>
#include <fcntl.h>
#include <aio.h>
#include <errno.h> #define BSZ 32768
#define NBUF 8 enum rwop{
UNUSED = ,
READ_PENDING = ,
WRITE_PENDING =
}; struct buf{
enum rwop op;
int last;
struct aiocb aiocb;
unsigned char data[BSZ];
}; struct buf bufs[BSZ]; unsigned char translate(unsigned char c)
{
if (isalpha(c)) {
if (c>='n') {
c -= ;
}
else if (c>='a') {
c += ;
}
else if (c>='w') {
c -= ;
}
else {
c += ;
}
}
return(c);
} int main(int argc, char *argv[])
{
int ifd, ofd, i, j, n, err, numop;
struct stat sbuf;
const struct aiocb *aiolist[NBUF];
off_t off = ; /*用于标示input文件读到哪里了*/ ifd = open(argv[], O_RDONLY);
ofd = open(argv[], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE);
fstat(ifd, &sbuf); /*获得要读取文件的信息*/ /*初始化buffers*/
for ( i=; i<NBUF; i++)
{
bufs[i].op = UNUSED; /*标志该buf是否被用上了*/
bufs[i].aiocb.aio_buf = bufs[i].data; /*给aio指定一个buf空间*/
bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE; /*不响应signal*/
aiolist[i] = NULL; /*aiolist清空*/
} numop = ;
while ()
{
for (i=; i<NBUF; i++) /*遍历各个bufs*/
{
switch(bufs[i].op)
{
case UNUSED: /*这个buf目前没被使用*/
if (off<sbuf.st_size) { /*文件还有内容没有读完*/
bufs[i].op = READ_PENDING; /*进行状态转换*/
bufs[i].aiocb.aio_fildes = ifd; /*将第i个buf的file descriptor与ifd关联上*/
bufs[i].aiocb.aio_offset = off; /*将第i个buf的偏移量设置为off*/
off += BSZ; /*由于buf一次读BSZ这么多byte, 所以在这里将off向后移动BSZ个位置*/
if (off >= sbuf.st_size) { /*如果读完了这次之后 已经读完了*/
bufs[i].last = ; /*猜测这个last就是标示执行完最后一次read的动作*/
}
bufs[i].aiocb.aio_nbytes = BSZ; /*告诉aiocb就是读了BSZ这么多字符*/
if (aio_read(&bufs[i].aiocb)<) { /**这一步真正执行了异步的read操作**/
err_sys("aio_read faile");
}
aiolist[i] = &bufs[i].aiocb; /*告诉第i个buf用上了, 并且是什么也告诉了*/
numop++; /*正在执行任务的op数量加1*/
}
break;
case READ_PENDING: /*这个buf正读着呢*/
if ((err = aio_error(&bufs[i].aiocb))==EINPROGRESS) { /*获取异步read的执行状态 如果正读一半呢, 则继续往下读*/
continue;
}
if (err!=) { /*异步read没执行成功的情况*/
if (err==-) { /*aio_error执行失败了*/
err_sys("aio_error failed");
}
else { /*异步read操作失败了*/
err_exit(err, "read failed");
}
}
if ((n=aio_return(&bufs[i].aiocb))<) { /*能执行到这 说明异步read可能执行成功了 最起码执行完了 所以在这里用aio_return函数来看一下读进来多少个字符*/
err_sys("aio_return failed");
}
if (n!=BSZ && !bufs[i].last ) { /*没读满BSZ 还不是最后一个 必然是读的过程中读少了 出问题了*/
err_quit("short read (%d/%d)", n ,BSZ); /*没读全 到底读了多少*/
}
/*能执行到这里 证明已经顺利读进来了 而且读全了*/
for (j=; j<n; j++) /*执行字符转换*/
bufs[i].data[j] = translate(bufs[i].data[j]);
bufs[i].op = WRITE_PENDING; /*进行角色转换 读 和 转换 都已经完成了 该写了*/
bufs[i].aiocb.aio_fildes = ofd; /*fd换成输出文件的*/
bufs[i].aiocb.aio_nbytes = n;
if (aio_write(&bufs[i].aiocb)<) { /**执行异步write操作**/
err_sys("aio_write failed");
}
break;
case WRITE_PENDING: /*如果buf正在写状态*/
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) { /*没写完呢 等着*/
continue;
}
if (err!=) {
if (err == -) { /*aio_return本身执行失败*/
err_sys("aio_error failed");
}
else { /*aio_return本身执行成功 但aio_write执行失败*/
err_exit(err, "write failed");
}
}
if ((n=aio_return(&bufs[i].aiocb))<) { /*异步write可能执行成功 查看写入了多少byte*/
err_sys("aio_return failed");
}
if (n!=bufs[i].aiocb.aio_nbytes) { /*如果写入byte不等于原来读进来的bytes数*/
err_quit("short write (%d/%d)", n, BSZ); /*看到底写了百分之多少*/
}
aiolist[i] = NULL; /*现在aiolist[i]这个buf已经完成了他的一次读写操作 被制空*/
bufs[i].op = UNUSED; /*执行状态转换 完成读和写的操作之后 又可以让这个bufs变成可用的*/
numop--; /*少了一个正在执行任务的op*/
break;
}
}
if (numop == ) { /*没有正在执行读写任务的op了*/
if (off >= sbuf.st_size) { /*已经完成全部读的任务了 可以退出while循环了*/
break;
}
}
else { /*等着至少一个op执行完再往下进行 s这样做的策略是: suspend是不耗费cpu的 一旦suspend有结果了 就可以保证上面的for循环至少能命中一个改变状态的op 减少了无谓的占用cpu的轮询次数*/
if (aio_suspend(aiolist, NBUF, NULL)<) {
err_sys("aio_suspend failed");
}
}
}
bufs[].aiocb.aio_fildes = ofd; /*这个时候所有的bufs都是可用的 只不过挑第0个buf用一下*/
if (aio_fsync(O_SYNC, &bufs[].aiocb)<) { /*催促一下所有正在往ofd中异步write操作字符都写进去*/
err_sys("aio_fsync failed");
}
exit();
}

   两份代码的执行结果如下(首先执行代码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技术实现了文件拷贝。

 #include "apue.h"
#include <fcntl.h>
#include <sys/mman.h>
#include <signal.h> #define COPYINCR (1024*1024*1024) /*1 GB*/ void sig_bus(int signo)
{
printf("catch SIGBUS signal\n");
fflush(stdin);
return;
} int main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
size_t copysz;
struct stat sbuf;
off_t fsz = ;
signal(SIGBUS, sig_bus);
//signal(SIGSEGV, sig_segv); fdin = open(argv[], O_RDONLY);
fdout = open(argv[], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE);
fstat(fdin, &sbuf);
//ftruncate(fdout, sbuf.st_size); /*设定output file 与input file长度一样*/ /*通过内存映射的方法 来完成两个大文件的copy*/
while (fsz<sbuf.st_size) {
if ((sbuf.st_size - fsz)>COPYINCR) { /*最多控制在1GB*/
copysz = COPYINCR;
} else {
copysz = sbuf.st_size - fsz; /*不足1GB 就把input文件剩下的都囊括进来*/
}
if ((src = mmap(, copysz, PROT_READ, MAP_SHARED, fdin, fsz))==MAP_FAILED) { /*把input文件给映射进去*/
err_sys("mmap error for input");
}
if ((dst = mmap(, copysz, PROT_READ|PROT_WRITE, MAP_SHARED, fdout, fsz)) == MAP_FAILED) { /*把output文件给映射出去*/
err_sys("mmap error for output");
}
memcpy(dst, src, copysz); /*内存拷贝*/
munmap(src, copysz); /*解除input文件与内存的关联*/
munmap(dst, copysz); /*解除output文件与内存的关联*/
fsz += copysz;
}
exit();
}

    代码执行结果如下:

    

    我们再用系统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

    

    

  

    

【APUE】Chapter14 Advanced I/O的更多相关文章

  1. 【APUE】Chapter17 Advanced IPC & sign extension & 结构体内存对齐

    17.1 Introduction 这一章主要讲了UNIX Domain Sockets这样的进程间通讯方式,并列举了具体的几个例子. 17.2 UNIX Domain Sockets 这是一种特殊s ...

  2. 【APUE】Chapter10 Signals

    Signal主要分两大部分: A. 什么是Signal,有哪些Signal,都是干什么使的. B. 列举了非常多不正确(不可靠)的处理Signal的方式,以及怎么样设计来避免这些错误出现. 10.2 ...

  3. 【APUE】Chapter15 Interprocess Communication

    15.1 Introduction 这部分太多概念我不了解.只看懂了最后一段,进程间通信(IPC)内容被组织成了三个部分: (1)classical IPC : pipes, FIFOs, messa ...

  4. 【APUE】Chapter16 Network IPC: Sockets & makefile写法学习

    16.1 Introduction Chapter15讲的是同一个machine之间不同进程的通信,这一章内容是不同machine之间通过network通信,切入点是socket. 16.2 Sock ...

  5. 【APUE】Chapter5 Standard I/O Library

    5.1 Introduction 这章介绍的standard I/O都是ISOC标准的.用这些standard I/O可以不用考虑一些buffer allocation.I/O optimal-siz ...

  6. 【APUE】Chapter4 File and Directories

    4.1 Introduction unix的文件.目录都被当成文件来看待(vi也可以编辑目录):我猜这样把一起内容都当成文件的原因是便于统一管理权限这类的内容 4.2 stat, fstat, fst ...

  7. 【APUE】Chapter3 File I/O

    这章主要讲了几类unbuffered I/O函数的用法和设计思路. 3.2 File Descriptors fd本质上是非负整数,当我们执行open或create的时候,kernel向进程返回一个f ...

  8. 【APUE】Chapter1 UNIX System Overview

    这章内容就是“provides a whirlwind tour of the UNIX System from a programmer's perspective”. 其实在看这章内容的时候,已经 ...

  9. 【APUE】Chapter13 Daemon Processes

    这章节内容比较紧凑,主要有5部分: 1. 守护进程的特点 2. 守护进程的构造步骤及原理. 3. 守护进程示例:系统日志守护进程服务syslogd的相关函数. 4. Singe-Instance 守护 ...

随机推荐

  1. CF549BLooksery Party题解

    题目描述 The Looksery company, consisting of nn staff members, is planning another big party. Every empl ...

  2. c语言描述的静态查找表

    顺序表的查找: 直接循环依次和目标比较就行 有序表的查找(二分查找): int search(SS *T,Type key){ int mid; ; int high=T.length; while( ...

  3. python学习笔记--变量和运算符

    一.变量命名规则 1.字母.数字.下划线组成 2.不以数字开头 3.关键字(也叫保留字),不能用作变量名 4.遵循PEP8命名规范 二.变量赋值 1.赋值符号 = 2.多重赋值 x=y=123 3.多 ...

  4. Angularjs基础(一)

    (一) 模型——视图——控制器 端对端的解决方案,AngularJS 试图成为WEB 应用中的一种段对端的解决方案.AngylarJS 的出众 之处如下:数据绑定,基本模板标识符,表单验证,路由,深度 ...

  5. HTML5新标签的兼容性处理

    普通浏览器 普通不支持HTML5新标签的浏览器 -- 能正常解析,但会当初成 inline 元素对待 在不支持HTML5新标签的浏览器里,会将这些新的标签解析成行内元素(inline)对待,所以我们只 ...

  6. Codeforces Round #491 (Div. 2)部分题解

    这场比赛好鬼畜啊,,A题写崩了wa了4遍,心态直接爆炸,本来想弃疗了,结果发现BCD都是傻逼题.. A. If at first you don't succeed...(容斥原理) 题目大意: 有$ ...

  7. hibernate笔记1

    1.  数据库中的表关系 一对一.一对多(多对一).多对一 2.  如何确立表中的表关系 一对多的关系如何实现:使用外键约束,一的方称为主表,多的方称为从表. 外键:从表中有一列,该列的取值除了nul ...

  8. CentOS下删除MySql

    1.查找以前是否装有mysql rpm -qa | grep -i mysql 显示之前安装了: MySQL-client-5.5.49-1.linux2.6.i386 MySQL-server-5. ...

  9. 什么是token及怎样生成token

    什么是token Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即 ...

  10. Hue联合(hdfs yarn hive) 后续......................

    1.启动hdfs,yarn start-all.sh 2.启动hive $ bin/hive $ bin/hive --service metastore & $ bin/hive --ser ...