首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Linux基础I/O:C语言文件操作“黑话”指南:读懂文件描述符的摩斯电码

Linux基础I/O:C语言文件操作“黑话”指南:读懂文件描述符的摩斯电码

作者头像
海棠蚀omo
发布2026-01-12 17:25:38
发布2026-01-12 17:25:38
1490
举报

从这篇开始我们就要进入基础I/O部分,其实也就是关于文件的部分,更深层次地去理解关于文件的相关知识,各位看官接着往下看。

在讲解下面的内容之前我们先做一些背景补充,后面的内容也会围绕着这些背景知识展开,了解它们能让我们更好地理解。

一.文件相关的背景知识

1.1各位还记得我们在讲解权限时是怎么描述一个文件的吗?或者说文件是怎样构成的?

没错,答案就是文件 = 内容 + 属性,而我们对文件所做的任何操作,无非就是对文件的内容或者属性进行操作,这点相信大家都能理解。

1.2我们如果要访问一个文件,首先要做的就是把对应的文件给打开,那么为什么要这么做呢?

我们上面说了我们对文件的操作是对文件的内容或者属性进行操作,也就是我们要通过我们写的代码和数据来对它们进行修改等操作对吧,那么要想让我们的代码能够执行,就要把我们的代码和数据加载到内存中,这点毫无疑问。

而只把我们的代码和数据加载到内存中是不够的啊,我们还要对文件进行操作呢,所以为什么要打开文件啊?

答案就是把文件加载到内存中!!!

1.3如果一个文件没有被打开,那么它在哪儿呢?

这点想必大家都清楚,我们自己电脑上的文件都保存在哪儿啊?

没错,就是硬盘,没有打开的文件就会保存在这里。

那么根据上面讲的我们可以把文件分为两类:

1.被打开的文件

2.未被打开的文件

而我们今天要讲的是被打开的文件,而未被打开的文件我们在后面的文件系统中会专门来讲,这里我们就先抛在一边。

1.4我们讲了文件为什么要被打开,那么谁来打开文件?

这个问题也并不难回答,想一想我们的代码和数据是在哪儿呢?

没错,就是进程,因为进程 = 内核数据结构 + 代码和数据,那么既然我们想通过自己写的代码来对文件进行操作,那么本质就是让进程来对文件进行操作,也就是进程来打开文件。

那么该如何打开文件呢?

我们在C语言阶段学过的fopen函数应该就是我们常见的打开文件的方式,但是在我们打开之前文件是处于未被打开的状态的,也就是它此时还在磁盘中,也就是在硬件中,我们如何通过fopen函数来打开文件呢?

既然是硬件,硬件归谁管啊?

操作系统,也就是说我们要使用操作系统中的资源,那么该如何使用呢?

系统调用嘛,所以说我们通过fopen函数来打开文件本质是通过系统调用来完成的,这点我们下面就会讲。

1.5我们能否在一个进程中打开多个文件?

答案当然是可以的,那是不是意味着在OS内,一定会存在大量被打开的文件?

确实如此,那既然OS中存在着大量被打开的文件,那操作系统要不要管理这些被打开的文件呢?

当然要管理,这就和进程是一样的,OS中存在很多进程,所以操作系统要对进程进行管理,那么对于文件来说也是一样的,那要如何进行管理呢?

和管理进程是一样的,先描述,在组织,这六个字贯穿了操作系统的全部,所以和进程一样,一定会存在一种数据结构体是用来描述被打开的文件,就如同PCB一样!!!

以上就是我们要先了解的背景知识,下面开始我们今天的内容。

二.复习C语言的文件接口

因为本篇讲的都是关于文件的相关知识,但是想必我们大家在日常写代码时并不常用关于文件的相关接口,所以这里我们先复习一下关于文件的相关接口。

2.1文件的“ w ”操作

关于文件w操作我们应该都不陌生,就是以写的形式来打开文件,下面我们用一个例子来帮助大家回忆其相关的操作:

我们这里就以w的形式来打开一个文件,并向文件中写了一个字符串,这里大家如果奇怪为什么fopen的第一个参数我没有传路径,可以去看:

Linux操作系统-程序在奔跑,进程在活着:揭开计算机的“生命”奥秘这篇文章,在这篇中我就讲了相关知识,看完大家就理解了。

回到正题,按照上面的操作,我们成功的把一个字符串写入到了文件当中,那么我们再看另一种现象:

我们上面直接在文件中加入了一些内容。但是当我们再次执行后查看文件中的内容,发现我们新增的内容没有了,这是怎么回事呢?这就与w本身又关了,我们来看:

这就是linux中对于w打开文件方式的描述,用大白话来讲就是将文件清空重新写,如果要打开的文件不存在,就新建一个。

上面的话中讲文件清空重新写就是对上面现象的最好解释,正是因为将文件清空了,我们新增的内容就没有了。

而上面的操作我们是通过C语言函数来实现的,我们接着来看系统中的操作:

这种在命令行中进行的操作相信大家都不陌生,我们可以看到通过“ > ”就可以完成和上面一样的操作,这种操作叫做输入重定向

我们在背景知识中说过要想对文件的内容进行操作,那么首先要做的就是打开文件,所以说上面的“ > ”在底层实现中同样也是和上面的w一样,清空文件,在将内容写入到文件中,他们的底层实现都是通过系统调用实现的,这点下面我们就会讲到。

那么相应的如果我们只是用w来打开文件,而不向其中写任何内容,我们借此就可以实现清空文件的操作,这里就不再演示,感兴趣的可以下去试试。

2.2文件的” a “操作

下面我们来看看区别于w的另外一种打开文件的方式a,同样的,这里我们接着上面的例子来说明:

当我们以a的方式来打开文件时,我们再次重复上面的操作,可以发现不同于w,以a方式打开文件并不会清空文件,那么我们来看看linux中是怎样描述a方式的:

针对a的描述用大白话来讲就是打开文件准备接着往后写,如果文件不存在,就新建一个,这个接着往后写意思就是光标会跳到最后面等待着你写新内容。

同样的,上面的操作是用C语言函数来实现的,那么也有系统中的操作:

与上面不同的是,我们这里用的是” >> “来实现和a相同的操作,这种操作叫做追加重定向。和上面的w与” > “一样,” >> “既然要想向文件中写内容,必然要打开文件,所以a和” >> “的底层实现也是通过系统调用完成的。

2.3其他操作

当然呢,关于文件的操作不止有w和a,除了这俩还有上面的一些,这里我没有讲r,因为r就是读文件,没什么可讲的,并且它也不是我们讲的重点,而剩下的后面有+号的都是既可以读又可以写的方式,为什么这些我没有讲呢?

因为我们在操作的过程中,对于又读又写的操作是很少会用的,要么就是读,要么就是写,并且读写文件还要注意读写位置的问题,这很影响我们的操作,所以这里就不再演示。

三.输出信息到显示器,有哪些方法

在讲解后面的内容前,我们依然要先讲一些补充知识来帮助我们更好地去理解。

3.1补充知识

我们来思考一个问题:今天我要向显示器写入12345,我们写了一个int 12345给显示器,还是向显示器写入了’1‘ ’2‘ ’3‘ ’4‘ ‘5’这五个字符?

这个问题的答案想必大家都知道,答案是这5个字符,所以显示器叫做字符设备

换个问题:我今天通过键盘,输入12345,我是输入了’1‘ ’2‘ ’3‘ ’4‘ ‘5’这五个字符,还是输入了int 12345?

这个问题的答案和上面是一样的,输入的同样是字符,所以键盘也叫做字符设备

有了上面的知识我们现在来看一种现象:

这个代码大家都能看懂,我的问题是:为什么printf函数输出int类型的数据要这样写?

上面我们说了我们想显示器写入的是字符,所以这里我们如果要输出一个int类型的数据,就要进行格式化输出!!!

在printf函数的底层实现中就会将int类型的数据转化为字符,之后将其填入字符串中你指定的位置,也就是%d所在的位置。

同样的,这种现象也是如此:

我们通过键盘输入的是一个一个的字符,那么scanf是怎么做到将这些字符转化为一个int类型的数据呢?

答案就和上面是一样的,scanf同样也会进行格式化输出,来将字符转化为int类型的数据。

我们早在初识linux时就说过,在linux中一切皆文件,那么显示器和键盘同样也是文件,其实在windows中也一样,而文件又分为哪些呢?

文本文件和二进制文件,那么这两者又有什么区别呢?

文本文件需要做格式化工作,而二进制文件则不用做格式化工作,这就是二者的区别,所以上面的显示器文件和键盘文件也就是文本文件。

那么此时我再问一个问题:一个文件是文本文件还是二进制文件是因为你调用了不同的接口还是文件本身的属性决定的?

答案当然是后者,这两者的因和果大家要理清楚,是因为文件本身的属性决定了这个文件是文本文件还是二进制文件,而确定了文件的类型后,我们才会去调用不同的接口去进行操作。

而有了上面的这些知识,我们接着往下看:

我们常说,一个进程在启动时会默认打开三个流,也就是我们常说的:stdin,stdout和stderr

这三个也叫做标准输入流,标准输出流和标准错误流,我们观察这三个的类型可以发现它们的类型都是FILE*,FILE*我们知道是什么,在C语言中就是文件指针,也就是这三个其实也是文件。

没错,并且:

这三个文件其实指的就是我们上面说的显示器文件的键盘文件,这个点大家想一下就能明白,我们是怎么知道程序输出的是什么,那不就是通过显示器吗?我们要想向程序中输入数据,那不就是通过键盘吗?

所以这三个对应显示器文件和键盘文件我们也就不奇怪了。

那么此时就会衍生出三个问题:

1.为什么进程在启动时要默认打开这三个文件?

2.这三个文件是什么?

3.这三个文件是怎么打开的?

第二个问题上面我们已经说明了,而第三个问题我们在讲解文件系统时才会来详细说这个问题,所以接下来我们就谈一谈第一个问题:为什么?

我们启动一个进程就是为了利用cpu资源来对我们进程中的代码和数据进行计算,这个目的相信大家都认可。

那么数据从哪儿来呢?

数据需要我们通过键盘来进行输入,那么cpu计算的结果我们怎么知道呢?

将结果打印在显示器上,我们就知道进程运行的结果了,那么如果程序在运行过程中出现异常而导致错误了呢?

此时就需要将异常的原因打印在显示器上,我们才知道是什么原因导致了程序异常并出现错误。

而正是因为上面的这三个问题都需要进程默认打开相应的文件,所以一个进程才要这样做:

用图来演示就如上面所示。

有了上面的这些知识,最后我们就来看看输出信息到显示器,可以用哪些方法。

3.2具体的方法

这里我就列举了四种方式来将数据打印到显示器,而有了上面的知识,我们就知道看似是打印到显示器,其实本质是向stdout中写入,也就是向文件中写入,因为stdout也是FILE*

四.open系统调用函数

上面我们说了fopen等C语言库中的函数底层就是系统调用,下面我就拿我们一定要执行的操作:打开文件来举例,也就是介绍fopen函数的底层实现open系统调用。

上面就是linux中对open函数的介绍,我们看到上图中有两个open函数,并且一个多了mode参数,一个并没有,那么这三个参数各表示什么呢?下面我就来一个个介绍。

我已第二个函数为例,先直接把它们各自代表的含义写出来,第一个参数想必不用过多介绍,后面两个就有的说了。

4.1int flags参数

这个参数我们上面写了就是文件的打开方式,可能有人会疑惑:打开方式上面不是讲了吗?这里为什么还要讲?

因为open函数的传参与fopen并不一样,我们先来看一些东西:

我们先看这几个” 东西 ”,我们只看意思的话除了第三个的O_TRUNC以外,其他的想必都能看出是什么意思。

我来解释一下这个0_TRUNC是什么意思:

我们来看fopen函数中对w的介绍中第一个单词就是Truncate,就是TRUNC的缩写,那么这个宏的意思也就不言而喻,就是清空文件的的意思。

那这些东西到底是什么呢?

不废话,这些“ 东西 ”是,而这些宏是一个数字,而这个数字只有1个比特位是1,也就是说这些数都是2的n次方,可能是1,2,4,8等等,那么这些宏有什么用呢?

是用来传给int flags这个参数的,并且每次不会只传一个数,而是多个数一起传,那么怎么一次性传多个值呢?

这里就要用到位运算,其实也就是利用了位图的思想来进行传参,下面我用一个例子来带大家了解何为位图传递参数:

上面我就以上面的宏一样的方式定义了几个变量,并且都是只有一个比特位是1,而我们通过上面的例子就能看到我们确实通过位运算的方式将多个值传了进去,甚至是将所有的值传进去也同样可以。

从结果我们也可以看到,虽然我们通过位运算只传了一个值进去,但是经过函数中代码后传进去的每个变量都得以执行,而这就是位图传参法!!!

同样的open函数用上面的宏进行传参的方式就是这种方式,通过 | 位运算将多个宏给传进去,而open函数的底层实现中又会通过 & 位运算来处理这些宏,进而实现多种功能。

废话不多说,我们下面以open函数来进行实操:

这里我们就使用open函数创建了log.txt的文件,大家一定很好奇我上面int flags所传的参数为什么是这三个?

O_WRONLY毋庸置疑就是只能写的意思,O_CREAT也就是创建的意思,O_YRUNC是清空的意思,那么这三个组合在一块的作用已经昭然若揭了。

没错,就是复刻了w的效果,w的效果我们上面也介绍了,正是向文件中清空文件并写入内容,如果文件不存在就创建文件,这不正好对应上面的三个宏的意思吗?

现在我们就理解了w在底层调用open函数是是如何进行传参的了,同样的我们来看看a方式是如何传参的:

只需要将后面的O_TRUNC给换成O_APPEND就由w变为了a,因为a并不会清空文件,通过输出的结果我们也可以看到确实创建出了log.txt文件。

但是我们看到创建出来的文件高亮了,并且前面文件的权限怎么还有个s呢?并且文件创建出来的初始权限也不对,这是怎么回事呢?

这就与我们下面要讲的第三个参数mode_t mode有关了,我们接着往下看。

4.2mode_t mode参数

这个参数就是设定文件的初始权限,那要如何操作呢?我们来看:

我们这里就传个666的权限,也就是-rw-rw-rw-形式的权限,但是文件创建出来真会如我们所愿吗?我们来看结果:

看来答案并不是我们想的那样,创建出来的文件权限是-rw-rw-r--,也就是664,为什么会这样呢?

我们之前在讲解文件的权限时就说过,创建出来的文件权限要减去权限掩码,减去后的权限才是文件最终的权限。

而这里我们的权限掩码默认是0002,所以我们传进去的0666要减去0002,也就变成了0664了。

讲到这里想必大家心中会有些许问题,我这里就用两个问题来概括:

1.为什么C语言要封装文件操作接口?

首先很容易想到的原因就是系统调用比较麻烦嘛,封装过后我们只需要了解函数的参数要传那些即可,但是如果要用系统调用函数,我们还得了解上面的各种宏,并且传参时还得注意不同宏之间的搭配,这就显得麻烦了。

但真正的原因肯定不在这里,那么到底是为什么呢?

我们先来思考一下:为什么我们写的C语言代码在windows上能够运行,在linux上能够运行,在macos上也能够运行?

因为C语言具有跨平台性可移植性啊,每种操作系统的系统调用都不尽相同,甚至名字都不一样,而对于不同的操作系统,C语言会对其系统调用进行封装,这也就是跨平台性,而正是因为有了跨平台性才使得C语言的代码不同的平台上也能够运行。

不仅C语言具有跨平台性,java,python等语言都具有跨平台性!!!

那么这里有会衍生出另外一个问题:为什么语言要跨平台?

这里就要有商人的思维了,如果一门语言只支持一个平台上运行,而一个平台的用户是有限的,如果不支持其他的平台,那么就会失去其他平台的用户,一门语言,用的人少,那么后面就会慢慢没落,直至淘汰。

所以语言要跨平台本质就是为了提高语言的竞争力!!!

2.如何做到跨平台?

这里我以C/C++为例,他们两个的跨平台总结来说就是简单粗暴,就是直接把所有平台的系统调用都给封装了,你是哪个系统,我就给你调用相应平台的封装:

在C语言标准中大概就是上面的操作,确实称得上是简单粗暴,这就是为什么C++间隔几年才会更新一次的原因,就是因为它要封装各个平台的系统调用,这是一件漫长的事情。

4.3open的返回值:文件描述符

想必大家看到看到open函数的返回值是一个int类型的数据时想必都很奇怪,下面就要介绍这篇的最后一个重点:文件描述符。

我们看open函数的返回值,成功的话就返回new file descriptor也就是文件描述符,我们先来见见这个文件描述符到底是多少:

我们看到这几个返回值不但不一样,并且还是连续的,那么这个数字到底代表什么呢?

在上面的背景补充知识中,在最后一点中我们说了操作系统要管理被打开的文件,一定就会有一个类似PCB的结构体来保存被打开文件的相关属性,那么这个结构体是什么呢?

这个结构体就是struct file,和PCB一样,在这个结构体中直接或间接地包含了被打开文件的内容和属性,并且各个结构体之间的结构同样也是链表,也就是操作系统对结构体的管理就变为了对链表的增删查改

而在进程的PCB中还有一个指针struct files_struct* files,这个指针指向了一个结构体,这个结构体叫做struct files_struct,叫做文件描述符表

而在文件描述符表中有一个数组叫做struct file* fd_array[],这个数组我们可以看出来它是个指针数组,那么它是干什么的呢?

想必大家都猜得到,没错,它指向的是这个进程中每一个被打开的文件对象,它们的指针就保存在这个数组中,其实也就是指向上面的struct file。

而通过这种方式,就成功地把进程和文件联系起来了:进程的PCB中的struct files_struct* files指针指向了struct files_struct,也就是文件描述符表,而表中的struct file* fd_array[]则保存了指向各个struct file,也就是各个文件的指针。

那么既然是数组,就会有相应的下标,到了这里什么是文件描述符已经很明显了:

文件描述符本质就是数组下标!!!

所以上面我们连着打开了四个文件,这四个文件的文件描述符是连续的,就是操作系统要在这个数组中找空位置来放被打开文件的文件指针。

那么有人就会有有疑问:那为什么文件描述符是从3开始的,而不是从0开始的?

大家回想一下上面我说进程在启动时会默认打开几个文件啊?

三个文件嘛,就是stdin,stdout和stderr,这三个文件不就正好对应了下标0,1,2吗?

所以我们在进程中打开的文件的文件描述符就会从3开始而不是从0开始。

用图来演示更为直接,所以在操作系统的角度识别一个文件,只认:int fd文件描述符!!!

那我们该如何得到这三个文件的文件描述符呢?

那么要解决这个问题就要引入下面的知识,我们接着往下看。

4.4FILE的相关知识

我们知道FILE*在C语言中指的是文件指针,它指向了一个被打开的文件,那么FILE是什么呢?

不卖关子,这个FILE在C语言标准库中是一个结构体,而在讨论这个结构体中有什么内容之前我们先来思考一个问题:上面我们说了,操作系统识别一个文件只认文件描述符,也就是一个整型数据,但是我们C语言库中封装的文件操作函数,如:fopen

它的返回值是一个FILE*啊,那么操作系统是怎么识别这个文件的呢?

答案想必已经在大家心中了,没错,既然操作系统只认文件描述符,那么FILE结构体中一定封装了文件描述符fd!!!

我们都知道C语言的文件操作封装了系统调用,现在我们就知道了C语言不仅仅会对系统调用接口进行封装,还会对数据类型也进行封装

而有了上面关于FILE的知识,那也就是说我们要向打印出stdin,stdout和stderr三者的文件描述符,就要访问它们对应的FILE结构体,从结构体中得到我们想要的文件描述符,那么该如何操作呢?我们来看看:

我们通过->的方式就可以访问FILE结构体中的变量,而我们想要的文件标识符在FILE结构体中名为_fileno,我们通过打印这个变量的值就可以看到这三个文件所对应的文件标识符了。

以上就是C语言文件操作“黑话”指南:读懂文件描述符的摩斯电码的全部内容。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.文件相关的背景知识
  • 二.复习C语言的文件接口
    • 2.1文件的“ w ”操作
    • 2.2文件的” a “操作
    • 2.3其他操作
  • 三.输出信息到显示器,有哪些方法
    • 3.1补充知识
    • 3.2具体的方法
  • 四.open系统调用函数
    • 4.1int flags参数
    • 4.2mode_t mode参数
    • 4.3open的返回值:文件描述符
    • 4.4FILE的相关知识
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档