news 2026/8/27 9:15:17

Linux基础IO:从文件描述符到重定向的底层逻辑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux基础IO:从文件描述符到重定向的底层逻辑

前言

本章通过六部分带你去梳理,基础IO的内容。

一、重新认识文件--从c接口出发

1、文件=属性+内容;

2、进程打开文件后会记录当前工作路径(cwd)。

这里可以通过实验来看

int main()

{

FILE *fp = fopen("myfile", "w");

if(!fp)

{

printf("fopen error!\n");

}

w hile(1);

fclose(fp);

return 0;

}
可以通过ls -l /proc/PID 来查看,此时PID是你运行程序的PID。

此时cwd就是当前工作路径

3、语言层接口(fopen,fwrite,fread)是系统封装函数,方便用户接入。

FILE *fopen(const char *restrict pathname, const char *restrict mode);
这个接口是打开一个文件的,有了就打开,没有就直接创建,mode是类型,看你选择是读还是写。

size_t fread(void ptr[restrict .size * .nmemb], size_t size, size_t nmemb,FILE *restrict stream);
这个接口是读的,从你的文件里读取内容到内存里。

size_t fwrite(const void ptr[restrict .size * .nmemb],size_t size, size_t nmemb, FILE *restrict stream);

这个接口是做写入的,将数据写入到这个文件里。

在这里不做详细介绍,可以取Linux系统中man相关函数去进行详细的了解。

//////////////////////////////////////////////////////////////////////////////////////////////////

int main()
{
FILE* fp=fopen("myfile","w");
char buf[1024];
const char* msg="hello world";
if(!fp)
{
printf("fopen is error\n");
}
int count=5;
while(count--)
{
fwrite(msg,strlen(msg),1,fp);
}
fclose(fp);
fp=fopen("myfile","r");
size_t i;
while((i=(fread(buf,1,sizeof(buf)-1,fp)))>0)
{
buf[i]='\0';
printf("%s\n",buf);
}
fclose(fp);
return 0;
}
上边是一段代码,可以练习读写文件的操作,同时完成一个简易的cat小程序。

二、跨过语言层--使用系统调用

我们上文讲述的fwrite,fopen等函数其实底层调用的就是这里的open,write等的系统调用。

1、系统调用

int open(const char * pathname,int flags,mode_t mode);

这个pathname就是你想要打开的文件。

flags是可以传入多个参数选项,下边列举几个:

O_RDONLY:只读打开

O_WRONLY:只写打开

O_RDWR:读写打开

O_CREAT:若文件不存在就创建

O_APPEND:追加写

返回值:-1:就是失败了,如果成功就会返回文件描述符(fd).

2、ssize_t write(int fd, const void buf[.count], size_t count);

fd就是上文提到的文件描述符,也就是open如果成功打开的返回值,fd是进程内部的资源编号,write,read等系统调用只认fd,也就是说通过fd可以找到你这个文件。

buf这个就是一个数组,在这里返回也就是返回的是buf的指针。这里buf其实就是后文要讲的用户缓冲区,提前了解一下就是,系统调用是有代价的,如果频繁的调用系统调用的话会导致效率低下,所以在这里采用了缓冲区的思想。

count就是第二个参数的大小。

返回值:>0的情况是成功写入了多少字节数

==0的情况在write极少出现,只需要记住>0即可

==-1出错

3、ssize_t read(int fd, void buf[.count], size_t count);

这个与上文的write类似,就不再多说了,着重说一下返回值。

返回值:>0的情况就是读到了多少字节数

==0的情况是读到了文件末尾

==-1就是出错了

代码练习:实现向文件写入5次,然后读取并打印到显示器上

int main()
{
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
int count=5;
const char* msg="hello world\n";
while(count--)
{
write(fd,msg,strlen(msg));
}
close(fd);
fd=open("myfile",O_RDONLY);
size_t i;
char buf[1024];
while((i=read(fd,buf,sizeof(buf)))>0)
{
buf[i]='\0';
printf("%s",buf);
}
close(fd);
return 0;
}

该部分思考

1、open 的返回值是什么?

答:失败了返回-1,成功了返回文件描述符fd。

2、为什么系统调用要传递mode(如0644)?

答:因为文件创建需要权限。

3、write的第三个参数是期望写入的字节数,第二个参数是缓冲区首地址,返回值实际写入的字节数,如果说返回比期望少该怎么办?

答:循环重试。(解析:出现这个情况就是说部分写入部分没有写入,发现写入信号中断就会进行循环重试,移动缓冲区首地址,变相的相当于又有了一块地方可以写入,此时就可以继续写,直到写完为止。)

三、文件描述符的秘密--0、1、2

图片:

1、实验并观察现象

1、先close(0),再open,打印fd,观察现象

int main()
{
close(0);
int fd=open("myfile",O_CREAT|O_RDONLY);
printf("fd的值:%d\n",fd);
return 0;
}

这里可以加一个部分就是可以回顾之前的知识,就是你可以在open那里的代码中第二个参数把O_CREAT去掉,此时就会返回-1,也就是打开失败,为什么呢?可以想一想。

2、再试试close(2),惯出现象。

{
close(2);
int fd=open("myfile",O_CREAT|O_RDONLY);
printf("fd的值:%d\n",fd);
return 0;
}

3、指令:ls -l /proc/PID/fd

复制新的ssh渠道去观察现象(对了记得把close注释掉要不然观察不全)。

会发现0,1,2,3.后续还会讲,此时点一点,这个数字其实就是文件描述符fd。

坑点:

注意:在这里有一个点会非常容易出错,就是open失败的场景,不妨看一下这个代码以及图示

int main()
{
close(2);
pid_t id=getpid();
printf("PID的值是:%d\n",id);
int fd=open("myfile",O_CREAT|O_WRONLY|O_TRUNC,0644);

if(fd<0)

{

printf("error\n");

}
while(1)
{
sleep(1);
}
close(fd);
return 0;
}
乍一看没有错误对吧

看,文件创建失败返回-1,为什么呢?注意如果按照本文代码一点一点的敲的话,会发现open的话之前会创建一个权限是只读的权限,此时如果你修改了权限但是之前的文件没有删除就会open失败,之前的文件是只读的,你如果要写入就会直接失败没有权限,同时可以看得到一个结论就是O_CREAT只管新文件创建。

删除后恢复正常。

2、文件描述符(fd)的分配规则

同样观察现象,代码和上边的代码一样改改就行这里就不放了,就是那个坑点的代码。

1、如果说close(2)的情况下

可以观察到,close(2)之后就少了一个2->/dev/pts/0,反而说原来的/home/.../myfile却被2指了。

2、再观察一组现象,把close(2)改为close(0)

可以观察到,/home/..../myfile又到了0。

也就是说我close哪个就去哪个,这也就是fd分配的规则就是:找当前未被占用的最小下标

3、剩余知识

1、Linux的三个标准流:stdin(0),stdout(1),stderr(2),分别为标准输入,标准输出,标准错误。

2、每一个进程都会有一个file_struct,里边有一个指针数组fd_array[],下标就是文件描述符fd,指向打开的struct_file。

该部分思考

1、为什么fd从3开始?

答:0、1、2都被标准流占用,自己的文件依据分配规则占用最小下标也就是3.

2、为什么操作系统无法用一个整数直接表示复杂文件信息?

答:fd只是一个索引,真正的文件信息都在struct_file,由内核维护。

四、重定向的本质--dup2与shell增强

1、给出代码观察实验现象引出重定向

int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
if(fd==-1)
{
perror("open fail");
}
printf("hello");
fflush(stdout);
close(fd);
return 0;
}

可以观察到,当关闭stdout时,运行hello可执行程序却没有打印,而这个值却放在了myfile里。修改代码并再次观察实验现象。

int main()
{
pid_t id=getpid();
printf("PID:%d\n",id);
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
if(fd==-1)
{
perror("open fail");
}
printf("hello\n");
fflush(stdout);
while(1)
{
sleep(1);
}
close(fd);
return 0;
}

根据fd分配规则,我们创建的文件自然会在fd=1的位置出现。fd=1的为标准输出,通过这个索引可以指向对应的文件,标准情况下就是显示器,所以说没有close(1)的时候能在显示器上看到。当close掉1之后,我们创建的文件就会被1索引,也就是说输出从显示器变到了我们创建的文件,此时计算机并不知道这些,依然写,所以数据就被写入到了我们的文件之中,显示器不显示而cat我们的文件可以发现数据在我们的文件之中。这也就是重定向。

2、dup2

格式:int dup2(int oldfd, int newfd);

部分代码:
int fd=open("myfile",O_WRONLY|O_CREAT,0644);
dup2(fd,1);
printf("hello\n");
fflush(stdout);
可以观察到,dup2第一个参数是旧的fd,第二个参数是新的fd。效果和上边的代码一样,但是这里没有close(1);也就是说让newfd指向oldfd的文件表,会自动关闭newfd打开的文件。自然就不用close了。这也就是dup2的一个大好处。

3、知识点详解

1、重定向的底层本质就是修改fd,也就是修改指向,让某个fd指向另外一个文件。

2、dup2(oldfd,newfd),newfd会复制oldfd的副本,即使newfd已经打开,会被覆盖。

该部分思考

1、重定向的本质是改变了什么?

答:改变了进程的fd数组项,使得原本指向终端的fd指向了别的文件。

2、为什么重定向必须在fork后的子进程中做?

答:因为shell是父进程,如果说修改shell的文件描述符,则会修改I/O环境,后续的进程都会受影响,而创建子进程时会复制父进程的文件描述符,也就是所谓的副本,此时修改文件描述符不会影响父进程。

五、一切皆文件--内核视角

1、内核部分

在这里可以发现,file是一个文件对象,其中最重要的就是有一个f_op指针指向struc file_operations(函数操作表),接下来看看这个函数操作表。

这是一部分,也就是说当用户调用read(fd,buf,n)的时候就会通过fd找到这个struct file此时,再通过调用f_op->read去完成相关操作。下边看一张图片去理解部分内容。

通过这一张图片可以发现,当调用read时会去调用不同硬件的read,和write怎么理解呢?

指令:ls -l /dev。这个指令可以打印硬件设备。

所以可以看到,各个硬件会被抽象成统一设备文件,此时各个硬件实现自己的read,write函数,当open设备的时候,内核把struct_file 里的f_op指针指向该驱动自己的file_operations实例。这体现了一个点就是多态。

该部分思考

1、为什么read系统调用既能读取磁盘文件,也能读键盘?

答:因为read通过fd找到对应的struct file,再通过f_op->read回调到不同的驱动函数。

2、Linux下一切接文件的好处是什么?

答:统一的API,简化开发,方便管理。

六、缓冲区之谜--为什么重定向之后printf出现两次

1、缓冲区类型

全缓冲区:这种缓冲方式要求填满整个缓冲区之后才进行I/O系统调用操作,一般情况下对于磁盘文件的操作通常使用全缓冲的方式访问。

行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符是,标准I/O库函数会执行系统调用操作。

无缓冲区:指的是标准I/O库不对字符进行缓存,直接调用系统调用。

2、代码实验:(简单认识行缓冲和全缓冲)

int main()
{
close(1);
int fd=open("myfile",O_CREAT|O_WRONLY|O_TRUNC,0664);
if(fd<0)
{
perror("open fail");
}
printf("hello world%d:",fd);
close(fd);
return 0;
}

此时可以发现文件内是没有信息的,可以cat看看

默认是全缓冲,此时没有存满用户缓冲区是不会去调用系统函数的,然后close就关掉了,如果想要写入就加一个fflush强制刷新即可,此时就是行缓冲。

int main()
{
close(1);
int fd=open("myfile",O_CREAT|O_WRONLY|O_TRUNC,0664);
if(fd<0)
{
perror("open fail");
}
printf("hello world%d:\n",fd);
close(fd);
return 0;
}

3、实验(认识系统调用无缓冲区和库函数调用有缓冲区)

int main()
{
const char* msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char* msg2="hello write\n";
printf("%s",msg0);
fwrite(msg1,strlen(msg1),1,stdout);
write(1,msg2,strlen(msg2));
fork();
return 0;
}
运行可执行程序可以发现这种情况

正常打印,是因为终端是行缓冲,遇到换行符会立即刷新,所以会直接在终端上看见。

但是当重定向到文件后,cat文件可以看到一个现象。

可以看到系统调用的write是只打印了一次,而使用库函数的两个分别打印了两次。

系统调用是无用户缓冲,也就是说会直接打印在终端上,而重定向到普通文件时数据的缓冲方式就变成了全缓冲,也就是说没有存满是不可能刷新的,此时fork创建子进程,子进程会继承父进程的数据,也就是说此时有了两份数据。进程退出之前会强制刷新,所以就看到了两份数据。

同时,如果说在fork之前刷新用户缓冲区,那么将数据刷洗到内核缓冲区,此时用户缓冲区数据清零,子进程继承不到所以重定向只会去打印一份。(代码不重复了)

该部分思考

1、用户缓冲区是哪个模块提供的?

答:C标准库提供的

2、为什么fork会使缓冲区的内容出现两份?

答:因为firk发生了写时拷贝,子进程会拿到父进程缓冲区的副本,退出时都会刷新。

后记

后续会更新文件系统相关内容,觉得写的不错的可以点点赞关注一下。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/27 9:14:56

MCU供电设计:LDO选型与电源纹波抑制实战指南

1. 为什么MCU系统对电源纹波如此敏感 干嵌入式这行久了&#xff0c;你会发现一个规律&#xff1a;很多莫名其妙的问题&#xff0c;比如串口偶发乱码、ADC采样值跳动、电机控制偶尔抖一下&#xff0c;查了一圈固件逻辑都没毛病&#xff0c;最后拿示波器一探供电引脚&#xff0c;…

作者头像 李华
网站建设 2026/8/27 9:14:52

ASP.NET固定资产管理系统源码解析:从架构设计到二次开发实战

简介&#xff1a;企业级Web应用开发中&#xff0c;三层架构与ORM框架是构建可维护、高性能系统的核心技术基础。三层架构通过分离表现层、业务逻辑层和数据访问层&#xff0c;实现了关注点分离&#xff0c;提升了代码的可测试性和可扩展性。ORM框架如Entity Framework或Dapper&…

作者头像 李华
网站建设 2026/8/27 9:13:07

一行命令骗过锁屏:MouseJiggler 免费鼠标模拟速查

一行命令骗过锁屏&#xff1a;MouseJiggler 免费鼠标模拟速查 【免费下载链接】mousejiggler Mouse Jiggler is a very simple piece of software whose sole function is to "fake" mouse input to Windows, and jiggle the mouse pointer back and forth. 项目地…

作者头像 李华
网站建设 2026/8/27 9:12:54

Switch大气层整合包一次跑通:抓住3个关键位置,新手开机不翻车

Switch大气层整合包一次跑通&#xff1a;抓住3个关键位置&#xff0c;新手开机不翻车 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 大气层整合包系统稳定版&#xff08;Atmosphere-stable…

作者头像 李华
网站建设 2026/8/27 9:11:17

Agent Loop深度解析:从循环原理到AI Agent核心架构与工程实践

最近在技术讨论区看到一条很受关注的话题&#xff1a;“谷歌最重要的人&#xff0c;离职去做的 Loop 有多重要&#xff1f;”标题很吸引眼球&#xff0c;但真正让我感兴趣的&#xff0c;不是某个具体人物或某款产品&#xff0c;而是 Loop 这个词背后的技术含义。在 AI 工程、后…

作者头像 李华
网站建设 2026/8/27 9:10:34

Grok Voice日处理1.5万客服电话:语音Agent生产级落地拆解

过去很多人听到“客服机器人”这几个字&#xff0c;脑子里浮现的还是“请按 1&#xff0c;请按 2”那种让人想直接挂断的语音菜单。但 Starlink 用 Grok Voice 日处理超 1.5 万客服电话这件事&#xff0c;已经不属于这一类了。它真正值得关注的地方不是“用 AI 接了个电话”&am…

作者头像 李华