六星经典CSAPP-笔记(10)系统IO

53 篇文章 1 订阅

六星经典CSAPP-笔记(10)系统I/O

1.Unix I/O

所有语言的运行时系统都提供了高抽象层次的I/O操作函数。例如,ANSI C在标准I/O库中提供了诸如printf和scanf等I/O缓冲功能的函数;C++中则重载了<<和>>用来支持读写。在Unix系统中,这些高层次的函数基于Unix的系统I/O函数来实现,多数时候我们都无需直接使用底层的Unix I/O。但学习Unix系统I/O能更好地理解一些系统概念,而且当高层次的函数不适用时我们也能轻松地实现想要的功能,例如访问文件的元数据。

Unix/Linux将各种I/O设备统一而优雅地抽象为文件,在此基础上提供了一套非常简洁的低层次的API接口,也就是Unix I/O。这套API主要由以下五个函数组成:

  • open():请求内核打开一个file,从而应用代码能够访问file对应的I/O设备。内核会返回一个非负整数,叫做描述符descriptor。内核会为打开的file维护一组数据结构(学习Linux内核时了解了一些),可以把描述符想成file在内核中的id,后续各种操作都需要它。
  • lseek():修改当前file的操作位置k,即从file开端的字节偏移量。读写文件时会自动移动k,也可以显式调用lseek()移动k。
  • read():从位置k开始,从file拷贝大于0个字节到内存中。当k超过文件的总字节大小时,会触发一个条件end-of-file(EOF)
  • write():类似地,从内存拷贝大于0个字节到file的位置k处,并更新k。
  • close():通知内核释放file在内核中对应的数据结构,将描述符放回到资源池中。进程结束时,内核会自动关闭所有打开的file。

EOF到底是什么东西?
首先必须明确一点,没有EOF字符这种东西。EOF是一种状态或条件,由操作系统内核去检测是否达到这种条件。当应用程序从read()中读取到0时,就说明达到EOF条件了。对于磁盘file,EOF表示位置k超过了文件大小。对于网络连接file,EOF表示连接的一端关闭了连接,另一端就会检测到EOF。

2.打开关闭文件

下面详细学习一下open()和close()函数:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/** Returns: new file descriptor if OK, −1 on error */
int open(char *filename, int flags, mode_t mode);

#include <unistd.h>

/** Returns: zero if OK, −1 on error */
int close(int fd);

flags参数指明想要怎样访问文件,它可以由几个模式组合而成:

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:读写
  • O_CREAT:文件不存在则创建
  • O_TRUNC:文件存在则truncate它
  • O_APPEND:写文件前先将位置k置为末尾,即追加模式

mode参数指明新建文件的访问权限,此参数可以省略。要注意的是:每个进程都有一个由umask()函数设置的umask,open()函数最终创建出的文件的访问权限是mode & ~umask

  • S_IRUSR/S_IWUSR/S_IXUSR:Owner的读、写、执行权限
  • S_IRGRP/S_IWGRP/S_IXGRP:Owner所在组中成员的读、写、执行权限
  • S_IROTH/S_IWOTH/S_IXOTH:其他任何用户的读、写、执行权限

3.读写数据

read()和write()至多拷贝n个字节,返回值:-1表示错误,0表示EOF,大于0则表示实际拷贝的字节数。

#include <unistd.h>

/** Returns: number of bytes read if OK, 0 on EOF, −1 on error */
ssize_t read(int fd, void *buf, size_t n);

/** Returns: number of bytes written if OK, −1 on error */
ssize_t write(int fd, const void *buf, size_t n);

ssize_t和size_t有什么区别?
size_t被定义为unsigned int,而ssize_t被定义为int。因为read()和write()要返回-1表示发生错误,所以要使用ssize_t作为返回值。

#include <unistd.h>

int main(int argc, char const *argv[])
{
    char c;

    while(read(STDIN_FILENO, &c, 1) != 0) {
        write(STDOUT_FILENO, &c, 1);
    }

    return 0;
}

在以下几种情况下,read()和write()可能会返回少于n个字节:

  • 没有数据可读了:假如我们以50字节chunk读取,而只有20个字节可读,调用read()就会遇到EOF。
  • 从终端读取:每次只能读取到终端上的一行文本。
  • 读/写Socket:Socket内部的缓冲限制和网络延迟会导致只能读取一部分数据。同理,进程间的Unix管道pipe、IPC等读写的情况也类似。

而当你从硬盘读取数据时,除非读到末尾触发EOF,你永远也不会碰到返回小于n字节的情况。同样写数据时也从不会碰到这种情况。

4.文件元数据

可以通过文件名或描述符获得文件的元数据。

#include <unistd.h>
#include <sys/stat.h>

/** Returns: 0 if OK, −1 on error */
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

/* Metadata returned by the stat and fstat functions */
struct stat {
    dev_t st_dev;       /* Device */
    ino_t st_ino;       /* inode */
    mode_t st_mode;     /* Protection and 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 */
};

例如,下面是一个结合了文件打开关闭和元数据读取的小例子,首先新建文件,然后利用fstat()函数查看新创建文件的访问权限,并以类似ls命令的格式打印出来:

// printf, getchar
#include <stdio.h>
// malloc
#include <stdlib.h>
// open, mode_t
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
// close
#include <unistd.h>

mode_t getumask();
char *mode2str(mode_t mode);

int main(int argc, char const *argv[])
{
    int fd;
    struct stat stat;

    /* 
     * 1.Print default umask. 
     * The typical default value for the process umask is:
     *      S_IWGRP | S_IWOTH  (octal 022)
     */     
    printf("umask: %s\n", mode2str(getumask()));

    // 2.Create a new file
    if ((fd = open("foo.txt", 
                O_WRONLY | O_CREAT, 
                S_IRUSR | S_IWUSR)) == -1) {
        fprintf(stderr, "Create file failed\n");
        exit(1);
    }

    // 3.Check if new file mode = mode & ~mask
    if (fstat(fd, &stat) == 0)
        printf("file mode: %s\n", mode2str(stat.st_mode));
    else
        printf("Get metadata failed\n");

    // 4.Close file
    close(fd);
    return 0;
}

/**
 * There is a getumask(), but only specified in glib_c
 * , not portable.
 * @return      current umask
 */
mode_t getumask()
{
    mode_t mode = umask(0);
    umask(mode);
    return mode;
}

/**
 * Acted like 'ls -l'
 * @param  mode file mode
 * @return      human-read
 */
char *mode2str(mode_t mode)
{
    char *str = malloc(10 * sizeof(char));
    int i = 0;

    str[i++] = S_ISREG(mode) ? 'F' : 'D';

    str[i++] = mode & S_IRUSR ? 'R' : '-';
    str[i++] = mode & S_IWUSR ? 'W' : '-';
    str[i++] = mode & S_IXUSR ? 'X' : '-';

    str[i++] = mode & S_IRGRP ? 'R' : '-';
    str[i++] = mode & S_IWGRP ? 'W' : '-';
    str[i++] = mode & S_IXGRP ? 'X' : '-';

    str[i++] = mode & S_IROTH ? 'R' : '-';
    str[i++] = mode & S_IRWXO ? 'W' : '-';
    str[i++] = mode & S_IXOTH ? 'X' : '-';

    return str;
}

5.文件共享

Unix有很多种共享文件的方式,在学习之前先要了解Unix内核为每个文件维护了哪些数据结构。正是有这些数据结构的存在,才产生了多种多样的共享方式:

  • 描述符表(descriptor table):每个进程都有一张独立的描述符表,每个entry中都保存了打开file的描述符和指向文件表的指针。
  • 文件表(file table):文件表是所有进程共享的,每个entry保存了当前位置,指向文件的描述符的个数(可能有多个)和指向v-node表的指针。当引用个数减少为0时,内核会删除掉该文件表项。
  • v-node表(v-node table):v-node表也是所有进程共享的,每个entry保存了元数据stat的信息。

下面就看一下最常见的三种情况。

5.1 不共享

notshared

5.2 同一文件被打开多次

opentwice

5.3 子进程继承父进程已打开文件

filetableshared

下面是一个小例子,open_twice()尝试重复打开一个文件两次,然后用两次得到的描述符从文件中读取一个字符,结果读到的是’f’,说明v-node表项相同,但file table未发生共享。而inherit_parent_fd()则是打开文件后,fork一个子进程,子进程先读取一个字符,父进程等子进程结束后再读取一个字符,结果读到的是’o’,说明file table发生共享,子进程的读取导致位置k发生偏移

// printf
#include <stdio.h>
// exit
#include <stdlib.h>
// open, seek
#include <fcntl.h>
// read, write
#include <unistd.h>
// wait
#include <wait.h>

void open_twice(char *filename);
void inherit_parent_fd(char *filename);

int main(int argc, char const *argv[])
{
    open_twice("foo.txt");
    inherit_parent_fd("foo.txt");

    return 0;
}

void open_twice(char *filename)
{
    int fd1, fd2;
    char c;

    fd1 = open(filename, O_RDONLY);
    fd2 = open(filename, O_RDONLY);

    read(fd1, &c, 1);
    read(fd2, &c, 1);

    printf("c = %c\n", c);
}

void inherit_parent_fd(char *filename)
{
    int fd;
    char c;

    fd = open(filename, O_RDONLY);
    if (fork() == 0) {
        read(fd, &c, 1);
        exit(0);
    }

    wait(NULL);
    read(fd, &c, 1);
    printf("c = %c\n", c);
}

6.标准I/O

6.1 标准I/O库

ANSI C定义了一组更多层次的I/O函数,叫做标准I/O库(libc),提供了Unix C外的另一种选择:

  • 打开关闭:fopen()和fclose()
  • 读写字节:fread()和fwrite()
  • 读写字符:fgets()和fputs()
  • 格式化I/O:scanf()和printf()

unix-rio

标准I/O库将打开的file抽象为stream。对于程序员来说,stream就是指向FILE类型的指针。stream或者说FILE类型,其实= file描述符 + buffer,其目的就是在内部维护一块缓冲区,从而避免频繁调用开销很大的系统调用。此外,每个ANSI C程序启动时都会自动打开三个stream,对应标准输入、输出、错误流:

#include <stdio.h>

extern FILE *stdin;     /* Standard input (descriptor 0) */
extern FILE *stdout;    /* Standard output (descriptor 1) */
extern FILE *stderr;    /* Standard error (descriptor 2) */

大多数C程序员在其整个职业生涯中都只使用标准I/O库,而不会直接使用底层的Unix I/O,这也是推荐的做法。但标准I/O库在处理网络全双工通信时会有些限制:

  • 限制1:调用output函数后,要调用fflush,fseek,fsetpos或rewind后,才能调用input函数。因为对Socket使用lseek是非法的,所以可以在每次调用input前,先调用fflush重置读取位置能够解决此问题。
  • 限制2:调用input函数后,要调用fseek,fsetpos或rewind,才能调用output函数,否则会遇到EOF。此问题只能为读和写分别打开两个FILE来解决。

6.2 I/O重定向

Unix Shell提供了I/O重定向操作符,例如unix> ls > foo.txt。这个功能是由dup2()函数来完成的。dup2会拷贝oldfd的描述符表entry覆盖到newfd的entry,假如之前newfd是打开状态的,那么dup2会先关闭newfd再开始拷贝。例如,默认stdout对应fd1,假设foo.txt对应fd4,那么dup2(4, 1)会将fd4的描述符表entry覆盖到fd1,所以最终stdout会和fd4一样,都指向foo.txt文件的file table表项。

#include <unistd.h>

/** Returns: nonnegative descriptor if OK, −1 on error */
int dup2(int oldfd, int newfd);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
计算机系统结构大学期末复习资料题库含答案 1.看下述程序段:( C ) k: R5=R2 k+1: R0=R1×R4 k+2: R2=R5+1 k+3: R4=R0×R3 k+4: R3=R4-1 K+5: …… k和k+2之间发生的是什么数据相关 I. 先写后读相关 II.写-写相关 III. 先读后写相关 A.只有I B.只有I、II C.只有I、III D.以上都不对 2.开发并行的途径有( D ),资源重复和资源共享。 A、多计算机系统 B、多道分时 C、分布式处理系统 D、时间重叠 3.在计算机系统设计中,比较好的方法是(D )。 A、从上向下设计 B、从下向上设计 C、从两头向中间设计 D、从中间开始向上、向下设计 4.执行微指令的是(C) a.汇编程序 b.编译程序 c.硬件 d.微指令程序 5. 软件和硬件在(B)意义上是等效的。 A. 系统结构 B.功能 C. 性能 D. 价格 6. 实现汇编语言源程序变换成机器语言目标程序是由(D ) A.编译程序解释 B.编译程序翻译 C.汇编程序解释 D.汇编程序翻译 7. 按照计算机系统层次结构,算术运算、逻辑运算和移位等指令应属于(A)级机器语言。 A. 传统机器语言机器 B.操作系统机器 C. 汇编语言机器 D.高级语言机器 8.对汇编语言程序员,下列(A)不是透明的。 A. 中断字寄存器 B.乘法器 C. 移位器 D.指令缓冲器 9.在采用基准测试程序来测试评价机器的性能时,下列方法按照评价准确性递增的顺序排列是(B )。(1)实际的应用程序方法 (2)核心程序方法 (3)玩具基准测试程序(小测试程序) (4)综合基准测试程序 A.(1)(2)(3)(4) B.(2)(3)(4)(1) C.(3)(4)(1)(2) D.(4)(3)(2)(1) 10. 下列体系结构中,最适合多个任务并行执行的体系结构是( D) A、流水线的向量机结构 B、堆栈处理结构 C、共享存储多处理机结构 D、分布存储多计算机结构 11. 从用户的观点看,评价计算机系统性能的综合参数是( B ): A、指令系统 B、吞吐率 C、主存容量 D、主频率 12. 设指令由取指、分析、执行 3 个子部件完成,每个子部件的工作周期均为△t,采用常规标量单流水线处理机。若连续执行 10 条指令, 则共需时间 ( C )△t。 A.8 B.10 C. 12 D. 14 13. 系统响应时间和作业吞吐量是衡量计算机系统性能的重要指标。对于一个持续处理 业务的系统而言, ( C ) ,表明其性能越好。 A. 响应时间越短,作业吞吐量越小 B. 响应时间越短,作业吞吐量越大 C. 响应时间越长,作业吞吐量越大 D. 响应时间不会影响作业吞吐量 14. 若每一条指令都可以分解为取指、分析和执行三步。已知取指时间t 取指=4△t,分 析时间t 分析=3△t,执行时间t 执行=5△t。如果按串行方式执行完100 条指令需要( C )△t。 A. 1190 B. 1195 C. 1200 D. 1205 15. 如果按照流水线方式执行,执行完100 条指令需要 (B)△t。 A. 504 B. 507 C. 508 D. 510 16. 并行访问存储器最大的问题就是访问冲突大,下面不属于并行访问存储器的缺点的是:( D ) A、取指令冲突 B、读操作数冲突 C、写数据冲突 D、译码冲突 17. 一条4段流水线,每段执行时间为1ns,求该流水线执行100条指令最大效率为(C) A.100% B.96.2% C.97.1% D.388% 18. 假设一条指令的执行过程可以分为“取指令”、“分析”和“执行”三段,每一段的执行时间均为 ,连续执行n条指令所需要花费的最短时间约为(B)(假设仅有“取指令”和“分析”可重叠并假设n足够大): A. B. C. D. 19. MISD是指(C) A.单指令流单数据流 B.单指令流多数据流 C.多指令流单数据流 D.多指令流多数据流 20. 形网络的网络直径和链路数分别为(A)和(D)。 A.N-1 B.N/2 C.2 D.N(N-1)/2 21. 软件和硬件在(B)意义上是等效的。 A.系统结构 B.功能 C.性能 D.价格

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值