本篇文章,继续和大家分享与Linux相关的知识。本次会涉及的主要内容是文件描述符file descriptor(后续我们简称fd)以及与它相关的知识。
共识原理
文件的共识原理,我们简单做一下梳理。第一个,文件=内容+属性。第二个,文件分为打开的文件和没打开的文件。第三个,打开的文件是谁打开的?是进程。研究打开的文件,本质是研究进程和文件的关系。第四个,没打开的文件放在哪?在磁盘上。我们最关心的是什么问题?没有打开的文件很多,这些文件如何被分门别类的放置好。第五个,根据冯诺依曼体系结构,我们知道,打开的文件必须加载到内存!我们之前说过,一个进程,OS默认会给它打开三个文件,标准输入stdin,标准输出stdout,标准错误stderr这三个文件,这注定了进程和文件的是1:n的关系,也就是一个进程会打开多个文件。这么多的文件,进程需要管理它们,如何管理?先描述,再组织。在内核中,每一个被打开的文件都必须有描述自己的结构体对象。在这个结构体对象里,包含了很多的文件属性,其中一个是结构体对象指针,可以将一个进程打开的文件串联成一个链表。此时,进程对被打开文件的管理,就变成了对链表的增删查改。
回忆C语言的文件接口
fopen
学习C语言时候,我们打开文件用的函数是fopen,它在man手册第三章。它的第一个参数是传文件所在的路径,第二个参数传打开文件的方式,比如说读方式,写方式,读写方式
fclose
在我们对文件操作完之后,需要使用fclose函数,关闭相应的文件。fclose函数只用传一个参数FILE*指针。
演示
这里简单演示,怎么使用fopen和fclose
我们使用ll指令,查看当前路径下的文件,发现没有我们要打开的文件。
编译运行,我们可以发现当前路径下多了一个文件log.txt。此时,我们就能知道fopen函数打开文件的方式。如果该文件存在就正常打开。如果文件不存在就先创建该文件,在打开该文件。
可问题来了,fopen函数怎么知道文件要创建在哪?我们在刚刚的代码基础上增加一些代码,让程序别这么快结束,并打印自己pid
编译运行,使用如下指令查看程序的信息
[common_108@iZf8zaj27gxmvq7veqrekfZ ~]$ ll /proc/12307
我们可以发现有一行信息叫当前工作路径,简称cwd。它的路径刚好不就是,我们程序所在的路径吗?当我们调用fopen函数打开函数的时候,如果文件不存在,会先创建文件。如果你第一个参数传给它的是绝对路劲,那么他就会在这个路径下创建文件。如果你给的只是文件名,它会自动把cwd这路径加到文件名前面,于是,我们就看到了文件被创建在我们程序所在的路径下了
按照我刚刚所说的,如果我们更改了程序的当前工作路径,所创建的文件的位置是不是也会发现更改?是的,我们可以使用chdir来改变程序的cwd,看看效果
删除刚刚创建的文件log.txt,我们再次编译运行程序。再次查看当前路径下的文件,我们发现没有log.txt这个文件存在,它去哪了呢?因为我们更改了程序的cwd,它被创建到了/home/common_108这个路径下
fwrite
向文件写入的数据的接口有很多,我们这里使用fwrite。fwrite的第一个参数,传你要写入文件的内容所在的位置,也就是指针。第二个参数,写入文件的内容的大小。第三个参数,传个数,你要写入该内容多少次。第四个参数,传FILE*,告诉它你要写入到那个文件里。
我们把刚刚的代码中的部分无关代码注释掉,并调用fwrite函数对文件进行写入。这里有一个问题:C语言的字符串会以'\0'字符结尾,我们在使用strlen计算字符串长度的时候,需不需要加一?答案是,不需要。因为那是C语言的的规定,和我文件有什么关系。
编译运行,我们就能查看到文件里写入了“hello Linux message”信息
我们将写入数据改为abcd\n,再次编译运行。
编译运行,并查看log.txt文件。我们可以发现原来的内容,不见了。
这是什么原因?这是因为使用fopen以w方式打文件,每次写入数据前,都会对文件进行清空处理。
我们可以简单验证一下,这次我们不调用fwrite函数,只调用fopen函数
编译运行,我们使用cat查看log.txt发现什么内容也没有
大家还记不记得重定向符号‘>’的功能,重定向一个不存在的文件,会创建这个文件。当我们使用重定向向已存在内容的文件里,写入数据,它也会先清空文件,再进行写入。是不是和以w方式调用fopen函数的功能几乎一样呢?
如果你想在已有的内容后面追加内容,则采用a的方式来调用fopen函数。
fprintf
这次写入数据我们换个函数,用fprintf。fprintf函数的第一个参数传FILE*指针,告诉它,你要向那个文件写入。第二个参数,类似于printf的用法,打印的格式
我来演示一下
编译运行,我们就可以看到每次运行之后,文件log.txt的内容逐渐增加
未打开的文件放在哪里?放在磁盘里,那是不是对文件的操作就注定了要对磁盘硬件操作?我们刚刚回忆的四个函数,是库函数,它对文件进行操作,需要使用硬件。库函数是用户的代表,不能直接访问硬件,需要通过操作系统来完成。所以,fopen这四个库函数,一定封装了系统接口。
过渡到系统,认识文件系统调用
我们来认识一下,操作系统为文件操作提供的接口。
open
open函数有两个,我们先来了解第一个。它的第一个参数,传路径名称,也是告诉它你要打开的那个文件。第二个参数,是一个整数,这个参数有固定的选项。常用选项有五个:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写),O_CREAT(不存在则创建),O_APPEND(追加写),O_TRUNC(清空文件再写入),这些选项本质就是一个个的宏
题外话:比特位方式的标志位传递方式
针对open的第二个参数,我们得扩展一个题外话:比特位方式的标志位传递方式。大家都知道,一个整数有32个比特位,每个比特位都可以标识一个信息。比如0表示没有该权限,1表示有该权限。open函数的第二个参数就是这个原理。
我们可以简单的实现一个类似的程序来理解,使用最右边的四个比特位来表示ONE、TWE、THREE、FOUR这四个宏。
编译运行,我们发现通过不同的或方式,我们可得到不同的组合功能。这便是open第二个参数的原理。
open函数的返回值是整形,叫文件描述符,file descriptor,简称fd。
如果我们想让open函数,刚刚库函数fopen的共功能。我们除了要使用O_WRONLY选项外,还要再或上O_CREAT和O_TRUNC这个选项。
删除掉刚刚的log.txt。编译运行,就能看到一个新的log.txt了,但是,我们发现这个文件的颜色有点不对,怎么是黄色?普通文件应该是没有颜色的。
这是为什么?这是因为这个文件的权限没做初始化。那如何进行初始化呢?别忘了,我们有第三个参数,open函数的第三参数,用来设置创建文件的初始权限。
这个参数的用法是,先定义umask值为0,再像十进制修改权限的方式那样,设置文件权限就可以了,我们这里设置了读写权限。这里有个问题,全局的umask和我们这定义的umask会用那个?就近原则,程序会用我们在程序里提供的umask
删除刚刚的log.txt。编译运行,我们就得到了一个正常的log.txt文件
write
向文件里写入数据,用到write函数。它是系统调用,第一个参数,传整形数字,也就是文件描述符fd。第二参数,传要写入内容的地址。第三个参数,传要写入内容的大小
我们调用它,向文件log.txt写入内容
编译运行后,cat查看文件,就能看到信息已经写入到了文件log.txt中
我们把写入内容换成”1“
编译运行,我们可以看到log.txt文件中的内容先被清理了,然后,再写入1。
通过刚刚的验证,我们可以得到下面的等价关系,在功能上的等价关系
如果做到与,a方式打开的fopen函数的功能等价,只需要将O_TRUNC选项改为O_APPEND选项即可
编译运行,我们可以发现,每次运行程序都会增加log.txt文件中的内容
功能等价关系如下:
close
关闭文件的函数close,用法很简单,把以打开的文件的fd传给它就可以了
read
read函数,是系统提供的读取文件的接口。它的第一个参数,传fd,告诉它从那个文件读数据。第二个参数,传一段空间的起始地址给它,告诉它把读到的数据放哪里。第三个参数,用来存放读到数据的这段空间有多大
我们简单演示一下
编译运行,它就获取到log.txt文件中的内容了
访问文件的本质(1)
访问文件的本质,就访问文件的本质,为什么后面要加一个(1),因为这里只能分享访问文件的本质,的一部分内容。
fd不是整形吗?那我们是不是可以把它打印出来呢?当然可以
编译运行,打印了一个3。
为什么会是3呢?请听我娓娓道来。
在一个处在运行的进程的task_struct中,有一个指针struct file_struct* files,它指向一个叫struct file_struct的结构体。在这个结构体中,有一个struct file *fd_arrar[] 指针数组,指向一个个被打开的文件的结构体对象struct file。struct file里面包含了很多的文件属性。
我们如何知道打开的位置在哪?通过相应的数组下标呀!其实,fd就是数组的下标。
我们可以试着打印几个看看
编译运行,可以看文件标识符像数组下标一样逐渐增加
fd是数组下标,那为什么?我们打开文件是从3开始,而不是0。还记不记得,一个程序启动的时候,会默认打开三个文件,标准输入stdin,标准输出stdout,标准错误stderr,它们依次使用了数组下标0,1,2的位置。所以,我们打开的文件的fd是从3开始的。
现在,我们需要有一个概念了,Linux下的一切皆文件。stdin对应的是键盘文件,stdout和stderr对应的是显示器文件。
我们可以简单验证一下,我们把fd为1的文件关闭掉再向显示器打印信息
编译运行,我们发现printf确实没法正常打印了,这说明printf函数底层封装了stdout。至于printf的返回值是12,那是因为它以为它打印成功了。
那为什么fprintf可打印呢?这是因为struct file结构体里存在一个引用计数变量count,记录了指向该文件的对象数量。我们刚刚close了数组下标为1的文件,操作系统会找到该文件,发现指向该文件的数量大1,只会对count进行自减一。然后,把数组小标为1的位置置空。你关闭了,管我什么事?stderr,也就是数组小标为2的位置,依然指向对应的显示器。所以,printf无法向显示器打印了,但fprintf可打印
刚刚我们在使用read函数的时候,从文件log.txt中读取内容。现在,我们从fd等于0的文件中读取内容
编译运行,我们发现程序进入阻塞状态
当我们从键盘输入内容后,程序才会继续进行
FILE和fd的关系
FILE和fd是什么关系呢?FILE是C语言自己封装的结构体,这个结构体里面必须封装了文件描述符fd。
怎么证明?
我们可以在stdin,stdout和stderr这三个结构体会有一个成员,叫_fileno
我们可以打印它们的值,正好就是0,1,2
编译运行,是不是对应上了
好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。