深入理解 Linux 系统文件 I/O:从 open 到重定向的底层逻辑》

深入理解 Linux 系统文件 I/O:从 open 到重定向的底层逻辑》

目录

传递标志位

 初识open 

文件操作

写文件操作

读文件操作

open函数返回值

文件描述符

文件描述符的分配规则

重定向

使用 dup2 系统调用

输出重定向

输入重定向

前言

在 Linux 系统中,程序与文件的交互离不开 “系统文件 I/O”—— 这是操作系统为用户层程序提供的一套底层接口,也是理解 “程序如何操作文件” 的核心钥匙。无论是我们日常使用的文本编辑器,还是后台运行的服务程序,其读写文件、处理输入输出的能力,最终都依赖于openwritereaddup2这些系统调用。



但对于很多开发者来说,系统文件 I/O 的知识点常常是零散的:标志位的组合有什么规律?open返回的文件描述符到底是什么?为什么fd=0、1、2总是被默认占用?重定向又是如何通过dup2实现的?这些问题看似独立,实则围绕 “文件描述符” 这一核心概念紧密相连。



本文将以 “文件描述符” 为线索,从 “传递标志位”“初识 open” 入手,逐步拆解写文件、读文件的操作逻辑,再深入讲解文件描述符的分配规则,最终聚焦重定向的实现原理(包括dup2调用与输入输出重定向)。无论你是刚接触 Linux 开发的新手,还是想夯实底层基础的开发者,都能通过本文理清系统文件 I/O 的脉络,理解从接口调用到系统底层的映射关系。

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:

传递标志位

了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:

 #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 1000 void 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返回值

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的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函数返回值

在认识返回值之前,先来认识⼀下两个概念: 系统调用 和 库函数  上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口回忆一下我们讲操作系统概念时,画的一张图

系统调用接口和库函数的关系,一目了然。所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。

文件描述符

 #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);

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.0,1,2对应的物理设备一般是:键盘,显示器,显示器

而现在知道,文件描述符就是从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 的起点,让你在底层开发的道路上走得更稳、更远。

Read more

【优选算法 | 二分查找】二分查找算法解析:如何通过二段性优化搜索效率

【优选算法 | 二分查找】二分查找算法解析:如何通过二段性优化搜索效率

算法相关知识点可以通过点击以下链接进行学习一起加油!双指针滑动窗口 在本篇文章中,我们将深入解析二分查找算法的核心原理。从基本概念到实际应用,带你了解如何利用二分查找高效定位元素,提升搜索效率。无论你是刚接触算法的新手,还是想优化代码性能的老手,二分查找都是你不可忽视的强大工具! 🌈个人主页:是店小二呀 🌈C/C++专栏:C语言\ C++ 🌈初/高阶数据结构专栏: 初阶数据结构\ 高阶数据结构 🌈Linux专栏: Linux 🌈算法专栏:算法 🌈Mysql专栏:Mysql 🌈你可知:无人扶我青云志 我自踏雪至山巅 文章目录 * 34. 在排序数组中查找元素的第一个和最后一个位置(重要) * 二段性(重要/必备知识) * 1.查找左端点 * 2.循环判断条件 * 3. left和right移动方式 * 3.求中点操作 * 4.总结二分模板 * 69.x 的平方根

By Ne0inhk
HDFS数据块机制深度解析:块大小设计与存储哲学

HDFS数据块机制深度解析:块大小设计与存储哲学

HDFS数据块机制深度解析:块大小设计与存储哲学 * 引言:块——HDFS存储的核心抽象 * 一、HDFS默认块大小 * 1.1 版本演进与默认值 * 1.2 查看和验证块大小 * 1.3 配置文件中的设置 * 二、为什么HDFS采用块存储? * 2.1 核心设计思想 * 2.2 详细解析:为什么块存储如此重要? * **2.2.1 减少寻址开销,提升I/O效率** * **2.2.2 支持超大文件,超越单机限制** * **2.2.3 简化存储设计,降低元数据复杂度** * **2.2.4 便于数据复制,增强容错性** * **2.2.5 支持数据本地性,

By Ne0inhk
【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列

【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人方向学习者 ❄️个人专栏:《算法通关指南》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、二叉树深度 * 2.1题目 * 2.2 算法原理 * 2.3代码 * 二、 求先序排列 * 3.1题目 * 3.2 算法原理 * 3.3代码 * 总结与每日励志 前言 本专栏聚焦算法题实战,系统讲解算法模块:以《c++编程》,《数据结构和算法》《基础算法》《算法实战》 等几个板块以题带点,讲解思路与代码实现,帮助大家快速提升代码能力ps:本章节题目分两部分,比较基础笔者只附上代码供大家参考,其他的笔者会附上自己的思考和讲解,希望和大家一起努力见证自己的算法成长 一、二叉树深度 2.

By Ne0inhk
【3D图像算法技术】如何在Blender中对复杂物体进行有效减面?

【3D图像算法技术】如何在Blender中对复杂物体进行有效减面?

在Blender中对复杂物体进行减面(也称为“简化模型”)是平衡Web游戏性能与视觉效果的核心步骤。Web游戏受限于浏览器渲染能力和网络传输效率,通常要求模型面数尽可能低(一般单个模型面数控制在1万面以内,复杂场景需更低),但需保留关键视觉特征(如轮廓、结构细节)。以下是具体流程及算法化实现思路: 一、复杂物体减面的核心流程(手动操作) 1. 准备与分析阶段 * 模型检查:删除冗余数据(如隐藏顶点、孤立顶点、重复材质),确保模型是“流形”(无破面、非流形边)。 * 结构分析:识别模型的“关键区域”(如角色面部、物体轮廓、高曲率细节)和“可简化区域”(如平坦表面、被遮挡部分)。 * 示例:角色模型中,面部和手部是关键区域(需保留细节),背部或衣物内侧是可简化区域。 2. 分阶段减面(核心步骤) 根据模型复杂度,采用“先整体简化,再局部修复”的策略,优先使用Blender内置工具:

By Ne0inhk