关于c:csapp之第10章系统级I⁄O

28次阅读

共计 7017 个字符,预计需要花费 18 分钟才能阅读完成。

0. 学习起因

大多时候,高级别 I / O 函数工作良好,没必要间接用 Unix I/O,为何需学习?

  • 理解 Unix I/ O 将帮忙了解其余零碎概念。I/ O 是零碎操作不可或缺的局部,因而常常遇到 I / O 和其余零碎概念之间的循环依赖
  • 有时必须用 Unix I/O,用高级 I / O 不太可能或不适合,如规范 I / O 库没提供读取文件元数据的形式,此外 I / O 库存在一些问题

1. Unix I/O

输出 / 输入(I/O)是主存和外部设备之间复制数据的过程,在 Linux 中,文件就是字节的序列。所有的 I/O 设施(如网络、内核、磁盘和终端等)都被模型化为文件,而所有的输出和输入都被当作对相应文件的读和写来执行。这种将设施映射为文件的机制,容许内核引出简略、优雅的利用接口 Unix I/O,使得所有输出和输入都以对立的形式执行,如 open()/close() 关上 / 敞开文件,read()/ write() 读 / 写文件。seek()扭转以后文件地位。Unix I/ O 次要分为两大类:

为辨别不同文件的类型,会有一个 type 来进行区别:

  • 一般文件:蕴含任意数据
  • 目录:相干文件组的索引
  • Socket:用于另一台机器上的过程通信

还有一些特地的类型仅做理解:命名管道(FIFOs)、符号链接、字符和块设施

一般文件

一般文件蕴含任意数据,应用程序通常需辨别文本文件和二进制文件。前者只蕴含 ASCII 或 Unicode 字符。除此之外的都是二进制文件(对象文件, JPEG 图片, 等等)。内核不能辨别出区别。

文本文件是文本行的序列,每行以 \n 结尾,新行是 0xa,和 ASCII 码中 LF 一样。不同零碎判断行完结的符号不同 (End of line, EOL),Linux & Mac OS 是\n(0xa) 等价 line feed(LF),而 Windows & 网络协议是 \r\n (0xd 0xa) 等价 Carriage return(CR) followed by line feed(LF)

目录

目录蕴含一个链接 (link) 数组,且每个目录至多蕴含两记录:.(dot) 当前目录、..(dot dot) 下层目录

操作命令次要有 mkdir, ls, rmdir。目录以树状构造组织,根目录是 /(slash)。

内核会为每个过程保留当前工作目录(cwd, current working directory),可用 cd 命令进行更改。通过路径名来确定文件的地位,分为绝对路径和相对路径。

2. 文件操作

2.1 关上文件

关上文件会告诉内核已筹备好拜访该文件

int fd; // 文件描述符 file descriptor

if ((fd = open("/etc/hosts", O_RDONLY)) < 0)
{perror("open");
    exit(1);
}

返回值是一个小的整型称为文件描述符(file descriptor),若该值等于 -1 则阐明产生谬误。每个由 Linux shell 创立的过程都会默认关上三个文件(留神这里的文件概念):

  • 0: standard input(stdin)
  • 1: standard output(stdout)
  • 2: standar error(stderr)

2.2 敞开文件

敞开文件会告诉内核已实现对该文件的拜访

int fd;     // 文件描述符
int retval; // 返回值

int ((retval = close(fd)) < 0)
{perror("close");
    exit(1);
}

敞开一个曾经敞开的文件是线程程序中的劫难(稍后会具体介绍),所以肯定要查看返回值,哪怕是看似良好的函数如 close()

2.3 读取文件

读取文件将字节从以后文件地位复制到内存,而后更新文件地位

char buf[512];
int fd;
int nbytes
    
// 关上文件描述符,并从中读取 512 字节的数据
if ((nbytes = read(fd, buf, sizeof(buf))) < 0)
{perror("read");
    exit(1);
}

返回值是读取的字节数量,是一个 ssize_t 类型(其实就是一个有符号整型),如果 nbytes < 0 那么示意出错。nbytes < sizeof(buf) 这种状况(short counts) 是可能产生的,而且并不是谬误。

2.4 写入文件

写入文件将字节从内存复制到以后文件地位,而后更新以后文件地位

char buf[512];
int fd;
int nbytes;

// 关上文件描述符,并向其写入 512 字节的数据
if ((nbytes = write(fd, buf, sizeof(buf)) < 0)
{perror("write");
    exit(1);
}

返回值是写入的字节数量,如果 nbytes < 0 示意出错。nbytes < sizeof(buf) 这种状况(short counts) 是可能产生的,且不是谬误。

2.5 读取目录

可用 readdir 系列函数读取目录的内容,每次对 readdir 的调用返回的都是指向流 dirp 中下一个目录项的指针,或没有更多目录项则返回 NULL,每个目录项都有构造体:

struct dirent{
    ino_t d_ino;      /* inode number */
      char d_name[256]; /* filename */
};

2.6 简略 Unix I/O 例子

拷贝文件到规范输入,一次一个字节:

#include "csapp.h"
int main(int argc, char *argv[])
{
    char c;
    int infd = STDIN_FILENO;
    if (argc == 2) {infd = Open(argv[1], O_RDONLY, 0);
    }
    while(Read(infd, &c, 1) != 0)
        Write(STDOUT_FILENO, &c, 1);
    exit(0);
}

后面提到的 short count 会在上面的情景下产生:

  • 读取的时遇到 EOF(end-of-file)
  • 从终端中读取文本行
  • 读和写网络 sockets

但上面的状况下不会产生

  • 从磁盘文件中读取(除 EOF 外)
  • 写入到磁盘文件中

最好总是容许 short count,这样就能够防止解决这么多不同的状况。

#include "csapp.h"
#define BUFSIZE 64

int main(int argc, char *argv[])
{char buf[BUFSIZE];
    int infd = STDIN_FILENO;
    if (argc == 2) {infd = Open(argv[1], O_RDONLY, 0);
    }
    while((nread = Read(infd, buf, BUFSIZE)) != 0)
        Write(STDOUT_FILENO, buf, nread);
    exit(0);
}

3. 元数据

元数据是用来形容数据的数据,由内核保护,能够通过 statfstat 函数来拜访,构造是:

struct stat
{
    dev_t           st_dev;     // Device
    ino_t           st_ino;     // inode
    mode_t          st_mode;    // Protection & file type
    nlink_t         st_nlink;   // Number of hard links
    uid_t           st_uid;     // User ID of owner
    gid_t           st_gid;     // Group ID of owner
    dev_t           st_rdev;    // Device type (if inode device)
    off_t           st_size;    // Total size, in bytes
    unsigned long   st_blksize; // Blocksize for filesystem I/O
    unsigned long   st_blocks;  // Number of blocks allocated
    time_t          st_atime;   // Time of last access
    time_t          st_mtime;   // Time of last modification
    time_t          st_ctime;   // Time of last change
}

对应的拜访例子:

int main (int argc, char **argv)
{
    struct stat stat;
    char *type, *readok;
    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\n", type, readok);
    exit(0);
}

3.1 共享文件

可用许多不同的形式共享 Linux 文件。要了解文件共享就得了解三个示意关上文件的数据结构。

  • 描述符表:每个过程都有独立的描述符表,表项是过程关上的文件描述符索引,每个关上的描述符表指向文件表中的一个表项
  • 文件表:关上文件的汇合是由一张文件表示意,所有过程共享该表,每个文件表的表项组成包含以后文件地位、援用计数、指向 v -node 表中对应表项的指针。敞开描述符缩小文件表项援用计数直到为 0 会删除文件表项
  • v-node 表:同文件表一样所有过程共享该 v -node 表,每个表蕴含 stat 构造中的大多数信息,包含 st_mode 和 st_size 成员

两个描述符援用两个不同的关上文件。描述符 1(stdout)指向终端,描述符 4 指向关上的磁盘文件

两个不同的描述符通过两个不同的关上文件表条目共享同一个磁盘文件,应用雷同的文件名参数调用 open 两次,要害思维是每个描述符都有本人的文件地位,所以对不同描述符的读操作可从文件不同地位获取数据

3.2 过程如何共享文件:fork

子过程继承其父过程的关上文件,留神:exec 函数不会扭转状况(应用 fcntl 来扭转)

在 fork 之后,子过程实际上和父过程的指向是一样的,这里须要留神的是会把援用计数加 1

4. I/ O 重定向

理解了这个,就晓得重定向如何实现。其实很简略,只有调用 dup2(oldfd, newfd)函数即可。扭转文件描述符指向的文件,就实现重定向,下图中咱们把原来指向终端的文件描述符指向了磁盘文件,也就把终端上的输入保留在了文件中:

具体来说:

  1. 关上规范输入应该重定向到的文件,产生在执行 shell 代码的子过程中,在 exec 之前

  2. 调用dup2(4,1),导致 fd=1 (stdout) 援用 fd=4 指向的磁盘文件,留神援用计数的变动

4.1 I/ O 重定向的例子

4.2 过程管制和 I /O

5. 规范 I /O

C 规范库中蕴含一系列高层的规范 IO 函数,比方

  • 关上和敞开文件: fopen, fclose
  • 读取和写入字节: fread, fwrite
  • 读取和写入行: fgets, fputs
  • 格式化读取和写入: fscanf, fprintf

规范 IO 会用流的模式关上文件,所谓流 (stream) 理论是文件描述符和缓冲区 (buffer) 在内存中的形象。C 程序个别以三个流开始,如下所示:

#include <stdio.h>
extern FILE *stdin;     // 规范输出 descriptor 0
extern FILE *stdout;    // 规范输入 descriptor 1
extern FILE *stderr;    // 规范谬误 descriptor 2

int main()
{fprintf(stdout, "Hello, Da Wang\n");
}

用缓冲区的起因,程序常常会一次读 / 写一个字符,比方 getc, putc, ungetc,同时也会一次读 / 写一行,比方 gets, fgets。如果用 Unix I/O 的形式来进行调用是十分低廉的,readwrite 因需内核调用,需大于 10000 个时钟周期。

方法是用 read 一次读取一块数据,再由高层的用户输出函数,一次从缓冲区读取一个字符(当缓冲区用完的时候须要从新填充)

5.1 规范 I / O 中的缓冲

规范 I / O 函数应用缓冲的 I /O,缓冲区通过 \n 刷新到输入 fd,调用 fflush 或 exit,或从 main 返回

可通过 strace 程序看缓冲如何起作用

6. RIO

RIO 和 C 准库 IO 库是在 Unix I/ O 上构建的两个不兼容的库,RIO 提供了不便高效的 IO 拜访,能够从一个描述符中读二进制非缓存输出和输入、缓存的文本行和二进制数据(线程平安可在同一描述符上任意穿插应用)。有两类输入输出函数:

① 无缓冲输入输出:和 Unix 的 read/write 接口雷同,对网络传输数据尤其有用

对于同一个形容表,能够任意的交织调用 rio_readn 和 rio_wiriten

/*
* rio_readn - Robustly read n bytes (unbuffered)
*/
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 */
}

从下面的代码不难看出,如果程序的信号处理程序返回中断,这个函数会手动重启 read 或者 write。

② 带缓冲的输出函数

这个函数有一个益处是,它从外部读缓冲区拷贝的一行,能无效地从局部缓存在外部内存缓冲区中的文件中读取文本行和二进制数据,当缓冲区为空的时候,主动调用 read 填满缓冲区,效率很高。

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);,rio_readlineb 从文件 fd 中读取最多 maxlen 字节的文本行,并将其存储在 usrbuf 中,当读到 maxlen 字节、EOF 产生、\n产生时进行。

ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);,rio_readnb 从文件 fd 读取最多 n 个字节,当读到 n 个字节、EOF 产生时进行,对 rio_readlineb 和 rio_readnb 的调用能够在同一个描述符上任意交织

缓存 IO 实现

读文件时,文件有关联的缓冲区来保留曾经从文件中读取但还没有被用户代码读取的字节

缓存 IO 申明

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;

#include "csapp.h"
#define MLINE 1024

int main(int argc, char *argv[])
{
    rio_t rio;
    char buf[MLINE];
    int infd = STDIN_FILENO;
    ssize_t nread = 0;
    if (argc == 2) {infd = Open(argv[1], O_RDONLY, 0);
    }
    Rio_readinitb(&rio, infd);
    while((nread = Rio_readlineb(&rio, buf, MLINE)) != 0)
        Rio_writen(STDOUT_FILENO, buf, nread);
    exit(0);
}

复制文件到规范输入,用 mmap 加载整个文件

#include "csapp.h"
int main(int argc, char **argv)
{
    struct stat stat;
    if (argc != 2) exit(1);
    int infd = Open(argv[1], O_RDONLY, 0);
    Fstat(infd, &stat);
    size_t size = stat.st_size;
    char *bufp = Mmap(NULL, size, PROT_READ,MAP_PRIVATE, infd, 0);
    Write(1, bufp, size);
    exit(0);
}

7. 总结

Unix I/O 是最底层的,通过零碎调用进行操作,C 的规范 I / O 库建设在此之上,对应的函数为:

I/O 长处 毛病
Unix I/O 最通用最底层的 I / O 办法,异步信号平安,即可在信号处理器中调用,提供拜访文件元数据的性能 底层和根底,故需解决的状况多且易错,高效率的读写需缓冲区,也易错,这是规范 I / o 要解决的问题
规范 I /O 缓冲通过缩小读和写零碎调用的数量来提高效率,短计数即取得的字符没达到 SIZE 的问题 不提供文件元数据拜访性能,规范 I / O 非异步信号平安,不适宜信号处理和网络套接字输出 / 出

抉择 I / O 函数

通用规定:尽量用高层的 I / O 函数,但了解用的函数。当用磁盘或终端文件时,用 规范 I /O,在信号处理程序外部,极少数须要最高性能时用Unix I/O

解决二进制文件

任意字节序列,蕴含 0x00 的字节的二进制文件,永远不要用面向文本的 I /O,如 fgets、scanf 等,而应用 rio_readn 或 rio_readnb,以及字符串函数,如 strlen、strcpy、strcat 等。

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0