本篇文章,继续和大家分享与Linux相关的知识。本次会涉及的主要内容是文件描述符file descriptor(后续我们简称fd)以及与它相关的知识。

共识原理

文件的共识原理,我们简单做一下梳理。第一个,文件=内容+属性。第二个,文件分为打开的文件和没打开的文件。第三个,打开的文件是谁打开的?是进程。研究打开的文件,本质是研究进程和文件的关系。第四个,没打开的文件放在哪?在磁盘上。我们最关心的是什么问题?没有打开的文件很多,这些文件如何被分门别类的放置好。第五个,根据冯诺依曼体系结构,我们知道,打开的文件必须加载到内存!我们之前说过,一个进程,OS默认会给它打开三个文件,标准输入stdin,标准输出stdout,标准错误stderr这三个文件,这注定了进程和文件的是1:n的关系,也就是一个进程会打开多个文件。这么多的文件,进程需要管理它们,如何管理?先描述,再组织。在内核中,每一个被打开的文件都必须有描述自己的结构体对象。在这个结构体对象里,包含了很多的文件属性,其中一个是结构体对象指针,可以将一个进程打开的文件串联成一个链表。此时,进程对被打开文件的管理,就变成了对链表的增删查改。

Linux-文件fd_Linux的文件操作

回忆C语言的文件接口

fopen

学习C语言时候,我们打开文件用的函数是fopen,它在man手册第三章。它的第一个参数是传文件所在的路径,第二个参数传打开文件的方式,比如说读方式,写方式,读写方式

Linux-文件fd_Linux的文件操作_02

fclose

在我们对文件操作完之后,需要使用fclose函数,关闭相应的文件。fclose函数只用传一个参数FILE*指针。

Linux-文件fd_Linux的文件标识符_03

演示

这里简单演示,怎么使用fopen和fclose

Linux-文件fd_Linux的文件操作_04

我们使用ll指令,查看当前路径下的文件,发现没有我们要打开的文件。

Linux-文件fd_Linux的文件操作_05

编译运行,我们可以发现当前路径下多了一个文件log.txt。此时,我们就能知道fopen函数打开文件的方式。如果该文件存在就正常打开。如果文件不存在就先创建该文件,在打开该文件。

Linux-文件fd_Linux的文件操作_06

可问题来了,fopen函数怎么知道文件要创建在哪?我们在刚刚的代码基础上增加一些代码,让程序别这么快结束,并打印自己pid

Linux-文件fd_Linux的文件标识符_07

编译运行,使用如下指令查看程序的信息

[common_108@iZf8zaj27gxmvq7veqrekfZ ~]$ ll /proc/12307

我们可以发现有一行信息叫当前工作路径,简称cwd。它的路径刚好不就是,我们程序所在的路径吗?当我们调用fopen函数打开函数的时候,如果文件不存在,会先创建文件。如果你第一个参数传给它的是绝对路劲,那么他就会在这个路径下创建文件。如果你给的只是文件名,它会自动把cwd这路径加到文件名前面,于是,我们就看到了文件被创建在我们程序所在的路径下了

Linux-文件fd_文件操作_08

按照我刚刚所说的,如果我们更改了程序的当前工作路径,所创建的文件的位置是不是也会发现更改?是的,我们可以使用chdir来改变程序的cwd,看看效果

Linux-文件fd_文件操作_09

删除刚刚创建的文件log.txt,我们再次编译运行程序。再次查看当前路径下的文件,我们发现没有log.txt这个文件存在,它去哪了呢?因为我们更改了程序的cwd,它被创建到了/home/common_108这个路径下

Linux-文件fd_Linux的文件标识符_10

fwrite

向文件写入的数据的接口有很多,我们这里使用fwrite。fwrite的第一个参数传你要写入文件的内容所在的位置,也就是指针。第二个参数,写入文件的内容的大小。第三个参数,传个数,你要写入该内容多少次。第四个参数,传FILE*,告诉它你要写入到那个文件里。

Linux-文件fd_文件操作_11

我们把刚刚的代码中的部分无关代码注释掉,并调用fwrite函数对文件进行写入。这里有一个问题:C语言的字符串会以'\0'字符结尾,我们在使用strlen计算字符串长度的时候,需不需要加一?答案是,不需要。因为那是C语言的的规定,和我文件有什么关系。

Linux-文件fd_文件操作_12

编译运行,我们就能查看到文件里写入了“hello Linux message”信息

Linux-文件fd_Linux的文件操作_13

我们将写入数据改为abcd\n,再次编译运行。

Linux-文件fd_Linux的文件操作_14

编译运行,并查看log.txt文件。我们可以发现原来的内容,不见了。

Linux-文件fd_Linux的文件操作_15

这是什么原因?这是因为使用fopen以w方式打文件,每次写入数据前,都会对文件进行清空处理。

Linux-文件fd_文件操作_16

我们可以简单验证一下,这次我们不调用fwrite函数,只调用fopen函数

Linux-文件fd_文件操作_17

编译运行,我们使用cat查看log.txt发现什么内容也没有

Linux-文件fd_文件操作_18

大家还记不记得重定向符号‘>’的功能,重定向一个不存在的文件,会创建这个文件。当我们使用重定向向已存在内容的文件里,写入数据,它也会先清空文件,再进行写入。是不是和以w方式调用fopen函数的功能几乎一样呢?

Linux-文件fd_Linux的文件操作_19

如果你想在已有的内容后面追加内容,则采用a的方式来调用fopen函数。

fprintf

这次写入数据我们换个函数,用fprintf。fprintf函数的第一个参数传FILE*指针,告诉它,你要向那个文件写入。第二个参数,类似于printf的用法,打印的格式

Linux-文件fd_Linux的文件标识符_20

我来演示一下

Linux-文件fd_Linux的文件标识符_21

编译运行,我们就可以看到每次运行之后,文件log.txt的内容逐渐增加

Linux-文件fd_Linux的文件操作_22

未打开的文件放在哪里?放在磁盘里,那是不是对文件的操作就注定了要对磁盘硬件操作?我们刚刚回忆的四个函数,是库函数,它对文件进行操作,需要使用硬件。库函数是用户的代表,不能直接访问硬件,需要通过操作系统来完成。所以,fopen这四个库函数,一定封装了系统接口。

过渡到系统,认识文件系统调用

我们来认识一下,操作系统为文件操作提供的接口。

open

open函数有两个,我们先来了解第一个。它的第一个参数,传路径名称,也是告诉它你要打开的那个文件。第二个参数,是一个整数,这个参数有固定的选项。常用选项有五个:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写),O_CREAT(不存在则创建),O_APPEND(追加写),O_TRUNC(清空文件再写入),这些选项本质就是一个个的宏

Linux-文件fd_Linux的文件操作_23

题外话:比特位方式的标志位传递方式

针对open的第二个参数,我们得扩展一个题外话:比特位方式的标志位传递方式。大家都知道,一个整数有32个比特位,每个比特位都可以标识一个信息。比如0表示没有该权限,1表示有该权限。open函数的第二个参数就是这个原理。

我们可以简单的实现一个类似的程序来理解,使用最右边的四个比特位来表示ONE、TWE、THREE、FOUR这四个宏。

Linux-文件fd_文件操作_24

编译运行,我们发现通过不同的或方式,我们可得到不同的组合功能。这便是open第二个参数的原理。

Linux-文件fd_Linux的文件标识符_25

open函数的返回值是整形,叫文件描述符,file descriptor,简称fd。

如果我们想让open函数,刚刚库函数fopen的共功能。我们除了要使用O_WRONLY选项外,还要再或上O_CREAT和O_TRUNC这个选项。

Linux-文件fd_Linux的文件标识符_26

删除掉刚刚的log.txt。编译运行,就能看到一个新的log.txt了,但是,我们发现这个文件的颜色有点不对,怎么是黄色?普通文件应该是没有颜色的。

Linux-文件fd_文件操作_27

这是为什么?这是因为这个文件的权限没做初始化。那如何进行初始化呢?别忘了,我们有第三个参数,open函数的第三参数,用来设置创建文件的初始权限。

这个参数的用法是,先定义umask值为0,再像十进制修改权限的方式那样,设置文件权限就可以了,我们这里设置了读写权限。这里有个问题,全局的umask和我们这定义的umask会用那个?就近原则,程序会用我们在程序里提供的umask

Linux-文件fd_Linux的文件标识符_28

删除刚刚的log.txt。编译运行,我们就得到了一个正常的log.txt文件

Linux-文件fd_文件操作_29

write

向文件里写入数据,用到write函数。它是系统调用,第一个参数传整形数字,也就是文件描述符fd。第二参数,传要写入内容的地址。第三个参数,传要写入内容的大小

Linux-文件fd_Linux的文件操作_30

我们调用它,向文件log.txt写入内容

Linux-文件fd_文件操作_31

编译运行后,cat查看文件,就能看到信息已经写入到了文件log.txt中

Linux-文件fd_Linux的文件标识符_32

我们把写入内容换成”1“

Linux-文件fd_Linux的文件标识符_33

编译运行,我们可以看到log.txt文件中的内容先被清理了,然后,再写入1。

Linux-文件fd_Linux的文件操作_34

通过刚刚的验证,我们可以得到下面的等价关系,在功能上的等价关系

Linux-文件fd_Linux的文件操作_35

如果做到与,a方式打开的fopen函数的功能等价,只需要将O_TRUNC选项改为O_APPEND选项即可

Linux-文件fd_Linux的文件操作_36

编译运行,我们可以发现,每次运行程序都会增加log.txt文件中的内容

Linux-文件fd_Linux的文件标识符_37

功能等价关系如下:

Linux-文件fd_Linux的文件操作_38

close

关闭文件的函数close,用法很简单,把以打开的文件的fd传给它就可以了

Linux-文件fd_Linux的文件操作_39

Linux-文件fd_文件操作_40

Linux-文件fd_Linux的文件标识符_41

read

read函数,是系统提供的读取文件的接口。它的第一个参数,传fd,告诉它从那个文件读数据。第二个参数,传一段空间的起始地址给它,告诉它把读到的数据放哪里。第三个参数,用来存放读到数据的这段空间有多大

Linux-文件fd_Linux的文件标识符_42

我们简单演示一下

Linux-文件fd_Linux的文件操作_43

编译运行,它就获取到log.txt文件中的内容了

Linux-文件fd_文件操作_44

访问文件的本质(1)

访问文件的本质,就访问文件的本质,为什么后面要加一个(1),因为这里只能分享访问文件的本质,的一部分内容。

fd不是整形吗?那我们是不是可以把它打印出来呢?当然可以

Linux-文件fd_文件操作_45

编译运行,打印了一个3。

Linux-文件fd_Linux的文件操作_46

为什么会是3呢?请听我娓娓道来。

在一个处在运行的进程的task_struct中,有一个指针struct file_struct* files,它指向一个叫struct file_struct的结构体。在这个结构体中,有一个struct file *fd_arrar[] 指针数组,指向一个个被打开的文件的结构体对象struct file。struct file里面包含了很多的文件属性。

Linux-文件fd_Linux的文件操作_47

我们如何知道打开的位置在哪?通过相应的数组下标呀!其实,fd就是数组的下标。

我们可以试着打印几个看看

Linux-文件fd_文件操作_48

编译运行,可以看文件标识符像数组下标一样逐渐增加

Linux-文件fd_文件操作_49

fd是数组下标,那为什么?我们打开文件是从3开始,而不是0。还记不记得,一个程序启动的时候,会默认打开三个文件,标准输入stdin,标准输出stdout,标准错误stderr,它们依次使用了数组下标0,1,2的位置。所以,我们打开的文件的fd是从3开始的。

现在,我们需要有一个概念了,Linux下的一切皆文件。stdin对应的是键盘文件,stdout和stderr对应的是显示器文件。

Linux-文件fd_Linux的文件标识符_50

我们可以简单验证一下,我们把fd为1的文件关闭掉再向显示器打印信息

Linux-文件fd_文件操作_51

编译运行,我们发现printf确实没法正常打印了,这说明printf函数底层封装了stdout。至于printf的返回值是12,那是因为它以为它打印成功了。

Linux-文件fd_Linux的文件标识符_52

那为什么fprintf可打印呢?这是因为struct file结构体里存在一个引用计数变量count,记录了指向该文件的对象数量。我们刚刚close了数组下标为1的文件,操作系统会找到该文件,发现指向该文件的数量大1,只会对count进行自减一。然后,把数组小标为1的位置置空。你关闭了,管我什么事?stderr,也就是数组小标为2的位置,依然指向对应的显示器。所以,printf无法向显示器打印了,但fprintf可打印

Linux-文件fd_Linux的文件标识符_53

刚刚我们在使用read函数的时候,从文件log.txt中读取内容。现在,我们从fd等于0的文件中读取内容

Linux-文件fd_文件操作_54

编译运行,我们发现程序进入阻塞状态

Linux-文件fd_Linux的文件标识符_55

当我们从键盘输入内容后,程序才会继续进行

Linux-文件fd_Linux的文件标识符_56

FILE和fd的关系

FILE和fd是什么关系呢?FILE是C语言自己封装的结构体,这个结构体里面必须封装了文件描述符fd。

怎么证明?

我们可以在stdin,stdout和stderr这三个结构体会有一个成员,叫_fileno

Linux-文件fd_Linux的文件操作_57

我们可以打印它们的值,正好就是0,1,2

Linux-文件fd_文件操作_58

编译运行,是不是对应上了

Linux-文件fd_Linux的文件标识符_59

Linux-文件fd_Linux的文件操作_60

好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。