
目录
[1. 进程对文件的管理](#1. 进程对文件的管理)
[1.1 操作系统,进程,文件三者的联系](#1.1 操作系统,进程,文件三者的联系)
[1.2 打开文件的方式](#1.2 打开文件的方式)
[1.3 标准调用库函数](#1.3 标准调用库函数)
[1.4 标准I/O库函数对系统调用的封装](#1.4 标准I/O库函数对系统调用的封装)
[2. 重定向](#2. 重定向)
[3. 文件的缓冲区机制](#3. 文件的缓冲区机制)
1. 进程对文件的管理
1.1 操作系统,进程,文件三者的联系
文件是静态的,是存储在硬盘上的数据。进程是动态的,是程序运行起来的实例,进程像操作系统发起请求,操作系统负责把文件资源安全地分配给进程使用。
交互机制:文件描述符(fd)
进程内部维护一张文件描述符表,操作系统内部维护着文件的信息(inode),通过open,write等系统调用进行交互。一个进程可以同时打开多个文件,多个进程也可以同时打开一个文件。对文件的操作本质上是进程对文件的操作
1.2 打开文件的方式
File* 类型是 C 语言标准库(<stdio.h>)中用来表示文件流的指针类型。在底层File是一个被封装好的结构体,通常包含文件描述符(fd),缓冲区(buffer)和标志位(flags)。flags用来记录文件是以什么方式被打开的
常见的文件打开方式
-
r只读,文件不存在时失败返回NULL
-
w只写,会清空原文件,文件不存在时创建新文件
-
a追加,不清空原文件内容在末尾追加,文件不存在时创建新文件
在 Windows 系统下,打开方式还可以加上 b来区分是否是以二进制模式打开,还是以文末模式(默认)打开,在linux下加不加b效果是一样的,但为了跨平台性,在读写二进制文件时建议加上b来区分
1.3 标准调用库函数
fopen,fclose属于标准I/O库函数,底层封装的是系统调用函数(open,close)
在 Linux 环境下,系统文件 I/O 最核心的四个基础调用函数如下:
- open 打开或创建文件 int open(const char *pathname, int flags, mode_t mode)
返回一个文件描述符(fd),失败返回 -1
-
close 关闭文件,int close(int fd) 释放文件描述符资源,失败返回 -1
-
write 向文件写入数据,ssize_t write(int fd, const void *buf, size_t count)
返回实际写入的字节数,失败返回 -1
- read 从文件读取数据,ssize_t read(int fd, void *buf, size_t count);
返回实际读取的字节数(0代表读到文件末尾),失败返回 -1
open函数的参数
open的第二个参数是位掩码,常用的有:
O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)------这三个必须选一个
O_CREAT(文件不存在则创建)、O_TRUNC(打开时清空文件)、O_APPEND(追加模式)
位掩码是一种编程手段,C语言中通常用宏定义来实现
#define O_RDONLY 00000000 // 二进制: ...0000
#define O_WRONLY 00000001 // 二进制: ...0001
#define O_RDWR 00000002 // 二进制: ...0010
#define O_CREAT 00000100 // 二进制: ...0100
#define O_TRUNC 00001000 // 二进制: ...1000
当第二个参数包含了O_CREAT时需要写第三个参数mode设置文件权限
使用方式如下:
// 对应 fopen("file.txt", "r");
int fd = open("file.txt", O_RDONLY);
// 对应 fopen("file.txt", "w");
// 需要组合:只写 + 创建 + 清空
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
// 对应 fopen("file.txt", "a");
// 需要组合:只写 + 创建 + 追加
int fd = open("file.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
系统不关心你是以文本写入还是二进制写入,它只看字节数,负责把指定字节的内容写入硬盘
1.4 标准I/O库函数对系统调用的封装
当你调用了fopen("log.txt","w")时,C标准库做了三件事:
-
将字符串模式("w","r")翻译成标志位
-
调用系统调用,成功时返回一个fd
-
分配一个FILE结构体,封装了fd,把FILE*指针返回
进程调用文件的具体流程如下
每个进程的 PCB 里确实有一个指针(通常叫 files),指向该进程独有的"文件描述符表",这个表本质上就是一个指针数组 。数组的下标就是我们常说的 fd,数组里的每个元素(指针),指向的是内核维护的一张全局的**"打开文件表"**
【进程空间】 【内核空间】
┌──────────────┐ ┌──────────────────────────────┐
│ 进程 PCB │ │ 文件描述符表 (数组) │
│ (task_struct)│ │ ┌───┬───┬───┬───┬───┐ │
│ │ │ │ 0 │ 1 │ 2 │ 3 │ 4 │ ... │ <-- 数组下标就是 fd (int)
│ files ──────┼────────┼─→│ │ │ │ │ │ │ │ │ │ │
└──────────────┘ │ └─┼─┴─┼─┴─┼─┴─┼─┴─┼─┘ │
│ │ │ │ │ │ │
│ ↓ ↓ ↓ │ │ │
│ stdin stdout stderr │ │ │
│ │ │ │
│ ↓ ↓ │
│ ┌───────────────┐ │
│ │ struct file │ │ <-- 打开文件表 (记录偏移量offset等)
│ │ (打开实例1) │ │
│ │ offset = 50 │ │
│ └───────┬───────┘ │
│ │ │
│ ↓ │
│ ┌───────────────┐ │
│ │ inode │ │ <-- 内存中的 inode (记录磁盘位置、权限等)
│ │ (文件实体信息) │ │
└──────┴───────┬───────┘ │
│ │
↓ │
【磁盘/硬件空间】 │
┌───────────────┐ │
│ 实际文件数据 │ │
│ (Hello World) │ │
└───────────────┘ │
open 系统调用的时候,操作系统确实会把文件的元数据(也就是 inode,包含文件大小、权限、在磁盘上的位置等)加载到内存里。但请注意,文件真正的海量数据(比如一部 2GB 的电影)并不会在 open 时就全部加载到内存 ,而是等你调用 read 时,系统才会按需把数据从磁盘搬运到内存的缓冲区中。
2. 重定向
linux操作系统有一句经典名言,一切皆文件
我们的输入设备,输出设备都是文件,显示器输出信息本质上就是往显示器文件里写内容,当一个新的进程被创建时(比如通过 fork 和 exec),操作系统内核会自动为该进程分配并打开前三个文件描述符(0,1,2)分别对应标准输入,标准输出和标准错误
C 标准库在程序启动时(main 函数执行前),会调用底层的系统接口,将文件描述符 0、1、2 封装成 FILE * 结构体指针,也就是我们熟知的 stdin、stdout 和 stderr
这也就是为什么printf向显示器打印信息,底层调用的就是write,向fd=1的文件里写入数据
那么重定向是什么呢?
一句话总结重定向就是更改文件描述表的指针指向
这样你原本想写入显示器的数据就写到指定文件上了
演示如下

3. 文件的缓冲区机制
缓冲区机制是为了提高效率,在计算机中,用户态(你的程序)和内核态(操作系统)之间的切换是非常消耗资源的。如果你想往磁盘上写入数据,硬盘等存储设备在读写数据时,批量读写的速度远远快于零散读写 。缓冲区机制本质上是一种**"以空间换时间"** 的策略,用内存中的一小块空间(缓冲区),暂时存放数据,通过攒够一波再统一处理的方式,极大地减少了昂贵的系统调用次数,并让硬件的读写更加顺畅
演示如下

C标准库也有缓冲区机制,刷新条件有三种:
1.立即刷新,无缓冲,通常用于报错
2.全缓冲,效率最高,一般用于普通文件
3.行缓冲,用于显示器
进程退出也会进行刷新,也可以fflush强制刷新
总的来说语言层的缓冲区可以自己控制,内核层的缓冲区比较"高冷",规则由操作系统内核的算法决定(主要看时间,内存空闲程度)
子进程会复制父进程的内存区域,包括文件描述符表,环境变量表,缓冲区信息等,而像进程pcb会在自己的内核空间新建+属性拷贝
终