Linux文件系统
目录
2. Block Bitmap和 inode Bitmap,
3.Group Descriptor Table 和Super Block

1.认识磁盘
磁盘被访问的最基本单位是扇区,512字节(4kb)
我们可以把磁盘看作是无数个扇区构成的存储介质,要把数据存到硬盘,第一个解决的问题是定位一个扇区,哪一面(用哪一个磁头,注意磁头不止一个,一个盘面有一个磁头,正反两面就是两个),哪一个磁道,哪一个扇区

即CHS寻址方式
为了更加清晰地定位扇区位置,我们可以进一步将其进一步抽象,形成逻辑上的线性存储结构

我们将每一个扇区线性连接,四个扇区组成了一个磁道(假设每个磁道只有4个扇区),多个磁道组成了一个扇面,例如最前面的是0号盘面,然后依次是1.2.3.4号盘面。如此就形成了一个数组类型的存储结构

CHS物理地址就能转化为LBA逻辑地址了
回归到硬件,我们需要知道,不仅仅CPU有寄存器,其它设备(外设)也有,因此磁盘理所当然也一样有。
控制寄存器顾名思义控制io方向,你是要读还是要写?
数据寄存器接收具体数据,或者一段地址
地址寄存器,有LBA地址,最终会被转化为CHS地址,你要存到磁盘的个位置?
状态寄存器,数据读写期间处于未就绪状态,数据读写完毕变为已就绪状态。

2.文件系统
现在我们要管理我们的磁盘,但是这个磁盘有800GB的空间,太大了怎么办?分区!我们可以使用一个结构体,存放start和end来标记每个区的起始和结束地址,我们在进程地址空间的时候就学过类似的管理方式,有五个区,那就创建一个能储存五个元素的数组。

但是就算是200GB也比较难管理,那就继续分,把能把一个10GB管理好,那么就能把每一个10GB管理好,也就能管理好这个区了。

(其中BootBlock放在这个区的最开始,用于储存操作系统启动时的一些相关内容,比如磁盘被划分了几个分区,起始分区结束分区是谁等等)
(而且为了保险起见防止数据丢失,其它区的开头也有可能储存了一个相同的BootBlock)
最后我们基于一个Block group来介绍
一个Block group中含有以下结构
1.inode Table 和Data blocks,
分布储存文件的属性和内容。
inodeTable中有许多的inode,里面储存了文件的所有属性,一个文件只有一个inode。
Data blocks中有许多的块,里面储存了文件的数据,因为文件大小不同,一个文件可能有一个块,也可能有多个块来储存它的数据
(也对应了上面说的,文件的属性和内容是分开存储的)

我们可以将inode视为一个结构体

里面包含了文件所有的属性,比如我们看到的读写权限,所属组拥有者等都储存在里面

但是需要注意的是,在Linux的文件属性中并不包含文件的名称,而是以inode编号来标识文件,如图即下面的1464409以及1464406

而里面包含的数组blocks则可以指向相应的数据

我们可以根据里面存储的块号来找到对应的数据 ,但这样是否可储存的内容太少了?一个块仅仅4kb,没关系,Linux中还引进了间接索引和三级索引。
例如前12个位置是直接索引,每个位置储存了一个块号,对应的每个块中直接存储了文件的数据。
而第12.13给位置是间接索引,它们里面也分别存储了一个块号,但是对应的每个块中并不存储文件数据,而是储存块号,这些对应的间接块中储存的才是文件的数据。
第14位置则是三级索引,它里面存储的是块号,而这些间接块号中存储的依然是块号,对应的数据储存在第三层的块中。
2. Block Bitmap和 inode Bitmap,

顾名思义,他们是位图,将比特位的位置与块号或者inode编号映射起来,来标识该位置的块是否被使用或者该inode编号是否有效。所以我们删除某一个文件,并不用真的把数据给清空了,只需要在Block Bitmap中把对应的比特位改为0.
3.Super Block 和Group Descriptor Table

Super Block并不单单针对组内,里面存放的是整个分区的基本信息。比如这个分区里面一共有多少个组,每个组的大小,每个组的inode数量,每个组的block数量,每个组的起始inode等等。也因为这个特性,因此Super Block不需要在每个组内都出现,而多储存几份也是为了防止一处损坏时,导致整个分区瘫痪。
gdt里面存的是整个组内的信息,如以及使用多少inode,还剩多少inode可以使用,下一个被申请的inode的值是多少等等
最后,每一个分区在被使用之前,都必须提前先将部分文件系统的属性信息设置进对应的分区中,方便完美后续使用这个区或者分组,这种行为被称为格式化。比如我们windows中c盘,d盘就是两个分区
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
每行包含7列
模式
硬链接数
文件所有者
组
大小
最后修改时间
文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
4.增删改查一个文件
这个问题其实很简单,系统先查看一下gdt,看到gdt记录了区中仅仅使用了20%的数据块,inode也有剩余并且找到了下一个将被申请的inode编号,把文件属性填入该inode,随即它把对应inode bitmap中对应的位置置为1,然后再给数据分配块号,将数据填入块中后,把相应的Block bitmap中对应的位置置为1,最后将文件名添加到目录即可
而删除文件也并不需要真的把数据块中的内容删除,只是把相应的Block bitmap中对应的位置置为0即可,删除 == 可被修改。
而查找和修改也是同样,但是最重要的依然是找到对应inode的inode编号。
我们事先已经知道,Linux中一个文件一个inode,一个inode一个inode编号(注意inode编号是在一个区内设置的,不能跨分区)我们要对文件进行操作,最重要的是要知道该文件的inode编号,但是我们平常在操作文件的时候似乎是根据文件名来对文件进行操作的,而inode中又不存储文件的名称,那么我们该如何找到inode并得到文件的inode编号呢?
5.理解目录
Linux下一切皆文件,目录本质上也是文件,因此它也有自己的inode,也有自己的属性和内容,那么目录也需要数据块来储存其内容,那么里面放的是什么呢?是该目录下文件的文件名和对应文件的inode的映射关系!
我们之前学习过
1.同一个目录下不能有同名文件。这是因为一个文件名对应映射一个inode
2.目录下,没有w权限,我们无法创建文件。这是因为文件与inode的映射关系就储存在目录文件的数据块中,我们不能写,那么就无法建立新的映射关系。
3.目录下,没有r权限,我们无法查看文件。这是因为,如果我们都读不了文件的内容了,那么也就读不到文件与inode的映射关系了。
4.目录下,没有x权限,我们无法进入这个目录。我们进入一个目录本质上就是更改环境变量,把目录名添加到环境变量中,但是在更改之前还加了条件判断,如果我们没有x权限就无法更改
可是因为目录也有inode编号,那么我们怎么找到目录的inode编号呢?一直向上寻找吗?没错。但是好在我们系统的根目录的名称是确定的/,它也有inode,只要找到对应的数据块就可以一直向下找到每一个文件的文件名和对应inode的映射关系。这也是为什么我们找一个文件需要带上路径。系统中任何对文件的操作都需要带路径,有时候我们看到没有使用路径只是因为例如环境变量等等许多地方已经储存了相应的路径了,我们省略了自己带路径,但是依然需要路径。至于相对路径,那是因为我们已经找到这个‘相对’对象的inode,也就可以由它来找其它文件了。
因为不断递归向上找的方式太慢了,所以系统一般会把我们最经常使用的若干目录以及相关的inode进行缓存,这样我们在访问路径时就能快一些。
6.软硬链接
a.软链接
软链接是一个独立的文件,具有独立的inode,也有独立的数据块,它的数据块里面保存的是指向的文件的路径,相当于windows的快捷方式。我们利用软链接可以在一个目录快捷访问其它目录下的文件
ln -s [目标路径] [软链接路径]

目录不能建立硬链接但是可以建立软链接,也只需要cd即可。
b.硬链接
所谓硬链接就是在特定目录的数据块中新增一个文件名和inode编号的映射关系,只不过这个inode编号是已经存在的,本质上就是一个取别名的过程,也因为这样,实际上inode内部有一个引用计数,引用计数为几就说明有几个文件名指向这个inode。我们通常使用硬链接来实现目录的切换,例如.与..。但是系统禁止我们再自己额外给目录建立硬链接,因为如果进行查询时可能会出现环状查询的问题,为了解决这个问题,系统中查询文件的时候一般是忽略.与..的,我们使用ls指令简单查询的时候,是不能看到.与..这两个文件的。(用ls-lia可以看到)在我们建立一个目录的时候,建立完成时我们就能看到目录的硬链接数为2,这是因为这个目录中有一个.文件指向了该目录本身。所以看这个目录下有几个目录,只需要把硬链接数-2即可。而另一个..文件指向的是上级目录。


我们可以用unlink解除链接

7.补充知识
1.

物理内存和磁盘存储和交换数据的单位大小都是4kb,分别叫做页框和页帧。采用4kb大小,一是因为如果交换数据的单位太小 ,io的次数会大大增加,而在软件层面,我们加载某一部分数据如100.200kb,那么很可能后面的一些数据我们也要用,这就是局部性原理,因此产生了预加载机制。
2.
操作系统既然要管理物理内存,那么也应该要能看到物理内存的地址,我们也是先描述再组织。一个4kb的物理内存我们用一个page管理,那么要管理好整个物理内存,我们只需要把每个page管理好即可。

我们要访问一个内存,只需要先直接找到这个4kb所对应的page,就能在系统中找到对应的页框。
所有访问内存的动作,就是在访问内存page数组
3.

最开始的时候磁盘就会把Super Block中的信息用双链表的结构加载到物理内存中,以此来得知有多少分区,每个分区的起始结束以及结构。
我们的struct file中只存了比较少的文件属性,因此每个struct file会对应有一个struct inode结构体,当我们打开这个文件时,才会把inode中的内容加载到struct inode结构体中。struct file中有一个address space结构体,文件的内容存储与它有关。
它的结构是这样的

它的里面有一个page tree结构体


这棵树是一棵字典树 ,它的叶子节点中储存的就是一个个的page

这就是文件的页缓冲区,也就是内核缓冲区
4.字典树

文件的内容按照4kb来划分,每个4kb对于开始位置都有一个偏移量,我们可以理解为每个4kb数据都有自己的一个编号,那么我们 通过这个编号,利用字典树的特性,就可以精准定位到每个page
5.io子系统
这部分涉及到驱动。把物理内存中的数据刷新到磁盘中

操作系统内还有一个驱动级别的数据结构struct request,我们把物理内存中的数据刷新到硬盘的io请求也要排队,并且为了能够提高效率,能够让较多的io在同一扇区或者附近扇区,io还会进行排序和合并。
8.静态库和动态库
1.站在制作者角度
libxxx.a静态链接
libxxx.so动态链接
把我们的方法提供给别人用
1.把源文件直接给他
2.把我们的源代码打包成库 最后我给你.h+库
那我们能不能不给.h呢?不行,因为.h相当于库里面方法的一份说明书。不然我们不知道这个库里面有什么函数,哪些函数怎么调用

如上图,正常情况下这五个文件一起编译,五个文件都形成.o,然后五个.o一链接就行了。
这里我们想用静态库,那么就是先把其它四个文件先形成.o,然后打包起来变成一个libxxx.a,最后和main.c形成的.o文件进行链接。本质上没有差别

上面是定义lib变量的写法,常规的写法也是可以的
libmymath.a:mymath.o
ar -rc $@ $^
mymath.o:mymath.c
gcc -c $^
.PHONY:clean
clean:
rm -f *.o *.a
这里的makefile里面mymatch.c形成mymath.o,只需要一个$^($^指当前目标的所有依赖且去重)指代mymath.c,因为gcc命令默认产生一个同名的.o文件.这里只是把一个.o文件形成.a,我们还可以将多个.o文件形成.a。如下。
libmymath.a:mymath.o func.o
ar -rc $@ $^
mymath.o:mymath.c
gcc -c $^
func.o: func.c
gcc -c $^
.PHONY:clean
clean:
rm -f *.o *.a
我们继续添加发布的功能

mkdir -p,表示递归创建目录(如果上级目录不存在,会自动创建上级目录),同时如果目标目录已经存在,不会报错(避免 “目录已存在” 的错误提示)
创建目录结构:
mkdir -p lib/include:创建lib目录下的include子目录(如果lib不存在也会自动创建)。
mkdir -p lib/mymathlib:创建lib目录下的mymathlib子目录。
复制文件:
cp *.h lib/include:把当前目录下所有.h后缀的头文件,复制到lib/include目录中。
cp *.a lib/mymathlib:把当前目录下所有.a后缀的静态库文件,复制到lib/mymathlib目录中。
发布后我们创建一个目录test,把lib移动到test中,模拟其它人下载了我的库

一些注意点:
1.引用头文件的时候需要带路径。不然系统找不到头文件。系统找头文件有两个路径,第一是系统中的默认路径/usr/include/,另一个就是同一目录下 。

我们可以在命令行-I指定这个文件到哪找头文件(在源代码里面直接带上路径也是可以的两者没有区别),-I即include

但是我们单单带这一个选项还不够,我们还需要再指明库在哪里 ,不指明的话,系统也是会默认去找。依然是当前目录和系统默认路径,比如 lib64目录下的一些库

所以我们还需要用-L来指明这个库在哪 -L即lib

可是这样依然不够,因为系统不知道我们具体要用哪个库,用-l来指明,-l即link

(需要注意的是,这里的库名是去掉前缀lib以及后缀.a的,中间那个才是它真正的名字,一般建议l后面紧跟名字)
因为我们在main.c文件中以及指明用哪个头文件了,所以只需要指明这个头文件在哪个目录,可是系统并不知道我们要用哪个库,因此要指明到具体的库的名称
编译完就能看到 a.out文件了

(小知识点,c语言中形参实例化是从右往左的,因此例如printf(....)中,如果输出的变量有相关关系,需要注意顺序)
总结:
1.第三方库,往后使用的时候必定要带 -l 选项(但是像系统提供的fork,wait,或者语言提供的,如c语言提供的printf等,我们不需要带 -l选项)
2.gcc默认进行动态链接,但是如果系统中只提供静态库,gcc则只能对该库进行静态链接
(ldd可以查看Linux系统下动态库依赖关系)

linux-vdso.so.1:是内核提供的虚拟动态共享对象(用于加速系统调用),没有实际文件路径。 libc.so.6:C 标准库文件,实际路径是/lib64/libc.so.6。
/lib64/ld-linux-x86-64.so.2:64 位 Linux 的动态链接器(负责加载程序依赖的动态库)。
3.如果系统中需要链接多个库,那么gcc可以链接多个库,只需要在-l 后面加
4.一个程序是可以混合链接的,既可以有动态链接也可以有静态链接
5.如果我们不想带-I,-L,我们可以把相应的文件放到系统路径下,但是由于是第三方库,我们依然要指明-l. 一般不建议这么做,因为我们自己写的东西可能会污染系统中的头文件和库文件

我们这里在做的,就是安装库。只是有些库的安装没有这么简单粗暴而已

6.我们可以建立软链接来省略-I和-L。由于头文件可能会有很多,我们不可能每个头文件都手动建立一个软链接,因此我们普遍链接到头文件所在目录,然后在代码中包含头文件的时候多带一个路径即可。这样在gcc编译时,也只需要带-l。但是这种方式也不太推荐。


现在我们再看 动态库的制作。动态库的形成也需要先把库文件编译成.o形式,因此与静态库的形成也比较类似
1.我们先形成.o文件,这里因为是动态库,所以后面还要加-fPIC(与位置无关码,后面再详细说)

2.然后依然是gcc完成形成库的工作,其实也就是说gcc负责了动态库形成的全过程,所以我们就可以从侧面进一步理解为什么gcc默认是动态链接了,动态库就相当于亲儿子,静态库相当于干儿子,肯定优先找亲儿子。(如果未指定形成的文件名,-c会形成同名的.o文件)
我们可以看到这里的形式和编译形成可执行文件是很相似的,但是因为里面并没有main函数,因此我们要告诉gcc,我们形成的是共享库(-shard)。

细心的同学可以发现 我们形成静态库的时候是不能执行的,因为静态库仅仅是提供源代码的,提供二进制的。形成可执行文件的时候仅仅只是把静态库里面的东西拷贝进去,可执行文件形成之后,这个文件和静态库就完全没有任何关系了。静态库是不会加载到内存的。而这里动态库有可执行选项。我们知道,动态库最重要的是和我们的可执行程序产生关联,可执行程序在运行时如果要访问动态库的内容那么就要跳转到动态库,所以动态库也必须被加载,也因此动态库需要有可执行选项,只有这样动态库才能被加载。
可执行权限其实就是文件是否会以可执行程序的形式加载到内存中。动态库不是不能执行,而是不能单独执行。
最后我们将操作集合到makefile中,这里我们将动静态库一起形成了。

接着我们依旧模仿库的下载,把mylib移动到test中,里面有一个main.c

为了测试方便,我们先把静态库相关代码注释


我们按照之前静态库的经验进行编译,发现可以成功编译形成可执行程序,但是在执行的时候报错了。我们使用ldd指令,发现系统找不到libmymenthod.so 。我们在gcc编译的时候确实已经告诉编译器这个动态库在哪了,但是编译完之后gcc就已经和可执行程序没有关系了,这时候是系统要加载动态库,我们也应该告诉系统(也即加载器)这个动态库在哪。
这时候我们就应该明白,加载也需要路径!
通常我们有一下几种方式来让系统可以找得到相关的动态库
1.直接把库文件拷贝到 lib64目录下
2.在lib64目录下建立相关软链接



建立好软链接后系统直接就能找到了。

不要软链接了就 sudo unlink /lib64/libmymenthod.so,再用ldd看就找不到了
3. 可以更改环境变量LD_LIBRARY_PATH,来让系统能够找到,这个路径是专门给用户搜寻用户自定义路径的

我们添加了相关路径之后,系统也能顺利找到相关动态库了
(不过需要注意,当我们重新启动机器的时候,我们自己设置的环境变量就会消失)
如果真的需要的话,我们可以可以更改系统启动时所对应的脚本~/.bash_profile

4.配置/etc/ld.so.conf.d/,ldconfig更新
ld.so.conf.d路径下的文件里面都是路径,我们可以建立文件(文件名可以随便取)把自己的动态库路径放进去,只需要写到包含这个动态库的目录就可以了,不需要具体到动态库名,因为我们编译的时候已经知道名称了。然后用ldconfig更新一下这样系统就能找到了。这个就是永久有效的




最后,我们可以把动态库和静态库一起链接,编译成功后我们就算把静态库删掉,程序依然能运行

但其实,我们以后要是用别人的库 ,最常用的还是第一种方法。因为我们使用的一般都是很成熟的库了
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)ncurses是一个基于终端的
9.动态库的加载
动态库在运行的时候是要被加载的(静态库不需要)
常见的动态库,被所有的可执行程序(动态链接),它们都要使用。因此动态库也被称为共享库
所以,动态库在被系统加载到内存之后,会被所有进程共享。

(这里如果把1.exe换成操作系统的代码,那么这就是内核级别虚拟机的原理)
动态库它有自己的内容和属性,有自己的inode,因此动态库也是一个文件。
我们需要使用动态库的时候,动态库也一样被加载到内存里,然后再经过页表映射到虚拟内存。我们正文的代码执行到相应语句时,会跳转到共享区,执行完相关代码后再返回回来。
动态库是文件,一个进程可以打开多个动态库,一个动态库也能被多个进程同时使用。所以在系统运行中,一定会存在多个动态库。那么操作系统就一定会将它们管理起来,即先描述再组织。系统中,所有库的加载情况它本身是很清楚的。

如果我们有一个共享库,其中有一个全局变量errno,如果被多个进程使用,会出现问题吗? 答案是不会。共享区的那一部分是存在于用户空间的,如果多个进程同时使用同一个全局变量,那么就会执行写时拷贝。我们之前谈过c标准库会给我们提供一个用户级缓冲区,c标准库就处于共享区,因此我们使用fork时,就会触发写时拷贝。

补充:关于地址
1.程序加载前的地址
程序编译好之后,内部有地址的概念吗?
是有的!例如我们在vs下调试代码,进入汇编的时候,我们发现我们每行代码都是有地址的,函数名和变量名在编译后其实就没有这些名称了,都会被转化成地址。我们可以再联想一下继承多态里面的内容,在编译的时候,虚函数表就形成了

同时在程序没有被加载的时候 就已经被分成许多区域了,例如正文段,共享区,初始化数据区,未初始化数据区。在编译的时候程序就已经形成了地址,因为它是磁盘上的地址概念,因此我们称它为逻辑地址。由于现在逻辑地址的编码已经严格按照地址空间的方式进行,即从0~4GB。所以实际上它在数值上已经等于虚拟地址了。我们也能看出,编译器在编写地址的时候,已经考虑到后续程序加载的问题了,编译器也会考虑操作系统。

(编译代码的时候为了统一编值,main函数要先让系统看到,还要看到其他的函数,不能边编译边解析,所以在c语言中我们需要先对函数进行声明)
我们再随意写一段代码看看。其实实际上我们只需要知道第一行的地址,因为每一条指令都有对应的长度,我们知道他们的长度就能顺利定位到下一行指令。cpu在被制作的时候已经内置一些内容,让它能认识这些基础指令(指令集),cpu很笨只会执行这一句句的基础指令,但是把一句句的基础指令按顺序执行,连续起来就能完成具体的任务
2.程序加载后地址
程序在从磁盘被加载到物理内存时,内部使用的还是逻辑地址,但是天然就会形成物理地址。 因为逻辑地址就是按照0-4GB的大小存储的,加载到物理内存的时候,每个位置正好一一对应。

ELF会包含一个入口地址,会放在程序的开头处。它使用的是逻辑地址,大小上也即虚拟地址,来标记main函数代码的起始地址

那cpu怎么知道我们 现在要执行哪条指令呢?
我们知道左侧进程pcb与虚拟地址等是与右侧物理内存解耦合的。是先要形成pcb后才加载对应代码。所以我们可以先不加载代码,先把磁盘中的入口地址load到cpu的EIP寄存器(pc指针),它通过这个虚拟地址找到相应的正文代码,再通过页表继续执行,但是这时候代码还没有被加载,那么就会触发缺页中断,就会把相应代码加载到物理内存。加载后相应的代码天然就有了物理内存,而我又有它的虚拟内存,那么自然就能顺利建立映射关系。然后就是从上往下执行,我们有起始地址,又知道每行指令的长度,那么自然就能不断向下执行。

当我们的代码向下执行,cpu 读取到的指令可能有数据,也可能有地址。例如我们要call一个地址。这个地址也是虚拟地址。如果已经加载,我们直接通过页表映射,没有加载那就先进行缺页中断。由此我们发现,我们从读取程序中的地址,到内部分析地址,到二次继续访问,全部用的都是虚拟地址。

3.动态库的地址
先引进绝对地址和相对地址,左侧可以看成是一个绝对地址(虚拟/线性地址)。右侧例如我处于50位置处,树的位置在40,树的地址:10,也能表示我的位置,那么这就是相对地址,或者被称为逻辑地址。但是如果我们把树的位置放置在0位置处,那么绝对地址和相对地址就一样了。所以在前面的那个场景我们就说这里的逻辑地址可以直接看成虚拟地址。

最重要的是,动态库在虚拟内存中是任意加载的,它也应该可以在任意位置加载。如果动态库里面函数的地址也是通过绝对地址确定位置,那么就必须保证每次动态库加载都在相应位置,但是库的加载是不确定的,此时我们的进程可能在使用其他的库,甚至有可能动态打开其他的动态库。所以我们必须想办法让库可以在任意位置都可以加载。所以我们使用相对地址,加载库的时候它在共享区任意位置加载都可以,操作系统要管理好库必然是要描述组织库的,所以库的起始地址一定会被记录。我们就可以通过起始地址和偏移量来找到库内的对应函数。所以我们创建使用动态库时,用gcc使用的-FPIC(与位置无关码),就是说用相对地址对库中函数进行编址。

更多推荐



所有评论(0)