一、理解文件
a.
文件 = 文件内容 + 文件属性(元数据)
对文件操作:**1.**对内容操作 **2.**对属性操作
b.
访问文件之前需要打开 文件,更需要找到文件,路径+文件名;
fopen打开文件,谁来打开???程序运行时 才打开文件,即进程打开的文件;
访问任何文件都要有路径;有时不写路径,因为进程中有路径的环境变量cwd;
所以思想要从程序与文件的关系转变成进程与文件的关系。
c.
打开文件其实是将文件加载到内存!
d.
Linux中有大量文件,分为:打开的和没打开的
那么文件的位置也就两种:1.内存级被打开的文件;2.磁盘文件。
e.
Linux系统中的大量文件需要管理,那么就需要先描述,在组织 ,那么肯定就有个结构体来组织。
二、文件操作C语言接口
1.打开文件
c
FILE* fopen(const char* filename, const char* mode);
- 返回值 :成功返回文件指针,失败返回
NULL - 模式
| 模式 | 含义 | 文件不存在 | 文件已存在 |
|---|---|---|---|
r |
只读 | 报错 | 从头读 |
w |
只写 | 创建新文件 | 清空重写 |
a |
追加 | 创建新文件 | 末尾写入不换行 |
r+ |
读写 | 报错 | 从头读写 |
w+ |
读写 | 创建新文件 | 清空重写 |
a+ |
读写 | 创建新文件 | 末尾读写 |
2.关闭文件
c
int fclose(FILE* stream);
- 必须关闭文件,否则会内存泄漏、数据丢失
- 成功返回
0,失败返回EOF
3.字符串读写
1) fgets - 读取一行字符串
c
char* fgets(char* str, int n, FILE* stream);c
- 读取最多
n-1个字符,自动加\0,遇到换行 / 文件尾停止 - 成功返回
str,失败 / 到末尾返回NULL
2) fputs - 写入一行字符串
c
int fputs(const char* str, FILE* stream);
- 写入字符串(不含末尾
\0),成功返回非负数,失败返回EOF
4.读写位置
理解<位置> :一个文件不论是有换行还是空格也都是由一个个字符拼接的,所以在磁盘中,文件就类似于一个一维数组 char[]。文件位置就是这个数组的下标。
1) fseek - 移动文件指针
c
int fseek(FILE* stream, long offset, int whence);
offset:偏移量(正数向后,负数向前)whence:起始位置SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件末尾
2) ftell - 获取当前指针位置
c
long ftell(FILE* stream);
- 返回距离文件开头的字节数
3) rewind - 指针回到文件开头
c
void rewind(FILE* stream);
示例:
c
fseek(fp, 5, SEEK_SET); // 从开头偏移5个字节
long pos = ftell(fp); // 获取当前位置
rewind(fp); // 回到开头
三、文件操作系统级调用
1.打开文件
c
int open(const char *pathname, int flags, mode_t mode);
- 返回值:文件描述符 fd(整数),失败 - 1
- 常用 flags
O_RDONLY只读O_WRONLY只写O_RDWR读写O_CREAT不存在则创建O_APPEND追加模式(文件末尾写)O_TRUNC清空原有内容
- mode:权限
0664;当新生成这个文件时需要设置的权限。
2.关闭文件
c
int close(int fd);
- 返回值:成功0;失败-1。
3.写文件
c
ssize_t write(int fd, const void *buf, size_t count);
**注意:**在打开文件时,要加flags的 O_APPEND 追加:永远写末尾,不会自动换行
第三个参数count细节:
- 字符串
c
char str[] = "hello";
write(fd, str, strlen(str));
只写有效字符,不写 \0
- 写整个数组(含末尾 0)
c
write(fd, str, sizeof(str));
会把字符串结束符 \0 也写入文件
**注意:**当写入的是一个指针弄的字符串时,不要用sizeof(),读的是指针的字节8。
四、文件描述符
在文件操作系统调用中,打开文件open()的返回值是:文件描述符 fd(整数),失败 - 1
我们试试多打开几个文件:

发现依次递增,那么前面的0、1、2哪里去了?
其实0、1、2是C默认打开的三个标准流:
c
#include <stdio.h>
extern FILE *stdin; // 0
extern FILE *stdout;// 1
extern FILE *stderr;// 2
那我们要是想在显示器上显示文字,直接把fd参数设置为1就行了!!!
c
write(1, buf, strlen(buf));
文件描述符的本质

文件描述符的分配规则
1. 核心分配规则
(1)从小到大找最小空闲整数分配
(2)关闭某个 fd 后,该数值立刻变为空闲,下次优先复用
(3)新打开文件,优先填补空缺,再往后递增
2. 举例演示
(1)初始占用:0、1、2
(2)第一次 open → 分配3
(3)close (1),释放 1 号
(4)再 open → 优先分配1(最小空闲)
(5)再 open → 分配4
3. 关键特性
- 每个进程独立一套 fd 表,互不干扰
- 进程最大 fd 数量有限,超出报错
- 子进程 fork 会拷贝父进程所有文件描述符
4. 坑点(也可以是重定向)
fd=1 是标准输出 stdout
close(1); 可以执行,但极易出问题
核心坑点
-
printf、puts 全部失效
输出本质往 fd=1 写,关掉后屏幕看不到打印
-
后续打开文件优先抢占 1 号描述符
按最小空闲分配,新 open 直接拿到 1
此时 printf 会写到这个文件里,不再输出屏幕
-
标准错误 fd=2 不受影响,perror、stderr 打印仍可用
示例直观演示
c
close(1); // 关闭标准输出
int fd = open("a.txt",O_WRONLY|O_CREAT,0664);
printf("测试文字");// 不会打印屏幕,全部写入a.txt

衍生常见坑
- 关闭 0:scanf 读键盘失效
- 关闭 2:错误提示看不到
- fork 子进程会继承关闭状态,子进程也无法正常输出
5. 优雅的重定向
调整结构体中的指向文件结构的指针数组;
把要重定向的目标文件结构指针拷贝到1或0中。
(1)系统提供了函数dup2()
c
int dup2(int oldfd, int newfd);
把oldfd 复制覆盖到newfd ,剩下oldfd
- 先关闭 newfd原本占用的文件
- 让 newfd 和 oldfd 指向同一个文件
- 返回值:成功返回
newfd,失败 - 1
(2)输出重定向实例
c
int fd = open("a.txt", O_WRONLY|O_CREAT|O_TRUNC, 0664);
dup2(fd, 1); // 把标准输出1 改成指向文件fd
printf("内容写到文件,不显示屏幕");
close(fd);
(3)关键特点
- 两个 fd 操作同一个文件,读写位置共享
- 关闭其中一个,另一个仍可用
- 执行后原有 newfd 功能失效
五、再看"一切皆文件"
一台设备有着许多硬件(外部设备):
键盘、显示器、网卡、其他设备;访问这些硬件一般就是用IO方法,有的也可能没有,每种设备的访问方法都不一样;

本质竟然还是多态!基类(struct file)->派生类(驱动硬件)。
六、理解缓冲区
1. 文件打开和读写的基本过程

2. 语言级缓冲区
read、write等系统级调用的成本会比较高,在语言级比如C/C++中的输入输出函数,为了在频繁的IO下保持效率,封装IO函数时,并不是读一次或写一次就调用系统级IO函数,而是存在一个语言级的缓冲区!

那语言级缓冲区具体在哪呢?
每一个文件的**FILE**对象中!!!
c
struct FILE {
int fd;
char inbuffer[];
char outbuffer[];
...
}
但观察语言级函数fopen()等:

返回值是FILE*呀,那FILE对象在哪里创建的呢?
其实是库函数fopen内部封装了创建FILE对象。
那么fclose()做的事还要释放FILE对象。
3. 测试缓冲区现象
(1):

解决:在printf写入语言级之后就刷新缓冲区fflush(stdout);


刷新缓冲区的本质
语言级缓冲区通过write(fd)拷贝到内核级缓冲区中;
也就是把用户数据交给了操作系统:不一定写进磁盘。
刷新策略:
- 进程结束时,会自动刷新(自动调用
fclose); - 如果目标文件是显示器:行刷新;
- 普通文件,缓冲区写满了才刷新:全缓冲。
(2):
c
int main() {
printf("hello world");
sleep(2);
exit(0); // 进程结束
return 0;
}

c
int main() {
printf("hello world");
sleep(2);
_exit(0); // 进程终止
return 0;
}

对比exit()和_exit():
exit()是C语言提供的库函数,会冲刷语言级缓冲区;
_exit()是系统级函数,不会冲刷语言级缓冲区。因为它是下层函数,没有上层语言级缓冲区的概念。
(3):
c
int main() {
// 向显示器打印字符串
// C语言
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char* s = "hello fputs\n";
fputs(s, stdout);
// 系统调用
const char* s2 = "hello write\n";
write(1, s2, strlen(s2));
fork();
return 0;
}

现象原因:
- 向文件写,全缓冲;
- 语言级函数有语言级缓冲区,系统函数没有;
- 分支进程。
具体过程:
首先系统级函数write直接将内容写到内核级缓冲区里了;
其次fork后父子进程各自结束,都要将各自的语言级缓冲区刷新至内核级缓冲区,也就刷新了两次语言级缓冲区!
4. 文件内核缓冲区
理解(1):只要用户把数据交给了文件内核缓冲区,就相当于交给了系统!!!
理解(2):OS对文件内核缓冲区的刷新策略:有立即刷新;或等OS不忙了再自主刷新。
当然也存在强制刷新内核缓冲区的系统函数,如:int fsync(int fd);。(sync:同步)
fscyc()针对单个文件生效,将该文件对应的所有脏数据(修改的)、文件元信息(文件大小、修改时间、权限属性等)全部写入磁盘,执行过程阻塞程序,直至磁盘写入完成才返回。
七、标准错误stderr
1. stderr = 标准错误(文件描述符 2)
专门用来输出 "错误信息" 的输出通道。
它和屏幕绑定,但独立于 stdout(1)。
2. 对比三个标准流
0 → stdin 标准输入(键盘)
**1 → stdout 标准输出(屏幕,正常消息) **
2 → stderr 标准错误(屏幕,错误消息)
3. out、err完全独立
stdout(1)
- 放正常运行信息
- 默认行缓冲(遇到 \n 才输出)
- 可以被重定向到文件
stderr(2)
- 放错误、警告、崩溃信息
- 默认无缓冲(立刻输出)
- 为了让你立刻看到错误
- 也可以重定向,但和 stdout 分开重定向
4. 例子
c
printf("hello\n"); // 走 stdout(1) 正常消息
perror("open failed"); // 走 stderr(2) 错误信息
fprintf(stderr, "error"); // 走 stderr(2)
在shell中运行:

printf内容进文件perror内容仍然显示在屏幕上
这就是 stderr 的意义:错误信息不跟着普通输出一起被重定向走!
5. 为什么要设计 stderr?
如果没有 stderr:
- 所有信息混在一起
- 错误信息会被重定向到文件,你看不到崩溃原因
- 程序崩了,你都不知道为啥崩
有了 stderr:
- 正常信息 → 可以重定向走
- 错误信息 → 永远能看到
- 错误输出不缓冲、立刻打印,保证崩溃前能看到错误
4. 例子
c
printf("hello\n"); // 走 stdout(1) 正常消息
perror("open failed"); // 走 stderr(2) 错误信息
fprintf(stderr, "error"); // 走 stderr(2)
在shell中运行:
外链图片转存中...(img-DESJxr1U-1779523730488)
printf内容进文件perror内容仍然显示在屏幕上
这就是 stderr 的意义:错误信息不跟着普通输出一起被重定向走!
5. 为什么要设计 stderr?
如果没有 stderr:
- 所有信息混在一起
- 错误信息会被重定向到文件,你看不到崩溃原因
- 程序崩了,你都不知道为啥崩
有了 stderr:
- 正常信息 → 可以重定向走
- 错误信息 → 永远能看到
- 错误输出不缓冲、立刻打印,保证崩溃前能看到错误