• Linux文件操作及原理详解


    目录

    前言

    一、引入

    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函数:

    1. int open(const char *pathname, int flags);
    2. 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

    1. int main()
    2. {
    3. umask(0); //先把umask设为0,否则会影响权限的设置
    4. int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
    5. if(fd < 0)
    6. {
    7. return 1;
    8. }
    9. close(fd);
    10. return 0;
    11. }

    2、文件描述符

    通过对open函数的学习,我们知道了文件描述符实际上就是一个整数。

    看下面这个代码:

    1. int main()
    2. {
    3. int fd1 = open("log.txt", O_WRONLY|O_CREAT, 0666);
    4. printf("fd1: %d\n", fd1);
    5. int fd2 = open("log.txt", O_WRONLY|O_CREAT, 0666);
    6. printf("fd2: %d\n", fd2);
    7. int fd3 = open("log.txt", O_WRONLY|O_CREAT, 0666);
    8. printf("fd3: %d\n", fd3);
    9. int fd4 = open("log.txt", O_WRONLY|O_CREAT, 0666);
    10. printf("fd4: %d\n", fd4);
    11. int fd5 = open("log.txt", O_WRONLY|O_CREAT, 0666);
    12. printf("fd5: %d\n", fd5);
    13. return 0;
    14. }

    运行结果如下:

    可以发现文件描述符是默认从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 系统调用

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

    函数原型如下:

    1. #include
    2. 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:文件属性最后修改时间

     总结

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

  • 相关阅读:
    Apache JMeter 和 Locust的对比
    <网络安全>《85 微课堂<工业控制系统 - SIS安全仪表系统>》
    手把手教您Dom4j的使用
    推荐16款最好的3dMax插件
    【C++风云录】解锁智慧之门:物联网安全工具和库助力打造安全可靠的智能家居
    微信小程序开发【从入门到精通】——页面导航
    Python获取Pandas列名的几种方法(含代码示例)
    Tomcat & 创建动态web工程 & xml文件解析
    [附源码]Python计算机毕业设计SSM辽宁科技大学二手车交易平台(程序+LW)
    ieee下载文献的方法
  • 原文地址:https://blog.csdn.net/weixin_59371851/article/details/126168437