Lazy loaded imageOS03 抽象-文件与I/O

type
status
date
slug
summary
tags
category
icon
password

抽象:文件与I/O

概述

Unix/POSIX哲学

Unix和类Unix操作系统遵循核心设计哲学:"一切皆文件"。这一强大抽象意味着磁盘文件、I/O设备(如键盘、显示器)、网络套接字,甚至进程间通信(IPC)机制如管道,都被统一视为文件对象。
这种统一性通过基本系统调用实现:open()read()write()close()。无论操作哪种类型的"文件",程序都使用相同接口。对于设备特定操作,系统提供了ioctl()系统调用来满足特殊I/O需求。
POSIX(可移植操作系统接口)标准正是为促进接口统一性和可移植性而制定的。它定义了操作系统应提供的标准接口,使为一种POSIX兼容系统编写的程序能相对容易地移植到其他POSIX系统上。

文件系统抽象

在POSIX标准中,文件被抽象为简单的字节序列。操作系统不关心这些字节的具体含义(无论是二进制数据还是文本),这种简单抽象提供了极大灵活性。
每个文件都有关联的元数据,即描述文件属性的信息,包括文件大小、创建时间、最后修改时间、所有者、权限位等。
目录是包含其他文件(包括普通文件和子目录)的特殊文件。路径(分为绝对路径和相对路径)唯一标识文件系统中的文件。文件系统主要有两种组织形式:最常见的树状层次结构通过目录嵌套形成,需要特殊数据结构维护这种关系,并允许不同目录下存在同名文件;另一种是键值对存储结构,只需维护文件名到文件内容的映射。

文件路径与进程关联

每个进程都有当前工作目录的概念。进程可使用chdir(const char *path)系统调用改变自己的当前工作目录。
路径解析规则如下:
  • 绝对路径(以/开头)从文件系统根目录开始解析,忽略当前工作目录。
  • ~开头的路径从当前用户的家目录开始解析,同样忽略当前工作目录。
  • 相对路径(如index.html./index.html)从进程的当前工作目录开始解析。

文件I/O的层次结构

文件I/O操作可在不同抽象层次上进行:
  1. 高层次I/O:基于流(stream)的接口,提供缓冲和格式化功能。
  1. 低层次I/O:基于文件描述符的接口,更接近操作系统原语。
  1. 系统调用:直接向内核发出的请求。
  1. 文件系统:管理文件存储、目录结构和索引的核心组件。
  1. I/O驱动:与物理硬件交互的设备驱动程序。

高层次I/O

高层次I/O以"流"的概念为中心。流是一个无格式的字节序列,附带当前位置指针,指示下一个读写操作发生的位置。
C标准库提供了一系列以f开头的高层次I/O函数:
  • FILE *fopen(const char *filename, const char *mode); 打开文件,返回指向FILE结构的指针。失败则返回NULL。
  • int fclose(FILE *f); 关闭文件流。
文件打开模式决定了如何访问文件:
模式参数
描述
"r"
以只读方式打开文件。文件必须存在,否则打开失败。
"w"
以写入方式打开文件。如果文件存在,内容会被清空;如果文件不存在,会创建新文件。
"a"
以追加方式打开文件。如果文件存在,写入的数据会追加到文件末尾;如果文件不存在,会创建新文件。
"r+"
以读写方式打开文件。文件必须存在,否则打开失败。
"w+"
以读写方式打开文件。如果文件存在,内容会被清空;如果文件不存在,会创建新文件。
"a+"
以读写方式打开文件。如果文件存在,写入的数据会追加到文件末尾;如果文件不存在,会创建新文件。
每个程序启动时都会自动打开三个标准流:
  • FILE *stdin:标准输入
  • FILE *stdout:标准输出
  • FILE *stderr:标准错误输出
这些流可以被重定向,例如在命令行中使用|管道符号可将一个进程的输出作为另一个进程的输入。
常用高层次I/O函数总结:
函数名
功能描述
fopen
打开文件并返回文件流指针
fclose
关闭文件流
fread
从文件流读取数据
fwrite
向文件流写入数据
fprintf
格式化输出到文件流
fscanf
从文件流格式化读取
fgetc
从文件流读取一个字符
fputc
向文件流写入一个字符
fgets
从文件流读取一行字符串
fputs
向文件流写入字符串
fseek
设置文件流位置(whence: SEEK_SET/SEEK_END/SEEK_CUR)
ftell
获取当前文件流位置
rewind
将文件流位置重置到开头
feof
检查是否到达文件末尾
ferror
检查文件流错误状态
fflush
刷新文件流缓冲区

低层次I/O

低层次I/O(POSIX I/O)具有以下特征:
  1. 先打开后使用:访问文件前必须显式打开,便于进行权限控制和建立必要的数据结构。
  1. 基于字节:操作系统隐藏数据格式细节,直接操作原始字节。
  1. 显式关闭:使用完毕后必须显式关闭文件。
  1. 内核缓冲读取:内核为进程维护输入缓冲区,读取操作可能阻塞进程。
  1. 内核缓冲写入:内核维护输出缓冲区,写入操作通常在数据移交内核后立即返回。
相关头文件:<fcntl.h>, <unistd.h>, <sys/types.h>
主要低层次I/O函数:
  1. int open(const char *filename, int flags [, mode_t mode])
      • 使用位向量标志(如O_RDONLY, O_WRONLY, O_CREAT等,通过按位或组合)打开文件
      • 返回文件描述符(非负整数),失败返回-1
      • 标准文件描述符:STDIN_FILENO(0), STDOUT_FILENO(1), STDERR_FILENO(2)
      • 可使用int fileno(FILE *stream)获取文件流对应的描述符
  1. int creat(const char *filename, mode_t mode)
      • 创建新文件(等效于open with O_CREAT|O_WRONLY|O_TRUNC)
  1. int close(int fd)
      • 关闭文件描述符
  1. ssize_t read(int fd, void *buffer, size_t maxsize)
      • 读取最多maxsize字节到buffer,返回实际读取字节数,0表示EOF
  1. ssize_t write(int fd, const void *buffer, size_t size)
      • 将buffer中的size字节写入文件,返回实际写入字节数
  1. int dup2(int old, int new)
      • 复制文件描述符
  1. int pipe(int pipefd[2])
      • 创建管道,向pipefd[1]写入的数据可从pipefd[0]读出
  1. off_t lseek(int fd, off_t offset, int whence)
      • 移动文件读写位置(注意:仅修改内核中的文件位置指针,不直接触发磁盘寻道)
  1. int fsync(int fd)
      • 强制将脏数据写入磁盘
  1. int rename(char *old, char *new)
      • 重命名文件,如果新文件名已存在则覆盖
  1. int stat
      • 获取文件元数据

目录操作

  1. int mkdir(char *name, int mode):创建目录
  1. DIR *opendir(char *dir):打开目录,返回DIR结构指针
  1. struct dirent *readdir(DIR *dp):读取目录项,包含文件名、inode号等信息
  1. int closedir(DIR *dp):关闭目录
  1. int rmdir():删除空目录(仅包含...

链接机制

Unix文件系统支持两种链接:
  1. 硬链接:使用ln file file2创建,多个文件名指向同一个inode。inode中维护引用计数,只有当引用计数降为0时文件才被真正删除。
  1. 软链接(符号链接):使用ln -s file file2创建,创建一个特殊的链接文件(类型为l),内容为目标文件路径。如果目标文件被删除,链接将变成悬垂引用。

高层次I/O的优势

高层次I/O在用户空间和内核空间各维护一个缓冲区:
  • fread/fwrite操作的是用户空间的缓冲区
  • 只有在特定条件下(缓冲区满、显式刷新等)数据才会被刷新到内核缓冲区
  • read/write直接操作内核空间的缓冲区
FILE数据结构通常包含:
  • 文件描述符(与内核通信的句柄)
  • 用户空间缓冲区
  • 锁(用于多线程同步)
使用高层次I/O的主要原因:
  1. 减少系统调用开销:通过缓冲区减少用户态和内核态切换次数
  1. 简化内核设计:将复杂的格式化处理放在用户空间
  1. 提供高级功能:如行缓冲、格式化I/O等用户层便利功能

进程状态:文件描述符管理

内核为每个进程维护一个文件描述符表,将文件描述符映射到内核中的打开文件描述结构。打开文件描述包含文件位置、访问模式、引用计数等信息。
关键行为:
  1. fork操作:子进程继承父进程的文件描述符,指向相同的打开文件描述,共享文件位置,引用计数增加。
  1. close操作:减少打开文件描述的引用计数,当计数降为0时真正关闭文件。
  1. dup2操作:复制文件描述符,新描述符指向相同的打开文件描述,共享文件位置,引用计数增加。

使用注意事项

  1. 避免在多线程进程中使用fork:Unix的fork只复制调用线程,如果其他线程持有锁或操作共享数据结构,可能导致死锁或内存泄漏。唯一例外是fork后立即调用execve。
  1. 不要混用高层次和低层次I/O:高层次I/O使用用户空间缓冲区,如果混用低层次I/O直接读写内核缓冲区,可能导致文件位置不一致和数据错误。
Prev
OS02 抽象-线程与进程
Next
OS04 抽象-进程间通信
Loading...
Article List
SunVapor的小站
计算机系统导论
操作系统
文档