跳到主要内容
Linux 系统文件 IO 与重定向原理 | 极客日志
C
Linux 系统文件 IO 与重定向原理 综述由AI生成 讲解 Linux 系统文件 I/O 基础,涵盖文件概念、C 库函数与系统调用的区别、文件描述符 fd 机制及分配规则。重点阐述了标准输入输出重定向的原理,演示了通过 close 和 dup2 系统调用实现输出重定向的代码示例,并提及在 minishell 项目中的实际应用。
SecGuard 发布于 2026/3/27 更新于 2026/6/2 28 浏览一、理解"文件"
1、狭义理解
文件在磁盘里
磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
磁盘是外设(即是输出设备也是输入设备)
磁盘上的文件 本质是对文件的所有操作,都是对外设的输入和输出 简称 IO
2、广义理解
Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)
3、文件操作的归类认知
对于 0KB 的空文件是占用磁盘空间的
文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
所有的文件操作本质是文件内容操作和文件属性操作
4、系统角度
对文件的操作本质是进程对文件的操作
磁盘的管理者是操作系统
文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的
二、回顾 C 文件接口
1、hello.c 打开文件
#include <stdio.h>
int main () {
FILE *fp = fopen("myfile" , "w" );
if (!fp) {
printf ("fopen error!\n" );
}
while (1 );
fclose(fp);
return 0 ;
}
打开的 myfile 文件在哪个路径下?
xz@xzlinux :~ $ ps ajx |head -1;ps ajx |grep catme PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 336450336595336595336435 pts/0 336595 R+ 10000 ./catme pts/ S+ grep--color=auto catme xz ls / / -l total ...... -r--r--r-- xz xz cpuset lrwxrwxrwx xz xz cwd -> -r-------- xz xz environ lrwxrwxrwx xz xz exe -> /catme dr-x------ xz xz fd ......
:
02
336584336599336598336569
1
336598
10000
:
00
@xzlinux
:~
$
proc
336595
0
1
0
May
815
:
34
1
0
May
815
:
34
/home/xz
/z/
IOleran
1
0
May
815
:
34
1
0
May
815
:
34
/home/xz
/z/
IOleran
2
4
May
815
:
34
cwd :指向当前进程运行目录的一个符号链接。
exe :指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此 OS 就能知道要创建的文件放在哪里。
2、hello.c 写文件 #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 ;
}
3、hello.c 读文件 #include <stdio.h>
#include <string.h>
int main () {
FILE *fp = fopen("myfile" , "r" );
if (!fp) {
printf ("fopen error!\n" );
return 1 ;
}
char buff[1024 ];
const char *msg = "hello bit!\n" ;
while (1 ) {
ssize_t s = fread(buff, 1 , strlen (msg), fp);
if (s > 0 ) {
buff[s] = 0 ;
printf ("%s" , buff);
}
if (feof(fp)) {
break ;
}
}
fclose(fp);
return 0 ;
}
#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 ;
}
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 ;
}
5、stdin & stdout & stderr #include <stdio.h>
extern FILE *stdin ;
extern FILE *stdout ;
extern FILE *stderr ;
6、打开文件的方式 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 po‐ sitioned at the beginning of the file.
w+ Open for reading and writing. The file is created if it does not exist, other‐ wise it is truncated. The stream is positioned at the beginning of the file.
a Open forappending(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. Output is always appended to the end of the file. POSIX is silent on what the initial read position is when using this mode. For glibc, the initial file position for reading is at the beginning of the file, but for An‐ droid/BSD/MacOS, the initial file position for reading is at the end of the file.
如上,是文件相关操作。还有 fseek ftell rewind 的函数,在 C 部分已经有所涉猎。
三、系统文件 I/O 打开文件的方式不仅仅是 fopen,ifstream 等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件 IO 之前,先要了解下如何给函数传递标志位,该方法在系统文件 IO 接口中会使用到:
1、一种传递标志位的方法 #include <stdio.h>
#include <string.h>
#define ONE 0001
#define TWO 0002
#define THREE 0004
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 | THREE);
func(ONE | THREE | TWO);
return 0 ;
}
操作文件,除了上面的 C 接口(当然,C++ 也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以系统代码的形式,实现和上面一模一样的代码:
2、hello.c 写文件: #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 xz!\n" ;
int len = strlen (msg);
while (count--) {
write(fd, msg, len);
}
close(fd);
return 0 ;
}
3、hello.c 读文件 #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 ;
}
}
close(fd);
return 0 ;
}
4、接口介绍 #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_APPEND: 追加写
返回值:
mode_t 理解:直接 man 手册,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的 open。
write read close lseek ,类比 C 文件相关接口。
5、open 函数返回值 在认识返回值之前,先来认识一下两个概念:系统调用 和 库函数
上面的 fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数(libc)。
而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
系统调用接口和库函数的关系,一目了然。
所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。
6、文件描述符 fd
通过对 open 函数的学习,我们知道了文件描述符就是一个小整数
6.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 ;
}
而现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 *files, 指向一张表 files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
首先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h(3.10.0-1160.71.1.el7.x86_64 是内核版本,可使用 uname -a 自行查看服务器配置,因为这个文件夹只有一个,所以也不用刻意去分辨,内核版本其实也随意)
6.2、文件描述符的分配规则 #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 ;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main () {
close(0 );
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 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
6.3、重定向 #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。这种现象叫做输出重定向。常见的重定向有:> , >> , <
6.4、使用 dup2 系统调用 #include <unistd.h>
int dup2 (int oldfd, int newfd) ;
#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 下标所表示内容,已经变成了 myfile 的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
6.5、在 minishell 中添加重定向功能 相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online