OS03 抽象-文件与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操作可在不同抽象层次上进行:
- 高层次I/O:基于流(stream)的接口,提供缓冲和格式化功能。
- 低层次I/O:基于文件描述符的接口,更接近操作系统原语。
- 系统调用:直接向内核发出的请求。
- 文件系统:管理文件存储、目录结构和索引的核心组件。
- 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)具有以下特征:
- 先打开后使用:访问文件前必须显式打开,便于进行权限控制和建立必要的数据结构。
- 基于字节:操作系统隐藏数据格式细节,直接操作原始字节。
- 显式关闭:使用完毕后必须显式关闭文件。
- 内核缓冲读取:内核为进程维护输入缓冲区,读取操作可能阻塞进程。
- 内核缓冲写入:内核维护输出缓冲区,写入操作通常在数据移交内核后立即返回。
相关头文件:
<fcntl.h>
, <unistd.h>
, <sys/types.h>
主要低层次I/O函数:
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)
获取文件流对应的描述符
int creat(const char *filename, mode_t mode)
- 创建新文件(等效于
open
with O_CREAT|O_WRONLY|O_TRUNC)
int close(int fd)
- 关闭文件描述符
ssize_t read(int fd, void *buffer, size_t maxsize)
- 读取最多maxsize字节到buffer,返回实际读取字节数,0表示EOF
ssize_t write(int fd, const void *buffer, size_t size)
- 将buffer中的size字节写入文件,返回实际写入字节数
int dup2(int old, int new)
- 复制文件描述符
int pipe(int pipefd[2])
- 创建管道,向pipefd[1]写入的数据可从pipefd[0]读出
off_t lseek(int fd, off_t offset, int whence)
- 移动文件读写位置(注意:仅修改内核中的文件位置指针,不直接触发磁盘寻道)
int fsync(int fd)
- 强制将脏数据写入磁盘
int rename(char *old, char *new)
- 重命名文件,如果新文件名已存在则覆盖
int stat
- 获取文件元数据
目录操作
int mkdir(char *name, int mode)
:创建目录
DIR *opendir(char *dir)
:打开目录,返回DIR结构指针
struct dirent *readdir(DIR *dp)
:读取目录项,包含文件名、inode号等信息
int closedir(DIR *dp)
:关闭目录
int rmdir()
:删除空目录(仅包含.
和..
)
链接机制
Unix文件系统支持两种链接:
- 硬链接:使用
ln file file2
创建,多个文件名指向同一个inode。inode中维护引用计数,只有当引用计数降为0时文件才被真正删除。
- 软链接(符号链接):使用
ln -s file file2
创建,创建一个特殊的链接文件(类型为l),内容为目标文件路径。如果目标文件被删除,链接将变成悬垂引用。
高层次I/O的优势
高层次I/O在用户空间和内核空间各维护一个缓冲区:
fread
/fwrite
操作的是用户空间的缓冲区
- 只有在特定条件下(缓冲区满、显式刷新等)数据才会被刷新到内核缓冲区
read
/write
直接操作内核空间的缓冲区
FILE
数据结构通常包含:- 文件描述符(与内核通信的句柄)
- 用户空间缓冲区
- 锁(用于多线程同步)
使用高层次I/O的主要原因:
- 减少系统调用开销:通过缓冲区减少用户态和内核态切换次数
- 简化内核设计:将复杂的格式化处理放在用户空间
- 提供高级功能:如行缓冲、格式化I/O等用户层便利功能
进程状态:文件描述符管理
内核为每个进程维护一个文件描述符表,将文件描述符映射到内核中的打开文件描述结构。打开文件描述包含文件位置、访问模式、引用计数等信息。
关键行为:
- fork操作:子进程继承父进程的文件描述符,指向相同的打开文件描述,共享文件位置,引用计数增加。
- close操作:减少打开文件描述的引用计数,当计数降为0时真正关闭文件。
- dup2操作:复制文件描述符,新描述符指向相同的打开文件描述,共享文件位置,引用计数增加。
使用注意事项
- 避免在多线程进程中使用fork:Unix的fork只复制调用线程,如果其他线程持有锁或操作共享数据结构,可能导致死锁或内存泄漏。唯一例外是fork后立即调用execve。
- 不要混用高层次和低层次I/O:高层次I/O使用用户空间缓冲区,如果混用低层次I/O直接读写内核缓冲区,可能导致文件位置不一致和数据错误。
Prev
OS02 抽象-线程与进程
Next
OS04 抽象-进程间通信
Loading...