Linux文件操作及原理详解

发布于:2023-01-23 ⋅ 阅读:(598) ⋅ 点赞:(0)

目录

前言

一、引入

1、几个基本概念

2、系统调用和库函数

二、系统文件I/O

1、接口介绍

2、文件描述符

 三、内存中的文件管理

1、系统层面

 2、语言层面

四、重定向

1、重定向原理

 2、重定向与缓冲区

3、dup2 系统调用

五、文件系统 

1、磁盘文件与磁盘

1.1 磁盘文件

 1.2  磁盘

2、磁盘中的文件管理

2.1文件系统介绍

2.2目录的管理存储

3、软硬链接 

3.1硬链接

3.2软链接

4、acm时间


前言

哈喽,小伙伴们大家好。相信大家在学习语言时都接触过文件操作,但仅仅站在语言层面上是无法真正理解文件的,那么今天我就带大家从系统角度重新学习文件。


一、引入

1、几个基本概念

如果小伙伴们学习过c语言文件的I/O操作, 应该对fopen, fclose, fread, fwrite等函数有一定了解,它们都是C标准库当中的函数。但单单从语言的角度很难真正理解这些操作,今天我想从系统的角度带大家重新认识一下I/O。

首先我们应该清楚几个概念:

  • 文件的操作如果没有指名路径,默认都是在当前路径。当前路径并不是指的可执行程序所在的路径,而是进程运行时所处的路径。
  • 打开文件,一定是在进程运行中打开的。读写,关闭,都是进程完成的。
  • 在linux下,一切接文件,键盘显示屏同样是文件。

2、系统调用和库函数

上面提到的fopen, fclose, fread, fwrite的函数都是c标准库中的函数,我们称之为库函数。而库函数是对系统调用接口的封装。我们来回顾一下这张图。

在下面的内容中,我将对系统调用接口进行一定讲解,当然,不同的操作系统的系统调用接口是不同的,我主要以linux系统下的为例。这也体现了库函数对系统调用接口封装的必要性,用户在不同的操作系统下可以使用同一套库函数进行操作,实现了平台间的可移植性。

二、系统文件I/O

1、接口介绍

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;
}

2、文件描述符

通过对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。

 三、内存中的文件管理

1、系统层面

一个进程是可以打开多个文件的,在实际情况中往往是很多进程一起运行,每个进程都会打开多个文件。也就是说在系统中任意时刻都可能存在大量被打开的文件,这个时候就要对打开的文件进行管理。说到管理,大家就应该立刻想到管理的流程“先描述,再组织”。

文件分为内存文件和磁盘文件。文件一开始是是存在磁盘中的,磁盘文件分为两部分,分别是内容和属性 。当打开文件时,文件加载到内存中就形成了内存文件,一开始往往加载的是文件的属性,再延后式的慢慢加载内容。操作系统会根据文件的属性进行描述,生成结构体,每个文件都对应一个结构体。

我们知道每个进程都有一个task_struct,其中task_struct中保存着一个指针,指向名为files_struct的结构体,这个结构体中有一个文件结构体指针数组,用来保存不同文件形成的结构体的地址。我们上面提到过,每个文件都会对应一个文件描述符,这个文件描述符其实是数组下标。操作系统会根据文件结构体地址在数组中的位置,分配对应的下标作为文件描述符。

 2、语言层面

刚刚我们学习了系统层面文件管理主要是通过文件描述符fd代表相应的文件,而在c语言定位文件主要靠FILE*指针。我们知道c语言是对系统接口的封装,一定会与系统接口产生关联。c语言中中定义了一个结构体FILE用来保存文件相关信息,文件操作的函数都会返回一个FILE*类型的指针,这个指针指向结构体FILE,而在FILE结构体中就封装了文件描述符fd,所以c语言文件操作的本质还是通过文件描述符fd。

fopen函数做了什么?

(1)给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)。

(2)在底层通过open函数打开文件,并返回fd,再把fd填充到FILE结构体变量中。

四、重定向

1、重定向原理

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

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

 2、重定向与缓冲区

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

 运行结果如下:

我们需要先明确一个概念:重定向会改变进程的缓冲方式。

 缓冲方式分为无缓冲,行缓冲,全缓冲。全缓冲可以提高数据写入的效率,就好比你买了五十个快递,如果快递员一个一个送需要送五十次,而如果等快递全到了再一起送过来只需要送一次。默认对文件写入是全缓冲,对显示器写入是行缓冲。

下面来分析上面代码的运行过程:在对文件写入时,由于是全缓冲,数据都存在缓冲区中没有刷新,此刻数据是属于父进程的,在fork之后,缓冲区的数据归父子进程共用,进程即将结束前父子进程会分别刷新缓冲区,而进程间具有独立性,某一个进程是不能影响另一个进程的数据的,无论父子进程谁先刷新,都会发生写时拷贝,所以c语言部分被打印了两次。

那么为什么系统部分只打印了一次呢?很简单,我们通常所说的缓冲区是用户缓冲区,是由c语言提供的,由struct FILE去维护,所以系统调用不会受到影响。由于计算机的层状结构,语言是无法直接与硬件进行交互的,所以用户缓冲区刷新要经过操作系统。操作系统中也有缓冲区的概念,但这不是我们要关心的,由操作系统自己进行维护即可。

3、dup2 系统调用

上面我们讲重定向原理的时候是手动对标准输入进行开关,但在实际中一般是很少这样的,系统提供了特定的接口供我们来完成重定向。

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

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

通过dup2完成重定向,代码如下:

运行结果如下: 

五、文件系统 

1、磁盘文件与磁盘

1.1 磁盘文件

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

下面显示出来的为文件属性:

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

 1.2  磁盘

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

 磁盘的具体结构这里不做解释,我们只需要知道磁盘以扇区为单位存储数据,并且每个磁道是一个圈,所以磁盘上的数据是一圈一圈存储的。磁盘这种介质称为圆形存储介质,而圆形存储介质可以看作线性存储介质(可以理解成把每一圈数据都打开连成一条直线)。

2、磁盘中的文件管理

2.1文件系统介绍

操作系统中负责管理和存储文件信息的部分称为文件系统。

磁盘为了方便管理,被分成了几个区,在windows系统中就相当于分成了C、D、E盘。磁盘的格式化操作实际就是在给磁盘输入管理信息,管理信息是什么是由文件系统决定的,不同操作系统下是不同的。而一个区还是太大了,为了进一步管理又被分成了几个组。

 Linux ext2文件系统,上图为磁盘文件系统图。在一个区中,Boot Block为启动块,包含计算机的一些启动信息。剩下部分ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。下面对一个Block group的构成进行分析:

  • 超级块(super block)和GDT:主要存放文件系统本身的结构信息和块组属性信息,例如inode被使用了多少,data blocks被使用了多少。这里不做详细解释,我们重点了解后四个部分。
  • 数据区(data blocks):文件的内容主要放在这里,磁盘是块设备,数据会被分割开来记录到一个个block中。
  • 信息区(inode tabile):文件的属性主要存放在这里,每个文件都对应着一个inode。可以把inode理解成一个结构体,里面保存了inode号,用来区分inode。同时里面还保存了一个数组,和数据区的数据块形成映射,通过一个文件的inode,就可以通过映射关系找到它的数据。

数据区和信息区存放着很多inode和block,我们需要知道哪些被文件占用,哪些处在空闲,便于后续分配。

  • 块位图(block bitmap):里面保存着一个二进制序列,根据0和1的分布描述data blocks中哪个数据块被占用,哪个数据块没有被占用。0代表空闲,1代表占用。
  • inode位图(inode bitmap):和快位图一样,里面保存着一个二进制序列,用来描述inode table中的inode的空闲情况。

2.2目录的管理存储

通过上面的介绍,相信大家已经对普通文件的管理和存储有了一定认识,那么目录呢?

目录同样是文件,分为属性和内容两部分。和文件不同的是,目录的内容中保存的是当前目录下的文件名以及每个文件对应的inode号。由此得出一个结论,文件的名称并不属于文件属性,没有保存在inode中,而是保存在所处目录的内容中(包括目录本身)。

建立一个新文件的过程:

  • 根据inode位图找到一个空闲的inode,把文件属性存到inode中并把位图相应的位置置成1。
  • 根据块位图找到空闲的block,把文件内容存到block中,并把块位图相应位置置成1。
  • 建议inode与block的对应关系。
  • 把文件名和它的inode号添加到当前目录的数据块中。

ls -l命令运行的过程:

根据当前目录的路径找到当前目录的inode,再根据对应关系找到目录的数据块。目录数据块中保存了当前目录下的文件名和inode号,把文件名打印出来,在根据inode号找到相应文件的inode,把里面保存的文件属性打印出来。

3、软硬链接 

3.1硬链接

听过上面的学习,我们知道了系统找文件并不是通过文件名,而是通过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,则将对应的磁盘释放。

3.2软链接

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

 

 将ls.lnk与ls命令建立软连接,发现可以通过ls.lnk直接调用ls命令。我们可以把软链接理解成创建了一个快捷方式,可以通过软链接找到相应路径下的文件。

4、acm时间

最后拓展一些格外内容。使用stat可以查看文件的一些格外信息。

这里记录了文件的三个时间:

  • access:文件最后访问时间 
  • modify:文件内容最后修改时间
  • change:文件属性最后修改时间

 总结

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