news 2026/6/9 21:12:18

【Linux系统编程】基础IO第二讲——文件描述符

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux系统编程】基础IO第二讲——文件描述符

文章目录

  • 1. open的返回值
  • 2. 文件描述符
    • 2.1 看看现象
    • 2.2 谈谈原理+看源码(什么是文件描述符)
    • 2.3 补充了解(缓冲区)
    • 2.4 文件描述符的分配规则
  • 3. 代码

上一篇文章中,我们还遗留了一些问题,后面的文章会一一解决。

这篇文章,先来解决:文件描述符。

1. open的返回值

上一篇文章我们已经学过open系统调用的用法了,但是open的返回值我们并没有详细介绍


open的返回值:
成功:返回最小的未使用的文件描述符(非负整数)
失败:返回 -1,并设置 errno 以指示错误原因。

那这篇文章我们就要来解开这个疑惑了

到底什么是文件描述符呢?
又为什么是一个非负整数呢?

2. 文件描述符

2.1 看看现象

再来看我们上篇文章的那段代码:


open的返回值不就是给打开的文件分配的文件描述符吗?(后续的操作如write、close都要使用这个文件描述符)
那我们把这个返回值打印出来看看他是几?

运行

我们看到是3,确实是一个非负整数,但是为什么是3呢?
我们可以再多打开几个文件看看:

我们来重新写一个代码

依次打开4个文件,并打印它们的文件描述符
看看结果

3,4,5,6
目前来看,我们打开的文件,文件描述符好像是从3开始,依次递增的整数。

那为什么是这样呢?这些数字又到底是什么呢?

2.2 谈谈原理+看源码(什么是文件描述符)

一个进程可以打开多个文件,那在操作系统内,就可能同时存在多个进程,它们一共打开了很多的文件。并且文件和进程之间还有一定的从属关系。

那这么多的进程,要不要被操作系统管理起来呢?

当然!
如何管理?
先描述,再组织!
在 Linux 内核中,使用struct file结构体 来描述一个被进程打开的文件。

每个 open 系统调用成功时,内核都会创建一个 struct file 实例,并返回一个文件描述符(fd)与之关联。
struct file内部,会直接或间接地包含与被打开文件相关的属性和数据等信息。
多个struct file之间,用一个双向循环链表组织起来。
那么这样,对文件的管理,就转换成了 对链表的增删查改。

那这么多的文件,如果表示它们与进程之间的从属关系呢?(哪些文件属于哪个进程打开的)

在进程的task_struct中,有一个结构体指针——struct files_struct* files
该指针指向一个struct files_struct结构体,该结构体中有一个成员struct file * fd_array[];,是一个struct file *的结构体指针 数组,那里面的元素不就指向一个个的struct file结构体嘛(就对应了该进程打开的所有文件)
那既然是数组的元素,就有下标啊。
每个文件的struct file*指针 的下标就是该文件的文件描述符。
进程每打开(open)一个文件,就会把对应的struct file结构体的指针存入fd_array数组中(找一个最小的且没被占用的下标位置),这也解释了为什么文件描述符是非负整数
然后返回其对应的下标(即open的返回值),也是该文件的文件描述符。
所以操作系统内部,是使用文件描述符来唯一标识一个被特定进程打开的文件的,这也是为什么我们后面使用write、close这些系统调用必须要传文件描述符(这里也能推断出C语言中的FILE中必定封装了文件描述符fd)。

那为什么我们打开的文件,文件描述符是从3开始呢?也就明白了!


因为进程启动时候,默认打开了三个文件(标准输入、标准输出、标准错误),占用了0,1,2下标,所以我们再打开新的文件,下标就从3开始。

当然我们也可以打印出来标准输入、标准输出、标准错误对应的文件描述符来看看:

上面我们提到C语言中的FILE中必定封装了文件描述符fd
我们可以来看下FILE的定义

而:

它们的类型是FILE*,那我们通过->不就直接可以访问其中的文件描述符成员嘛!

所以,我们运行程序,前三个就应该是0,1,2

没有问题!
在C++中,cin、cout、cerr分别对应标准输入流对象、标准输出流对象、标准错误流对象,它们的类定义中,必定也有一个成员变量是文件描述符。

2.3 补充了解(缓冲区)

之前的文章中我们简单提过用户级缓冲区:

用户级缓冲区是在 FILE 结构体内部维护的。

那其实除了用户缓冲区还有内核缓冲区
同样这篇文章中我们提到:
我们使用printf打印一个字符串,并不是直接输出到显示器上的,而是先会暂存到缓冲区,等待合适的时机刷新到显示器“文件中”(inux下一切皆文件)。
现在应该再完善一点:
我们使用fwrite写入一个字符串到某个文件(以fwrite为例),并不是直接写入到文件的,而是先会暂存到用户缓冲区,即先从用户空间(比如一个字符数组存的字符串/一个常量字符串存在常量区,都在用户空间)到用户缓冲区,fwrite 最终会调用 write 系统调用,write系统调用会把数据从用户缓冲区拷贝到内核缓冲区,然后等待合适的时机,再刷新到文件中(比如内核缓冲区满了),这样可以减少磁盘IO次数,提高效率。
那如果使用的本身就是write这些系统调用,那就是先从用户空间拷贝到内核缓冲区,后面一样。
所以write这些接口,并不是直接把数据写入文件,其本质是一个拷贝函数,最终数据刷新到文件是合适的时机下由OS完成的。
两层缓存:C 库缓冲减少系统调用,内核页缓冲减少磁盘 I/O。当然这两层缓存都要放在内存中,内存是 CPU 可以直接访问的,这符合冯·诺依曼体系结构。

那同样地,读文或修改文件内容也都需要经过对应的缓冲区。
(先了解一下,后面我们还会谈缓冲区)

2.4 文件描述符的分配规则

其实上面已经讲完这个规则了:

新分配的文件描述符,永远是fd_array数组(也叫文件描述符表)中,最小的且没被占用的下标

我们也可以再来多做一些验证:

我们正常打开一个文件,文件描述符就从3开始分配,因为0,1,2被占用了。
那我们调用close可以关闭我们自己打开文件的描述符,当然也可以关闭0,1,2

那我现在把文件描述符0关闭,然后打开一个文件,分配的规则不是分配最小的且没被占用的下标嘛。
那现在我打开的这个文件就应该分配到0,因为现在0没被占用,并且最小

把2关掉

那就是2了。

那把1关掉呢?


那这次运行就应该打印1

但是!
这么什么都没打印呢?
🆗,1对应的是什么啊?
是标准输出(显示器)!
而printf为什么默认往显示器打印啊?
其实是因为printf底层固定是往文件描述符1对应的文件中打印的。

但是我们现在把1关闭了,然后新打开一个文件,所以

所以文件描述符1就不再指向标准输出,而是指向我们打开的log1.txt文件。
所以我们打印的信息就不会写入到显示器了,而是写到我们自己新打开的文件了。(我们把这个叫做重定向,后面会详细讲解重定向
我们来看一下

怎么回事,没有啊

但是,为什么这个文件里也没有呢?

修改代码

最后的close注释掉
然后重新运行

这次文件里面就被写入了1。
怎么回事?为什么close注释掉就有了
或者可以这样

把close放开,但是close之前调用一下fflush

也可以(现在是一个追加写的模式)
那出现这种现象的原因是什么呢?
这个问题先留着,我们后面会解决

这篇文章先到这里,下一篇——重定向

3. 代码

#include<stdio.h>#include<sys/types.h>#include<unistd.h>#include<string.h>#include<sys/stat.h>#include<fcntl.h>intmain(){close(1);intfd1=open("log1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);printf("%d\n",fd1);// 关闭文件fflush(stdout);close(fd1);return0;}// int main()// {// printf("stdin->%d\n", stdin->_fileno);// printf("stdout->%d\n", stdout->_fileno);// printf("stderr->%d\n", stderr->_fileno);// int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 追加写// printf("%d\n", fd1);// printf("%d\n", fd2);// printf("%d\n", fd3);// printf("%d\n", fd4);// // 关闭文件// close(fd1);// close(fd2);// close(fd3);// close(fd4);// return 0;// }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 21:11:18

i.MX 6ULZ物联网处理器选型与开发实战:从核心架构到低功耗设计

1. 项目概述&#xff1a;为什么选择i.MX 6ULZ这颗“经济适用型”物联网心脏在物联网和智能硬件的世界里&#xff0c;选型一颗合适的处理器&#xff0c;就像给一个项目寻找“心脏”。这颗心脏不能太弱&#xff0c;否则带不动复杂的应用逻辑和网络协议&#xff1b;也不能太“费电…

作者头像 李华
网站建设 2026/6/9 21:10:17

计算机网络(4) -- http协议

一、浏览器与服务器通信过程 浏览器与 web 服务器在应用层通信使用的是 HTTP 协议&#xff08;超文本传输协议&#xff09;&#xff0c;而 HTTP协议在传输层使用的是 TCP 协议。那么浏览器需要和 web 服务器三次握手建立连接后&#xff0c;才可以发送 HTTP 请求报文&#xff0c…

作者头像 李华
网站建设 2026/6/9 21:10:15

终极歌词获取指南:如何快速免费下载网易云和QQ音乐LRC歌词

终极歌词获取指南&#xff1a;如何快速免费下载网易云和QQ音乐LRC歌词 【免费下载链接】163MusicLyrics 云音乐歌词获取处理工具【网易云、QQ音乐】 项目地址: https://gitcode.com/GitHub_Trending/16/163MusicLyrics 还在为找不到心爱歌曲的歌词而烦恼吗&#xff1f;1…

作者头像 李华
网站建设 2026/6/9 21:08:09

智能可观测性:基于LLM的日志异常模式挖掘与根因推理

智能可观测性&#xff1a;基于LLM的日志异常模式挖掘与根因推理一、告警风暴与根因迷失&#xff1a;传统可观测性的瓶颈 微服务架构下的可观测性体系通常由日志、指标和链路追踪三大支柱构成。然而在生产环境中&#xff0c;这三者的协同效果远不如预期。一次典型的故障场景&…

作者头像 李华
网站建设 2026/6/9 21:05:21

Kinetis K30 MCU时钟与ADC配置:从FLL/PLL原理到高精度测量实践

1. 项目概述与核心价值在嵌入式系统开发&#xff0c;尤其是对精度和实时性有严苛要求的领域&#xff0c;比如工业传感器、医疗设备或者高精度测量仪器&#xff0c;工程师们常常面临两个核心挑战&#xff1a;如何为MCU提供一个既稳定又灵活的“心跳”——时钟系统&#xff0c;以…

作者头像 李华