目录
[1. 理解"文件"](#1. 理解“文件”)
[1.1 狭义理解](#1.1 狭义理解)
[1.2 广义理解](#1.2 广义理解)
[1.3 文件操作的归类认识](#1.3 文件操作的归类认识)
[1.4 系统角度](#1.4 系统角度)
[2. C文件接口](#2. C文件接口)
[2.1 hello.c打开文件](#2.1 hello.c打开文件)
[2.2 hello.c 写文件](#2.2 hello.c 写文件)
[2.3 hello.c 读文件](#2.3 hello.c 读文件)
[2.4 实现简单cat 指令](#2.4 实现简单cat 指令)
[2.5 输出信息到显示器](#2.5 输出信息到显示器)
[2.6 stdin && stdout && stderr](#2.6 stdin && stdout && stderr)
[2.7 打开文件的方式](#2.7 打开文件的方式)
[3. 系统文件IO](#3. 系统文件IO)
[3.1 hello.c 写文件](#3.1 hello.c 写文件)
[3.2 hello.c 读文件](#3.2 hello.c 读文件)
[3.3 接口介绍](#3.3 接口介绍)
[3.4 open 函数返回值](#3.4 open 函数返回值)
[3.5 文件描述符fd](#3.5 文件描述符fd)
[3.5.1 0 & 1 & 2](#3.5.1 0 & 1 & 2)
[3.5.2 文件描述符的分配规则](#3.5.2 文件描述符的分配规则)
[3.5.3 重定向](#3.5.3 重定向)
[3.5.4 重定向的核心-dup2()](#3.5.4 重定向的核心-dup2())
正文开始:
1. 理解"文件"
1.1 狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设(即是输出设备也是输入设备)
- 磁盘上的文件,本质是对文件的所有操作,都是对外设的输⼊和输出 简称 IO
1.2 广义理解
- Linux 下⼀切皆文件(键盘、显示器、网卡、磁盘...... 这些都是抽象化的过程)
1.3 文件操作的归类认识
- 对于 0KB 的空文件是占用磁盘空间的
- 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
- 所有的文件操作本质是文件内容操作和文件属性操作
1.4 系统角度
- 对文件的操作本质是进程对文件的操作
- 磁盘的管理者是操作系统
- 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的
2. C文件接口
2.1 hello.c打开文件
cpp
#include <stdio.h>
int main()
{
//尝试以写入模式("w")打开名为"myfile"的文件
//如果文件不存在则创建,如果存在则清空内容重新写入
FILE* fp = fopen("myfile", "w");
if (!fp)
{
printf("fopen error!\n");
return 1;
}
fclose(fp); //关闭文件
return 0;
}
打开的myfile文件在哪个路径下?
- 是在程序的当前路径下
系统如何知道程序的当前路径在哪里?
可以使用 ls /proc/进程id -l 命令查看当前正在运行进程的信息:
cpp
[hyb@VM-8-12-centos io]$ ps ajx | grep myProc
506729 533463 533463 506729 pts/249 533463 R+ 1002 7:45 ./myProc
536281 536542 536541 536281 pts/250 536541 R+ 1002 0:00 grep --
color=auto myProc
[hyb@VM-8-12-centos io]$ ls /proc/533463 -l
total 0
......
-r--r--r-- 1 hyb hyb 0 Aug 26 17:01 cpuset
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 cwd -> /home/hyb/io
-r-------- 1 hyb hyb 0 Aug 26 17:01 environ
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 exe -> /home/hyb/io/myProc
dr-x------ 2 hyb hyb 0 Aug 26 16:54 fd
......
其中:
- cwd : 指向当前进程运行目录的一个符号链接
- exe :指向启动当前进程的可执行文件(完整路径)的符号链接
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。
2.2 hello.c 写文件
cpp
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("myfile", "w");
if (!fp)
{
printf("fopen error!\n");
}
const char* msg = "hello linux!\n";
int count = 5;
while (count--)
{
fwrite(msg, strlen(msg), 1, fp); //msg->message
}
fclose(fp);
return 0;
}
cpp
fwrite() 函数是C语言标准库中的一个重要函数,用于向文件写入数据。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
1. const void *ptr
- 指向要写入数据的缓冲区的指针
- 在上述代码中是:
msg(指向字符串"hello linux!\n"的指针)
2. size_t size
- 每个要写入的元素的大小(以字节为单位)
- 在上述代码中是:
strlen(msg)= 13字节。 "hello linux!" = 12字节,'\n' = 1字节 - 字符串结尾的'\0'不会被计入,因为
strlen()不计算空字符
3. size_t nmemb
- 要写入的元素数量
- 在上述代码中是:
1(每次写入1个元素)
4. FILE* stream
- 指向FILE对象的指针,指定输出流
- 在上述代码中是:
fp(指向"myfile"文件的指针)
2.3 hello.c 读文件
cpp
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("myfile", "r");
if (!fp)
{
printf("fopen error!\n");
return 1;
}
char buf[1024];
const char* msg = "hello linux!\n";
while (1)
{
//注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if (s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if (feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
cpp
fread函数原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
void* ptr:
- 指向存储读取数据的内存缓冲区的指针
- 在上述代码中是:
buf(大小为1024的字符数组)
2.4 实现简单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.5 输出信息到显示器
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.6 stdin && stdout && stderr
- C会默认打开三个输入输出流,分别是:stdin、stdout、stderr
- 这三个流的类型都是FILE* ,fopen返回值类型,文件指针
cpp
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
FILE: 是C语言提供的一个结构体,里面封装了文件描述符fd
2.7 打开文件的方式
cpp
r Open text file for reading.
The stream is positioned at the beginning of the file.
r + Open for reading and writing.
The stream is positioned at the beginning of the file.
w Truncate(缩短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.
w+ Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.
3. 系统文件IO
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。
首先要了解如何给函数传递标志位,该方法在系统文件IO接口中会使用到:
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;
}
操作文件,除了上述的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接⼝来进行文件访问, 先来直接以系统代码的形式,实现和上面⼀模⼀样的代码。
3.1 hello.c 写文件
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 | O_TRUNC, 0644); //打开文件
if(fd < 0)
{
perror("open");
return 1;
}
int count = 5; //写入五行
const char *msg = "hello linux!\n";
//此处计算的len不必像C语言层面那样要考虑'\0'而len + 1。
//只需计算要输入内容的len即可,因为这是系统层面的
int len = strlen(msg);
while(count--)
{
write(fd, msg, len);
}
close(fd); //关掉文件
return 0;
}

补充:
cpp
#include <unistd.h>
//期望写入的字节数
ssize_t write(int fd, const void* buf, size_t count);
系统不关心写入方式,可随便写
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 1. 设置文件创建掩码
umask(0);
// - 0644: 文件权限(所有者可读写,其他人只读)
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
int a = 1234567;
int cnt = 1;
while(cnt)
{
//单做字符串来写入
//snprintf将格式化的字符串写入buffer,避免缓冲区溢出
char buffer[16];
snprintf(buffer,sizeof(buffer), "%d", a);
write(fd, buffer, strlen(buffer));
cnt--;
}
close(fd);
return 0;
}
3.2 hello.c 读文件
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 linux!\n";
char buf[1024];
while(1)
{
ssize_t s = read(fd, buf, strlen(msg));//类⽐write
if(s > 0) //返回值大于0说明读取成功
{
printf("%s", buf);
}
else
{
break;
}
}
close(fd);
return 0;
}
3.3 接口介绍
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: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏"或"运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定⼀个且只能指定⼀个
O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限
o_TRUNC: 清空写
O_APPEND: 追加写
返回值:
成功:新打开的⽂件描述符
失败:-1
若是新建文件,需将mode_t mode参数加上用以提供权限,即使用第二个声明
mode_t 可自行借助man手册理解
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
3.4 open 函数返回值
在认识返回值之前,需先认识⼀下这两个概念: 系统调用 和库函数
- 上面的fopen、fwrite、fclose、fread 都是C标准库当中的函数,我们称之为库函数(libc)
- 而open、close、read、write、lseek 都属于系统提供的接口,称之为系统调用接口

由上图系统调用接口和库函数的关系,⼀目了然。
所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便⼆次开发。
3.5 文件描述符fd
文件描述符就是一个小整数
3.5.1 0 & 1 & 2
- Linux进程默认情况下会有三个缺省打开的文件描述符,分别是标准输入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;
}


可知,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体。表示⼀个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
3.5.2 文件描述符的分配规则
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或2再看
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果发现:fd:0 或者fd:2 ,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的⼀个下标,作为新的文件描述符。
3.5.3 重定向
联系上文,若关闭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, 00644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>,<
思考重定向的本质是什么呢?

Linux中重定向的本质是文件描述符fd的复制和绑定操作,其底层是通过系统调用(dup2、open)修改进程的文件描述符表来实现的。其核心原理包括:
1. 文件描述符表
每个进程都有一个文件描述符表,记录了该进程打开的文件、管道、套接字等资源。标准输入(stdin)、标准输出(stdout)、标准错误(stderr)分别对应文件描述符 0、1、2。
2. 重定向的底层操作
当执行重定向时,Shell通过以下步骤实现:
cpp
# 示例:将输出重定向到文件
ls > file.txt
实际发生的系统调用:
- 打开(或创建)file.txt → 获得新文件描述符(假设为3)
- 使用dup2(3, 1) 将文件描述符 3 复制到描述符 1
- 关闭文件描述符 3(如果不再需要)
3. 重定向类型解析
输出重定向:
cpp
> file # 截断写入,相当于 open(file, O_WRONLY|O_CREAT|O_TRUNC)
>> file # 追加写入,相当于 open(file, O_WRONLY|O_CREAT|O_APPEND)
2> file # 重定向 stderr
&> file # 重定向 stdout 和 stderr
- 输入重定向:
cpp
< file # 从文件读取输入
3.5.4 重定向的核心-dup2()
cpp
// 将 oldfd 复制到 newfd,如果 newfd 已打开则先关闭
dup2(oldfd, newfd);
重定向的实现流程:
*
cpp
// 以 "ls > file.txt" 为例
int fd = open("file.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // STDOUT_FILENO = 1
close(fd);
// 现在写入文件描述符1的数据都会进入file.txt
shell执行重定向的顺序:
*
cpp
# 重定向从左到右处理
ls > file1 2>&1 # 先重定向stdout到file1,再重定向stderr到stdout(即file1)
ls 2>&1 > file1 # 先重定向stderr到stdout(终端),再重定向stdout到file1
-
示例代码:
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);
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是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。