【深入解析文件系统原理inode,软硬链接,动态静态库区别】
理解文件系统中inode的概念
认识软硬链接,对比区别
认识动态静态库,学会结合gcc选项,制作动静态库
一、理解文件系统中inode的概念
根据我们之前学到的知识,我们知道一个文件是被进程打开的,那系统中所有的文件都被打开了嘛?其实大部分文件都是没有被打开的,那么这些文件被保存在哪里呢?磁盘、SSD,那么操作系统肯定要对这些文件进行管理,那么一个文件要被打开的时候,操作系统如何快速定位一个文件呢?其实我们可以类比一下菜鸟驿站,你买的快递包裹就是文件,你的快递和别人的快递都存放在菜鸟驿站,也就是磁盘,你也就是进程去获取一个包裹,就需要菜鸟驿站提供的取件码快速定位一个包裹,而菜鸟驿站的老板就是一个文件管理模块,我们称之为文件系统,他要对这些包裹进行管理,放到不同的取货柜上,方便进程你要找到一个包括的时候给你快速定位到你的包裹。文件系统的核心功能就是帮助我们更好的把磁盘文件管理起来,能让用户快速找到某个文件,快速定位一个文件的具体方式就是路径。
1.磁盘机械结构
总结:
一个磁盘由多个盘片叠加而成。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面,盘面和磁头的比例是一比一的,盘面和磁头通过高速旋转来写或者读信息,盘面是由中众多的小磁铁组成,通过小磁铁指向南极还是北极来标示二进制序列,从而来标示信息。
2.磁盘的物理存储
盘面的旋转是确定在哪一个扇区,磁头的摆动是确定在哪一个柱面/磁道。
3.磁盘的逻辑存储
每个扇区是512字节的大小,每个分区会划分不同的块组,将所有的块组管理好也就将整个磁盘管理好来了。
那么我们假如我们有800GB的空间做好管理呢?先分区(C盘、D盘等等)然后再分组管理。这样我们就只需对分区里面的一个小组进行管理即可,其他分区里面的小组管理方法直接ctri + c、v就行。我们这里来举一个学校的例子,假设学校有800个人,先对800个学生分为8个班级,然后再对一个班级分为几个小组,只要一个小组能管理好,其他小组管理方法直接ctri + c、v,那么这个班级就能管理好,其他班级管理方法直接ctri + c、v,那么全校就能管理好,所以管理好全校的本质就是对这个小组进行管理。
所以对于磁盘空间我们只需要要管理好Block Group 0就可以啦!现在我们就来解释一下,先来了解一下磁盘文件特性。
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
每行包含7列:
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
上面通过什么查看到磁盘中的文件呢?inode
- inode ,中文译名“索引节点”,也叫“i节点”
- Linux系统中文件的内容和属性是分开存储的。
- 文件属性,包括文件的创建者、创建日期、文件大小、文件权限等信息,实际信息存储在块中,而存储文件属性的区域就叫做inode,因此一个文件必须占用一个 inode, 并且至少占用一个block存储文件内容。
- inode不包含文件名,文件名是存放在目录当中的。Linux系统中一切皆文件,因此目录也是一种文件。
- 每个inode都有一个号码(即 inode编号),操作系统用 inode编号来识别不同的文件。Linux内部使用 inode编号来识别文件,而非文件名,对于系统来说,文件名是 inode编号的别称,是便于用户识别文件的,文件名和inode编号是一一对应的关系, 每个inode编号对应一个文件名。
inode是一个大小为128字节的空间,保存的是对应文件的属性,该块组内,所有文件的inode空间的集合,需要标识唯一性,每一个inode,都要有一个inode编号!那我们如何查看一个文件的inode编号呢?
ls -i
命令在Linux和Unix系统中用于列出目录内容,并显示每个文件或目录的inode编号。inode编号是文件系统中每个文件或目录的唯一标识符,用于在文件系统中定位和管理文件。当你运行 ls -i
命令时,你会看到每行输出中除了文件名之外,还有一个数字,这个数字就是该文件或目录的inode编号。
系统中标识一个文件用的不是文件名,而是inode。知道这个inode就能知道文件更多的属性。
每一个文件都有一个inode属性,包含文件的权限,文件大小,时间,而每一个inode都要有一个inode编号,这个inode编号在整个分区内具有唯一性
上面的图我们知道inodeTable里面是文件的属性,而Datablocks里面是文件的内容,那我们我们如何找打一个文件呢?属性和内容怎么对应起来呢?在inode里面还存在一个大小为15的blocks数组,里面存放的是当前文件inode的数据块的块号的编号。从今往后,要在磁盘中找到一个文件只需: 找到inode,然后在nodeTable里面找到inode,此时就能进一步找到文件的属性和该文件所对应的数据块数组,在数据块里面通过数组下标找到文件的内容。
所以未来我们要新建一个文件,并向文件里面写入内容"hello world",首先我们要在inode Bitmap里面要找到一个inode没有被使用,找到inode之后,在inode Table把对应的属性标上,然后再查Block Bitmap找到一个blocks块没有被使用,找到blocks块把内容"hello world"写入到Data block块中,然后再将块的下标映射到blocks数组中,也就是inode属性修改一下,,然后返回文件的inode编号即可。
那删除一个文件呢?我们不需要找到该文件的文件名,只需要找到该文件的inode,在inode Bitmap里面根据inode Table的索引位置,将inode Bitmap里面该位置置为0,属性里面块数组我们也能找到,用了那些数据块我们也知道,我们只需根据块号将Block Bitmap里面对应的块号置为0,所以文件就被删除了,我们只需要删除位图就可以删除这个文件。
那现在的问题就是如何快速找到一个inode,通过inode编号找到inode。inode编号,在整个给分区内是唯一的,而在分组内不是唯一的。找到一个文件,就是通过inode编号找,前提是你怎么知道你的文件在哪一个分组呢?
在inode里面还存在一个大小为15的blocks数组,里面存放的是当前文件inode的数据块的块号的编号,那按照15个数组大小算,每个块的大小是4KB,那么一个文件的最大就是60KB,那这样文件是不是太小啦。
所以对于一个磁盘我们怎么管理呢?我们只需要找到每个分区的Super Block,然后将这些结构体用链表管里起来,未来管理文件系统只需要对链表进行管理即可,访问文件只需要变量链表,然后根据Super Block里面的描述字段找到文件。所以对文件系统的管理就变成对Super Block的管理。
inode里面是不包含文件名的,但是我们通常增删查改一个文件都是通过文件名来进行的,那这又怎么解释呢?任何一个普通文件,一定是存在一个目录当中的!inode本身并不直接包含文件名。那么,文件名是如何与inode关联起来的呢?这里的关键是目录。目录首先有自己的 inode属性 + 目录内容组成,目录内容是一个文件名和一个指向相应inode编号的映射关系。所以我们就能理解为什么目录没有r权限,无法查文件名,目录没有w权限,无法在该目录下增删和修改文件,原因就在于这些操作的本质就是修该目录内容(一个文件名和一个指向相应inode编号的映射关系),目录从而间接限制了我们对文件的操作。
文件创建过程:
-
查找可用的inode:首先,文件系统会在inode Bitmap中查找一个没有被使用的位置。inode Bitmap是一个数据结构,用于记录哪些inode是已使用的,哪些inode是空闲的。一旦找到一个空闲的inode,文件系统就会将其标记为已使用。
-
设置文件属性:接下来,文件系统会根据用户的请求和默认值,在inode Table中为这个新文件设置相应的属性。这些属性包括文件类型、权限、所有者、创建时间等。
-
分配数据块:文件系统会为文件分配一个或多个数据块(Data Block),用于存储文件的实际内容。这些数据块可能来自不同的位置,但它们都会被记录在inode中,以便文件系统知道从哪里读取或写入文件数据。
-
写入文件内容:用户或应用程序可以向文件写入数据,这些数据会被存储在之前分配的数据块中。
-
更新目录:最后,文件系统会在用户指定的目录下创建一个新的目录项,该目录项包含文件名和指向新文件的inode的指针。这样,用户就可以通过文件名来访问和操作新文件了。
我们来详细举例一下。
创建一个新文件主要有一下4个操作:
- 1. 存储属性:内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
- 2. 存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据 复制到300,下一块复制到500,以此类推。
- 3. 记录分配情况:文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 4. 添加文件名到目录:新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文 件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
文件删除过程:
-
删除文件内容:首先,文件系统会删除文件本身的内容。这通常意味着释放文件所占用的数据块,并将这些数据块标记为可用。
-
清除inode:接着,文件系统会根据文件的inode,在inode Bitmap和Block Bitmap中将其inode的映射置为0,表示这个inode和数据块现在都是空闲的。
-
删除目录项:然后,文件系统会在包含该文件的目录中删除对应的目录项。这个目录项包含文件名和指向inode的指针,因此删除目录项就断开了文件名与inode之间的关联。
-
更新文件系统:最后,文件系统会更新其内部的数据结构,以反映这些变化。这可能包括更新目录的修改时间、减少文件系统中的文件计数等。
需要注意的是,虽然文件的内容和数据块在删除文件时会被释放,但inode本身并不会立即被删除或清除。只有当所有的inode都被使用完,且文件系统需要更多的inode时,才会回收和重用旧的inode。同样,目录项也只是从目录中删除,而不是从文件系统中完全移除。对于一个文件,进行增删查改,都是文件所处的目录有关系!!!
所以我们就能理解如何根据文件名找到一个文件进行增删查改,你通过文件名来访问文件时,文件系统实际上会执行以下步骤:
- 查找目录:首先,文件系统需要找到包含所需文件名的目录。这通常是通过从根目录(
/
)开始并跟随路径中的每个目录来完成的,根目录的inode编号(2)是明确的。 - 搜索目录条目:在找到目标目录后,文件系统会搜索该目录中的目录条目,以找到与所请求文件名匹配的条目。
- 获取
inode
:一旦找到匹配的目录条目,文件系统就可以获取与该文件名关联的inode
。 - 执行操作:有了
inode
,文件系统就可以读取、写入、删除或执行与该inode
关联的文件或目录所请求的操作。
查找一个文件,在内核中,都要逆向的递归般得到根目录(/
),从根目录进行路径解析。根目录的inode编号是2,根目录的内容是其他的目录名/文件名和inode的映射关系。所以现在我们就能通过目录 + 文件名找打inode,但是我们前面也提过,inode值在分区内有效,那么我们怎么判断我的文件在哪一个分区呢?一个要被写入文件系统的分区,要被Linux使用,必须要先把这个具有文件系统的分区进行"挂载"!所谓的挂载就是一个文件系统所对应的分区,挂载在对应的目录中,我们可以通过df -h来查看,df命令用于显示文件系统的磁盘空间使用情况,它可以帮助你找到文件或目录所在的挂载点(即分区)。
在Linux系统中,分区的访问是通过挂载的路径来实现的。每个分区(或称为文件系统)都会被挂载到文件系统的某个目录(即挂载点)下,用户通过访问这个挂载点下的路径来访问该分区上的文件和目录。
例如,如果您的系统有一个名为/dev/vda1
的分区,它可能被挂载到根目录(/)目录下。要访问这个分区上的文件,您只需要在文件管理器中浏览到根目录(/)目录,或者在终端中使用cd
命令切换到该目录,然后就能像操作其他目录一样操作该分区上的文件和目录了。
那么我们怎么判断我的文件在哪一个分区呢?要判断文件位于哪个分区,您首先查看文件所处的路径,然后确定这个路径是否位于某个已挂载分区的挂载点之下即可,使用这个路径的前缀匹配和已挂载分区是否匹配即可判断。比如我们现在有一个路径"/home/xyc",然后找到路径的最前缀路径是"/",然后此时系统有一个名为/dev/vda1
的分区,它可能被挂载到根目录(/)目录下,所以路径"/home/xyc"就在名为/dev/vda1
的分区上。所以访问一个文件,可以根据路径的前缀,优先区分出文件在哪一个分区下!!!
但是我们发现我们在执行指令的时候只给了文件名,为什么程序就能找到绝对路径呢?在Linux和Unix系统中,每个进程都有一个与之关联的当前工作目录(Current Working Directory,简称cwd)。当您在命令行中执行一个指令并只提供文件名时,系统会使用当前工作目录作为基准来查找文件。一旦我们确定那个目录是确定的,那么这个分区也就确定了,所以新操作的文件都在指定的所挂载目录的分区创建,这就是为什么inode只在分区内部有效的原因。
二、软硬件链接
1.操作观察现象
ln -s test.txt link.soft 是一个在Linux中创建软链接的命令。这条命令创建了一个名为 link.soft 的软链接,该链接指向 test.txt 文件。
ln hello.txt link.hard 是一个在Linux中创建硬链接的命令。这条命令创建了一个名为 link.hard 的硬链接,该链接指向 hello.txt 文件。
硬链接现象观察和分析。
为什么我们默认新建一个文件,他的硬链接数默认是1呢?
- 在Linux中,新建一个文件时,其硬链接数默认是1,这是因为硬链接数本质上表示的是有多少种方式可以访问到当前的文件或目录。对于新创建的文件,我们只有一个指向它的文件名,即它的原始文件名,因此它的硬链接数自然就是1。
- 硬链接(hard link)是文件的一个或多个文件名,它实际上是把文件名和计算机文件系统使用的节点号链接起来。这就意味着,我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。但在新建文件时,我们还没有创建其他链接,所以硬链接数就是1。
- 当使用
ln
命令为文件创建额外的硬链接时,文件的硬链接数就会增加,因为现在我们有了更多的文件名可以访问到该文件。每次创建一个新的硬链接,文件的硬链接数就会加1。- 总的来说,硬链接数反映了可以访问到特定文件或目录的不同路径的数量,而在新建文件时,由于只有一个路径可以访问它,所以硬链接数默认为1。
2.软硬链接的原理和总结
原理:
- 硬链接本质就是在指定的目录下,插入新的文件名和目标文件的映射关系,并让inode的引用计数++
- 软连接本质就是一个独立文件, 软连接内容里面放的目标文件的路径!
- 软连接类似windows下的快捷方式 软连接类似windows下的快捷方式!
总结:
真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个 inode。
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -1i abc def 263466 abc 263466 def
- abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬链接数为2。
- 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应 的磁盘释放。
- 硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 def
3.软硬链接的应用场景
未来我们写了一个程序,但是我们并不想将我们的源码给别人,那么此时我们就可以使用软链接的方法,首先将我们的可执行程序放到bin目录下,然后将bin目录下的可执行程序用run进行软链接,后面我们要运行可执行程序就可以直接run就可以。
这里很奇怪,我们新建的目录啥文件没有,为什么新建的目录的硬链接数是2呢?
新建的目录的硬链接数默认为2,这与Linux文件系统的设计有关。对于普通目录,有两个特殊的目录条目被视为硬链接:.
(当前目录)和 empty。
.
(点)指向当前这个目录本身。- empty 指向当前这个目录本身。
这两个目录条目被算作是这个目录的第一个和第二个硬链接。因此,即使一个目录还没有其他硬链接或软链接指向它,由于.
和empty
这两个链接的存在,它的硬链接数也不会少于2。
那我们再在empty里面创建一个目录呢?
很明显,对于a这个目录而言,此时的硬链接数是2,我们再去看看empty的硬链接数是多少?
我们发现此时的硬链接数又变成了3
一个目录下有多少个子目录,我们可以通过硬链接数-2计算得到!-2是减去的empty和empty/.。
根据上面的公式,我们可以计算出根目录下有16个子目录。
我们再来观察一个新现象
Linux中不能给目录建立硬链接,可以给目录建立链接,为什么呢?
目录本身包含了一系列指向其他文件或目录的条目。如果允许对目录创建硬链接,那么可能会引入循环引用。例如,如果目录A硬链接到目录B,并且目录B又包含指向目录A的条目,那么就会形成一个循环。当文件系统遍历目录结构时,这种循环会导致无限循环,使得系统无法正确定位到访问的目录。但是不对呀!刚才我们不是对empty的硬链接数都为3啦!除非系统自己给目录建立硬链接,我们上面的硬链接都是系统建好的!!!但是此时的empty的硬链接也是一个环路呀!为什么此时又允许呢?这些硬链接是系统建好的情况,其实并不构成循环。在这种情况下,系统创建的硬链接是在确保文件系统的完整性和一致性的前提下进行的。这些系统级的硬链接通常是在文件或目录被创建时自动生成的,它们不会形成用户可见的循环引用。
三、动态库和静态库
1.动静态库的回顾
我们之前使用过动态库吗?当然使用过啦,我们现在的程序每次编译运行都会使用库。
上面的程序我们就分别使用了c标准库和stdc++标准库,我们的库会分为动态库和静态库。
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
我们上面使用了c标准库和stdc++标准库这些都是动态库,我们的操作系统默认安装动态库,我们现在使用的云服务器默认是没有安装的,那怎么查看一个可执行程序是动态库还是静态库呢?
如果我们想链接静态库呢?首先我们就需要安装静态库。
sudo yum install -y glibc-static
sudo yum install -y libstdc++-static
默认编译程序,用的是动态链接的,如果想要静态链接,就要加上选项-static。
库文件名称和引入库的名称
如:libc.so -> 去掉前缀lib,去掉后缀.so表示c库。
如:libstdc++. so -> 去掉前缀lib,去掉后缀.so表示stdc++库。
2.动静态库的制作和使用
那如何形成.o文件呢?
gcc -c source_file.c (-o output_file.o)
其中,source_file.c
是你的源代码文件,output_file.o
是生成的.o文件(不带默认生成同名的.o文件)。-c
选项告诉gcc只进行编译而不进行链接。
现在有个室友要使用我们的程序,我么直接把.h和.o文件给他。
现在室友就要来测试这个程序啦
gcc -o main.o mymath.o mystdio.o
然后室友就需要将main.c形成同名的main.o文件
然后我们再将几个.o文件链接起来形成我们的可执行程序。
此时我们的室友也能形成可执行程序,但是如果我们的.o文件特别多呢?那么就要链接很多的.o文件,此时就很容易出现漏掉的情况,此时我么就可以尝试把所有的.o文件打包。
ar -rc libmyc.a mymath.o mystdio.o 是一个在Linux中使用的命令,用于创建或更新一个静态库文件。该命令的各个部分解释如下:
- ar: 归档工具(archiver)的命令行工具,用于创建、修改和提取静态库文件(通常是 .a 文件)。
- -r: 替换或插入模式。如果归档文件(这里是 libmyc.a)中已经存在同名对象文件,则替换它;如果不存在,则添加它。
- -c: 创建归档文件。如果归档文件不存在,则创建它。与 -r 一起使用时,它确保即使归档文件已经存在,也会被重新创建(但实际上,由于 -r 选项的存在,它更像一个“重新构建”过程)。
- libmyc.a: 这是你想要创建或更新的静态库文件的名称。
- mymath.o mystdio.o: 这些是你想要添加到静态库 libmyc.a 中的对象文件。
所以,ar -rc libmyc.a mymath.o mystdio.o 命令的作用是将 mymath.o 和 mystdio.o 这两个对象文件添加到(或如果它们不存在则创建)名为 libmyc.a 的静态库文件中。如果 libmyc.a 已经存在,并且其中包含了 mymath.o 或 mystdio.o,那么这些对象文件将被替换为新的版本。
此时我们只需要将头文件和打包的库给我们的室友就可以啦!
于是室友就兴奋的区去写了main.c程序,一编译出问题了
此时出现了链接错误,怎么解决呢?因为gcc编译的时候使用的是标准c库,而不认识我们打包的库,所需要我么亲自告诉编译器要使用一下我们打包的库。
gcc main.c -lmyc -L . 是一个使用 gcc 来编译 C 语言程序的命令。这个命令的组成部分和它们的意义如下:
- main.c: 这是你想要编译的 C 语言源代码文件。
- -lmyc: 这个选项告诉链接器(linker)在链接阶段需要链接名为 myc 的库。库通常包含一些预编译的函数,这些函数可以在你的程序中使用。-l 选项后面的 myc 是库的名字,去掉前缀 lib 和后缀 .a 或 .so(对于静态库和动态库)。在这个例子中,链接器会查找名为 libmyc.a的库文件。
- -L .: 这个选项告诉链接器在当前目录(. 表示当前目录)中查找库文件。默认情况下,链接器会在标准库路径中查找库文件,但如果库文件不在这些路径中,你就需要使用 -L 选项来指定库文件的路径。
所以,gcc main.c -lmyc -L . 命令的作用是编译 main.c 文件,并在链接阶段链接名为 myc 的库,该库应该在当前目录中。如果链接器在当前目录中找到 libmyc.a 或 libmyc.so 文件,它将会把这个库中的函数链接到你的程序中。
为什么我们之前编译链接运行程序都没有带上-l、指定库和-L 指示路径呢,而我们今天的需要呢?这是因为我们的库是在当前路径下的,gcc不会在当前路径寻找我们的库,只会寻找头文件。如果我们不想gcc在当前目录去寻找,也想和标准c库一样,能够自己找到,我么该怎么做呢?首先将我们的头文件拷贝到ls /user/include中,然后将我们打包的库拷贝到ls /lib,所以未来我们再次使用就只要写gcc main.c -lmyc就可以链接得到可执行程序了,gcc会自动去链接。
生成动态库
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
现在我们就要对.o文件进行打包,但是现在我们实现的是动态库的打包,我们不需要使用其他工具,使用gcc即可。
但是上面操作还是不太便捷,我们可以使用makefile一下子帮我们生成.o文件和动态库。
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
%.o:%.c
gcc -c -fPIC $<
#mymath.o:mymath.c
#gcc -c -fPIC $<
#mystdio.o:mystdio.c
#gcc -c -fPIC $<
.PHONY:clean
clean:
rm -rf *.o libmyc.so
此时我们再来重新形成一下我们的.o文件和动态库
现在我们就要把我们的头文件和库动态交给我们的室友,让他来使用一下。
然后我们的室友就直接开始编译程序啦!
此时就出现问题啦!为什么呢?因为我们当前所要使用的库gcc编译器找不到,因为gcc只认识c的静态库和动态库,而我们自己写的库属于第三方库,gcc编译器根本不认识它,所以我们编译的时候必须告诉编译器你要链接哪一个库。
此时还是找不到,因为gcc编译器只会在默认的库路径(ls /usr/lib64)下去找,但是我们的库在我们的当前路径下,所以gcc编译器找不到。
现在我们就可以来总结一下啦!
如果我们不想设置-L .,我们该如何操作呢?
cp libmyc.so /lib64
我们上面的可执行程序确实形成了,也能正常运行,但是它到底是不是用了我们的库呢,我们来看看
编写库的人:未来要给别人(用库的人),交付的是:头文件 + 库文件,但是我们不给源代码。所以我们进行项目构建的时候,我们可以通过makefile一键构建,但是我们也想一键式发布该怎么做呢?
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
mymath.o:mymath.c
gcc -c -fPIC $<
mystdio.o:mystdio.c
gcc -c -fPIC $<
.PHONY:clean
clean:
rm -rf *.o libmyc.so mylib
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -rf *.h mylib/include
cp -rf *.so mylib/lib
我们来使用make output一键式发布。
我们来观察一下mylib里面的内容有啥?
所以未来我们要给要使用我们的库的人交付时,我们就可以直接将mylib进行打压缩包发送给他。
我们上面的打压缩包的操作也可以直接在makefile里面实现。
现在就轮到我们使用库的人的操作啦!首先它肯定要将压缩包下载下来啦!
然后再进行解压缩我们的mylib。
然后我们的室友就愉快的去编译mian.c啦!
此时问题就来啦,说是头文件没有找到,任何编译器都会在两套路径下搜索头文件,一个是我们的当前目录下,另一个是系统默认的头文件路径下/usr/include/,所以我们这里肯定找不到,我们的头文件在mylib/include下。
此时虽然出现错误,但是不再是我们的头文件找不到,说明此时我们已经找到了头文件,上面的这个错误我们已经见到很多次啦,就是链接时出现错误。
此时可执行程序就已经形成了,我们来运行一下。
此时程序报错啦!显示没有libmyc.so这个库,但是我们之前静态库也是这样做的呀,为什么到动态库这里就不行了呢?这是应为静态库的本质是把库中的代码拷贝到我们的main.c程序中,多以对静态来讲,只要我们编译形成了可执行程序,静态库后序就不再使用了,所以运行时就不再需要任何查找静态库的需要,而对于我们的动态库,程序运行的时候还是需要使用动态库的。但是我们不是通过上面的指令告诉了库在当前路径下吗?为什么还是找不到呢?上面我们告诉的是谁呀?是我们的gcc编译器,编译器的确也找到了库,并且给我们形成了可执行程序。但是我们要思考一个问题,可执行程序加载运行期间还和编译器有关系吗?没有关系,所以曾经我们告诉gcc编译器的库路径在运行期间是无效的。
我们可以通过ldd来查看可执行程序的所依赖的共享库列表
此时我们的libmyc.so库找不到,那要怎么处理呢?
解决1:sudo cp mylib/lib/* /lib64/,直接将库进行拷贝到系统库路径下。
所以我们把库安装在系统的库目录下(/lib64/),既可以直接编译,也可以支持运行。
注意:运行期间是不需要头文件的!!!
解决2:echo $LD_LIBRARY_PATH:该环境变量是系统程序运行时,动态库查找的辅助路径,将不在系统默认库搜索路径下的库路径,添加到LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:
/home/xyc/study/linux_-warehouse/lesson1/testlib/user/mylib/lib
但是这种做法当我们的xshell退出后,由于我们的环境变量时内存及的,所以我们自己添加的环境变量就没有了,所以上面的用法每次重新开启xshell都要再设置一次。如果我们想让其成为永久的,就需要在打开.bash_profile或.bashrc中添加我们上面的路径即可。
解决3:给我们的库进行软链接到/lib64/系统库目录下。
然后我们通过ldd来查看可执行程序的所依赖的共享库列表
解决4:在系统的配置文件/etc/ld.so.conf.d/下添加我们库的路径
ls /etc/ld.so.conf.d/ 是一个在Linux系统中使用的命令,用于列出/etc/ld.so.conf.d/目录下的文件和子目录。这个目录通常包含一系列的.conf文件,这些文件用于配置动态链接器(dynamic linker)的搜索路径。动态链接器在程序运行时负责解析动态链接的库(例如,.so文件,即共享对象文件)。
当时上面的操作太麻烦啦,如果我们只想使用:gcc -o mytest main.c -lmyc就可以链接我们的程序,该如何做呢?我们只需要将我们下载的头文件拷贝到系统的头文件目录下,将我们下载的库拷贝到系统的库目录下就可以啦。
拷贝头文件:
拷贝库:
然后我们就可以使用gcc -o mytest main.c -lmyc就可以链接我们的程序,形成我们的可执行程序了。
此时我们的可执行程序也就能运行啦。
使用外部库
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况 的函数(ncurses库)
#include <math.h>
#include <stdio.h>
int main(void)
{
double x = pow(2.0, 3.0);
printf("The cubed is %f\n", x);
return 0;
}
gcc -Wall calc.c -o calc -lm
-lm表示要链接libm.so或者libm.a库文件。
3.动静态库的查找问题
如果我们想一次性生成动态库和静态库,我们该怎么做呢?先来改一下我们的makefile
libmyc.a:mymath.o mystdio.o
ar -rc $@ $^
rm *.o
%.o:%.c
gcc -c $<
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
mymath.o:mymath.c
gcc -c -fPIC $<
mystdio.o:mystdio.c
gcc -c -fPIC $<
.PHONY:clean
clean:
rm -rf *.o libmyc.so libmyc.a mylib mylib.tgz
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -rf *.h mylib/include
cp -rf *.so mylib/lib
cp -rf *.a mylib/lib
tar -czf mylib.tzg mylib
形成静态库:
形成动态库:
tree mylib查看我们的目录和文件。
此时我们再来运行程序:gcc -o mytest main.c -I ./mylib/include -lmyc -L ./mylib/lib,那么此时链接的是哪一个库呢?gcc默认使用的是动态库。
如果我们要使用动态库呢?执行:gcc -o mytest main.c -I ./mylib/include -lmyc -L ./mylib/lib -static即可。但是如果我们只给user提供静态库,此时执行的时候不带上-static选项,我们来看一下结果是什么样的?
如果我们只提供静态库,那我们的可执行程序也没有办法,只能对该库进行静态链接,但是程序不一定整体上是静态链接的,局部是静态链接的。如果我们只提供动态库,默认只能动态链接,假如非要进行静态链接,那么就会发生链接报错。
4.动态库的加载问题
4.1.站在系统的角度理解库是如何做到共享的
内存层面的共享:
-
内存映射:当两个进程都需要加载同一个动态库时,操作系统并不会为每个进程都分配一份库文件的完整副本。相反,它会将库文件映射到进程的地址空间中。这意味着库文件的内容在物理内存中只存在一份,而两个进程通过各自的虚拟地址空间来访问这份内容。
-
共享内存区域:操作系统会维护一个共享内存区域,这个区域包含了所有被多个进程共享的内存页面。当两个进程映射同一个动态库时,这个库在内存中的页面就会被加入到这个共享区域中。这样,两个进程就可以通过各自的虚拟地址来访问这些共享的页面。
进程地址空间中的共享区:
-
地址空间布局:每个进程都有自己独立的虚拟地址空间。在这个地址空间中,通常会划分出不同的区域来存放不同类型的数据。其中,共享区是进程地址空间中用于存放共享内存的区域。
-
动态库的映射:当进程加载动态库时,操作系统会将动态库的内容映射到进程地址空间的共享区中。这样,两个进程虽然拥有独立的地址空间,但它们的共享区都映射到了同一份动态库的内容上。
-
地址转换:当进程中的代码或数据需要访问动态库中的函数或变量时,它们会使用虚拟地址来引用。操作系统负责将这些虚拟地址转换为物理地址,确保进程能够正确地访问到共享的内存页面。
举例来说,假设有两个进程A和B,它们都加载了同一个动态库libtest.so。当这个库被加载时,操作系统会将其内容映射到内存中的某个位置,并在A和B两个进程的地址空间中创建相应的映射。这样,无论是进程A还是进程B,它们都可以通过各自的虚拟地址来访问libtest.so中的函数和变量,实现库的共享。
需要注意的是,虽然库在内存层面实现了共享,但每个进程仍然有自己的代码和数据副本,以及独立的栈和堆空间。这意味着进程间的执行状态和数据是隔离的,确保了进程的安全性和稳定性。
4.2.操作系统需要对库进行管理
4.3.可执行程序本身就地址
其实我们的可执行程序没有被加载到内存的时候,可执行程序在磁盘上存储时就已经包含了各种信息,包括表头(通常称为文件头或ELF头,对于ELF格式的可执行文件)和函数的入口点逻辑地址。这些信息是程序在编译和链接过程中生成的,用于指导操作系统如何加载和执行程序。
现在我们再来详谈可执行程序。
4.4 可执行程序加载到内存的一般过程
- 进程创建与进程地址空间分配:当操作系统决定执行一个可执行程序时,它首先为该程序创建一个新的进程。这个进程拥有自己独立的进程地址空间。这个地址空间在进程看来包含了内核和自身。对于32位系统,这个空间的大小通常是4GB。
- 可执行文件的读取与解析:操作系统读取可执行文件,并解析其头部信息。这包括了解程序所需的内存大小、依赖的库、main函数的入口地址等信息。对于ELF格式的可执行文件,操作系统会特别关注其段信息。
- 内存映射:通过mmap系统调用,操作系统将可执行文件的代码段和数据段映射到进程的虚拟地址空间中。这个映射过程实际上是为这些段分配了虚拟内存地址,但此时并不占用实际的物理内存。只有当程序执行到相应的代码或访问相应的数据时,内核才会为这些页分配物理内存。
- 设置程序计数器与开始执行:最后,操作系统将程序的入口点地址(即main函数的地址或启动例程的地址)设置为程序计数器(PC)的初始值。然后,将控制权交给程序,程序开始从入口点执行。
4.5.含有动态库程序的加载过程
-
可执行程序的加载:
当操作系统决定执行一个含有动态库的程序时,它首先会加载该程序的可执行文件。这通常包括为程序分配虚拟地址空间,并将可执行文件的代码段和数据段映射到这个虚拟地址空间中。此时,程序中的静态部分已经被加载到内存中,但动态库部分尚未处理。 -
解析动态库依赖:
在加载可执行文件的过程中或之后,操作系统会检查程序中是否有动态库的依赖。这通常是通过解析可执行文件中的元数据(如ELF文件的动态段)来完成的。这些元数据包含了程序所依赖的动态库列表以及相关的符号引用信息。 -
动态链接器的介入:
当操作系统识别到动态库依赖时,它会调用动态链接器来处理这些依赖。动态链接器是一个负责在运行时解析和链接动态库的工具。它根据可执行文件中的依赖信息,确定需要加载哪些动态库,并查找这些库文件在文件系统中的位置。 -
加载动态库:
动态链接器找到所需的动态库文件后,会将其加载到进程的虚拟地址空间中。加载过程包括打开库文件、解析其格式(如ELF格式)、读取库文件中的代码和数据,并将其映射到进程的虚拟地址空间中。 -
确定动态库起始地址:
在映射动态库到虚拟地址空间时,操作系统会为动态库分配一个起始地址。这个起始地址通常基于进程的内存布局和动态库的大小来确定。起始地址的选择要确保不会与程序的其他部分或已加载的动态库发生地址冲突。 -
解析符号引用:
动态链接器会解析可执行程序中的动态库符号引用。这些引用通常以偏移量的形式存在,表示了动态库中函数或数据相对于库起始地址的位置。动态链接器会根据动态库的起始地址和这些偏移量,计算出实际的内存地址,并将程序中的引用更新为这些地址。 -
映射到共享区:
在进程的虚拟地址空间中,有一个或多个区域被标记为共享区。这些区域用于存储多个进程都可以访问的数据和代码。当动态库被加载时,它的内容会被映射到这些共享区中。这样,如果有多个进程加载了相同的动态库,它们可以共享相同的物理内存页,从而节省系统资源。 -
程序执行:
一旦动态库被加载和映射到虚拟地址空间中,并且符号引用被解析和绑定,程序就可以开始执行了。当程序调用动态库中的函数或访问其数据时,它会通过虚拟地址空间中的共享区来访问这些函数和数据。
原文地址:https://blog.csdn.net/qq_64446981/article/details/136745275
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!