20155219 《信息安全系统设计基础》第十三周学习总结

我认为第十章是最重要的

首先我为什么认为这一章很重要

Linux/unix IO的系统调用函数很简单,它只有5个函数:open(打开)、close(关闭)、read(读)、write(写)、lseek(定位)。但是系统IO调用开销比较大,一般不会直接调用,而是通过调用Rio包进行健壮地读和写,或者调用C语言的标准I/O进行读写。尽管如此,Rio包和标准IO也都是封装了unix I/O的,所以学习系统IO的调用才能更好地理解高级IO的原理。

同时了解Unix I/O可以帮你理解其他的系统概念。比如进程、存储器层次结构、链接和加载等。

有时候除了使用Unix I/O 以外别无选择。有些重要情况下,使用高级I/O函数不能实现想要的功能,比如标准I/O库没有提供读取文件元数据的方式,比如文件大小或文件创建时间等。

对本章的深入学习

带着问题对本章进行学习:

1.什么是RIO包健壮地读写

Robust I\O健壮的I\O包,是为自动处理不足值情况下构造的。

首先有rio_t这个结构体,是一个读缓冲区的格式:

#define RIO_BUFSIZE     4096
typedefstruct
{
    int rio_fd;      //与缓冲区绑定的文件描述符的编号
    int rio_cnt;        //缓冲区中还未读取的字节数
    char *rio_bufptr;   //当前下一个未读取字符的地址
    char rio_buf[RIO_BUFSIZE];
}rio_t;

这个是rio的数据结构,通过rio_readinitb(rio_t *, int)可以将文件描述符与rio数据结构绑定起来。注意到这里的rio_buf的大小是4096,为linux中文件的块大小。

  • RIO函数分两类:

  1. 无缓冲的输入输出函数:对网络中读写二进制数据尤其有用;

假设我们要编写一个程序来计算文本文件中文件行的数量该如何实现?

一种方法是read函数一次读取一个字节,检查是否有换行符。
缺点是:效率不高,因为每读一个字节就要陷入一次内核。

更好地方法是调用一个包装函数(rio_readlineb),从一个内部读缓冲区拷贝一个文本行,当缓冲区为空时,再自动调用read重新填满缓冲区。

下面这个rio_writen不需要写缓冲。

ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
   size_t nleft = n;
   ssize_t nwritten;
    char *bufp = (char *)usrbuf;
 
    while(nleft > 0)
    {
       if((nwritten = write(fd, bufp, nleft))<= 0)
       {
           if(errno == EINTR)
                nwritten = 0;
           else
                return -1;
       }
       bufp += nwritten;
       nleft -= nwritten;
    }
 
    return n;
}

因为,比如说,我们在写一个http的请求报文,然后将这个报文写入了对应socket的文件描述符的缓冲区,假设缓冲区大小为8K,该请求报文大小为1K。那么,如果缓冲区被设置为被填满才会自动将其真正写入文件(而且一般也是这样做的),那就是说如果没有提供一个刷新缓冲区的函数手动刷新,我还需要额外发送7K的数据将缓冲区填满,这个请求报文才能真正被写入到socket当中。所以,一般带有缓冲区的函数库都会一个刷新缓冲区的函数,用于将在缓冲区的数据真正写入文件当中,即使缓冲区没有被填满,而这也是C标准库的做法。然而,如果一个程序员一不小心忘记在写入操作完成后手动刷新,那么该数据(请求报文)便一直驻留在缓冲区,而你的进程还在傻傻地等待响应。
因此,上面那个函数的fd就是int型的。


  1. 带缓冲的输入函数:可以高效地从文件中读取文本行和二进制数据。其中带缓冲的RIO输入函数是线程安全的。

ssize_t rio_readn(int fd,void *usrbuf,size_t n);

ssize_t rio_writen(int fd,void *usrbuf,size_t n);

rio_readin()是read()函数的扩展,自动处理了不足值。其实就是不断地调用read()函数,判断该函数的返回值,直到读取完毕碰到EOF。

在读第十章系统级I/O时,书中提到无缓冲的输入输出函数rio_readn(int fd, void *usrbuf, size_t n)和rio_writen(int fd, void *usrbuf, size_t n)
“对将二进制数据读写到网络和从网络读写二进制数据尤其有用”

对同一个描述符,可以任意交错地调用rio_readn和rio_writen。一个问本行的末尾都有一个换行符,那么像读取一个文本中的行数怎么办,使用read读取换行符这个方法不是很妥当,可以调用一个包装函数(rio_readineb),它从一个内部读缓冲区拷贝一个文本行,当缓冲区为空时,会自动地调用read重新填满缓冲区。也就是说,这些函数都是缓冲区操作而言的。

对函数ssize_t rio_readin(int fd, void *usrbuf, size_t n) 进行扩充。

ssize_t rio_readin(int fd, void *usrbuf, size_t n)  
{  
  size_t nleft = n;  
  ssize_t nread;  
  char *bufp = usrbuf;  
    
  while(nleft>0){  
    if((nread = read(fd,bufp,nleft))<0){  
      if (errno == EINTR)  
    nread = 0;  
      else  
    return -1;  
    }  
    else if (nread == 0)  
      break;  
    nleft -= nread;  
    bufp += nread;  
  }  
  return (n-nleft);  
}  

然后书中又提到
“统计文本文件中的行数”
这一问题引出了带缓冲的输入输出函数。

rio_read(rio_t *rp, char *usrbuf, size_t n)

是一个带缓冲的read函数,可以将rio_readn中的read函数换成rio_read函数就将可以处理不足值的不带缓冲的rio_readn(int fd, void usrbuf, size_t n)函数转化成带有缓冲的。无缓冲的rio_readn可以用在网络中,带缓冲的rio_readn应该不可用在网络中(这是我的猜测,是不是有无缓冲是一个可不可以用在网络中的标准*)。

如果带缓冲的rio_readn不在网络情况下使用,则如果要读的字节数少于缓冲大小,则从缓冲中读取应读的字节数即可;如果要读的字节数大于缓冲容量,则需要再填充一次缓冲,从这个角度看,带缓冲的rio_readn没有比不带缓冲的rio_readn有任何优势。

缓冲的优势是将fd中的数据读入缓冲区,然后可以从缓冲区再读这些数据就成为了一个指针移动的操作。这对每次读取一个字节的一些应用非常有好处,因为如果每次读取一个字节都调用read()时,都要陷入内核。所以带缓冲的rio_read非常高效。


2.带缓冲I/O和不带缓冲I/O有什么区别?

所谓不带缓冲,并不是内核不提供缓冲,系统内核对磁盘的读写都会提供一个块缓冲(也有人称为内核高速缓存),当调用一次read或write函数,直接进行系统调用,将数据写入到块缓冲进行排队。因此所谓的不带缓冲的I/O是指进程不提供缓冲功能,内核还是提供缓冲的。

而带缓冲的I/O是指进程对输入输出流进行了改进,比如调用标准I/O库函数往磁盘写数据时,标准IO库提供了一个流缓冲,先把数据写入流缓冲区中,当达到一定条件,比如流缓冲区满了或者手动刷新了流缓冲,这时候才会把数据一次送往内核提供的缓冲,再经块缓冲写入磁盘。

因此,带缓冲I/O一般会比不带缓冲I/O调用系统调用的次数要少。

ssize_t write(int filedes, const void *buff, size_t nbytes) 
size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *fp)

拿这两个函数来说,首先要清楚,所谓的带缓冲并不是指上面两个函数的buff参数。

现在假设内核设的缓存是100字节,如果你使用write,且buff的size是10字节,当你要把10个同样的buff写到文件时,需要调用10次write,也就是10次系统调用,此时因为延迟写的技术,并没有写到硬盘,如果想立即写入硬盘,需调用fsync。(涉及写操作机制几个概念,同步写机制、延迟写机制、异步写机制,此处不说了,可以查一下)

标准I/O,也就是带缓存的IO,也称为用户态的缓存,区别于内核所设的缓存。假设缓存长度为50字节,把100字节的数据写到文件,只需2次系统调用,因为先把数据写到流缓存,当其满或者手动刷新之后才填入内核缓存,所以2次就够了。

至于究竟写到了文件中还是内核缓冲区中,对于进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的。
C标准库这类缓冲区不具有这一特性,因为进程的用户空间是独立的。

利用上述函数读出文档中的所有数据。

#include <stdio.h>  
#include <string.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <fcntl.h>  
#include <sys/stat.h>  
  
#define MAXLINE 1024  
  
/*unbuffer input/output function*/  
  
ssize_t rio_readn(int fd,void *usrbuf,size_t n)  
{  
    size_t nleft = n;  
    ssize_t nread;  
    char *bufp = usrbuf;  
  
    while(nleft > 0){  
        if((nread = read(fd,bufp,nleft)) < 0){  
            if(errno == EINTR){/*interrupted by sig handler return*/  
                nread = 0;  
            }else{  
                return -1;/*error*/  
            }  
        }else if(nread == 0){  
            break;  /*EOF*/  
        }else{/*read content*/  
            nleft -= nread;  
            bufp += nread;  
        }  
    }  
    return (n - nleft);  
}  
  
ssize_t rio_writen(int fd,void *usrbuf,size_t n)  
{  
    size_t nleft = n;  
    ssize_t nwritten;  
    char *bufp = usrbuf;  
  
    while(nwritten = write(fd,bufp,nleft) <= 0){  
        if(errno == EINTR){  
            nwritten = 0;  
        }else{  
            return -1;  
        }  
        nleft -= nwritten;  
        bufp += nwritten;  
    }  
    return n;  
}  
/******************************************************************************/  
#define RIO_BUFSIZE 8192  
typedef struct{  
    int rio_fd; /*To operate the file descriptor*/  
    int rio_cnt;/*unread bytes in internal buf*/  
    char *rio_bufptr;/*next unread byte int internal buf*/  
    char rio_buf[RIO_BUFSIZE];/*internal buf*/  
}rio_t;  
void rio_readinitb(rio_t *rp,int fd)  
{  
    rp->rio_fd = fd;  
    rp->rio_cnt = 0;  
    rp->rio_bufptr = rp->rio_buf;  
}  
static ssize_t rio_read(rio_t *rp,char *usrbuf,size_t n)  
{  
    int cnt;  
    while(rp->rio_cnt <= 0){/*Read the file content if buf is empty*/  
        rp->rio_cnt = read(rp->rio_fd, rp->rio_buf,sizeof(rp->rio_buf));  
        if(rp->rio_cnt < 0){  
            if(errno != EINTR){  
                return -1;  
            }  
        }else if(rp->rio_cnt == 0){/*EOF*/  
            return 0;  
        }else {/*reset buf ptr*/  
            rp->rio_bufptr = rp->rio_buf;  
        }  
    }  
    /*when n < rp->rio_cnt, need copy some times */  
    cnt = n;  
    if(rp->rio_cnt < n){/*one time copy end*/  
        cnt = rp->rio_cnt;  
    }  
    memcpy(usrbuf,rp->rio_bufptr,cnt);  
    rp->rio_bufptr += cnt;  
    rp->rio_cnt -= cnt;  
    return cnt;  
}  
ssize_t rio_readlineb(rio_t *rp, void *usrbuf,size_t maxlen)  
{  
    int n,rc;  
    char c,*bufp = usrbuf;  
    for(n = 1; n < maxlen; n++){  
        if (( rc = rio_read(rp,&c,1)) == 1){  
            *bufp++ = c;  
            if(c == '\n'){  
                break;  
            }  
        }else if (rc == 0){  
            if(n == 1){/*EOF no data read*/  
                return 0;  
            }else{/*EOF some data read*/  
                break;  
            }  
        }else{/*ERROR*/  
            return -1;  
        }  
    }  
    *bufp = 0;/*string end sign :'\0'*/  
    return n;  
}  
  
ssize_t rio_readnb(rio_t *rp,void *usrbuf,size_t n)  
{  
    size_t nleft = n;  
    ssize_t nread;  
    char *bufp = usrbuf;  
  
    while(nleft > 0){  
        if((nread = rio_read(rp,bufp, nleft)) < 0){  
            if(errno == EINTR){/*interrupted by sig handler return*/  
                nread =0;  
            }else{/*errno set by read() */  
                return -1;  
            }  
        }else if(nread == 0){/*EOF*/  
            break;  
        }  
        nleft -= nread;  
        bufp += nread;  
    }  
    return (n-nleft);/*return >=0*/  
}  
  
int main()  
{  
    int n;  
    rio_t rio;  
    char buf[MAXLINE];  
  
    int fd = open("1.txt",O_RDONLY,755);  
    if(fd <=0){  
        printf("error\n");  
    }  
    rio_readinitb(&rio,fd);  
    while ((n = rio_readlineb(&rio,buf,MAXLINE)) != 0){  
           rio_writen(1,buf,n);  
    }  
    close(fd);  
    return 0;  
}  

如图2

3.ssize_t和size_t有些什么区别?

答:size_t 被定义为 unsigned int,而ssize_t(有符号的大小)被定义为 int。read函数返回一个有符号的大小,而不是一个无符号的大小,是因为出错返回-1,这使得read的最大值减小了一半,从4G减小到了2G。

4.什么是“不足值”?

答:“不足值”的情况:指的是某些情况下,read和write传送的字节比应用程序要求的要少,这些不足值不表示有错误。
造成这种情况的原因有:

1.读时遇到EOF。要求的字节数超过了读缓冲区内未读的字节的数量。

2.从终端读取文本行。如果打开文件是与终端想关联的(比如键盘和显示器),那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小。

3.读和写socket。内部缓冲约束和较长的网络延迟会引起read和write返回不足值。

5.做网络应用程序应该用什么I/O函数?

答:Unix对网络的抽象是一种称为套接字的文件类型,也是文件描述符。标准I/O流,程序能够在同一个流上执行输入和输出,因此从某种意义上来说是全双工的。然后,对流的限制和对套接字的限制,有时候会互相冲突。比如下面两个限制:

1.如果中间没有插入对fflush、fseek、fsetpos或者rewind的调用,一个输入函数不能跟随在一个输出函数之后。fflush是清空流缓冲区,后三个函数使用 Unix I/O lseek 函数重置当前的文件位置。

2.如果中间没有插入对fseek、fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非输入函数入到EOF。

在开发网络应用中,对套接字使用 lseek 是非法的。因此对上面限制一 来说还可以采用刷新缓冲区来满足;然而对第二个限制的唯一办法是,对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写。
但是它要求应用程序在两个流上都要调用 fclose,这样才能释放相关存储器资源,多个操作试图关闭同一个描述符,第二个close就会失败,在线程化程序中关闭一个已经关闭了的描述符是会导致灾难的。

引用书上原文:

因此,我们建议你在网络套接字上不要使用标准I/O函数来进行输入和输出。而要使用健壮的RIO函数。如果需要使用格式化的输出,使用 sprintf 函数在存储器中格式化一个字符串,然后用 rio_writen 把它发送到套接口。如果需要格式化输入,使用 rio_readlineb 来读一个完整的文本行,然后用 sscanf 从文本行提取不同的字段。

6.关于共享文件的问题

内核用三个相关的数据结构来表示打开的文件:

  • 描述符表(descriptor table)每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。

  • 文件表(file table) 打开文件的描述符表项指向问价表中的一个表项。所有的进程共享这张表。每个文件表的表项组成包括由当前的文件位置、引用计数(既当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的应用计数。内核不会删除这个文件表表项,直到它的引用计数为零。

  • v-node表(v-node table)同文件表一样,所有的进程共享这张v-node表,每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
    image

描述符1和4通过不同的打开文件表表项来引用两个不同的文件。这是典型的情况,没有共享文件,并且每个描述符对应一个不同的文件。
image

多个描述符也可以通过不同的文件表表项来应用同一个文件。如果同一个文件被open两次,就会发生上面的情况。关键思想是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
image
父子进程也是可以共享文件的,在调用fork()之前,父进程如第一张图,然后调用fork()之后,子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了他们的描述符。

给你的结对学习搭档讲解你的总结并获取反馈

在重新学习第十章后,在第二遍学习的时候,感觉比第一遍要容易一些,也解决了第一次遇到的一些问题,但在第二遍学习的过程中,又会出现一些新的思考。在自己第一次读代码的时候,只能根据程序运行的结果,大致的猜测出每一句代码的含义,后来在看课本的时候总是会有恍然大悟的感觉,在运行代码后再学习课本内容,更能系统地理解知识点。

关于书本上的习题

家庭作业:

10.6
输出 fd2 = 4
已经有0 1 2被打开,fd1是3,fd2是4,关闭fd2之后再打开,还是4。

10.7

int main(int argc, char **argv)
{
    int n;
    rio_t rio;
    char buf[MAXBUF];

    Rio_readinitb(&rio, STDIN_FILENO);
    while((n = Rio_readnb(&rio, buf, MAXBUF)) != 0)
        Rio_writen(STDOUT_FILENO, buf, n);
} 

10.8
只需要将stat那句话改为: fstat(atoi(argv[1]), &stat);。

10.9
这里应该是表明,输入重定向到了foo.txt,然而3这个描述符是不存在的。
说明foo.txt并没有单独的描述符3。
所以Shell执行的代码应该是这样的:

if (Fork() == 0) {/* Child */
    int fd = open("foo.txt", O_RDONLY, 0);
    dup2(fd, 1);
    close(fd);
    Execve("fstatcheck", argv, envp);
} 

10.10

这里使用一个重定向的技术即可。如果参数个数为2,那么就将标准输入重定向到文件。
程序并没有检测各种错误。

int main(int argc, char **argv)
{
    int n;
    rio_t rio;
    char buf[MAXLINE];
    
    if(argc == 2){
        int fd = open(argv[2], O_RDONLY, 0);
        dup2(fd, STDIN_FILENO);
        close(fd);
    }
    Rio_readinitb(&rio, STDIN_FILENO);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
        Rio_writen(STDOUT_FILENO, buf, n);
}