目录
[1.1 狭义理解:文件在磁盘里](#1.1 狭义理解:文件在磁盘里)
[1.2 广义理解:Linux 下一切皆文件](#1.2 广义理解:Linux 下一切皆文件)
[1.3 文件操作的归类认知](#1.3 文件操作的归类认知)
[1.4 系统角度](#1.4 系统角度)
[二、回顾 C 文件接口](#二、回顾 C 文件接口)
[2.1 打开文件:进程在哪里?](#2.1 打开文件:进程在哪里?)
[2.2 写文件:fwrite](#2.2 写文件:fwrite)
[2.3 读文件:fread 与简易 cat](#2.3 读文件:fread 与简易 cat)
[2.4 输出到显示器的方法](#2.4 输出到显示器的方法)
[2.5 stdin & stdout & stderr](#2.5 stdin & stdout & stderr)
[2.6 打开文件的方式](#2.6 打开文件的方式)
[三、系统文件 I/O](#三、系统文件 I/O)
[3.1 传递标志位的方法](#3.1 传递标志位的方法)
[3.2 写文件(系统接口版)](#3.2 写文件(系统接口版))
[3.3 读文件(系统接口版)](#3.3 读文件(系统接口版))
[3.4 open 接口详解](#3.4 open 接口详解)
[3.5 open 返回值:系统调用 vs 库函数](#3.5 open 返回值:系统调用 vs 库函数)
[四、文件描述符 fd](#四、文件描述符 fd)
[4.1 标准输入 0 & 标准输出 1 & 标准错误 2](#4.1 标准输入 0 & 标准输出 1 & 标准错误 2)
[4.2 fd 的本质:fd_array 的下标](#4.2 fd 的本质:fd_array 的下标)
[4.3 文件描述符的分配规则](#4.3 文件描述符的分配规则)
[5.1 输出重定向的本质](#5.1 输出重定向的本质)
[5.2 使用 dup2 系统调用](#5.2 使用 dup2 系统调用)
一、理解 "文件"
1.1 狭义理解:文件在磁盘里
- 文件存储在磁盘 上,磁盘是永久性存储介质 ,所以文件在磁盘上的存储是永久性的(断电不消失,区别于内存)。
- 磁盘是外设------ 既是输入设备(读文件 = 从磁盘读入),也是输出设备(写文件 = 写到磁盘)。
- 因此,磁盘上的文件本质是:对文件的所有操作,都是对外设的输入和输出,简称 IO。
结论:对文件的操作,本质就是 IO。
1.2 广义理解:Linux 下一切皆文件
Linux 里 "一切皆文件"------ 键盘、显示器、网卡、磁盘...... 这些硬件设备都被抽象成了文件。这是 Linux 的核心哲学,具体抽象实现见下篇。
1.3 文件操作的归类认知
- 即使是一个 0KB 的空文件,也要占用磁盘空间(要记录属性 / 元数据)。
- 文件 = 文件属性(元数据) + 文件内容。
- 所有文件操作,本质是内容操作 和属性操作两大类。
1.4 系统角度
- 对文件的操作,本质是进程对文件的操作;
- 磁盘的管理者是操作系统(OS),用户程序不能直接碰磁盘。
二、回顾 C 文件接口
关键认知:文件读写的本质并不是通过 C/C++ 库函数实现的(库函数只提供方便),真正干活的是文件相关的系统调用接口。
2.1 打开文件:进程在哪里?
cpp
#include <stdio.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
while(1)
fclose(fp);
return 0;
}
fopen("myfile", "w") 打开的文件在哪个路径?------在程序的当前路径 。系统怎么知道?通过 /proc/[进程id]:
cpp
[hyb@VM-8-12-centos io]$ ps ajx | grep myProc
506729 533463 533463 506729 pts/249 533463 R+ 1002 7:45 ./myProc
[hyb@VM-8-12-centos io]$ ls /proc/533463 -l
total 0
...
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 cwd -> /home/hyb/io
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 exe -> /home/hyb/io/myProc
- cwd:指向当前进程运行目录的符号链接;
- exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
结论:打开文件本质是进程打开。进程知道自己在哪里,即便文件不带路径,OS 也知道文件放哪。
2.2 写文件:fwrite
cpp
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while(count--){
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
2.3 读文件:fread 与简易 cat
先看 "有坑" 的写法:
cpp
while(1){
// 注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp); // 坑:用 strlen(msg) 当读取长度
if(s > 0){
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
坑有三处:
- 读取长度用错 :
fread(buf, 1, strlen(msg), fp)每次最多读strlen(msg)字节,文件若比它长就永远读不完,应用sizeof(buf); - 返回类型 :
fread返回size_t,不是ssize_t; feof时机:只有 "尝试读过且到末尾" 才为真,更稳的是读不满就认为读完。
修正后的简单 cat:
cpp
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("argv error!\n");
return 1;
}
FILE *fp = fopen(argv[1], "r");
if(!fp){
printf("fopen error!\n");
return 2;
}
char buf[1024];
while(1){
int s = fread(buf, 1, sizeof(buf), fp); // 读满缓冲区
if(s > 0){
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
2.4 输出到显示器的方法
cpp
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
2.5 stdin & stdout & stderr
C 默认打开三个输入输出流:stdin、stdout、stderr ,类型都是 FILE*(同 fopen 返回类型)。
cpp
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
2.6 打开文件的方式
表格
| 模式 | 含义 |
|---|---|
r |
只读,流定位在文件开头 |
r+ |
读写,流定位在文件开头 |
w |
截断到零长度或创建,流定位在文件开头 |
w+ |
读写,不存在则创建、存在则截断 |
a |
追加写,不存在则创建,流定位在文件末尾 |
a+ |
读、追加写,读位置在开头,输出总追加到末尾 |
另有 fseek/ftell/rewind 三个定位函数,请自行复习。
三、系统文件 I/O

语言层(fopen/ifstream)不是最底层,系统(内核)才是打开文件最底层的方案 。先学一个小技巧 ------如何给函数传递标志位。
3.1 传递标志位的方法
cpp
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & THREE) printf("flags has THREE! ");
printf("\n");
}
int main() {
func(ONE);
func(THREE);
func(ONE | TWO);
func(ONE | THREE | TWO);
return 0;
}
用位 表示选项,用按位或 | 拼参数,用按位与 & 判存在。open 的 O_* 就是这套设计。
3.2 写文件(系统接口版)
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello bit!\n";
int len = strlen(msg);
while(count--){
write(fd, msg, len); // fd: 文件描述符;msg: 数据首地址;len: 期望写入字节数
}
close(fd);
return 0;
}
write返回值 = 实际写入的字节数。perror根据errno打印错误描述。实际创建权限 =mode & ~umask。
3.3 读文件(系统接口版)
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello bit!\n";
char buf[1024];
while(1){
ssize_t s = read(fd, buf, strlen(msg));
if(s > 0){
printf("%s", buf);
}else{
break; // s==0 读到末尾,s<0 出错
}
}
close(fd);
return 0;
}
read/write 返回值语义 :
write返回实际写入字节数,失败 -1;read返回实际读到字节数,0 表示 EOF,-1 表示出错;都可能 "部分读写"。
3.4 open 接口详解
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname:目标文件;flags:一个或多个常量 "或" 运算;mode:新文件访问权限(配O_CREAT用)。
表格
| 常量 | 含义 |
|---|---|
O_RDONLY |
只读 |
O_WRONLY |
只写 |
O_RDWR |
读写 |
O_CREAT |
不存在则创建(配合 mode) |
O_APPEND |
追加写 |
O_TRUNC |
截断为 0 |
O_RDONLY/O_WRONLY/O_RDWR 必须且只能指定一个。 mode_t 直接 man。0644=rw-r--r--,0666=rw-rw-rw-。用两参还是三参看是否创建文件。
返回值:成功 = 文件描述符(非负小整数),失败 =-1。
3.5 open 返回值:系统调用 vs 库函数
fopen/fclose/fread/fwrite→ 库函数(libc);open/close/read/write/lseek→ 系统调用接口。
分层图(回忆操作系统概念课):
cpp
用户部分: 用户操作接口 / shell / lib 库 / 部分指令
系统软件部分: system call(系统调用接口)
操作系统(内存管理·进程管理·文件管理·驱动管理)
驱动(网卡·硬盘·其他)
硬件部分: 底层硬件(网卡 / 硬盘 / 其他)
可以认为 f* 系列都是对系统调用的封装。 补充:每次系统调用都要用户态↔内核态切换,有开销,所以库函数用缓冲区攒批写入(见下篇缓冲区)。
四、文件描述符 fd
通过 open 知道:文件描述符就是一个小整数。
4.1 标准输入 0 & 标准输出 1 & 标准错误 2
Linux 进程默认有 3 个缺省打开的文件描述符:标准输入 0、标准输出 1、标准错误 2。 物理设备一般是:键盘、显示器、显示器。
可直接读写 0/1/2:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
4.2 fd 的本质:fd_array 的下标
cpp
task_struct files_struct file
│ *files ───────────▶ file **fd_array[] ────▶ 下标0→file* 标准输入
(指针数组) 下标1→file* 标准输出
下标2→file* 标准错误
下标3→file* myfile(新打开)
打开文件时,OS 创建 file 结构体(已打开文件对象,含 inode 元信息、位置、引用计数)。进程 *files 指向 files_struct,其核心是指针数组 fd_array[] ,每个元素指向一个打开的文件。所以文件描述符就是该数组的下标 ------ 拿着 fd 就能找到对应文件。
内核源码验证:
task_struct:/usr/src/kernels/<版本>/include/linux/sched.hfiles_struct:/usr/src/kernels/<版本>/include/linux/fdtable.hfile:/usr/src/kernels/<版本>/include/linux/fs.h
cpp
struct task_struct { ... struct files_struct *files; ... };
struct files_struct { ... struct file **fd_array; ... };
4.3 文件描述符的分配规则
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){ perror("open"); return 1; }
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出 fd: 3(0/1/2 被占)。先 close(0) 或 close(2) 再 open,则 fd: 0 或 fd: 2。
结论:分配规则 = 在 fd_array 中找到当前未被使用的最小下标,作为新文件描述符。
五、重定向
5.1 输出重定向的本质
关闭 1 再打开新文件:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){ perror("open"); return 1; }
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
本应输出到显示器的内容输出到了 myfile,fd=1,这就是输出重定向 。常见符号:>、>>、<。
本质:close(1) 后 fd_array[1] 空出,open 按最小未使用规则把 myfile 放到下标 1。于是往 1 号 fd 写的内容都进了 myfile。重定向 = 改变 fd_array 某下标指向的文件对象。
5.2 使用 dup2 系统调用
cpp
#include <unistd.h>
int dup2(int oldfd, int newfd);
把 oldfd 复制到 newfd,让 newfd 指向 oldfd 指向的文件:
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0) { perror("open"); return 1; }
close(1);
dup2(fd, 1); // 让 1 指向 fd 指向的文件
for (;;) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) { perror("read"); break; }
printf("%s", buf);
fflush(stdout);
}
return 0;
}
为什么 printf 进文件? printf 往 stdout 写,stdout 底层找 fd:1;此时 fd:1 已变成 ./log 的地址,不再是显示器,所以输出都进文件。追加(>>→O_APPEND)和输入(<→dup2 到 0)同理,下篇自定义 shell 有完整实现。
小结(上篇)
- 文件:磁盘永久存储,本质是 IO;一切皆文件。
- C 文件接口 :
fopen/fwrite/fread/fclose,打开文件本质是进程打开(/proc/[pid]/cwd定位)。 - 系统文件 I/O :
open/close/read/write,标志位(位运算)传参。 - 系统调用 vs 库函数 :
f*是对系统调用的封装。 - 文件描述符 fd :从 0 开始的小整数,本质是
fd_array下标,分配规则 = 最小未使用下标。 - 重定向 :改变 fd 指向的文件对象,用
dup2实现。

