目录
哈喽,小伙伴们大家好。相信大家在学习语言时都接触过文件操作,但仅仅站在语言层面上是无法真正理解文件的,那么今天我就带大家从系统角度重新学习文件。
如果小伙伴们学习过c语言文件的I/O操作, 应该对fopen, fclose, fread, fwrite等函数有一定了解,它们都是C标准库当中的函数。但单单从语言的角度很难真正理解这些操作,今天我想从系统的角度带大家重新认识一下I/O。
首先我们应该清楚几个概念:
上面提到的fopen, fclose, fread, fwrite的函数都是c标准库中的函数,我们称之为库函数。而库函数是对系统调用接口的封装。我们来回顾一下这张图。

在下面的内容中,我将对系统调用接口进行一定讲解,当然,不同的操作系统的系统调用接口是不同的,我主要以linux系统下的为例。这也体现了库函数对系统调用接口封装的必要性,用户在不同的操作系统下可以使用同一套库函数进行操作,实现了平台间的可移植性。
open,write,read,close,lseek等接口是系统提供的,下面重点介绍一下open接口,其它的对照man手册,类比C文件相关接口即可。
open函数:
- int open(const char *pathname, int flags);
- int open(const char *pathname, int flags, mode_t mode);
参数:
pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数多个参数选项,多个参数进行或运算,构成flags。
(1)O_RDONLY: 只读打开
(2)O_WRONLY: 只写打开
(3)O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
(4)O_CREAT : 若文件不存在,则创建它。
(5)O_APPEND: 追加写
上面的这些都是定义出来的宏,每个宏都是一个某一位为1其余位为0的整数,整数的每一位都代表一种操作选项。对不同的参数选项进行或运算就能形成对应操作位为1的flags,这时候只需要检测哪几位为1然后执行对应的操作即可。假设O_WRONLY对应的是0x01,O_CREAT对应的数是0x02,或到一起后形成flags为0x03,后两位为1,则执行写操作和创建操作。这样非常巧妙的用一个参数就表示了所有选项。
mode:如果文件是被新创建出来的,需要用mode来指明新文件的访问权限。如果文件原来参在,则使用两个参数的open函数即可。
返回值:
成功:新打开的文件描述符,用来代表文件
失败:-1
- int main()
- {
- umask(0); //先把umask设为0,否则会影响权限的设置
- int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
- if(fd < 0)
- {
- return 1;
- }
- close(fd);
- return 0;
- }
通过对open函数的学习,我们知道了文件描述符实际上就是一个整数。
看下面这个代码:
- int main()
- {
- int fd1 = open("log.txt", O_WRONLY|O_CREAT, 0666);
- printf("fd1: %d\n", fd1);
- int fd2 = open("log.txt", O_WRONLY|O_CREAT, 0666);
- printf("fd2: %d\n", fd2);
- int fd3 = open("log.txt", O_WRONLY|O_CREAT, 0666);
- printf("fd3: %d\n", fd3);
- int fd4 = open("log.txt", O_WRONLY|O_CREAT, 0666);
- printf("fd4: %d\n", fd4);
- int fd5 = open("log.txt", O_WRONLY|O_CREAT, 0666);
- printf("fd5: %d\n", fd5);
- return 0;
- }
运行结果如下:

可以发现文件描述符是默认从3开始的,那么之前的数呢?
在c语言中我们学过,有三个输入输出流是默认打开的,分别是标准输入,标准输出,标准错误。而这是c语言的概念,从底层系统角度来看,它们三个分别对应这0,1,2三个文件描述符,已经被打开了。
文件描述符分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。如果把1关闭,再打开新文件,那么新文件的文件描述符就是1。
一个进程是可以打开多个文件的,在实际情况中往往是很多进程一起运行,每个进程都会打开多个文件。也就是说在系统中任意时刻都可能存在大量被打开的文件,这个时候就要对打开的文件进行管理。说到管理,大家就应该立刻想到管理的流程“先描述,再组织”。

文件分为内存文件和磁盘文件。文件一开始是是存在磁盘中的,磁盘文件分为两部分,分别是内容和属性 。当打开文件时,文件加载到内存中就形成了内存文件,一开始往往加载的是文件的属性,再延后式的慢慢加载内容。操作系统会根据文件的属性进行描述,生成结构体,每个文件都对应一个结构体。
我们知道每个进程都有一个task_struct,其中task_struct中保存着一个指针,指向名为files_struct的结构体,这个结构体中有一个文件结构体指针数组,用来保存不同文件形成的结构体的地址。我们上面提到过,每个文件都会对应一个文件描述符,这个文件描述符其实是数组下标。操作系统会根据文件结构体地址在数组中的位置,分配对应的下标作为文件描述符。
刚刚我们学习了系统层面文件管理主要是通过文件描述符fd代表相应的文件,而在c语言定位文件主要靠FILE*指针。我们知道c语言是对系统接口的封装,一定会与系统接口产生关联。c语言中中定义了一个结构体FILE用来保存文件相关信息,文件操作的函数都会返回一个FILE*类型的指针,这个指针指向结构体FILE,而在FILE结构体中就封装了文件描述符fd,所以c语言文件操作的本质还是通过文件描述符fd。
fopen函数做了什么?
(1)给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)。
(2)在底层通过open函数打开文件,并返回fd,再把fd填充到FILE结构体变量中。

在关闭标准输入1后再打开新文件myfile,运行该程序,发现本应该打印到显示屏上的内容打印到了文件myfile中。这种现象叫做输出重定向。

原因是标准输出关闭后,makefile分配到的文件描述符为1。printf函数只会根据文件描述符为1来找相应的文件,所以实现了输出重定向。

我们先来看一个奇怪的现象,我们调用了两个语言函数,一个系统接口。运行的时候发现在显示器上打印会打印三行信息,但如果重定向到文件中就会打印五行信息,似乎c语言的代码部分被执行了两次。但这很奇怪不是吗?按理来说子进程是不会执行前面的代码的,那究竟为什么c语言的部分打印了两次呢?

运行结果如下:
我们需要先明确一个概念:重定向会改变进程的缓冲方式。
缓冲方式分为无缓冲,行缓冲,全缓冲。全缓冲可以提高数据写入的效率,就好比你买了五十个快递,如果快递员一个一个送需要送五十次,而如果等快递全到了再一起送过来只需要送一次。默认对文件写入是全缓冲,对显示器写入是行缓冲。
下面来分析上面代码的运行过程:在对文件写入时,由于是全缓冲,数据都存在缓冲区中没有刷新,此刻数据是属于父进程的,在fork之后,缓冲区的数据归父子进程共用,进程即将结束前父子进程会分别刷新缓冲区,而进程间具有独立性,某一个进程是不能影响另一个进程的数据的,无论父子进程谁先刷新,都会发生写时拷贝,所以c语言部分被打印了两次。
那么为什么系统部分只打印了一次呢?很简单,我们通常所说的缓冲区是用户缓冲区,是由c语言提供的,由struct FILE去维护,所以系统调用不会受到影响。由于计算机的层状结构,语言是无法直接与硬件进行交互的,所以用户缓冲区刷新要经过操作系统。操作系统中也有缓冲区的概念,但这不是我们要关心的,由操作系统自己进行维护即可。
上面我们讲重定向原理的时候是手动对标准输入进行开关,但在实际中一般是很少这样的,系统提供了特定的接口供我们来完成重定向。
函数原型如下:
- #include
- int dup2(int oldfd, int newfd);

从函数描述中可以看出,dup2使用的方法不是把默认打开的文件关闭,而是直接进行拷贝,oldfd 处的内容直接替换到拷贝到newfd中,oldfd处的内容会有两份。
通过dup2完成重定向,代码如下:

运行结果如下:

文件分为内存文件和磁盘文件。上面我们讲解了内存文件的管理方法,下面我们来看一下磁盘文件。磁盘文件包括两部分,分别是文件属性和文件内容,这两部分都是直接在磁盘中存储的。linux把文件属性和内容分离存储。文件属性保存在inode中,inode是某一个文件的属性集合,linux中几乎每个文件都有一个inode,为了区分inode,我们使用inode编号。而文件内容存在磁盘的block中。(究竟什么是inode和block下面会提到)
下面显示出来的为文件属性:

下面显示出来的为文件内容:

磁盘是计算机中几乎唯一的一个机械设备,效率比较低,目前所有的普通文件都是在磁盘中存储的。磁盘是永久性介质,与之相比的是内存为掉电易失介质。

磁盘的具体结构这里不做解释,我们只需要知道磁盘以扇区为单位存储数据,并且每个磁道是一个圈,所以磁盘上的数据是一圈一圈存储的。磁盘这种介质称为圆形存储介质,而圆形存储介质可以看作线性存储介质(可以理解成把每一圈数据都打开连成一条直线)。
操作系统中负责管理和存储文件信息的部分称为文件系统。
磁盘为了方便管理,被分成了几个区,在windows系统中就相当于分成了C、D、E盘。磁盘的格式化操作实际就是在给磁盘输入管理信息,管理信息是什么是由文件系统决定的,不同操作系统下是不同的。而一个区还是太大了,为了进一步管理又被分成了几个组。

Linux ext2文件系统,上图为磁盘文件系统图。在一个区中,Boot Block为启动块,包含计算机的一些启动信息。剩下部分ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。下面对一个Block group的构成进行分析:
数据区和信息区存放着很多inode和block,我们需要知道哪些被文件占用,哪些处在空闲,便于后续分配。
通过上面的介绍,相信大家已经对普通文件的管理和存储有了一定认识,那么目录呢?
目录同样是文件,分为属性和内容两部分。和文件不同的是,目录的内容中保存的是当前目录下的文件名以及每个文件对应的inode号。由此得出一个结论,文件的名称并不属于文件属性,没有保存在inode中,而是保存在所处目录的内容中(包括目录本身)。
建立一个新文件的过程:
ls -l命令运行的过程:
根据当前目录的路径找到当前目录的inode,再根据对应关系找到目录的数据块。目录数据块中保存了当前目录下的文件名和inode号,把文件名打印出来,在根据inode号找到相应文件的inode,把里面保存的文件属性打印出来。
听过上面的学习,我们知道了系统找文件并不是通过文件名,而是通过inode。可以令多个文件对应一个inode。

通过ln命令使abc与def建立链接,两个文件inode号相同,称为硬链接。可以把def理解成abc的一个别名。通过ls命令查看文件信息,在权限后面的数字代表文件的硬链接数,可以看到abc和def的链接数为2。

我们新建一个目录test,并在test中再建两个新目录,这时候查看test信息,我们发现test的链接数居然是4。这是因为test和test目录中的 . 形成了硬链接,并与test1和test2中的 .. 形成了硬链接,这就是为什么我们可以通过.和..访问当前目录和上级目录。由此我们可以得出一个结论,一个目录下的目录数等于该目录的链接数减2,注意普通文件是不存在..的。
我们在删除文件时干了两件事情:1.在目录中将对应的文件名记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

用ln -s命令建立软连接,可以发现abc与abc1的inode号是不同的。

将ls.lnk与ls命令建立软连接,发现可以通过ls.lnk直接调用ls命令。我们可以把软链接理解成创建了一个快捷方式,可以通过软链接找到相应路径下的文件。
最后拓展一些格外内容。使用stat可以查看文件的一些格外信息。

这里记录了文件的三个时间:
总结
以上就是本文要讲的全部内容。本文主要从系统角度分析了一些文件的操作以及原理,希望能给小伙伴们带来帮助,感谢大家的阅读。山高路远,来日方长,我们下次见。