Lazy loaded imageICS10 系统级I/O

type
status
date
slug
summary
tags
category
icon
password
page icon
在计算机系统中,输入/输出(I/O)是连接程序与外部世界的桥梁。I/O操作主要涉及两个层次:系统级和C标准库级。在C标准库层面,开发者可以通过调用如fopenfscanffprintf等函数进行文件操作。而在系统层面,程序则通过直接调用Unix系统调用(如openreadwrite)来实现更底层的I/O控制。此外,Robust I/O(RIO)是CSAPP提供的一个经过封装的特殊I/O库,它提供了健壮的错误处理、信号处理以及数据截断等机制。

Unix I/O

文件抽象

Linux系统将所有I/O设备抽象为文件,例如磁盘分区(/dev/sda2)、终端(/dev/tty2)等。内核自身也通过文件形式组织,例如/boot/目录存放内核映像文件。通过open()close()read()write()lseek()这五个基本操作,即可完成对所有类型文件的通用操作。

文件类型

  • 普通文件:包含文本(ASCII/Unicode编码)或二进制数据。在Linux/MacOS上,文本文件以\n作为换行符,而Windows系统使用\r\n组合,这源于打字机时代的回车(Carriage Return)和换行(Line Feed)操作传统。
  • 目录:包含文件链接的特殊文件,每个目录至少包含.(当前目录)和..(父目录)两个条目。
  • 套接字(socket):用于进程间或网络通信的特殊文件,通过读写操作实现数据包收发。
  • 其他类型:包括管道、符号链接等。
需要特别注意的是:系统内核并不区分文本和二进制文件,后缀名只是作为提示;所有文件都视为字节序列处理。

目录操作

每个进程都有内核维护的当前工作目录(Current Working Directory,CWD),路径可分为绝对路径(以/开头)和相对路径,相对路径即从CWD引出。Linux系统中,通过mkdir创建目录,ls查看目录内容,rmdir删除空目录,cd修改当前工作目录。

C语言下的文件操作

打开文件

open函数返回文件描述符,这是操作系统分配给打开文件的整型标识符,标准输入/输出/错误的描述符分别为 STDIN_FILENO(0)、STDOUT_FILENO(1)、STDERR_FILENO(2)。
参数详解
参数
类型
说明
pathname
const char*
文件路径,绝对的或相对的
flags
int
打开模式(必选)和附加选项(可选)的组合
mode
mode_t
文件权限(仅在O_CREAT时生效),由S_IRUSR等权限位组合定义
Flags参数
返回值
  • 成功:返回一个非负整数,表示文件描述符。
  • 失败:返回 1,并设置 errno
可以通过以下示例健壮地打开文件:

文件读写与关闭

C语言中可以使用 readwrite 函数进行文件的读写;使用 close 函数关闭已打开的文件。

短计数现象(Short Counts)

Short Counts指的是 read/write 返回的字节数小于请求值,可能由以下原因引起:
  • 读取时遇到 EOF(文件结尾)
  • 从终端读取行数据,用户按下回车键时被截断
  • 网络套接字读写
  • 信号中断操作
但以下场景不会出现短计数:
  • 常规磁盘文件读写(除非遇到EOF

元数据管理

元数据(metadata)是描述数据的数据,通常包括条目:device,inode,mode(whether protected),uid,gid(group),size。每个文件的元数据由操作系统内核维护。用户可以使用 stat/fstat 访问文件的metadata。

文件信息获取

权限管理

Linux中,可以在通过命令行指令 chmod 修改文件权限:
文件的权限通常由三个八进制数表示,规则如下:
notion image
755 即表示文件所有者的权限是7,也即4+2+1(可读可写可执行);所属组用户和其他用户的权限是5,也即4+1(可读可执行)。

内核文件管理

Linux通过三级结构管理打开文件:
  1. 文件描述符表:每个进程独立维护,以文件描述符为索引,记录进程打开的所有文件(0/1/2为标准输入输出流)。每个条目指向该文件在文件表中的一个条目。
  1. 文件表(Open File Table:所有进程共用,每个条目记录该文件被描述符引用的次数(refcnt)、文件偏移量、引用计数等共享状态。每个条目还指向该文件在v-node表中的条目。
  1. v-node表:存储文件元数据(inode、权限等)
进程可以使用 dup / dup2 使不同的文件描述符指向同一个文件表条目,它们共享文件读写权限和偏移量(也即读写位置)。单个进程多次打开同一个文件,或者多个进程同时打开一个文件,可以使得多个文件表条目指向同一个文件,它们不共享读写权限和偏移量。
进程 fork 时,子进程的描述符表与父进程一致,内核会将进程所有打开文件的文件表条目的refcnt加一。
 
page icon
文件描述符表 (File Descriptor Table)
每个进程都维护一个文件描述符表,其中的每一项是一个文件描述符(一个小整数索引),指向一个文件表项。
文件表 (File Table)
文件表是系统范围的数据结构,用于存储有关打开文件的信息。每个文件表项包含与打开文件相关的状态,例如文件读/写偏移量、访问模式等。支持多个进程通过自己的文件描述符表共享同一个文件表项。
vnode(或 inode)
vnode 是虚拟节点(Virtual Node)的简称,是操作系统中文件系统的抽象层。vnode 表示文件系统中具体文件的元数据和底层信息,是对文件的统一抽象。
特性
多个文件描述符指向同一个文件表项
多个文件表项指向同一个 vnode
共享范围
同一文件表项
同一文件的 vnode
偏移量
共享
独立
文件元数据
通过文件表共享
通过 vnode 共享
访问的数据
同一文件数据
同一文件数据
进程间影响
偏移量改变会影响其他文件描述符
偏移量互不影响
典型场景
dup()fork()
多个进程分别 open() 同一文件

I/O 重定向

在命令行中,可以使用 > 重定向输出,使用 < 重定向输入。例如,ls > file.txtls 命令的输出重定向到 file.txt 文件中。
在C语言中,可以使用 dup2(a, b) 函数将文件描述符表中 fd = a 位置的条目复制到 fd = b 中,覆盖 fd = b 的内容(即将 b 重定向到 a)。例如,b = 1 时,标准输出流将被重定向到 a

标准I/O

标准输入输出函数

标准I/O函数包含在共享库 libc.so 中,函数名通常以 f 开头,例如 fopen/fclosefread/fwritefgets/fputsfscanf/fprintf(格式化输入输出)。这些函数将文件视为流(stream),并使用缓冲区来提高效率。
在包含 stdio.h 头文件时,会定义三个类型为 FILE* 的全局变量,分别表示三个标准流:
  • stdin:标准输入流
  • stdout:标准输出流
  • stderr:标准错误流
例如,使用 fprintf(stdout, "Hello World!") 将字符串写入 stdout 文件中,即使用标准输出流将内容打印在终端上。

含缓冲的输入输出流(Buffered I/O)

动机:使用标准I/O进行读写操作时,每次调用系统调用(system call)都会涉及内核操作,耗时较长。如果每次只读写一个字符,时间代价将非常昂贵。
解决方案:通过使用缓冲区,每次读写一块较长的文本。当用户调用标准输入函数时,先从缓冲区读取数据;当缓冲区为空时,再调用内核进行实际的输入操作。
行缓冲(Line Buffer)是常见的缓冲方式:当输出遇到换行符 \n 时,缓冲区会被刷新(flush)。例如,以下代码:
write(1, "Hello\n", 6) 等效,即一次性将 "Hello\n" 写入标准输出流。

Robust I/O (RIO)

Robust I/O(RIO)实际上是对Unix I/O的封装,增加了错误处理功能,适用于网络服务器等项目。RIO包含两组函数:
  1. 无缓冲区的二进制数据输入输出
    1. rio_readnrio_writen:与Unix的 readwrite 接口相同,接收文件描述符 fd、缓冲区指针 buf 和字节数 n,返回实际传输的字节数。
  1. 带缓冲区的文本/二进制数据输入输出
    1. rio_readlinebrio_readnb:使用 rio_readinitb 来初始化或重置缓冲区。

I/O 的比较

  • Unix I/O:最通用、基础、低级的方式,提供了所有功能。
    • 优点:只有Unix I/O能访问文件元数据;它是异步信号安全的,可以在信号处理函数中使用。
    • 缺点:难以处理短计数(Short Counts),缓冲区容易出错。
  • 标准I/O
    • 优点:使用缓冲区提高了效率;自动处理短计数。
    • 缺点:不能访问文件元数据;不是异步信号安全的;对网络套接字不适用。
  • 使用准则
    • 尽可能使用最高级的I/O函数,例如标准I/O。
    • 处理磁盘、终端文件时,使用标准I/O。
    • 在信号处理函数内部使用Unix I/O;当需要追求极致效率时,使用Unix I/O。
处理二进制文件时,不能使用以下函数:
  • 基于文本的I/O函数:fgetsscanfrio_readlineb
  • 字符串函数:strlenstrcpystrcat
原因是这些函数容易被 0EOF 干扰。处理二进制文件时,应使用 rio_readnrio_readnb

补充

系统I/O库中的缓冲区类型包括:文本缓冲区(text buffer)、行缓冲区(line buffer)和无缓冲区(no buffer)。printfscanf 使用行缓冲区;freadfwrite 使用文本缓冲区。
进程 fork 时会将缓冲区一并复制。为了避免这种情况,可以在 printf 后加上 fflush 清空缓冲区。
 
 
 
 
Prev
ICS09 虚拟内存
Next
ICS11 网络编程
Loading...
Article List
SunVapor的小站
计算机系统导论
文档