【Linux系统编程:基础IO 下】dup2 实现输出重定向、输入重定向、追加重定向 | 理解磁盘 | 理解文件系统中inode的概念 | 软硬链接|制作打包与使用动静态库

本文主要是介绍【Linux系统编程:基础IO 下】dup2 实现输出重定向、输入重定向、追加重定向 | 理解磁盘 | 理解文件系统中inode的概念 | 软硬链接|制作打包与使用动静态库,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在前面

这里先接着《基础IO 上》中的缓冲区的内容作些补充,这里主要补充 dup2 接口。

✔ 测试用例一:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main()
{close(1);int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;	}fprintf(stdout, "hello world!: %d\n", fd);close(fd);return 0;
}
  • close 1 后,1 就不再表示显示器文件,而 open log.txt 后,1 就表示 log.txt 文件,所以 fprintf 并不会往显示器上输出,而是会往 log.txt 里输出,可是 log.txt 中没有内容。通常数据流动过程是:先把语言上的数据写到用户层的缓冲区 ➡ 然后数据通过文件描述符在操作系统内核中,找到自己的 task_struct ➡ 然后通过 task_struct 中的 struct files_struct* files 指针找到 struct files_struct 中 struct files* fd_array[] 以文件描述符为下标的位置 ➡ 然后再通过下标的内容找到要写的 struct_file,并把用户层缓冲区的数据拷贝到内核层缓冲区 ➡ 操作系统再由自己的刷新策略和时机通过磁盘驱动刷新到磁盘设备。注意因为用的是 C,所以这里的用户层缓冲区是 C 提供的,如果是其它语言,那么用的缓冲区就是其它语言提供的。所以之所以操作系统没有由用户层把数据刷新到内核层是因为现在 1 指向的是磁盘文件。显示器是行刷新策略,磁盘是全缓冲策略,这两种策略既可以被用户层采纳,也可以被内核层采纳。

    在这里插入图片描述

  • 为什么语言都要在用户层提供一个缓冲区,printf 直接把数据刷新到内核缓冲区不行吗

    上层只要把数据写到用户层缓冲区中就不用管了,剩下的就由操作系统来完成,所以对用户来讲,就完成了用户层和内核层之间的完全解耦。而用户要自己拷贝数据到内核层,还需要提升权限,效率太低。

    所以用户层中存在缓冲区可以让用户和底层之间的差异屏蔽掉,以此来提升效率。同理内核层中存在缓冲区也有着解耦、提高效率的意义。

✔ 测试用例二:

#include<stdio.h>  
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main()
{//c callprintf("hello printf\n");fprintf(stdout, "hello fprintf\n");fputs("hello fputs\n", stdout);//system callconst char* msg = "hello write\n";write(1, msg, strlen(msg));fork();return 0;                          
}                                      
  • 这里的 fork 看起来没有什么价值,也不会影响到什么,fork 之后,父子进程马上就退出了。

    在这里插入图片描述

    但是当我们重定向后,就会出现很诡异的现象。

    在这里插入图片描述

    我们发现往显示器上输出的结果是合理的,但是往普通文件上输出的结果却很诡异,它输出了 7 条信息,且使用 C 语言接口的都输出了两次,使用系统调用接口的输出了一次。毫无疑问这种现象是和随手写的 fork 是相关联的,因为去掉 fork 再重定向是合理的。

    这里先看 C 语言接口,我们都知道,这里输出的三条数据并不会直接写到操作系统里,而是先写到 C 语言的缓冲区中,fork 之后,程序就分流了,但不幸的是,程序马上要退出了,而 C 语言缓冲区中的数据也要被刷新,此时父子进程谁先运行,谁就先刷新,而缓冲区中的数据也是数据,即使是 C 语言的数据,也不能凌驾于进程之上,当父或子想刷新时,那么立马要发生写时拷贝。至此,我们就能理解重定向后,刷新策略由行刷新变为全缓冲,也就是说 fork 时,数据还在 C 缓冲区中,而 fork 后,父子进程谁先运行谁就发生写时拷贝,所以重定向后,C 接口的数据输出了两份;而向显示器输出时,因为显示器的刷新策略是行刷新,且这里的每条数据都有 \n,所以每执行完 printf,数据就立马刷新出来,此时 fork 时便无意义了。

    而重定向后,系统接口没有受影响的原因是 write 会绕过语言层缓冲区,写到内核层缓冲区,而其实只要是数据都要写时拷贝,但大部分情况只针对用户数据,对于内核数据,数据属于操作系统不会写时拷贝,属于进程会写时拷贝,但这种情况很少考虑,现在我们就认为写时拷贝主要拷贝的是用户数据。

  • 我们现在也能解释这里的打印顺序不一样的原因是因为只有在多进程的情况下,把数据缓冲起来,交叉式的刷新才可出现了顺序不一样的现象。

  • 通常我们不建议所语言接口和系统接口混合使用,因为可能会出现一些难以理解的现象。

一、dup2

✔ 测试用例三:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>//输出重定向
int main01()
{int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}dup2(fd, 1);//此时再写入就不是标准输出,而是fd                             const char* msg = "hello dup2->output\n";int i = 0;while(i < 5){write(1, msg, strlen(msg));i++;}close(fd);return 0;
}//输入重定向
int main02()
{int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}dup2(fd, 0);//此时再读就不是从标准输入读,而是fdchar buffer[1024];ssize_t sz = read(0, buffer, sizeof(buffer) - 1);if(sz > 0)                                       {buffer[sz] = 0;printf("%s", buffer);}close(fd);return 0;
}//追加重定向  
int main03()
{int fd = open("log.txt", O_WRONLY|O_APPEND);if(fd < 0){perror("open");return 1;}dup2(fd, 1);//此时再写入就不是标准输出,而是fdconst char* msg = "hello dup2->append\n";int i = 0;while(i < 5){write(1, msg, strlen(msg));i++;}                                            close(fd);return 0;                                    
}                                                
  • 我们之前自己实现重定向时是先 close 被重定向的文件,再 open 想重定向的文件。有没有可以不 close 被重定向的文件,直接重定向,此时系统提供了类似的接口 dup2 来解决 close 多此一举的行为。

  • 使用 dup2,需要包含 unistd 头文件,

    在这里插入图片描述

    要输出的文件描述符是 1,而要重定向的目标文件描述符是 fd (echo “hello” > log.txt),dup2 应该怎么传参 —— dup2(1, fd) || dup2(fd, 1) ❓

    很明显,依靠函数原型,我们就能认为 dup2(1, fd),因为 1 是先打开的,而 fd 是后打开的,可实际上并不是这样的。文档中说 newfd 是 oldfd 的一份拷贝,这里拷贝的是文件描述符对应数组下标的内容,所以数组内容,最终应该和 oldfd 一致。换言之,这里就是想把 1,不要指向显示器了,而指向 log.txt,fd 也指向 log.txt。所以这里的 oldfd 对应 fd,newfd 对应 1,所以应该是 dup2(fd, 1)。

  • 运行结果

    输出重定向

    在这里插入图片描述

    输入重定向

    在这里插入图片描述

    追加重定向

    在这里插入图片描述

  • 所以现在我们就明白了:

    echo "hello world" > log.txt —— echo 是一个进程;“hello world” 默认是调用 printf 或 write 往显示器上输出;log.txt 是调用 open 使用 O_WRONLY|O_CREAT 打开;> 是调用 dup2,将默认标准输出 1 的内容改为 log.txt;

    在这里插入图片描述

    < 就是 dup2(fd, 0),且 open 文件的方式是 O_RDONLY;

    在这里插入图片描述

    >> 同 >,都是 dup2(fd, 1),只不过它打开文件的方式是 O_WRONLY|O_APPEND;

    在这里插入图片描述

  • 进程替换时,是否会干扰重定向对应的数据结构

    在这里插入图片描述

    它们当然不会互相影响。换言之,将来 fork,创建子进程,子进程会以父进程的大部分数据为模板,子进程进行程序替换时,并不会影响曾经打开的文件,也就不会影响重定向对应的数据结构。

二、理解文件系统

从头到位我们都在说打开的文件,磁盘中包含了上百万个文件,肯定不可能都是以打开的方式存在,其实文件包含打开的文件和普通的未打开的文件,接下来我们重点谈未打开的文件。我们知道打开的文件是通过操作系统被进程打开,一旦打开,操作系统就要维护多个文件,所以它是需要被操作系统管理的,也就是说这种方式,磁盘上和内存上都有这个文件,它们不是完全一样的,内存中的文件更强调的是属性和方法,磁盘中的文件更强调的是数据,它们是通过缓冲区关联的;而普通的未打开的文件在磁盘上,未被加载到内存中,它当然也要被管理;其中管理打开的文件和管理未打开的文件在操作系统中有一个功能模块叫做文件系统。之前我们谈过进程 vs 程序,一个被打开的程序就是进程,只不过我们在解释进程时不是严格把它当作文件来解释,需要明白的是进程是要被加载到内存的,程序就是一个磁盘文件,打开的文件是进程,而普通未打开的文件是程序。

在这里插入图片描述

💦 磁盘
  • ls -l 可以看到当前路径下文件的元数据,也就是文件的属性。其中这里的硬链接数我们还没有谈过,一会我们会谈。

    在这里插入图片描述

    这里在命令行上输入 ls -l,bash 解析 ls -l,fork 创建子进程,让子进程通过进程替换执行 ls -l,ls -l 会在当前路径下把文件的属性通过磁盘读到内核,再由内核读到用户空间显示出来。stat 命令还可以查看更详细的信息。

    在这里插入图片描述

    如果不深入的话,这块也没啥价值,谁不知道文件在磁盘上,谁不知道 ls -l 读取当前路径下文件的属性,所以我们还要研究它的原理。但是在此之前,我们需要先认识磁盘,关于磁盘这个话题,还会在数据库中再谈一次。

  • 众所周知,磁盘分为机械硬盘 (HDD) 和固态硬盘 (SSD),现在很多的电脑都是机械硬盘和固态硬盘组合使用,但服务器上大多都是机械硬盘,只有一些高效率的存储集群会用到固态硬盘,机械硬盘和固态硬盘在存储技术上肯定是不同的,而我们主要了解机械硬盘,因为它多用于服务器上,其次虽然固态硬盘要比机械硬盘快不少,但在 CPU 看来,都很慢,所以我们就了解最慢的。

    如下图,虽然磁盘的盘面看起来很光滑,但是它上面有一些同心圆,这些同心圆用圆白线划分,每一圈叫做磁道,数据写在这些有颜色的区域上。实际上你并不是把一圈的空间都用完,所以这里还使用了一些直白线划分,被圆白线和直白线划分出来的区域叫做扇区。所以当盘片在旋转、磁头摆动就可以找到这个盘面的任何一个扇区进行读写。 实际磁头和盘面并不是接触的,它们之间的距离就像一架飞机在离地 1 米在滑行,所以在一些老点的记本发生抖动时,磁头就会与盘面接触,此时就有可能会刮花盘面,电脑就可能会发生蓝屏等。

    在这里插入图片描述

    盘面是有两面的,且两面都是同心圆,根据配置不同,有些磁盘可能还有多组盘片,我们可以从上至下的分为不同的盘面,也叫做你是第几个盘面。

    在这里插入图片描述

  • 虽然在 C 语言中我们知道访问内存的基本单位是 1byte ,但是在操作系统的角度认为内存的基本单位一般是 4kb,在操作系统看来,内存就是一个数组,每一个元素是 4kb,之前在谈进程地址空间时也说过它叫做页框,4kb 是页帧,所以操作系统申请内存时是按 4kb 为单位进行分配和加载的,语言层面上并不关心底层是怎么做的,比如你要 malloc 1byte,那么操作系统也不可能直接给你 4kb,有可能 C 语言本身就缓冲了一部分空间,让你去使用,如果超出这一部分空间,操作系统再重新分配。磁盘存储也有基本单位,一个基本单位是一个扇区,它是磁盘读取的最小单元,大部分磁盘的一个扇区是 512byte,但你会发现虽然这里好像越靠近圆心,扇区越小,其实它们都是 512byte,原因是越靠近圆心的虽然扇区越小,但是比特位也相对外圈更密集。内存和磁盘之间也是有交互的,它们之间的交互我们称为 output、input,也叫做 IO,一般内存和磁盘之间 IO 交互时,不是纯硬件级别的交互,而是要通过文件系统完成,也就是通过操作系统。这里用户和内存之间交互的基本单元大小是 1byte,一般内存和磁盘之间交互时的基本单元大小是 4kb,所以文件系统在往磁盘读数据时,要读 8 个扇区,这就是数据由磁盘加载到内存的过程。

    在这里插入图片描述

  • 其中我们再看 stat 中展示的信息,我们把内存和磁盘之间交互时的基本单元大小 4kb 叫 Blocks,这里的 IO Block:4096 就是 8 × 512。

  • 一般像这样的机械硬盘,物理上是圆状,操作系统很难去管理它,因为操作系统如果不对它进行抽象化处理,那么操作系统中的代码可能就是 read(盘面,磁道,扇区),操作系统需要知道这三个参数的话,那么一定要在操作系统读取磁盘的代码中以硬编码的形式写到操作系统中。但是如果有一天,你给自己的电脑加了一块固态硬盘,你要对固态硬盘进行读操作,就不能再用以前的方法了,因为固态硬盘与机械硬盘的结构不一样,它没有盘面、磁道、扇区,所以操作系统中曾经设计好的代码就得修改。很显然,这样的设计导致它们之间出现了强耦合,这是很不合理的。

    在这里插入图片描述

    所以我们需要对磁盘抽象化处理,将圆状结构的磁盘空间抽象成线性结构的磁盘空间,很多人就纳闷了,这里举两个例子方便理解,a) 其实在 C 语言中我们见过的 int arr[3][4] 二维数组就是把线性的数据结构抽象成了好理解的有行有列的结构。 b) 曾经风靡一时的磁带是把数据存储于那条黑色的带子上,可能是为了空间的原因,把带子卷起来形成一个圆状,所以磁带在物理上,既可以是圆状,也可以是线状。

    在这里插入图片描述

    同样的,也能把磁盘抽象成线性结构。把磁盘上的磁道抽象成线性形状,比如磁盘的所有磁道被我们抽象成了一条 500GB 的线性空间,我们可以把它看作一个很大的数组 —— 扇区 array[NUM],其中每一个元素是 512byte,操作系统要申请 4kb,那就给数组的 8 个元素。所以将磁盘抽象后,操作系统就摆脱盘面、磁道、扇区的束缚了,操作系统只关心你想访问的哪个下标,这里的地址我们称为逻辑区块地址(Logical Block Address, LBA),这里抽象出来的数组下标是和机械硬盘中盘面、磁道、扇区构成映射关系的,这里的映射关系是由对应的机械磁盘驱动维护的,操作系统想往 2 下标处写数据,最终 2 下标一定是能对应到具体磁盘中某个扇区上。如果要往固态硬盘中写数据,也是把它抽象成线性的数组,它也有自己的固态硬盘驱动维护数组下标和固态硬盘之间的映射关系。至此,通过抽象的方法,就完成了操作系统和磁盘之间的解耦。所以最终操作系统对磁盘的管理,转换成了对数组的管理。

    在这里插入图片描述

💦 inode
  • 500G 的磁盘空间抽象成每个元素是 512byte 的数组,那样非常大,不易管理,所以操作系统还要对这 500G 的数组进行拆分,比如这里拆分成了 100G、100G、150G、150G,所以这里只要管理好了第一个 100G 的空间,然后把管理的方法复制到其它空间,其它的空间也能被管理好。这里我们把拆分的过程叫做分区,这也就是我们的电脑上为什么会有 C 盘、D 盘、E 盘。至此我们仅仅是对空间进行划分,要把空间管理好,还需要写入相关的管理数据,比如把中国 960 万平方公里,划分了不同大小的省份,你要管理好一个省,我们不考虑地质差异等因素,只要一个领导、一个团队他们把一个省管理好了,那么他们的管理方法就可以复制到其它省,同样的,刚刚我们分区的工作只是把中国划分成不同的省份,接下来我们还要分配每个省的省长、省中每个市的市长、市中每个镇的镇长等,以此来管理一个省。这里我们把分配的过程叫做格式化过程,所谓的格式化在计算机中就是写入文件系统,也就是说我们要把文件系统写入某个分区中,这个文件系统的核心包括数据 + 方法,数据就类似这个省有多少人口、粮食等,方法就类似这个省有生育政策、耕种政策等。同样文件系统包含的数据就是文件系统的类型等,方法就是操作各种文件的方法。

    当然不同的分区当然可以使用不同的文件系统,Linux 下就使用五六种不同的文件系统,Linux 可以支持多种文件系统,包括 Ext2、Ext3、fs、usb-fs、sysfs、proc。这就好比,各个省份需要因地制宜的分配不同的团队。我们今天谈的都是 Ext 系列的文件系统,另外也不谈其它的文件系统如何,我们就认为磁盘上不同分区的文件系统是一样的。

    因为一个省也很大,为了更好的管理,还要分配市长、镇长等,同样的分区后的 100G 空间还要再划分,比如这里划分了 10 组 10G 的空间,然后把它看作成一个一个的块组(Block group),一个块组中又有多个 4kb 空间,而磁盘存储是有块基本单位的,文件系统认为一块是 4kb,我们只要把一个块组管好,整个文件系统内的块组就能管好,所以问题又转换为怎么把这 10G 的空间管好,所以接下来划分的才是文件系统写入的相关细节,也是我们要研究的,这个区域的信息,大家都有,可能略有差异。

    在这里插入图片描述

    这里 Linux 文件系统以 Ext 系列的为话题, 因为不同的文件系统可能略有差异。在块组之前,有一个 Boot Block,它是启动的意思,一般一个磁盘的 0 号分区的 0 号块组上的第一扇区存储着一些启动信息,这里不是重点。这里我们重点谈一个块组细分下来的后四个信息:

    A) Super Block 是文件系统的核心结构,用于描述文件系统的属性,包括文件系统名、文件系统版本、块组中有哪些使用和未使用,一般计算机启动时,Super Block 会被加载到操作系统,其中每一块组好像都有一个 Super Block,但实际可能 10 个块组中只有两三个有 Super Block。

    B) Group Descriptor Table 是块组描述符表,Super Block 描述的是整个块组相关的信息,这里描述的是一组的信息,每一个块组都必需要有一个 Group Descriptor Table。

    C) 我们说过文件 = 内容 + 属性。这里的内容和属性采用分离存储,属性放在 inode Table 中。一个组中可以放多少个 inode 是一定的,基本上,一个文件或目录一个 inode,inode 是一个文件的所有属性集合,属性也是数据,也要占用空间,所以即便是一个空文件,它也要占用空间,这里的属性集合包含文件权限、大小等,但不包含文件名,这个下面再说。

    内容放在 Date blocks 中。比如这里的块组是 10G,那么 inode Table 占 1G,Date blocks 占了 8G, 一个 inode 是 512byte,粗略的算一下,1G 大概 42 亿多字节,除以 512 大概也有几千万,所以这样一个块组能保存几千万文件的 inode 信息,这里 inode Table 和 Data blocks 的划分可能会出现你用完了,我没用完,你没用完了,我用完了的情况,这种情况并没有有效的方法解决。

    Date blocks 相当于一个数据块集合,以 4k 为单位,对应的数据块属于哪些文件,是由 Data Blocks 和 inode Table 维护的。如下图,inode Table 包含了若干大小相同的块,这些块有不同的编号,对应就是文件的属性,Data blocks 也包含了若干大小相同的块,这些块也有不同的编号,对应就是文件的内容。此时新建文件或目录,就给文件申请 1 号 inode,并把文件的各种属性写入到 1 号 inode,1 号 inode 中包含了一个数组 block b[32],比如 1 号 inode 需要 2 个数据块,所以 [0] = 2,[1] = 3,所以 1 号 inode 就可以找到对应的数据块。换言之,要在磁盘上查找一个文件,我们只需要知道这个文件的 inode 是多少,至此,我们知道真正标识文件的不是文件名,而是文件的 inode 编号。既然 inode 大小是确定的,万一文件是 10 个 T,此时数据块就不够了,文件系统的处理策略是数据块不仅可以保存数据的内容,还可以保存其它数据块的编号,它类似于 b+ 树。换言之,对于保存较大的文件,可能就需要多级索引的形式。

    在这里插入图片描述

    这里 ls - i 就可以查看文件或目录对应的 inode 了,可以看到这里的 inode 并不是严格连续申请的。它依然能看到文件名,是因为我们需要识别。

    在这里插入图片描述

    如何知道文件的 inode 编号 && 如何理解目录

    我们知道程序员定位一个文件,是通过绝对路径或相对路径定位的,但不管是绝对路径还是相对路径最终一定是要有一个目录。目录当然是一个文件,也有独立的 inode,也有自己的数据块,目录中的 block 数组能找到对应的数据块,目录的数据块维护的是文件名和 inode 的映射关系。换言之,在目录下创建文件时,除了给文件申请 inode、数据块之外,还要把文件名和申请创建成功之后文件的 inode 编号写到目录的数据块中。所以现在就能理解为什么大多数操作系统下同一个目录中不允许存在同名文件。所以只要我们找到了目录就可以找到目录下的文件和对应的 inode,然后就可以通过 inode 读取文件的内容。所以 ls -l 时就可以读到文件的属性信息,它是在当前目录对应的 inode 下找到对应数据块中文件名和文件名映射的 inode,再去找对应文件的 inode,此时就看到文件的属性了。所以 echo “hello world” > file.txt 是先启动进程,这个进程当然知道自己所在的目录,所以它就可以拿着 file.txt 文件名找它对应的 inode,把数据追加到对应的数据块中。所以我们说 inode 不存储文件名,只是往目录的数据块中写入文件名和文件对应的 inode。

    touch 一个空文件 file.txt,ext* 文件系统,做了什么工作 ❓

    它先在 inode Bitmap 申请没有被使用的位,然后把文件对应的属性信息 inode Table 里写,再把申请好文件的文件名和 inode 编号写入 Data blocks,建立映射关系。如果想 echo “hello world” > file.txt,现在是在当前目录下,file.txt 和 inode 就找到了,再通过 file.txt 文件的 inode 去对应的 Data blocks 中写入数据。所以现在就能理解为什么大多数操作系统下同一个目录中不允许存在同名文件。一方面是不方便人查看,另一方面是若一个目录下有同名文件了,那么 inode 去操作文件时操作的是哪个文件呢。

    D) Block Bitmap 和 inode Bitmap 是位图,就是用比特位 0 1 来表示。Block Bitmap 用来标识数据块的使用情况,inode Bitmap 用来标识 inode 的使用情况,每个比特位都对应一个块。换言之,当你新建文件时,它并不是遍历 inode 区域,这样太慢了,它只需要在系统启动时,将 Blok Bitmap 和 inode Bitmap 预加载到系统中,你要新建文件,就把 inode Bitmap 的比特位由 0 至 1,文件需要多少数据块,就把 Block Bitmap 的比特位由 0 至 1。所以我们可以通过位图,可以快速的完成 inode 的申请和释放,同时也能确认当前磁盘的使用情况。但是位图依然还是需要去遍历哪些使用和未使用,以及做位操作等,所以这里通过 Group Descriptor Table 来管理。

    如何理解删除文件

    之前我们说过,计算机中删除一个文件并不是真正的删除,而是把那块空间标识为无效,就像拷贝一部电影到 u 盘需要 1 分钟,但是删除 u 盘上的电影只需要 1 秒钟;亦或是盖一个房子需要 1 年,而销毁一个房子只需要在墙上写上拆 。而现在理解的是不用把 inode 属性清空,不用把 inode 对应的数据块清空,只要把两个位图中对应的比特位由 1 到 0,再把所在的目录下中的对应的映射关系去掉,此时空间就是无效的,下一次再新建文件时,就可以直接把无效的空间覆盖。

    按上面这样说,删除后的文件当然可以恢复,Windows 下的回收站就是一个目录,当你删除时就是把文件移动到回收站目录下,然后把其它目录下数据块中的映射关系移动到回收站目录下的数据块中。Windows 下就算把回收站的内容删除也是能恢复的,Linux 下,如果要恢复删除的文件是有一些恢复工具的,但有可能在恢复过程中,创建各种临时文件,可能就会把想恢复的文件的信息覆盖掉,你想自己恢复删除的文件,就需要更深入的了解文件系统原理。

三、软硬链接

💦 软链接

在这里插入图片描述

ln -s file.txt soft_link 给 file.txt 建立软链接 soft_link(可随意)。 file.txt 的 inode 是 657160,soft_link 的 inode 是 657162,也就是说软链接 soft_link 就是一个普通的正常文件,有自己独立的 inode,soft_link 中的数据块中保存着它指向的文件 file.txt 的路径,就类似于 Winodws 下的快捷方式,比如桌面看到的软件保存的是其它的路径,在系统中可能你要运行的可执行程序在一个很深的目录下,就可以在较上层的目录中建立软链接。

💦 硬链接

在这里插入图片描述

ln file.cpp hard_link 给 file.c 建立硬链接 hard_link(可随意)。其中我们发现硬链接和它链接的文件的所有属性都是一样的,包括 inode,所以硬链接没有独立的 inode,所以严格来说硬链接不是一个文件。我们还发现了链接后的 file.c 的有一个属性信息由 1 变为 2,所以这里 ls -l 显示的这一列数据表示的不是软链接,而是硬链接。而当把 file.txt 删除时,hard_lind 就由 1 变为 2。所以硬链接本质就是在 file.c 文件所在目录的数据块中重新创建一个映射关系,也就是给 file.c 的 inode 重新起了一个别名。

硬链接的应用 ❓

在这里插入图片描述

为什么创建普通目录的硬链接是 2 ?创建普通文件的硬链接是 1 ?—— 普通文件是 1 好理解,因为当前目录中只包含一组 file 和 file 的 inode;

在这里插入图片描述

而普通目录是 2 的原因是因为除了当前目录下包含了 dir 和 dir 的 inode,还有 dir 目录下中隐藏的 " . ",这个点叫做当前路径,此时我们发现这个点的 inode 和 dir 的 inode 是一样的,所以 dir 的 inode 编号是 2。 这个点就是 dir 的别名,因为当前路径的使用频率很高,所以它是为了方便我们对当前路径的索引,如果没有这个别名,那就只能是 " dir/xxx/… ",完全没有 " ./xxx/… " 方便。

在这里插入图片描述

我们再在 dir 下建立一个目录 other,此时 dir 的硬链接数就变成了 3,other 的硬链接数就是 2。—— other 的是 2 能理解,因为 other 和 inode + . 和 inode。

在这里插入图片描述

而 dir 之所以是 3,是因为要 " cd … ",所以 other 下还有一个点点,它是 dir 的别名。

所以硬链接最典型的应用场景就是方便进行路径转换。

💦 文件的 ACM

在这里插入图片描述

Access 是文件最近被访问的时间;

Modify 是文件内容最近被修改的时间;

Change 是文件属性最近被修改的时间;


touch file.txt 时就刷新文件的 ACM:

在这里插入图片描述

vim file.txt 写入内容后也会更新 file.txt 的 ACM (这也很好理解,因为它对文件进行访问,内容修改,属性也修改的原因是写入了内容,文件的大小属性就变了):

在这里插入图片描述

chmod u+x file.txt 后,更新的只有 Change:

在这里插入图片描述

vim file.txt 不写入内容后它竞然也会刷新 file.txt 文件的 ACM (不同版本下的 linux,可能结果也不同):

在这里插入图片描述

cat file.txt 后并没有刷新 file.txt 的 Access,是因为 linux 2.6 之后,把访问的频率降低了,而是要真正有效访问或者访问多次后才会刷新,不同的版本策略不一样。那是因为文件在磁盘上,文件内容和属性的操作其实并不高频,比较高频的是文件的访问,而如果你经常 cat 文件,且每次都要刷新时间的话,效率就太低了。:

在这里插入图片描述

我们之前说过 make 时,它会自动检测源文件是否作修改,若没有修改则不会再被重新编译,实际只要你 vim 了,尽管没有修改,它依然还是会编译。其中 make 就是通过文件的修改时间来甄别文件是否是最新的,可执行程序的修改时间一定是晚于原文件的时间,当对原文件修改后,原文件的修改时间又比可执行程序的时间新了,所以就可以 make。所以 make 时,若原文件的修改时间晚于可执行程序,那么就可以编译:

在这里插入图片描述

四、动态库和静态库

💦 前言

为什么要使用别人的代码 ❓

为了程序开发的效率和程序的鲁棒性(健壮性)。说人话就是用了别人的代码,开发效率就高了,这句很好理解,别人把功能实现了,然后你再基于别人的代码作二次开发,所以效率当然高。其次这里的别人当然不是随便的一个别人,而指的是顶尖工程师,也就是说你的代码出问题了,当然不怀疑是人家的库的问题,这就增加了代码的鲁棒性(健壮性)。换言之,我写我的,别人写别人的,机制上我们的工作就得以分解,我可能就是一个写调用、逻辑特别严谨的程序员,别人就是写库特别好的人,所以我们两个都有比较完善的工作方式,所以合起来就可以增加代码的效率和鲁棒性(健壮性),而不要越俎代庖的既当爹又当妈。程序的鲁棒性(健壮性),更直观的理解是在百度时输入乱码,而百度并不会崩溃。

C/C++ 体系中如何使用别人的功能 ❓

  1. 动态库:libc.so,libc++.so

    静态库:libc.a,libc++.a

    一般在命名上去掉前缀 lib,去掉 . 和之后的内容,剩下的就是库名,所以这里就是 c 库和 c++ 库。

    生成可执行的方式有静态链接和动态链接,对应静态库和动态库 (这个概念之前略有了解)。
    比如你是一个新入学的大一新生,你在写作业,突然有了一个上网的需求,因为环境不熟悉,所以问了学长附近的网吧,随后就去玩了一个钟,接着回来继续写作业,这叫动态链接。后来大三了,要准备找工作了,家里给买了台电脑,当你正在学习时,你想上网,就不用去网吧了,只需要打开自己的电脑玩个一个钟后又继续学习,也就是说静态链接,它并没有和外界产生关联。
    一般我们写的程序中,大部分都是动态链接,因为动态链接中不需要把库中的内容过多的拷贝,所以相对而言,它的编译效率较高,这是其一;有时候我们下一些软件,比如 VS2017,它上面会有一些组件 C++、C# 等,这些组件并没有在你的硬盘中,而当你要安装时,它会帮你找到这些组件下载,这样有个好处就是咱需要啥就下啥,而不是直接一堆东西装在机器上,而自己需要使用的组件寥寥无几,所以这里就使用动态库的方式实现,这是其二。当然静态链接也有自己的使用场景,一般在服务器上大部分也都是动态链接,不过有时需要将服务在很多机器上部署,那么单纯的动态链接有可能会出问题,因为动态链接是在程序运行之后才去对应加载到内存中,万一有一个库丢失了,那么程序就挂了。所以有时候一些程序它也会采用静态链接,它的好处就是它不依赖任何的动态库,坏处也很明显,效率低。比如静态链接的大小是动态链接的一百倍,要把静态链接这个程序下下来,只能全部下下来,而动态链接是边下边用。总的来说,它俩各有利弊。

    动态链接的生成的可执行程序的体积往往比较小,节省资源(磁盘,内存),但是它依赖第三方库,有一定的风险,一旦库丢失,可执行程序不可执行(万一网吧被查封了)。

    静态链接虽然生成的可执行程序的体积较大,浪费资源(磁盘,内存),但是它不依赖第三方库,一旦库丢失,可执行程序可以执行(网吧被查封了,照样可以玩)。

    ldd 就可以查看可执行程序所依赖的库,Linux 中,默认生成的可执行程序是动态链接:

    在这里插入图片描述

    动态链接改静态链接,可以看到光是一个简单的程序,静态链接的大小就比动态链接高出 100 倍以上,静态链接可能只有少部分场景会使用到,一般都是用动态链接的:

    在这里插入图片描述

    所以一般为了更好的支持开发,第三方库或语言库都必须提供静态库和动态库,以方便程序员根据需要进行可执行程序的生成。

    今天我们要再次理解动静态库就从如何打包动静态库和如何使用动静态库理解。

  2. 开源代码

  3. 基本的网络功能调用

💦 静态库的制作打包与使用(不使用 makefile)
  1. 准备工作

    在这里插入图片描述
    别人要使用这些接口,最直观的就是直接给源文件,别人就直接在当前目录创建文件,然后直接包含它们就好。但是如果不想让别人看到源文件是怎么实现的,而仅仅是告诉它们的作用,所以就需要使用库。其实动静态库的属性就是不想暴露自已的源代码,所以可以把打包给别人。

    我们让别使用我们的库,前提是别人需要知道你的库能给我提供什么方法。—— 这是通过头文件来体现的(头文件当然可以暴露)。光有头文件还不行,里面只有声明,还要有实现,所以就将方法所在的源文件进行编译然后打包到库。

  2. gcc -c Add.c -o Add.o/gcc -c Add.c:

    先将源文件汇编后生成 .o 文件,它虽然也是二进制文件,但是它还不可以执行,因为还差最后一步链接。所以库就是在编译过程中,在链接的前一步停下来,编译成可被链接的目标文件

    在这里插入图片描述

  3. ar -rc libmymath.a add.o sub.o

    ar 是 gnu 归档工具,rc 表示 replace and create,意思就是如果要生成的库中已经包含了对应的 .o 文件,就 replace,否则就 create。
    此时 libmymath.a 就是静态库,所以我们将来只需要把 Add.h,Sub.h,libmymath.a 打包交付给其它人即可,而 libmymath.a 就由 Add.h,Sub.h 来说明。这也就是为什么大部分库在提供的时候,一般是提供库文件 + 头文件。这也就是为什么在使用 C 语言的时候,永远都是 #include<stdio.h>,然后在写 printf 的时候,你直接调用,最后链接库,本质上就是因为系统在装的时候就把库文件和头文件给装了,而 C 语言的源代码就不需要在系统中了。
    就相当于把源文件先编译一大部分,先不要链接,再把所以编译好的 .o 文件打个包,让别人去用,这就是静态库。

    在这里插入图片描述

  4. 使用静态库

    所以就先把 .h 和 .a 的文件都放在一个目录下:

    在这里插入图片描述

    李四想用张三写好的 Add,Sub 代码,但是张三不想让李四看到自己是怎么实现的,所以就将它们打包好变成库 st_lib,复制给李四,李四人拿到后就可以通过头文件知道了这个库是干嘛的:

    在这里插入图片描述

    所以李四就在 main.c 中使用张三写好的库,随后 gcc main.c -o main,报错说 Add.h 是没有这个文件或目录的,原因是 Add.h 既没有在当前路径下,也没有在默认路径下,这里的当前路径是指要和 main.c 在同一级路径下;所以这里 gcc 还要再加一个选项 -I,后面跟上 ./st_lib,意思就是 gcc 在编译程序,找头文件的话,除了在当前路径下,系统路径下,你也要在 ./dir 下去找。此时还有报错,但对比上一次报错,显然头文件已经找到了。此时报的是在链接时找不到 myadd 方法;所以还 gcc 还要在加上一个选项 -L,后面跟上 ./st_lib,表明库文件在这个目录下。只不过这里是巧合,头文件和库文件都在 ./st_lib 下。然后又报错了,原因是指明了的库路径,但并没有指明要访问的是这个路径下的哪一个库。这不是废话,如果这个路径下有多个库,此时就需要指明了;所以还要再加一个选项 -l,后面跟上 libmymath.a 去掉前后缀,也就是 mymath。此时就完成了链接,形成了可执行程序:

    在这里插入图片描述

    1. -I + 路径:告诉 gcc 除了默认路径以及当前路径,在指定路径下也找一下头文件。而一般 linux 下头文件的默认路径在 /usr/include/ 下,其中我们就看到了最熟悉的 C 文件 stdio.h

      在这里插入图片描述

    2. -L + 路径:告诉 gcc 除了默认路径以及当前路径之外,在指定的路径下也找一下库文件。而一般 linux 下库文件的默认路径在 /lib/ 或 /lib64/ 下, 其中你往下翻,也可以找到比较熟悉的 C 语言的静态库和动态库,libc.a 和 libc.so。这里要说一下,实际系统在搜索头和库的时候,除了这些默认路径,还有其它的路径。另外不同 linux 发行版,甚至同一发行版,如 Centos6.5/7/8,在路径上都可能不太一样,所以具体问题具体对待

      在这里插入图片描述

    3. -l + 库名:需要进一步具体的告诉我你要链接哪一个库,不然库这个么多,总不能都链吧

为什么 C/C++ 在编译的时候,从来没有明显的使用过 -I/L/l 等选项呢 ❓

  1. 库文件和头文件在默认路径下 gcc 可以找到
  2. gcc 编译 C 代码, 默认就应该链接 libc 库

如果我们自己也不想使用这些选项呢 ❓

头文件和库文件分别拷贝到系统的默认路径下,这个过程就叫做库的安装。我们之前学过环境变量 PATH,然后写了个 Hello World,让它不使用路径就可以执行,其实是把可执行程序拷贝到环境变量的路径下,其实就是安装可执行程序。

不过我们自己写的库叫做第三方库,也就是除了系统,语言之外的库。所以一般也要带上 -l name 表明你要链接的是哪一个库。

这里不需要指定静态链接的方式 ❓

这是因为当前库中只有静态库,所以这里就算不写 static,它也只能用静态的。也就是说有动态库和静态库时,gcc 编译默认是动态库,但只有静态库的时候,那就只能是静态库。

💦 动态库的制作打包与使用(使用 makefile)
  1. 准备工作

    在这里插入图片描述

    这里打包动态库依旧不想把源文件暴露出去。

  2. gcc -fPIC:产生位置无关码 (position independent code)

    gcc -shared:生成共享库格式

    在这里插入图片描述

    我们都知道在栈区和堆区中间有一个共享区,一般动态库的代码是映射在这个区域,库文件当然也是一个文件,它当然也占磁盘空间。当把程序运行,其中需要执行库中的代码,本质就是进程来执行库中的代码。换言之,进程一旦运行起来,同时也要把库加载到内存,当然也可以局部加载,然后就把库映射到共享区,此时代码区的代码就可以直接访问共享区中的库。这样做的一个好处就是,如果有多个进程时,那么可以统一把要使用的一个库从内存映射到自己的共享区,这样的话,相当于可执行程序是不需要携带库代码的,从而可以有效的节省资源。
    如果是静态链接,形成进程后就没有用共享区,此时代码区中就包含了你的代码和库的代码,这样的话如果有 10 个 C 代码,每个程序都把库代码都拷贝一份到代码区,此时在内存中就会有 10 份重复的库代码,而实际只需要 1 份即可。所以动态库最典型的特点就是所有和我使用同一种库的进程可以把库从内存映射到共享区,以节省内存资源。
    其次,有可能 A 进程共享区中只是一部分区域映射到 C 库,B 进程共享区中也只是一部分区域映射到 C 库,但是不管最终物理到虚拟地址是怎么映射的,可执行程序加载到物理内存的任何位置,最后一定要保证库中产生的各种代码与你这个库加载到内存中的位置和映射到共享区的位置是没有关系的,这就叫做产生与位置无关码。这有点抽象,如果要真的理解,需要了解编译原理中可执行程序的格式。这里可以简单化的理解因为库是随时随地可能加载的,它也可能在内存中的任何位置,也可能被映射到共享区的任何区域,所以必须保证库中的代码不会出错。比如有一万行码执行,因为它本身与位置无关,代码中出现了一个函数调用,它在编译的时候地址是 0x1234,加载到内存,映射后,函数的地址相对于调用方的地址发生变化了,代码就可能执行不起来,所以必须保证调用目标函数的地址它本身不会随着程序的加载位置以及映射区域的位置变化而变化。这就是产生位置无关码。
    比如有一条跑道,李四距离终点 80m,张三距离终点 100m,而当终点往前移动 20m 后,此时李四和张三原来距离终点的距离就不对了,但是无论终点怎么变,张三距离李四 20m 是不变的,这就是与位置无关码。
    最后再说一下,就是这个动态库是不随着本身加载到共享区的任意位置而会影响到库中代码地址发生变化,而导致库中的代码不可执行,所以 gcc 一定要用 fPIC 选项来产生与位置无关码。
    在这里插入图片描述

  3. 使用动态库

    张三打包好库 dy_lib:

    在这里插入图片描述

    李四拿到了库:

    在这里插入图片描述

    然后李四写好代码就开始编译,这里同静态库一样需要使用 -I,-L,-I 选项;然后 ./main 时报错说不能打开共享文件,原因是没有这个文件或目录,这不扯淡吗,刚不是很明显的把头文件在哪里,库文件在哪里,库文件的名字是什么告诉了 gcc,现在怎么又不认识了,此时 ldd 确实也是有个库找不到。刚才的选项是给编译器看的。而此时 ./main 时已经和 gcc 没有关系了,所以这里是运行时的问题,所以在运行的时候也要能让系统帮我们找到运行时需要使用的动态库。那为什么之前动态链接的其它程序可以直接运行,因为运行时别人的库可以被找到,它们在默认路径下。这里有一些方案,但这里只推荐这一种:这里有一个环境变量 LD_LIBRARY_PATH,在祼的机器上是没有这个环境变量,但如果你曾经做过 vim 配置,可能就会有。如果想让你的库被运行起来,需要把库路径导入其中。这里推荐导入绝对路径,导入成功再 ldd,发现这个库已经能找到了。此时就可以 ./main。

    在这里插入图片描述

💦 其它测试
  1. 当目录下同时存在动静态库,那么 gcc 在编译时默认就是动态链接,而使用 static 后就是静态链接

    在这里插入图片描述

  2. 在动态库操作的时候,需要把动态库所在的路径导入到环境变量 LD_LIBRARY_PATH,但是重新登录时,自己导入的环境变量的路径就没了。这里当然不推荐把变量导入到登录脚本。

    若想让它每次在重新登录上的时候不会被重新配置,可以把库的路径导入到 /etc/ld.so.conf.d/ 目录下。这个配置文件是永久生效的,导入成功后需要 ldconfig 刷新,不过也不推荐这种方案,原因是你写的库导入到这个目录下可能也会污染系统本身的环境变量信息。除非将来你需要导入第三方开源的库。

    在这里插入图片描述

💦 后言

动态库实际要比静态库要更复杂,因为它除了要解决库的编译问题,还要解决运行时的问题。

所以关于动静态库的制作打包使用如果觉得比较麻烦的话,可以直接把你写的库拷贝到系统的头文件和系统的库文件中,此时最多再添加一个 -l 选项即可。但是严重不推荐这样做,因为你不能保证你写的库是特别优秀的库,而且你写的库文件和头文件甚至可能会污染系统的库文件和头文件。如果将来安装一些开源的库时,可以直接默认的安装,但短期内不推荐,因为你需要强迫的让自己去加深这一套流程的印象,虽然进入公司了后这个工作基本是不需要自己做的,所以这仅仅是为了理解别人那系列的缺少省动作。所以在学习时,强烈建议先把你的库放在当前目录下,手动的导入。

系统中还有很多外部库,这里推荐一个 C++ 的库 —— jsoncpp。

这篇关于【Linux系统编程:基础IO 下】dup2 实现输出重定向、输入重定向、追加重定向 | 理解磁盘 | 理解文件系统中inode的概念 | 软硬链接|制作打包与使用动静态库的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/299316

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

Hadoop集群数据均衡之磁盘间数据均衡

生产环境,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可以执行磁盘数据均衡命令。(Hadoop3.x新特性) plan后面带的节点的名字必须是已经存在的,并且是需要均衡的节点。 如果节点不存在,会报如下错误: 如果节点只有一个硬盘的话,不会创建均衡计划: (1)生成均衡计划 hdfs diskbalancer -plan hadoop102 (2)执行均衡计划 hd

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

springboot3打包成war包,用tomcat8启动

1、在pom中,将打包类型改为war <packaging>war</packaging> 2、pom中排除SpringBoot内置的Tomcat容器并添加Tomcat依赖,用于编译和测试,         *依赖时一定设置 scope 为 provided (相当于 tomcat 依赖只在本地运行和测试的时候有效,         打包的时候会排除这个依赖)<scope>provided