深入理解 Linux 系统文件 I/O:从 open 到重定向的底层逻辑》
目录
传递标志位
初识open
文件操作
写文件操作
读文件操作
open函数返回值
文件描述符
文件描述符的分配规则
重定向
使用 dup2 系统调用
输出重定向
输入重定向
前言
在 Linux 系统中,程序与文件的交互离不开 “系统文件 I/O”—— 这是操作系统为用户层程序提供的一套底层接口,也是理解 “程序如何操作文件” 的核心钥匙。无论是我们日常使用的文本编辑器,还是后台运行的服务程序,其读写文件、处理输入输出的能力,最终都依赖于
open
、write
、read
、dup2
这些系统调用。但对于很多开发者来说,系统文件 I/O 的知识点常常是零散的:标志位的组合有什么规律?
open
返回的文件描述符到底是什么?为什么fd=0、1、2
总是被默认占用?重定向又是如何通过dup2
实现的?这些问题看似独立,实则围绕 “文件描述符” 这一核心概念紧密相连。本文将以 “文件描述符” 为线索,从 “传递标志位”“初识 open” 入手,逐步拆解写文件、读文件的操作逻辑,再深入讲解文件描述符的分配规则,最终聚焦重定向的实现原理(包括
dup2
调用与输入输出重定向)。无论你是刚接触 Linux 开发的新手,还是想夯实底层基础的开发者,都能通过本文理清系统文件 I/O 的脉络,理解从接口调用到系统底层的映射关系。
传递标志位
#include <stdio.h>#define ONE_FLAG (1<<0)//0000 0000 0000...0000 0001#define TWO_FLAG (1<<1)//0000 0000 0000...0000 0010#define THREE_FLAG (1<<2) //0000 0000 0000...0000 0100#define FOUR_FLAG (1<<3)//0000 0000 0000...0000 1000void print(int flags){if(flags & ONE_FLAG){printf("One!\n");}if(flags & TWO_FLAG){printf("Two!\n");}if(flags & THREE_FLAG){ printf("Three!\n");}if(flags & FOUR_FLAG){printf("Four!\n");}printf("\n");}int main(){print(ONE_FLAG);print(ONE_FLAG | TWO_FLAG);print(ONE_FLAG | TWO_FLAG | THREE_FLAG);print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);print(TWO_FLAG | FOUR_FLAG);return 0;}
print
函数通过&
运算判断该参数中哪些标志位被设置,从而触发对应的if
分支。这种通过位运算组合标志的方式,能高效地用一个整数表示多种状态,广泛用于选项控制、权限管理等场景。
初识open
pathname: 要打开或创建的目标文件flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。参数:O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限O_APPEND: 追加写返回值:成功:新打开的文件描述符失败:-1
flag,标记位 ;flags的选项
等
mode 权限位
open返回值
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main(){int fd=open("log.txt",O_CREAT | O_WRONLY);if(fd<0){perror("open");return 1;}return 0;}
我们发现创建的文件的权限有问题,是乱的,创建文件需要把权限带上。
修改代码如下
int fd=open("log.txt",O_CREAT | O_WRONLY,0666);
因为umask权限掩码的影响所以权限有点出入,受了系统的影响。可添加umask(0);
所以open提供第三个参数是让我们新建文件时指定权限。
文件操作
写文件操作
#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("log.txt",O_CREAT | O_WRONLY,0666);if(fd<0){perror("open");return 1;}printf("fd=%d\n",fd);int count = 5;const char *msg = "hello world!\n";int len = strlen(msg);while(count--){write(fd, msg, len);//fd: 后?讲, msg:缓冲区?地址, len: 本次读取,期望写?多少个字节的数据。 返回值:实际写了多少字节数据}close(fd);return 0;}
如果我们再次把msg内容改下,再次写入的时候,会发现是覆盖式写入,不是C语言的清空,因为我们还要传递一个清空的标志位即可,这是系统调用,所以是有区别的。
msg="aaaa\n"后
清空写入
所以加一个清空的标志位
// 增加 O_TRUNC 标志,打开时自动清空文件
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
追加写入
int fd=open("log.txt",O_CREAT | O_WRONLY | O_APPEND);
清空和追加写入就想到了C语言的fopen的w和a模式,语言层fopen封装了底层的open。
补充
write在写的时候,是void* ,说明即可字符串写入也可二进制写入等。
文本写入 VS 二进制写入---语言层提供的概念
在系统层面,系统关心写入方式吗?系统不关心!!!---随便写。
二进制写入
int a=123456;while(cnt--){wtrite(fd,&a,sizeof(a)); }
发现是乱码,实际上写的是整型变量a
字符串格式化写入
char buf[16];
snprintf(buf,sizeof(buf),"%d",a);
write(fd,buf,sizeof(buf));
读文件操作
int main(){ umask(0);int fd=open("log.txt",O_RDONLY);if(fd<0){perror("open");return 1;}printf("fd=%d\n",fd);while(1){char buffer[64];int n=read(fd,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;printf("%s",buffer);}else if(n==0){break;}
}
open函数返回值
文件描述符
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <string.h>#include <stdlib.h> int main(){umask(0);int fd1=open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);int fd2=open("log2.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);int fd3=open("log3.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);int fd4=open("log4.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd1<0) exit(1);if(fd2<0) exit(1);if(fd3<0) exit(1);if(fd4<0) exit(1);printf("fd1:%d\n",fd1);printf("fd2:%d\n",fd2);printf("fd3:%d\n",fd3);printf("fd4:%d\n",fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;}
0,1,2去哪里了??? 0,1,2叫做标准输入,标准输入标准错误!!!
在OS接口层面,只认fd,即文件操作符!!!
以前C语言中FILE*fopen,FILE是C语言提供的一个结构体,一定封装了文件操作符。
默认打开的stdin,stdout,stderr封装了0,1,2.
语言的封装增加了平台的可移植性。
printf("stdin->%d\n",stdin->_fileno);
printf("stdout->%d\n",stdout->_fileno);
printf("stderr->%d\n",stderr->_fileno);
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!

文件描述符的分配规则
// close(0);
// close(1);
close(2);
int fd1=open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd1<0) exit(1);printf("fd1:%d\n",fd1);//close(fd1);
这里我们把关闭文件先注释掉,不注释效果一样输出一样。注释掉是后面单独测fd=1。
在没有关闭的情况下,fd1为3,关闭0,为1,关闭1没有输出,关闭2,为2.
文件描述符的分配原则:在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
close(1);
int fd1=open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd1<0) exit(1);
printf("fd1:%d\n",fd1);
观察下面的结果
当我们关闭1时,虽然./myfile没有输出,但是1写进了log1.txt中。
这 种现象叫做输出重定向。
重定向是修改文件描述符的指针指向,数组下标是不变的。
常见的重定向有: > , >> , <
如果我们在文件打开后加上close(1)或者close(fd)后,cat log1.txt文件将不会输出1.(这个现象在后面的缓冲区后面讲解)。
后面这种写法可以输出
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);
return 0;
}
使用 dup2 系统调用
#include <unistd.h>
int dup2(int oldfd, int newfd);
输出重定向
int fd1=open("log1.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd1<0) exit(1);dup2(fd1,1);// close(fd1); //默认向显示器输出printf("fd1:%d\n",fd1);printf("hello C!\n");printf("hello C!\n");fprintf(stdout,"hello stdout!\n");fprintf(stdout,"hello stdout!\n");
这种写法是清空时的输出。
追加 :int fd1=open("log1.txt",O_CREAT | O_WRONLY | O_APPEND,0666);
输入重定向
int fd1=open("log1.txt",O_RDONLY);
if(fd1<0) exit(1);
dup2(fd1,0);
close (fd1);
while(1){char buffer[64];if(!fgets(buffer,sizeof(buffer),stdin)) break;printf("%s",buffer);}
int main(int argc,char *argv[]){if(argc!=2) exit(1); int fd1=open(argv[1],O_RDONLY);if(fd1<0) exit(1);dup2(fd1,0);close (fd1);while(1){char buffer[64];if(!fgets(buffer,sizeof(buffer),stdin)) break;printf("%s",buffer);}
return 0;
}
重定向:打开文件的方式+dup2
结束语
系统文件 I/O 是 Linux 开发的 “基本功”,也是连接用户层程序与内核文件系统的桥梁。从
open
函数的标志位组合,到文件描述符的 “最小未使用” 分配规则,再到dup2
实现重定向的巧妙逻辑,每一个知识点背后,都藏着操作系统 “高效管理资源” 的设计思想 —— 比如用文件描述符简化对文件的引用,用标志位灵活控制文件打开方式,用重定向实现输入输出的灵活切换。掌握这些内容,不仅能让你在编写文件操作代码时更从容(比如避免因标志位错误导致的文件覆盖,或因文件描述符泄漏引发的资源问题),更能帮你理解上层应用的底层逻辑:比如 Shell 的重定向命令
>、<
如何实现,日志输出为何能从控制台转向文件。当然,系统文件 I/O 的学习不止于此 —— 后续还可以深入探究 “文件描述符表与 inode 的关联”“缓冲区与同步机制” 等进阶话题。但只要夯实了本文的基础,再面对更复杂的文件系统问题时,你就能快速抓住核心。希望本文能成为你理解 Linux 系统文件 I/O 的起点,让你在底层开发的道路上走得更稳、更远。