目录
一、理解 "文件"
1.1 狭义理解:文件在磁盘里
1.2 广义理解:Linux 下一切皆文件
1.3 文件操作的归类认知
1.4 系统角度
二、回顾 C 文件接口
2.1 打开文件:进程在哪里?
2.2 写文件:fwrite
2.3 读文件:fread 与简易 cat
2.4 输出到显示器的方法
2.5 stdin & stdout & stderr
2.6 打开文件的方式
三、系统文件 I/O
3.1 传递标志位的方法
3.2 写文件(系统接口版)
3.3 读文件(系统接口版)
3.4 open 接口详解
3.5 open 返回值:系统调用 vs 库函数
四、文件描述符 fd
4.1 标准输入 0 & 标准输出 1 & 标准错误 2
4.2 fd 的本质:fd_array 的下标
4.3 文件描述符的分配规则
五、重定向
5.1 输出重定向的本质
5.2 使用 dup2 系统调用
小结(上篇)
一、理解 "文件"
1.1 狭义理解:文件在磁盘里
- 文件存储在磁盘上,磁盘是永久性存储介质,所以文件在磁盘上的存储是永久性的(断电不消失,区别于内存)。
- 磁盘是外设—— 既是输入设备(读文件 = 从磁盘读入),也是输出设备(写文件 = 写到磁盘)。
- 因此,磁盘上的文件本质是:对文件的所有操作,都是对外设的输入和输出,简称 IO。
结论:对文件的操作,本质就是 IO。
1.2 广义理解:Linux 下一切皆文件
Linux 里 "一切皆文件"—— 键盘、显示器、网卡、磁盘…… 这些硬件设备都被抽象成了文件。这是 Linux 的核心哲学,具体抽象实现见下篇。
1.3 文件操作的归类认知
1.4 系统角度
- 对文件的操作,本质是进程对文件的操作;
- 磁盘的管理者是操作系统(OS),用户程序不能直接碰磁盘。
二、回顾 C 文件接口
关键认知:文件读写的本质并不是通过 C/C++ 库函数实现的(库函数只提供方便),真正干活的是文件相关的系统调用接口。
2.1 打开文件:进程在哪里?
#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]:
[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
#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
先看 "有坑" 的写法:
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;
坑有三处:
修正后的简单 cat:
#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 输出到显示器的方法
#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 返回类型)。
#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 传递标志位的方法
#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 写文件(系统接口版)
#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 读文件(系统接口版)
#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 接口详解
#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 → 系统调用接口。
分层图(回忆操作系统概念课):
用户部分: 用户操作接口 / shell / lib 库 / 部分指令
系统软件部分: system call(系统调用接口)
操作系统(内存管理·进程管理·文件管理·驱动管理)
驱动(网卡·硬盘·其他)
硬件部分: 底层硬件(网卡 / 硬盘 / 其他)
可以认为 f* 系列都是对系统调用的封装。 补充:每次系统调用都要用户态↔内核态切换,有开销,所以库函数用缓冲区攒批写入(见下篇缓冲区)。
四、文件描述符 fd
通过 open 知道:文件描述符就是一个小整数。
4.1 标准输入 0 & 标准输出 1 & 标准错误 2
Linux 进程默认有 3 个缺省打开的文件描述符:标准输入 0、标准输出 1、标准错误 2。 物理设备一般是:键盘、显示器、显示器。
可直接读写 0/1/2:
#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 的下标
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.h
- files_struct:/usr/src/kernels/<版本>/include/linux/fdtable.h
- file:/usr/src/kernels/<版本>/include/linux/fs.h
struct task_struct { … struct files_struct *files; … };
struct files_struct { … struct file **fd_array; … };
4.3 文件描述符的分配规则
#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 再打开新文件:
#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 系统调用
#include <unistd.h>
int dup2(int oldfd, int newfd);
把 oldfd 复制到 newfd,让 newfd 指向 oldfd 指向的文件:
#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 有完整实现。
小结(上篇)





