chapter11_系统级IO

  1. IO

    1. 定义

      IO(输入/输出)是在主存外部设备(磁盘驱动器、终端、网络)之间拷贝数据的过程

      输入:IO设备 --> 主存

      输出:主存 --> IO设备

    2. 所有语言都提供较高级别的IO工具

      eg.

       printf/scanf --- C
      
       cin >> / cout << --- C++
      

      但本质是由Unix内核提供的系统级Unix IO函数实现的

  2. Unix IO

    1. 所有的IO设备,例如网络、磁盘、终端等,都可以被模型化为文件,而所有的IO设备的输入和输出都可以被当作对相应文件的读和写来执行

      —> 这种方式使得Unix内核可以定义简单统一的接口处理各种设备的IO

    2. Unix IO的低级应用接口

      (1) 打开文件 open

      返回一个unsigned int fd作为文件描述符

      内核记录文件信息,应用程序记录文件描述符

      (2) 改变当前文件的位置 seek

      文件位置:从文件开头起始的字节偏移量

      (3) 读写文件 read/write

      读操作:从文件拷贝n个字节到存储器

      写操作:从当前文件位置开始,从存储器拷贝n个字节到文件

      (4) 关闭文件 close

      内核释放文件信息数据结构,回收文件描述符

      只要一个进程终止,无论何种方式,打开的文件资源都会被释放

  3. 打开文件 open

    1. 每个进程开始时,默认会打开三个文件

      作用名称fd
      标准输入STDIN_FILENO0
      标准输出STDOUT_FILENO1
      标准错误STDERR_FILENO2
    2. 函数接口

       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>
      
       int open(char* filename, int flags, mode_t mode);
      
       返回值:成功:fd; 失败:-1
      
       flags:进程访问文件的方式,可以是一系列掩码的或(O_RDONLY, O_WRONLY, O_RDWR)
      
       mode:新文件的访问权限 (u, g, o 的 r, w, x)
      
  4. 关闭文件 close

    1. 函数接口

       #include <unistd.h>
      
       int close(int fd);
      
       返回值:成功:0 错误:1
      
  5. 读写文件 read/write

    1. read 函数接口

       #include <unistd.h>
      
       ssize_t read(int fd, void* buf, size_t n);
      
       buf: 存储器位置
       n: 最多从当前文件位置拷贝n个字节
       返回值:错误:-1 EOF:0 成功:实际拷贝的字节数
      
    2. write 函数接口

       #include <unistd.h>
      
       ssize_t write(int fd, const void* buf, size_t n);
      
       fd, buf, n同read
       返回值:返回值:错误:-1 成功:实际拷贝的字节数
      
    3. read/write的返回值和参数n 是小于等于的关系(小于的情况称为不足值),原因是

      (1) read文件时遇到EOF

      (2) 从终端read每次传送一个文本行

      (3) 从网络read/write套接字socket时,内部缓冲约束和较长的网络延迟会引起read/write数量不够

      注:读写磁盘文件时,不会遇到不足值(除非读磁盘文件结尾的EOF)

  6. Rio(Robust I/O)进行健壮读写

    1. 无缓冲的输入函数

       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;      /* and call read() again */
                   } else {
                       return -1;      /* errno set by read() */ 
                   }
               } else if (nread == 0) {
                   break;              /* EOF */
               }
      
               nleft -= nread;
               bufp += nread;
           }
      
           return (n - nleft);         /* return >= 0 */
       }
      

      返回值:成功:传送的字节数 EOF:0 失败:-1

      注:这个方法被中断后会手动重启(套在while循环里)

    2. 无缓冲的输出函数

       ssize_t rio_writen(int fd, void* usrbuf, size_t n) {
           size_t nleft = n;
           ssize_t nwritten;
           char *bufp = usrbuf;
      
           while (nleft > 0) {
               if ((nwritten = write(fd, bufp, nleft)) <= 0) {
                   if (errno == EINTR)  /* Interrupted by sig handler return */ {
                       nwritten = 0;    /* and call write() again */
                   } else {
                       return -1;       /* errno set by write() */
                   }
               }
      
               nleft -= nwritten;
               bufp += nwritten;
           }
      
           return n;
       }
      

      返回值:成功:传送的字节数 失败:-1

      注:这个方法不会返回不足值,因为会在while循环中重试

    3. 带缓冲的输入函数

      (1) 目标:每次读取一行,根据’\n’确定

      (2) 主要思想:首先定义一个结构体

       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;
      

      这个结构体和一个fd绑定,定义一个rio_buf用于保存字符,rio_cnt代表buffer中未读的字符,rio_bufptr指向未读的起始字符位置;

      笨办法是每次读一个字节,然后每次判断这个字符是不是’\n’决定是否换行;这个方法的缺点是效率低,因为每次请求读字节都会陷入内核

      带有buffer的读法是:每当rio_t->rio_cnt小于等于0时,就读sizeof(rio_buf)这么多个字节,把buffer填满;然后取的时候根据要读的数量取,此时取这一行为发生在用户态,不需要经过内核,所以效率提高了。

      代码比较长,不贴了

  7. 读取文件元数据

    1. 使用 stat 和 fstat 读取文件元数据

       #include <unistd.h>
       #include <sys/stat.h>
      
       int stat(const char* filename, struct stat* buf);
       int fstat(int fd, struct stat* buf);
      

      stat结构体很复杂,包含了一个文件的各种信息

    2. 文件

      (1) 普通文件

      包括文本文件二进制文件对于内核来说文本文件和二进制文件是一样的

      (2) 目录文件

      (3) 套接字

    3. 宏指令可以判断文件类型

       S_ISREG()
       S_ISDIR()
       S_ISSOCK()
      
  8. 共享文件

    1. 内核维护三种数据结构,用于描述打开的文件

      (1) 描述符表:文件描述符的集合

      fd文件表表项地址
      0文件A
      1文件B

      (2) 文件表:打开文件的集合

      文件文件位置引用计数v-node表项地址
      文件Axxx1nodeA
      文件Bxxx2nodeB

      (3) v-node表:文件信息,基本等同于stat结构体,包括文件访问权限、大小、类型等

    2. 多个描述符可以通过不同的文件表表项,引用同一个文件;也可以引用不同的文件

      即,两个fd不一样,文件表中的文件名也不一样,但是v-node表征的实际文件是同一个

      注:这样设计的原因是,每个fd都有自己的文件位置,对不同fd的读操作可以从文件的不同位置获取数据

    3. 父子进程共享fd,并且共享文件位置

  9. IO重定向

    1. 目标:将磁盘文件和标准输入输出联系起来

    2. 原理

      使用 dup2 方法

       #include <unistd.h>
      
       int dup2(int oldfd, int newfd);
      

      调用 dup2 之前

       oldfd ---> 文件A ---> nodeA
       newfd ---> 文件B ---> nodeB
      

      调用 dup2 之后

       oldfd ---> 文件B ---> nodeB
       newfd ---> 文件B ---> nodeB
      

      所以,对oldfd的输出,在底层由nodeA切换到了nodeB

      例如

       ls > a.txt
      

      从 stdout --> a.txt

    3. 几个示例

       cmd1 | cmd2 : pipe,将cmd1的标准输出作为cmd2的标准输入
      
       > file :将标准输出重定向到file
      
       < file :将file作为标准输入
      
       >> file :将标准输出重定向到file,如果file存在,append到文件中,即附加到文件的后面,而不是覆盖文件
      
  10. 标准IO

    1. 标准IO库将一个打开的文件模型化为一个流

    2. 一个流是一个指向File类型的指针

    3. Unix IO、标准IO、RIO的关系

      (1) Unix IO:通过系统调用来访问

      open/read/write/lseek/stat/close

      (2) RIO:这本书对Unix IO封装了一下写了一个库,用于自动处理不足值的情况

      (3) 标准IO:封装Unix IO,带缓冲

      注:输入和输出都带缓冲,因为不带缓冲总是要陷入内核进行系统调用比较耗时

    4. 绝大多数情况下都不应该直接用Unix IO

      对于磁盘和终端设备,标准IO函数就很适合;

    5. 但是标准IO不适合Socket,因为标准IO流是全双工的,而Socket有限制,不能全双工

      所以Socket应该用Unix IO

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值