zoukankan      html  css  js  c++  java
  • 20145206 《信息安全系统设计基础》第九周学习总结

    20145206 《信息安全系统设计基础》第九周学习总结

    教材学习内容总结

    输入/输出(I/O)是在主存和外部设备之间拷贝数据的过程。

    第一节 Unix I/O

    这一节涉及到操作系统的基本抽象之一——文件。也就是说,所有的I/O设备都被模型化为文件,而所有的输入输出都被当做对相应文件的读/写。相关的执行动作如下:

    1.打开文件:

    应用程序向内核发出请求→要求内核打开相应的文件→内核返回文件描述符

    文件描述符:一个小的非负整数,用来在后续对此文件的所有操作中标识这个文件。有三个已经被指定了的如下:

    标准输入——0(STDIN_FILENO)
    标准输出——1(STDOUT_FILENO)
    标准错误——2(STDERR_FILENO)
    

    括号中是常量表示形式,使用时需要加头文件<unistd.h>

    也就是说,在Unix生命周期一开始,0、1、2就被占用,以后的open只能从3开始——习题10.1.

    在UNIX下还有

    stdin,stdout,stderr
    ```表示同样的含义。
    
    二者的主要区别为:
    
    

    1.数据类型不同,前者为int类型,后者为FILE*;

    2.STDIN_FILENO主要用在read(),write()等中,后者主要用在fread(),fwrite()以f开头。

    
    2.改变当前的文件位置
    
    通常,读,写操作都从当前文件偏移量处开始(也就是文件位置),并使偏移量增加所读写的字节数,可以理解为光标所在的位置。
    
    当打开一个文件的最初时候文件的偏移量为0.
    
    通过seek操作,可以显示的设置文件的当前位置为k。
    
    3.读写文件
    
    (1)读
    
    读操作就是从文件拷贝n>0个字节到存储器,并且改变文件当前位置。(如果当前位置是k,则改变为k+n)
    
    ※EOF的来源:
    
    这里有一个一直以来的理解上的误区:文件结尾处没有明确的EOF信号,是当文件当前位置的数值超过了文件大小时,会处罚一个称为end-of-file的条件,能够被应用程序检测到,这就是所谓的EOF信号。
    
    (2)写
    
    写操作是从存储器拷贝n>0个字节到一个文件,然后更新当前文件位置。
    
    4.关闭文件
    
    应用通知内核关闭文件→内核释放文件打开时的数据结构→恢复描述符→释放存储器资源。
    
    ##第二节 打开和关闭文件
    
    1.open函数
    
    (1)函数定义:
    
    

    include <sys/types.h>

    include <sys/stat.h>

    include <fcntl.h>

    int open(char *filename, int flags, mode_t mode);

    
    (2)参数解析:
    
    返回值:类型为int型,返回的是描述符数字,总是在进程中当前没有打开的最小描述符。如果出错,返回值为-1.
    filename:文件名
    flags:指明进程打算如何访问这个文件,可以取的值见下:
    
    

    O_RDONLY:只读
    O_WRONLY:只写
    O_RDWR:可读可写

    O_CREAT:文件不存在,就创建新文件
    O_TRUNC:如果文件存在,就截断它
    O_APPEND:写操作前设置文件位置到结尾处

    
    这些值可以用或连接起来。
    mode:指定了新文件的访问权限位,符号名称如下:
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110190858170-1537862232.png)
    
    2.close函数
    
    (1)函数定义:
    
    

    include <unistd.h>

    int close(int fd);

    
    (2)参数解析:
    
    返回值:成功返回0,出错返回-1
    关闭一个已经关闭的描述符会出错
    
    fd:即文件的描述符。
    
    ##第三节 读和写文件
    
    1.读 read
    
    (1)函数原型:
    
    

    include <unistd.h>

    ssize_t read(int fd, void *buf, size_t n);

    
    (2)参数解析:
    
    返回值:成功则返回读的字节数,EOF返回0,出错返回-1。返回值为有符号数。
    fd:文件描述符
    buf:存储器位置
    n:最多从当前文件位置拷贝n个字节到存储器位置buf
    2.写 write
    
    (1)函数原型:
    
    

    include <unistd.h>

    ssize_t write(int fd, void *buf, size_t n);

    
    (2)参数解析:
    
    返回值:成功则返回写的字节数,出错返回-1。返回值为有符号数。
    fd:文件描述符
    buf:存储器位置
    n:最多从存储器位置buf拷贝n个字节到当前文件位置
    需要注意的是,read和write在正常情况下返回值是实际传送的字节数量。
    
    3.通过lseek函数可以显式的修改当前文件的位置
    
    4.不足值
    
    不足值指在某些情况下,read和write传送的字节比应用程序要求的要少,原因如下:
    
    

    读的时候遇到EOF
    从终端读文本行
    读和写socket

    
    ##第四节 用RIO包健壮的读写
    
    RIO,Robust I/O,针对的出现不足值的问题。
    
    1.RIO的无缓冲的输入输出函数。
    
    这些函数的作用是直接在存储器和文件之间传送数据,常适用于网络和二进制数据之间。
    
    rio_readn函数和rio_writen定义:
    
    

    include "csapp.h"

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

    
    参数:
    
    fd:文件描述符
    usrbuf:存储器位置
    n:传送的字节数
    返回值:
    
    rio_readn成功则返回传送的字节数,EOF为0(一个不足值),出错为-1
    rio_writen成功则返回传送的字节数,出错为-1,没有不足值。
    2.RIO的带缓冲的输入函数
    
    可以高效的从文件中读取文本行和二进制数据。
    
    一个概念:一个文本行就是一个由换行符结尾的ASCII码字符序列。
    
    范例:如何统计文本文件中文本行的数量——通过计算换行符。需要用到的函数:
    
    

    include "csapp.h"

    void rio_readinitb(rio_t *rp, int fd);//将描述符fd和地址rp处的一个类型为rio_t的读缓存区联系起来。

    ssize_t rio_readlineb(rio_t *rp,void *usrbuf, size_t maxlen);//从文件rp中读出一个文本行,包括换行符,拷贝到存储器位置usrbuf,并用空字符结束这个文本行。最多赌到maxlen-1个字节,最后一个给结尾的空字符。
    ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);//从文件rp中读取最多n个字符到存储器位置usrbuf中。

    
    成功则返回传送的字节数,EOF为0,出错为-1。
    【课本代码】
    
    图10-4:
    
    

    include "csapp.h"

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

    Rio_readinitb(&rio, STDIN_FILENO);//连接标准输入和rio地址
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) //当成功返回时,将rio中的内容拷贝到存储器位置buf中,最多读maxline-1
    Rio_writen(STDOUT_FILENO, buf, n);//把存储器位置中的数据拷贝到标注输出中。
    exit(0);
    

    }

    
    先连接标准输入和地址rio,再根据返回值判断是否成功将rio中的一行内容拷贝到了buf中,如果是再把这一行拷贝到标准输出中,即可实现一次一行的从标准输入拷贝一个文本文件到标准输出。
    
    图10-5:
    
    

    define RIO_BUFSIZE 8192

    typedef struct {
    int rio_fd; /* descriptor for this internal buf /
    int rio_cnt; /
    unread bytes in internal buf */
    char rio_bufptr; / next unread byte in internal buf /
    char rio_buf[RIO_BUFSIZE]; /
    internal buffer */
    } rio_t;

    void rio_readinitb(rio_t *rp, int fd)
    {
    rp->rio_fd = fd;
    rp->rio_cnt = 0;
    rp->rio_bufptr = rp->rio_buf;
    }

    
    由代码可以看出,rio_t数据结构的组成部分有文件描述符,缓存区中还没有读过的数值,下一个需要读的字节,文本行。在rio_readinitb函数中,创建了一个读缓存区,把文件描述符赋值,还没有读过的数值是0,下一个要读的字节就是文本行的起始,这代表这个读缓存区是空的。
    
    图10-6:
    
    

    RIO读程序的核心是rio_read函数

    static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
    {
    int cnt;

    while (rp->rio_cnt <= 0) {  /* 如果缓存区为空,调用read填满它 */
    rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, 
               sizeof(rp->rio_buf));
    if (rp->rio_cnt < 0) {
        if (errno != EINTR) /* 出错返回-1*/
        return -1;
    }
    else if (rp->rio_cnt == 0)  /* EOF返回0 */
        return 0;
    else 
        rp->rio_bufptr = rp->rio_buf; /* reset buffer ptr */
    }
    
    /* 一旦缓存区非空,就从读缓存区拷贝n和rp->rio_cnt中较小值个字节到用户缓存区,并且返回拷贝的字节数 */
    cnt = n;          
    if (rp->rio_cnt < n)   
    cnt = rp->rio_cnt;
    memcpy(usrbuf, rp->rio_bufptr, cnt);
    rp->rio_bufptr += cnt;
    rp->rio_cnt -= cnt;
    return cnt;
    

    }

    
    rio_readnb函数
    
    

    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) 
        nread = 0;      /* 调用read填充 */
        else
        return -1;      /* 错误,返回-1 */ 
    } 
    else if (nread == 0)
        break;              /* EOF */
    nleft -= nread;
    bufp += nread;
    }
    return (n - nleft);         /* 返回成功传送的字节数*/
    

    }

    
    rio_readlineb函数
    
    

    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++) { //最多是maxlen-1个
    if ((rc = rio_read(rp, &c, 1)) == 1) {
        *bufp++ = c;
        if (c == '
    ')//找到换行符,就退出
        break;
    } else if (rc == 0) {
        if (n == 1)
        return 0; /* EOF,并且没有读到数据 */
        else
        break;    /* EOF,有数据,出现不足值 */
    } else
        return -1;    /* 错误,返回-1 */
    }
    *bufp = 0;
    return n;//返回成功传送的字节数
    

    }

    
    ##第五节 读取文件元数据
    
    元数据即文件信息,需要用到的函数是stat和fstat。定义如下:
    
    

    include <unistd.h>

    include <sys/stat.h>

    int stat(const char *filename, struct stat *buf);
    int fstat(int fd,struct stat *buf);

    
    返回值:成功为0,错误为-1
    参数:
    
    stat需要输入文件名,而fstat需要输入的是文件描述符。
    
    关于stat数据结构如下图:
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110191613702-1336599406.png)
    
    需要注意的有两个,st_mode和st_size。
    
    

    st_size:包含文件的字节数大小
    st_mode:包编码文件访问许可位和文件类型。

    
    许可位在第一节提到了,Unix文件类型如下,并有对应的宏指令,含义均为“是xx吗”,这些宏在sys/stat.h中定义:
    
    

    普通文件 二进制或文本文件(对内核没差) S_ISREG()
    目录文件 关于其他文件的信息 S_ISDIR()
    套接字 通过网络与其他进程通信的文件 S_ISSOCK()

    
    查询和处理一个文件的st_mode位:
    
    

    include "csapp.h"

    int main (int argc, char **argv)
    {
    struct stat stat;
    char *type, *readok;

    Stat(argv[1], &stat);//文件选择argv[1],写入一个stat数据结构
    if (S_ISREG(stat.st_mode))     /* 如果是一个文本文件 */
    type = "regular";
    else if (S_ISDIR(stat.st_mode))//如果是一个目录文件
    type = "directory";
    else 
    type = "other";
    if ((stat.st_mode & S_IRUSR)) /* 检查阅读权限 */
    readok = "yes";
    else
    readok = "no";
    
    printf("type: %s, read: %s
    ", type, readok);
    exit(0);
    

    }

    
    ##第六节 共享文件
    
    内核用三个相关的数据结构来表示打开的文件:
    
    描述符表
    文件表:打开文件的集合是由一张文件表来表示的。
    v-node表
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110191832077-103930137.png)
    
    示例:
    
    典型的无共享的:
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110191943889-2101661560.png)
    
    描述符1和4指向文件表中不同的表现,进而引用了两个不同的文件。
    
    文件共享:
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110192043624-1932964320.png)
    
    这里可以看到,描述符1和4指向了文件表中的不同表项,但是引用了同一个文件,关于这种情况书上给了一个实例:同一个filename调用open函数两次,这时描述符是不一样的,文件位置也不一样,但是都是同一个文件。这体现的关键思想是:
    
    每个描述符都有它自己的文件位置 ,所以对不同描述符的读操作可以从文件的不同位置获取数据。
    
    子进程继承父进程的打开文件:
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110192145092-963306637.png)
    
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110192227874-406771655.png)
    
    
    初始状态如图6,只有父进程进行了打开文件,然后子进程会有一个父进程描述符表的副本,因而能够共享相同的打开文件表集合,同时也就共享相同的文件位置。
    
    而由于文件表的性质,关闭一个描述符的时候只会减少相应的文件表表项中的引用计数,内核不会删除这个文件表表项直至引用计数清零,所以要想内核删除相应文件表表项,父子进程都必须关闭它们的描述符。
    
    

    这几种状况的应用,在10.2和10.3题。

    练习10.2中,因为fd1和fd2有独立的文件描述符,它们各自有各自的描述符表、文件表、v-code表,所以它们的读取是各自独立的,最后得值是f;

    练习10.3中,Fork是子程序,和父程序共享同一个描述符表、文件表、v-code表,指向相同的文件,所以在子程序执行过后,父程序在其基础上进行,读取下一个字符,是o。

    
    ##第七节 I/O重定向
    
    I/O重定向操作符: >
    
    

    ls > foo.txt

    这句代码的含义就是使外壳加载和执行ls程序,并且将标准输出重定向到磁盘文件foo.txt。
    
    I/O重定向函数: dup2
    
    函数定义为:
    
    

    include <unistd.h>

    int dup2(int oldfd, int newfd);

    
    返回值:成功返回描述符,错误返回-1
    这个函数执行的操作是,拷贝描述符表表项oldfd,覆盖描述表表项newfd,如果后者被打开,则在拷贝前关闭它。
    
    例题10.5中,初始情况下fd1和fd2的描述符分别是3和4,所以是两个不同描述符表,指向两个不同的文件,但是由于在读了fd2一个字节之后,将fd1重定向到了fd2,所以此时再读fd1相当于在读fd2,也就是结果是o。
    
    ##第八节 标准I/O
    
    1.标准I/O库:
    
    ANSI C定义了一组高级输入输出函数,称为标准I/O库,包含:
    
    

    fopen、fclose,打开和关闭文件
    fread、fwrite,读和写字节
    fgets、fputs,读和写字符串
    scanf、printf,复杂的格式化的I/O函数

    
    2.流——类型为FILE的流是对文件描述符和流缓冲区的抽象
    
    标准I/O库将一个打开的文件模型化为一个流。
    
    每个ANSI C程序开始的时候都有三个打开的流:stdin、stdout、stderr,对应于标准输入、标准输出和标准错误 (参见第一节笔记),定义如下:
    
    

    include <stdio.h>

    extern FILE *stdin;
    extern FILE *stdout;
    extern FILE *stderr;

    
    ##第九节 套接字
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161110193407155-339220289.png)
    
    网络套接字上最好不要使用标准I/O函数,而是使用RIO函数,原因:
    
    如果没有清楚缓存区,输入函数后面不能接输出函数,输出函数后面也不能接输入函数,而对套接字使用lseek是非法的,打开两个流有很麻烦,所以!在网络套接字上不要使用标准I/O函数来进行输入和输出!
    
    ##错误处理
    
    附录A中主要讲了这本书中的错误处理方式,有一个方法——错误处理包装函数,这个思想很有意思,相当于给基本函数再套上一层皮,然后run这个皮,发现了错误就终止,完全正确的话就跟没有这层皮一样。
    
    1.错误处理风格
    
    (1)Unix风格
    
    遇到错误后返回-1,并且将全局变量errno设置为指明错误原因的错误代码;
    
    如果成功完成,就返回有用的结果。
    
    (2)Posix风格
    
    返回0表示成功,返回非0表示失败;
    
    有用的结果在传进来的函数参数中。
    
    (3)DNS风格
    
    有两个函数,gethostbyname和gethostbyaddr,失败时返回NULL指针,并设置全局变量h_errno。
    
    (4)错误报告函数
    
    

    void unix_error(char msg) / unix-style error /
    {
    fprintf(stderr, "%s: %s ", msg, strerror(errno));
    exit(0);
    }
    /
    $end unixerror */

    void posix_error(int code, char msg) / posix-style error */
    {
    fprintf(stderr, "%s: %s ", msg, strerror(code));
    exit(0);
    }

    void dns_error(char msg) / dns-style error */
    {
    fprintf(stderr, "%s: DNS error %d ", msg, h_errno);
    exit(0);
    }

    void app_error(char msg) / application error */
    {
    fprintf(stderr, "%s ", msg);
    exit(0);
    }

    
    2.错误处理包装函数
    
    Unix风格
    
    成功时返回void,返回错误时包装函数打印一条信息,然后退出。
    
    

    void Kill(pid_t pid, int signum)
    {
    int rc;

    if ((rc = kill(pid, signum)) < 0)
    unix_error("Kill error");
    

    }

    
    Posix风格
    
    成功时返回void,错误返回码中不会包含有用的结果。
    
    

    void Pthread_detach(pthread_t tid) {
    int rc;

    if ((rc = pthread_detach(tid)) != 0)
    posix_error(rc, "Pthread_detach error");
    

    }

    
    DNS风格
    
    

    struct hostent *Gethostbyname(const char *name)
    {
    struct hostent *p;

    if ((p = gethostbyname(name)) == NULL)
    dns_error("Gethostbyname error");
    return p;
    

    }

    ##课后习题
    
    1.下面程序的输出是什么?
    
    

    include "csapp.h"

    int main()
    {
    int fd1,fd2;
    fd1=Open("foo.txt",O_RDONLY,0);
    Close(fd1);
    fd2=Open("baz.txt",O_RDONLY,0);
    printf("fd2=%d ",fd2);
    exit(0);
    }

    
    【unix进程生命周期开始的时候,就已经有了三个打开的描述符:标准输入、标准输出、标准错误,分别为0,1,2。所以fd2的值应该是3】
    
    2.下面代码的功能是什么?
    
    

    include "csapp.h"

    int main(void)
    {
    char c;
    while(Read(STDIN_FILENO,&C,1) !=0)
    Write(STDOUT_FILENO,&c,1);
    exit(0);
    }

    
    【每次一个字节,从标准输入传递到标准输出】
    
    3.假设磁盘文件foobar.txt由6个ASCII码字符“foobar”组成。那么,下列程序的输出是什么?
    
    

    include "csapp.h"

    int main()
    {
    int fd1,fd2;
    char c;
    fd1=Open("foobar.txt",O_RDONLY,0);
    fd2=Open("foobar.txt",O_RDONLY,0);
    Read(fd1,&c,1);
    Read(fd2,&c,1);
    printf("c=%c ",c);
    exit(0);
    }

    
    【描述符fd1和fd2都有各自的打开文件表表项,所以有它们各自的文件位置(也就是说互不影响,不会因为fd1先执行就使得fd2打开的文件位置推后)。则fd2打开文件读出的第一个字母还是f。】
    
    4.就像前面那样,磁盘文件foobar.txt由6个ASCII码字符“foobar”组成。那么,下列程序的输出是什么?
    
    

    include "csapp.h"

    int main()
    {
    int fd;
    char c;
    fd=Open("foobar.txt",O_RDONLY,0);
    if(Fork()==0)
    {
    Read(fd,&c,1);
    exit(0);
    }
    Wait(NULL);
    Read(fd,&c,1);
    printf("c=%c ",c);
    exit(0);
    }

    
    【父子进程共享相同的文件表表项,因此依次读取的是“f”和“o”。输出为o。】
     
    ## 本周代码托管截图
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161113163030764-1930111957.png)
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161113161030374-447119173.png)
    
    代码行数统计:
    
    ![](http://images2015.cnblogs.com/blog/877170/201611/877170-20161113161318374-354342784.png)
    
    代码托管链接:
    
    http://git.oschina.net/ZouJR/Linux
    
    ## 学习进度条
    
    |            | 代码行数(新增/累积)| 博客量(新增/累积)|学习时间(新增/累积)|重要成长|
    | --------   | :----------------:|:----------------:|:---------------:  |:-----:|
    | 目标        | 5000行            |   30篇           | 400小时            |       |
    | 第一周      | 0/0           |   1/2            | 20/30             |  学习了一些Linux核心命令     |
    | 第二周      | 250/250           |   1/3            | 20/50             |  学习了vim编辑器基础     |
    | 第三周      | 280/530          |   1/4            | 18/68             | 熟练在vim里编程并运行     |
    | 第五周      | 300/830          |   1/5            | 15/83             | 学会生成汇编文件并查看     |
    | 第六周      | 200/1030          |   1/6            | 12/95             | 了解Y86指令     |
    | 第七周      | 139/1169          |   1/7            | 10/105             | 了解存储器层次结构     |
    | 第八周      | 0/1169          |   2/9            | 8/113             | 复习前面所学内容     |
    | 第九周      | 182/1351          |   2/11            | 9/122             | 了解Unix I/O     |
    
    ## 参考资料
    -  [《深入理解计算机系统V2》学习指导]([http://www.cnblogs.com/rocedu/p/5826467.html])
    -  ...
  • 相关阅读:
    Java 抽象类 初学者笔记
    JAVA super关键字 初学者笔记
    Java 标准输入流 初学者笔记
    JAVA 将对象引用作为参数修改实例对象参数 初学者笔记
    JAVA 根据类构建数组(用类处理数组信息) 初学者笔记
    JAVA 打印日历 初学者笔记
    Python 测试代码 初学者笔记
    Python 文件&异常 初学者笔记
    Python 类 初学者笔记
    ubuntu网络连接失败
  • 原文地址:https://www.cnblogs.com/ZouJR/p/6052161.html
Copyright © 2011-2022 走看看