• 20145225 《信息安全系统设计基础》第9周学习总结


    第十章 系统级I/O

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

    第一节 Unix I/O

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

    1.打开文件:

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

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

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

    2.改变当前的文件位置

    通常,读,写操作都从当前文件偏移量处开始(也就是文件位置),并使偏移量增加所读写的字节数,可以理解为光标所在的位置。

    当打开一个文件的最初时候文件的偏移量为0。

    通过seek操作,可以显示的设置文件的当前位置为k。

    3.读写文件

    (1)读

    读操作就是从文件拷贝n>0个字节到存储器,并且改变文件当前位置。(如果当前位置是k,则改变为k+n)

    (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:指明进程打算如何访问这个文件。

    • mode:指定了新文件的访问权限位,符号名称如下:

    2.close函数

    (1)函数定义:

    #include <unistd.h>

    int close(int fd);

    (2)参数解析:

    • 返回值:成功返回0,出错返回-1
    • fd:即文件的描述符。

    第三节 读和写文件

    1.读 read

    参数解析:

    • 返回值:成功则返回读的字节数,EOF返回0,出错返回-1。返回值为有符号数。
    • fd:文件描述符
    • buf:存储器位置
    • n:最多从当前文件位置拷贝n个字节到存储器位置buf

    2.写 write

    参数解析:

    • 返回值:成功则返回写的字节数,出错返回-1。返回值为有符号数。
    • fd:文件描述符
    • buf:存储器位置
    • n:最多从存储器位置buf拷贝n个字节到当前文件位置

    3.通过lseek函数可以显式的修改当前文件的位置

    4.不足值

    不足值指在某些情况下,read和write传送的字节比应用程序要求的要少,原因如下:

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

    第四节 用RIO包健壮的读写

    RIO,Robust I/O,针对的出现不足值的问题。

    1.RIO的无缓冲的输入输出函数。

    这些函数的作用是直接在存储器和文件之间传送数据,常适用于网络和二进制数据之间。

    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数据结构如下图:

    需要注意的有两个,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表

    示例:

    典型的无共享的:

    描述符1和4指向文件表中不同的表现,进而引用了两个不同的文件。

    文件共享:

    这里可以看到,描述符1和4指向了文件表中的不同表项,但是引用了同一个文件,关于这种情况书上给了一个实例:同一个filename调用open函数两次,这时描述符是不一样的,文件位置也不一样,但是都是同一个文件。这体现的关键思想是:

    每个描述符都有它自己的文件位置 ,所以对不同描述符的读操作可以从文件的不同位置获取数据。

    子进程继承父进程的打开文件:

    初始状态如图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;

    第九节 套接字

    网络套接字上最好不要使用标准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;
    }
  • 相关阅读:
    .NetCore Grpc 客服端 工厂模式配置授权
    DOCKER 拉取 dotnet 镜像太慢 docker pull mcr.microsoft.com too slow
    Introducing .NET 5
    VSCode 出现错误 System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached.
    Omnisharp VsCode Attaching to remote processes
    zookeeper3.5.5 centos7 完全分布式 搭建随记
    Hadoop2.7.7 centos7 完全分布式 配置与问题随记
    MySQL索引 索引分类 最左前缀原则 覆盖索引 索引下推 联合索引顺序
    SQL基础随记3 范式 键
    MySQL调优 优化需要考虑哪些方面
  • 原文地址:https://www.cnblogs.com/nizaikanwoma/p/6059887.html
Copyright © 2020-2023  润新知