操作系统——Linux文件及文件系统、软硬链接、动静态库(基础IO)

引言

        在计算机和操作系统的学习过程中,文件管理系统是一个十分庞杂的知识体系,也是相当重要的话题。我们可以通过该内容的知识,对计算机的组成及工作原理,有一个初步且深入的认识。在计算机学科中,底层决定上层,上层反过来影响底层。所以底层的学习,是提高未来学习上层技术的效率的基础。而学习文件系统就是加深我们对底层认识的一个重要知识点。

一、文件的概念

        对Linux系统(及大多数操作系统)而言,一切皆文件。例如我们所用的键盘,所看的屏幕,包括我们使用的麦克风摄像头等等一切外设,对Linux而言都可以看作是一个个文件。

        关于文件,首先我们先理解为,一个文件 = 文件的内容(数据)+ 文件的属性。当我们创建了一个文件,文件里存放的我们想要的数据,对应就是文件的内容;对计算机和OS而言,需要管理我们的文件,就需要有每一个文件对应的属性。

        正如我们之前的文章所说,OS对文件的管理方式,依然也是“先描述,再组织”。所以要想理解Linux下的文件系统,也需要从两个方面去切入:1.属性;2.内容。

二、文件的内容

        文件的内容,也就是我们日常生活中,Word / Excel 或是 PPT 文件里的内容,是我们最常接触的,所以以此为切入点。

C语言方法

        首先,理解的文件的内容,就是文件中存放的我们想要保存的数据。因此对于我们而言,必不可少的就是对这个文件的 读与写 操作。在C语言中我们学习过相对应的文件读写打开的方法:fopen

FILE *fopen(const char *path, const char *mode);

        其中 path 指向要操作的文件,mode 对应对该文件的操作方式

        r:以读的方式打开文件

        r+:以读和写的方式打开文件

        w:以写的方式打开文件,若文件不存在就创建它,流被定位于文件的开头,即打开文件时清空文件内原有的内容

        w+:以写和读的方式打开文件,若文件不存在就创建它,流被定位于文件的开头

        a:追加写的方式打开文件,文件不存在就创建它,流被定位与文件的末尾

        a+:追加写和读的方式打开文件,不存在就创建,读流被定位在文件的开头,写流丁文在文件的末尾

系统IO接口

        除了以上这些C语言标准库提供给我们的文件操作方法函数之外,我们还要知道,操作系统也会提供给用户相应的文件操作接口,这些接口也属于我们之前所说的系统调用接口。以下介绍Linux提供给用户的文件IO的系统调用接口:

#includ e <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);

        pathname 要操作的文件

        flags 操作方法:

  • O_RDONLY:只读方式打开文件。
  • O_WRONLY:只写方式打开文件。
  • O_RDWR:可读写方式打开文件。
  • O_CREAT:如果文件不存在,则创建它。该标志必须与后面的 mode 参数一起使用。
  • O_EXCL:与 O_CREAT 标志一起使用,确保文件不存在。如果文件已经存在,则 open() 函数会失败,并返回错误。
  • O_TRUNC:如果文件已经存在,将其长度截断为零。
  • O_APPEND:在文件末尾追加数据,而不是覆盖已有的数据。

        mode 创建文件时的权限,它是一个八进制数,只有在使用O_CREAT时才有效

(如果需要同时用到多种方法,需要用 “ | ” 连接。)

        所以我们可以知道,C标准库里的 fopen("log.txt", "w");  对应到系统接口函数,就是 open("log.txt", O_WRONLY | O_CREAT, 0666);

        但当我们实际用 open 时,还需注意在调用 open 前调用 umask 函数更改当前进程的权限掩码。避免创建文件时的权限错误。

        以上的 fopen fclose fwrite fread 等函数,都是C语言标准库提供给我们的,open close write read等接口是操作系统提供给用户的。也就是说,C语言库的方法,是由系统接口封装而来的,上层的一个简单的操作,在下层可能需要很多步骤。所以封装或面向对象,就是一个减少需要大量重复的工作,以极小的性能成本换取工作效率的过程。

C/C++语言的跨平台性

        当我们在用C语言提供给我们的方法时,我们会发现,这些方法在Windows下和Linux下都可以使用。而C标准库是由系统接口封装而来的,Windows和Linux的接口也肯定不一样,C标准库的方法可以实现跨平台调用,是如何实现的?是因为C语言标准库(包括C++)在封装接口时,将不同操作系统的接口都进行了封装,根据用户当前正在使用不同系统的具体情况,再进行代码裁剪,从而实现语言的跨平台使用。

程序、进程运行的当前路径

在使用 open 或 fopen 的时候,程序或进程怎么判定“当前路径”的?

        答:程序开始运行时(进程被创建时),用户所处的当前路径,就是程序或进程的工作路径。


三、文件的属性

        在上面我们使用C标准库时,我们看到函数的返回类型 FILE* ,这是个什么类型?它和系统接口中的返回值 int fd 有什么区别?

文件描述符(重点)

        在系统文件IO接口中,open的返回值 -- fd 就是被打开文件的文件描述符(file descriptor)。当我们连续创建多个文件并打开时。

文件描述符的分配机制:

        当我们打开文件时,系统会给被打开的文件,选择当前空闲的最小的文件描述符,分配给新打开的文件。我们发现文件描述符fd是从3开始的,后面紧接着是4/5/6,那么0/1/2呢?0/1/2分别对应着三个标准流:0 stdin 标准输入(键盘);1 stdout 标准输出(显示器);2 stderr 标准错误(显示器)。

        文件描述符是在文件被加载到内存时,OS分配的,那么OS在管理文件时,文件描述符充当一个什么身份?

        答:在一个程序中,打开一个文件,我们可以用一个FILE结构体对被打开的文件进行描述。为了OS能管理这个结构体,我们用一个 FILE* 指针指向该结构体。而一个程序中往往会打开很多文件,因此为了方便管理,在进程的 PCB (也就是Linux下的 task_struct)内有一个 files* 指针指向的名为 files_struct 结构体,该结构体里面存放的是进程打开文件的数据结构,里面有一个 fd 指针数组指针,指向一个数组名为 fd_array[] -- 文件描述符 的数组,里面存放的是该进程打开的所有文件的 FILE* 指针。而文件描述符,其实就是 fd_array[] 数组的数组下标!(这段解释很重点!!需要配图理解)

        其中,**fd 并不是直接存在 files_struct 结构体中,而是被存放在该结构体中的 *fdt 指针所指向的名为 fdtable -- 文件描述符表 的结构体中。由于这不重要,所以在图中不体现。

关于 FILE 及缓冲区

        首先我们知道,C语言标准库中的文件操作函数,一定是对系统接口的封装,而系统接口要想实现文件操作就一定需要文件描述符 fd。所以在C语言的 FILE 结构体内部,一定封装了 fd。

        另外,我们在写的文件操作程序时,比如向一个文件中每隔 3s 写入一次“hello world”,连着写5次。如果我们用C语言标准库接口去实现该程序,会发现必须要等程序运行结束(在这里是 12s)才能在文件中看到成功写入,而用系统接口就可以看到每隔 3s 写入一次的效果。

        这是因为在C语言标准库里,FILE 内维护着文件操作的缓冲区,只有当达到一定条件时缓冲区刷新了,才能真正的写入。而系统接口没有缓冲区(即写透状态),也就会立刻写入。

        缓冲区存在的意义自然就是:减少IO次数:在程序运行的过程中,IO是一个及其占用时间和资源的操作,而有了缓冲区,就可以先攒一些数据,再一起IO;

        一般缓冲区的刷新条件:1.行刷新;2.满刷新;3.异常刷新;4.程序结束刷新;5.fflush主动刷新。

重定向的实现

        有了以上的知识,我们现在可以理解,输入重定向,输出重定向以及追加重定向是如何实现的:

输出重定向:

        当我们正常使用 printf 函数打印时,默认会将数据打印到 stdout 中去,如果我们在 printf 前,将1号文件描述符关掉:close(1) ; 此时1号描述符就被释放调,再以写的方式打开一个新文件,此时新打开的文件就会默认占用1号描述符,而 printf 是向1号中打印,因此原本该打印到屏幕上的数据,就被写入我们新打开的文件中,这就是输出重定向。

输入重定向:

        与输出重定向原理一样,在输入前将0号操作符对应的 stdin 关闭,再以读的方式打开新文件占用0号操作符,原本应该读取键盘的数据,此时就变成读取新文件内的数据。

        但以上两种方式不推荐,因为关闭 stdin 或 stdout 会对其他正在执行的程序造成影响,因此使用第二种方法:

#include <unistd.h>
int dup2(int oldfd, int newfd);
        dup2 :将 newfd 号描述符所对应的 FILE* ,指向 oldfd 号描述符所对应的 FILE* 所指向的文件:
dup2 ( fd, 1 ) ;

ext2文件系统 -- 文件在磁盘中的存储

        学习文件系统,我们需要站在微观和宏观两个角度去认识和理解。其中,微观指的是单个文件内部的数据是如何在该文件中存储的,即文件的属性+内容;宏观指的是计算机中的所有文件在整个存储系统内是如何存储的,即文件在磁盘中的存储。

        文件又分为被打开被加载到内存中的文件和在磁盘中未被使用的文件,在磁盘中的文件怎么存储,文件在磁盘中和在内存中有什么区别?

        内存:掉电易失存储区        磁盘、硬盘、U盘、光盘、磁带:永久性存储器

        磁盘结构 -- 先来认识一下磁盘:

机械硬盘--磁盘的内部构造
磁盘盘片侧视图
单个盘面的俯视图及结构示意图

        磁盘的最小单位就是一个扇区,其大小通常是 512 字节或 4KB 。磁盘通过 CHS (Cylinder Head Sector -- 柱面-磁头-扇区)  寻址的方式,找到对应的一个扇区。

        操作系统通过 LBA (Logical Block Addressing -- 逻辑块地址) 寻址方式,为磁盘进行访问和读写操作,这种寻址方式通过逻辑块地址表示磁盘上的数据块。将磁盘的物理结构抽象为一个连续的逻辑地址空间,使得操作系统和应用程序可以简化对磁盘的读取和写入操作。

        首先认识一下 文件系统:

对磁盘进行分区,若干个区就有若干个这种结构的文件系统。

        Block group :文件系统会根据分区大小划分数个block group。块组的大小通常是2MB到64MB之间,这取决于文件系统的设置和磁盘的大小。

        块组的大小可以影响文件系统的性能和可靠性。较小的块组可以提高文件系统的并发性和可靠性,但可能会浪费一些磁盘空间;而较大的块组则可以提高磁盘空间的利用率,但可能会导致更多的碎片和性能下降。

每个block group都有相同的结构组成:

        Super Block:存放文件系统本身的结构信息。主要有:Block和inode的总数,未使用的个数,一个Block和inode的大小,最近一次挂载时间,最近写入时间等等其他文件系统的相关信息。该块一旦被破坏,整个文件系统也就被破坏了。

        GDT:块组描述符。

        Block Bitmap:块位图,记录哪些数据块被占用,哪些块空闲。

        inode Bitmap:inode位图,记录哪些 inode 被占用。

        inode Table:索引节点表,一般被组织成一个数组,其中每个元素都是一个inode。每个inode由固定大小的结构体表示,里面存放对应文件的属性,如权限、所有者、时间戳、大小和数据块地址等等。

        Data Blocks:数据块,用于存放文件的数据,多个 4KB 大小的单个数据块的集合。

        我们可以发现,在这套文件系统中,文件的属性和内容数据是分开存放的。其实也就是,每个文件都有自己的 inode 结构体,该结构体里有自己的属性,和唯一的inode编号用以标识自己。每个 inode 内都有对应 Data Blocks 中的空间的地址,可以通过地址找到它对应的单个 4KB 大小的 block,偏移访问属于该文件的全部 blocks。

        其中,一般的,为了合理使用磁盘的空间,一次并不会给一个 inode 分配太多的 data block。例如在 Linux 下,给一个在 inode 结构体中分配一个 blocks[15] 大小的指针数组。不是每块data block都存放的是文件内容:数组中的前12个指针各自指向一个直接块 (Direct block),直接存放文件内容;13/14为 Indirect block 各自指向一块间接块,该区域存放的是指向更多blocks[15]号块指针;15为 Double indirect block 指向一块双重间接块,内部是二级间接块。

文件创建和删除的原理

创建:

        多于该文件系统而言,当创建一个文件,首先要找到一个空闲的inode,把文件信息记录到其中,再在数据块中找到一个空闲块和该inode连接上,如果前两步都顺利,最后一步就是将用户名和inode建立对应关系。

        用户通过文件名,拿到对应的inode,通过inode拿到文件的属性+内容。

删除:

        直接将文件名和与其对应的inode解除联系,再将inode和对应的data block的状态改为空闲即可。

软硬链接的理解

有了以上的认识,软硬链接也就极好理解:

硬链接

        创建方式:

ln 目标文件 硬链接名

我们通过实践发现:

        1.硬链接和目标文件的inode编号一样,说明两个文件名指向了同一个inode。我们在Linux上看到的是文件名,而磁盘上找文件用的是inode,在Linux中可以让多个文件名对应一个inode,相当于给目标文件创建了一个别名,本质上还是一个文件。

        硬链接的含义,其实就是将一个新的文件名,直接和目标文件的inode建立链接。那也就是说当程序打开硬链接并正在运行时,目标文件是不能被修改的。

        2.当给一个文件创建了一个硬链接,该文件的引用计数就会+1,如图中的 mytest/ 的引用计数由1变为2。此时我们删去mytest/ 或 test.link ,首先该文件不会丢失,其次引用计数会变回1。

        也就是说,想要真正删除一个文件,就是让该文件的引用计数减到0,对应的磁盘空间就会被释放。

软链接

        创建方式:

ln -s 目标文件 软链接名

        软链接文件与目标文件的inode并不相同,也就是说创建软链接就是创建了一个新文件,两个文件拥有不同的inode,是两个独立的文件,软连接文件在磁盘层面上指向了目标文件的内容。当程序通过软连接访问目标文件时,目标文件是允许被修改的。

两者各自的使用场景

硬链接:

  • 备份文件:创建一个原始文件的硬链接,作为备份文件,以确保在原始文件被意外删除或丢失时还有备份。
  • 共享文件:如果多个用户需要访问相同的文件,可以使用硬链接来避免重复存储文件。
  • 一些应用程序可能要求使用硬链接来查找文件。例如,当安装某些软件包时,软件包管理器可能会使用硬链接来将文件分散到不同的位置。

软链接:

  • 指向动态库:在Linux系统中,软链接通常用于指向共享库,这样多个应用程序可以共享同一个库文件。
  • 快捷方式:在Windows系统中,软链接通常用于创建桌面快捷方式,以便用户可以更方便地访问文件或程序。
  • 跨文件系统引用文件:因为软链接可以跨越文件系统和分区,所以可以使用软链接在不同的文件系统之间引用文件。

四、动静态库

        库的存在,是为了开发人员能够把自己的方法提供出去,或是用别人提供的方法。学习动静态库的创建、发布和使用,也是为了后续的学习。

        一般的库,是将源文件经过“预处理 -> 编译 -> 汇编”后,形成 .o 文件,将所有的 .o 文件打包成库文件,再将与源文件对应的头文件打包,再同库文件打包,最终形成了我们发布或使用的库。

测试代码:

///lib.c//
#include "lib.h"

void print_lib(void){
    printf("I am static lib");
}

///lib.h//
#ifndef __LIB_H__
#define __LIB_H__ 
#include <stdio.h>

void print_lib(void);

#endif 

///main.c//
#include "lib.h"

int main(){
    print_lib();
    return 0;
}

静态库

        程序在编译链接的时候,会将静态库直接与main文件进行链接,也就是说,使用静态库就相当于把库中的代码直接拷贝到调用处。在链接生成二进制可执行文件后,即使删掉库文件,程序依然可以正常运行。

        创建方式:

ar -rc libxxxx.a 目标文件.o

        ar - 建立,修改档案。归档。

        rc - replace & create

库命名规则:lib  xxxx(库名称).a

        链接静态库:

gcc main.c -I 头文件所在路径 -L 库文件所在路径 -l 库名称 -static

        -I :后面跟头文件所在路径,不写默认当前路径。

        -L:库文件所在路径,不加默认当前路径。

        -static :如果-L指向的路径下只有静态库,不加-static也默认使用静态库,如果路径下有动态库,不加-static既默认使用动态库,所以使用静态库时最好加上。

        -l:库名称,一定是去掉前缀“lib”和后缀“.a”,中间的库名称。

这种方式最直观也最简单,但每次编译都需要写这么长的命令行,比较麻烦。

        1.可以写一个编译脚本。(推荐)

        2.可以把库文件放到系统库文件夹,把头文件放到系统头文件夹,就可以直接gcc main.c啦,但极其不推荐。因为自己写的库或外来库对Linux而言都属于第三方库,一旦操作不好,就会对系统造成不可逆的损坏。

动态库

        动态库相较于静态库而言,程序在调用动态库时,将库函数的索引放在程序内,当程序运行到该库函数时,再去跳转到库函数执行代码。所以用动态库编译的程序一般比用静态库编译的内存小,但它也依赖动态库,一旦动态库被删除或修改,程序也将受到影响。

        创建方式:

gcc -fPIC -c lib.c -o lib.o

        -fPIC:表示编译生成的代码中使用相对位置地址,表示生成的目标文件时位置独立的(Position Independent Code)。位置独立的代码可以实现共享,可以被多个程序调用。

gcc -shared -o libxxxx.so lib.o

        使用动态库

和静态库的指令一样的格式,但不加-static

gcc main.c -I 头文件路径 -L 库文件路径 -l 库名称

此时就会生成一个可执行程序,然而当我们运行时发现可能会报错

这是因为,动态库的加载方式是运行时动态链接,需要在程序执行时从系统内查找并加载所需程序。因此此时有三种方式解决这一问题:1.将库文件拷贝一份到可执行程序所在目录下(或者更推荐用软连接的方式,但要注意软连接名称必须和库文件完全一致)。2.添加环境变量。3.将.so文件添加到系统的动态库(也叫共享库)目录下。

  • 34
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值