基础IO详解

在进行基础IO的讲解之前,先来理解文件以及C提供的接口文件操作接口以及操作系统提供的接口。

目录

理解文件

文件操作的相关接口

C语言提供的文件操作的相关接口

系统调用

理解文件描述符

fd的分配规则

重新理解一切皆文件

缓冲区

缓冲区的意义

重新理解fwrite函数

 缓冲区的刷新策略问题

三种刷新策略

两种特殊情况

缓冲区和OS

模拟实现缓冲区

理解文件系统

磁盘的物理结构以及存储结构

磁盘的逻辑结构

对磁盘的管理

super block

inode & inode bitmap & inode table

data block & data block bitmap

GDT

软硬链接

硬链接

再次理解. ..文件

软链接

ACM时间

动态库和静态库

静态库

动态库

动静态库的加载


理解文件

先引入一个问题:如果创建一个空文件,那么这个空文件要占用空间吗?

实际上是会占据空间的,这个被占据的空间中存放了文件的属性信息。

对于文件的理解,要明确在数据层面,文件包括了文件内容和文件属性,没错,文件属性也是一种文件的相关数据。

对于文件的操作,无外乎为对文件属性或对文件内容进行操作。

文件在磁盘上放着,想要访问代码,要将写好的代码加载道程序中,然后程序运行起来才可以访问文件,所以就得出一个结论,文件本质上是通过进程访问的。

在Linux下,一切皆文件。那显示器,键盘,这些我们认为是硬件的东西也是文件吗?

内存可以将信息传入道显示器上在显示器上打印出来,而键盘讲内容输入到内存中,所以对于文件有一下定义:

站在系统角度,能够被input读取,或者能够output写出的设备就叫做文件。除了键盘和显示器,文件还可以是网卡,声卡...

文件操作的相关接口

C语言提供的文件操作的相关接口

我们都知道C语言的fopen可以选择打开文件的方式,当用w的方式打开文件时,文件如果不存在,就会创建一个文件,那么文件创建的路径是哪里呢?文档说没有指定就是当前路径,那么这个当前路径是什么意思呢?

其实本质上这个当前路径在Linux下就是可执行程序的路径,因为创建的进程信息中有进程的路径,所以本质上就是进程创建了文件。

对文件的操作函数这里不做详细解释,但是有一个重点大家要知道,所有库提供的对文件进行操作的函数本质上是调用了系统提供的函数接口,最终实现对文件的操作。那为什么不直接使用操作系统为我们提供的统一的函数接口呢?

其中的一个原因是因为操作系统为我们提供的统一的函数接口是复杂的。可能看似很简单的操作,但是编译器为我们做了很多的工作。

   #include <stdio.h>
   #include <string.h>
   int main()
   {
    FILE*fp = fopen("bite","r");
    if(fp == NULL)
    {
      perror("fopen::fp");
      return 1;
    }
    //进行文件操作
   // const char* s1 = "linux so easy!";
   // fwrite(s1,strlen(s1),1,fp);
    //按行读取
    char line[64];
    while(fgets(line,sizeof(line),fp)!=NULL)
    {
      fprintf(stdout,"%s\n",line);                                                                                                                
    }
  
    fclose(fp);//关闭文件
    return 0;
  }

系统调用

先来介绍第一个接口open

b660ef114ce148f78c8b4ef21c955e68.png

 第一个参数pathname和fopen一样,是被打开文件的地址,flags是打开的一些参数。

2992032695dc49bb83a91a934e352af2.png

flags必须包含O_RDONLY,O_WEONLY,O_RDWR 这三个选项中的一个,第一个表示只读,第二个表示只写,第三个表示读和写,之后还可以配合其他的选项:

826ff12a8aac4025a7a08931029c896a.png

但是只有一个参数是如何传递了不同的值呢?

在这里引入一个标志位的概念:

当我们想用一个数表示不同的状态时,有一种方法是设置值为固定的数,每个数代表不同的状态,还有一种方法是位操作,用int中不重复的一个bit位来标识一种状态

#include <stdio.h>

#define ONE 0x1    //0000 0001
#define TWO 0x2    //0000 0010
#define THREE 0x4  //0000 0100

void func(int flags)
{
	if (flags & ONE)
		printf("hello,one\n");
	if (flags & TWO)
		printf("hello,two\n");
	if (flags & THREE)
		printf("hello,three\n");
}

int main()
{
	func(ONE);
	func(ONE|TWO);
	func(ONE|TWO|THREE);
	return 0;
}

上面的代码就是标志位传递给操作系统的一种方案。

e4aa1fdf706440f5913b6391b4975a52.png

 如果本来当前路径下没有bite这个文件,执行的时候,是会报错的,报错内容显示没有这个文件。

eb7154067a354b9991685307f62f3ad1.png

 但如果写成这个样子就会在当前路径下创建名为bite的文件。但是创建出来的文件是这个样子的:

8260042e58484123bd21797c46a6ba49.png

 这个文件权限不对呀,为什么不是默认的权限呢?这就是第二个open函数的第三个参数的作用了

530f53dd99d940f7b10716698867bf1f.png

b32aa8517d7e4f10a5a56334ddfd4116.png

 但是还是other用户的权限还是少了一个w,这是由于umask的影响,可以在main函数第一行写一句“umask( 0 )”,就可以让other的权限称为6。

这么一堆的操作最终才实现了C语言接口中的fopen函数,但是还是有些不同,后文会介绍到这些不同点,所以这就是为什么语言要对系统接口进行封装的原因了,如果不进行封装,就会导致用户的操作成本大大增大。

write

4808722ea6c24eefa39ba78bfd6fbf53.png

bc270883135f464cb9293597e82faefe.png write的第一个参数是文件描述符,buf是要写入的字符串,count是输入buf的前几个字符。基本的使用大家一定了解了,但是请看接下来这种情况:

232157bfaa0c4e1b82e65aca2f7095cf.png

 20b6ee6a4ccf4e8e9f6203a974b2b1dc.png

bite文件中的内容竟然变成了这个样子,那么也就意味着与C库函数不同,系统接口是不会清空文件信息的,而是覆盖式写入。查阅原文档可以发现:

6709802995d64739bd6a95694c0ccc80.png

 70d9f33198b341d7a4dca843831ecde0.png

5f2bd714b9f5419f8612670880bd5d1c.png 同理,当O_TRUNC变为O_APPEND之后就可以变为追加字符串的作用。

read

a6cbb8f0ef134a23b9683b7c2ca685e3.png

 第一个参数位文件描述符,第二个参数是将文件内容读取到buf中,第三个是读取的长度

73cdefc61122416f96a717d7000bea8d.png

 这里要注意一个点,C语言在意字符串以'\0'结尾,但是操作系统的接口不在意这些,所以在创建容纳读取字符串的buffer时要先初始化为'\0'。

理解文件描述符

上文中的代码截图中我们创建了一个变量fd用于存放open函数的返回值,我没称这个值为文件描述符,但是通过查阅原文档,我们发现打开文件失败会返回一个小于0的值,但是如果连续打开一堆文件,他们的文件描述符以3开始依次递增。这是为什么呢?为什么不从0开始呢?

在学习C语言的时候,我们认识了三个标准流:stdin,stdout,stderr,这三个流都是FILE*类型的,想必大家也猜到了,这三个流分别占用了0,1,2这三个文件描述符。

958179b2360347c4bb989bf4ba975221.png

这里问大家一个问题:这里的返回值FILE是啥意思?

其实本质上FILE是以一个结构体,是由C标准库提供的。但是C文件库函数是调用系统底层的接口来实现对文件的操作的,所以对于系统而言,FILE是C标准库提供的,所以操作系统一定不认识FILE,只认文件描述符,这也就证明在FILE结构体中,是有fd这个文件描述符的。

但是,fd究竟是个什么玩意呢?

在操作系统中,一个进程可以打开多个文件,有多个进程要打开多个文件,那么操作系统如何对这些文件进行管理,那么描述的方式也是先描述再组织。

在内核中,操作系统为了管理每一个被打开的文件,要构建一个结构体:

struct file

{

        struct file* next;

        struct file* prev;

        //包含了一个被打开文件的几乎所有的内容(这里的内容不仅包含属性)

}

通过这个结构体,再用这个双链表组织起来,就可以很好的对文件进行管理。但是进程可以创建多个文件,那么如何分辨哪个文件是哪个进程创建的呢?就通过fd来确认,每个被创建的文件都有一个文件描述符

0711e9dbd4794f6989db4621f113224d.png

所谓的文件描述符,本质是一个数组下标! 

fd的分配规则

先说结论:fd的分配规则是按照填充最小的fd对应位置的空间,也就是说,假如下标为0,1,2位置的文件已经被打开,但是此时我又将0位置的文件关闭,关闭之后我又创建了一个文件,那么此时新被创建的文件的fd就会被分配为下标为0的这个位置。

de61f7be22c547d5888612fa26f6cb3c.png

最后打印出的结果就是0,也就是被创建的log.txt文件的文件描述符被改为了0。

上面的代码中,我关闭了stdin对应的文件转而将其对应位log.txt文件,这就完成了重定向,修改之后系统就不会继续从键盘中读取信息了,而是从log.txt文件中读取。

为了支持重定向,系统也为我们提供了接口:

e475cf7e6dfd4df583db91a90673404f.png

31e85e7024ad499598b09082b45a64d8.png

要注意dup2函数的行为是拷贝数组下标对应的内容, 其次就是让newfd被oldfd拷贝(重定向后内容写入oldfd指向的文件中),顺序可千万别记错。

dup2函数如果调用成功则返回新的文件描述符,如果调用失败则返回-1。

940f89d450fd490faf522a95f4a12795.png

程序运行的结果会在log.txt文件中写入3(被创建的log.txt的文件描述符)上面的代码我们就完成了一个简单的输出重定向。同理输入重定向也很好理解。

重新理解一切皆文件

在之前的博客中提到过一切皆文件当时我们只是做了一个感性的理解,今天我们做一个深入的理解

448d309b64ba4304bbe502212b19f44c.png

其实在驱动的硬件中就会有当前硬件的读写方式,我们上面知道操作系统默认会打开输入流,输出流,错误流这三个文件,那么键盘不可以读,显示器不可以写,那么如何将他们看成统一的文件来进行操作呢?其实在驱动中有键盘,显示器,磁盘以及其他硬件的驱动文件,这些驱动文件中有硬件的对应的读写方式,对这些方法进行封装,提供了访问的接口。

缓冲区

先放一段代码:

  #include <stdio.h>
  #include <unistd.h>
  #include <string.h>
  int main()
  {
    //C接口
    printf("hello,printf\n");
    fprintf(stdout, "hello,fprintf\n");
    const char* fputsStrings = "hello,fputs\n";
    fputs(fputsStrings, stdout);
  
    //条件接口
    const char* wchar = "hello,write\n";                                                                                                                                                   
    write(1, wchar, strlen(wchar));
  
    fork();
    return 0;
  }

 大家猜一下会打印什么结果,如果直接打印到屏幕上:

33629ebe4d9e46fe99ed9e843769e03b.png

如果重定向到一个文件中:

41c30637014444019618c67eeadff440.png

 为什么会出现这种情况?首先说明,这个问题一定和缓冲区有关,我们发现系统调用的打印只执行了一次,但是C接口的打印却执行了两次,所以缓冲区一定不在内核中!

缓冲区是什么?

缓冲区本质上就是内存中的一块存放数据的空间。

缓冲区的意义

为什么要存在缓冲区,其实缓冲区就是为了实现数据从内存到外设这个过程中可以更节省资源。

举个例子:我们现在邮寄东西通常都会用快递,那为什么是让快递帮我们寄东西而不是我们自己去送呢?如果我们自己去送就会消耗很多时间和资金,但是使用快递就避免了这些问题。

有缓冲区的存在,就可以将数据先存放再缓冲区中,然后制定特定的缓冲区刷新策略对缓冲区中的内容进行刷新,所以缓冲区的意义就是节省进程进行数据IO的时间

重新理解fwrite函数

fwrite首先将数据先写入到C语言的缓冲区中,然后C语言再调用系统接口write将数据写入到内核缓冲区中。

C语言有一个struct FILE,也就是我们调用fopen函数的返回值的类型,但是这个FILE和上面我提到的struct file不一样,那个file是磁盘中的文件对应的读写方式的以及其他信息的一个结构体。而struct FILE结构体中包含了文件的fd,C语言提供的缓冲区等的一些信息。

91469592b70b492b9b8c8fc0ce1db11b.png

 缓冲区的刷新策略问题

首先要明确一点,缓冲区会结合自己的设备,定制自己的缓冲区。

有缓冲区就会存在对应的缓冲区刷新策略,与计算机的交互要让我们人类使用计算机更舒服,所以也会有各种的缓冲区刷新策略。缓冲区的刷新策略有三种刷新策略和两种特殊情况

三种刷新策略

  • 行刷新:当遇到换行符即进行刷新,显示器默认使用的就是这个刷新策略,因为内容一行一行出现在屏幕上更符合我们的直觉
  • 立即刷新:顾名思义就是立刻刷新缓冲区
  • 满刷新:当缓冲区填满之后才进行缓冲区的刷新操作

两种特殊情况

  • 用户强制刷新,如fflush()
  • 进程退出一般都会调用exit函数进行缓冲区的清理

缓冲区和OS

上面我们已近说过了,缓冲区并不属于内核,而是语言提供的,那么在C语言中的FILE结构体中,维护了一段空间给缓冲区。FILE结构体中还记录了其他的一些信息,文件描述符fd...

其实在内核中也有一段内核缓冲区,在C语言的缓冲区的内容被刷新后,这些内容并不是直接被刷新到了文件中,而是先被刷新到内核缓冲区中,但是内核缓冲区什么时候刷新,这个决定权在于操作系统。所以说内容从内存到磁盘还是很复杂的。

但是如果现在有内容已经被写到内核缓冲区中了,但是突然断电了,就会导致数据的丢失,如何避免?

调用fsync函数可以将内核缓冲区中的内容刷新到外设中。

d278b5c785a742fcbde16dcc2d0a5cfe.png

理解文件系统

文件系统存在的目的:

在我们的磁盘中存在很多的文件,这些文件并没有被全部加载到内存中,那么操作系统是否需要把这些文件管理起来呢?

是需要管理起来这些文件的,否则的话我们怎么能打开磁盘中特定的一个文件?就需要将这些文件管理起来。

磁盘的物理结构以及存储结构

先来放一张磁盘的图片:

04c2cf04df2b4148bea7876f062acb04.png

相信大家一定都见过磁盘,但是具体的磁盘的内容,我来介绍一下:

bd7666aeeeed40e991887816b4d4b982.png

磁盘的组成部分以及详解:

  • 圆形的碟片(主要记录数据的部分)
  • 机械手臂,与在机械手臂上的磁头(可擦写碟片上的数据)
  • 主轴马达,可以转动碟片,让机械手臂的磁头在碟片上读写数据
  • 扇区:最小的物理存储单位,依据磁盘设计的不同,目前主要有512byte和4KB两种格式
  • 柱面:将扇区组成一个圆,就是柱面

解决几个问题:

3a6fa42cda7d49729df8efd6c30f5715.png

这里的扇区的面积大小都不一样,那么它们存储的大小都一样吗?

 都是一样的,为了减少软件编码的难度,所以就设计每个扇区的大小都是一样的。那么就会导致靠近圆形的扇区的存储密度会相对较大,而靠近最外则的存储密度则相对较小。

在单面上 ,如何定位一个扇区呢?

磁头来回摆动确认在哪一个磁道,盘片在旋转的就是让磁头定位扇区。

如果有多块磁盘,那么对应每个盘面都有一个磁头,那么磁头的运动是什么样的? 

多个磁头会同步运动,每个磁头都会统一到对应的磁道。

如何在磁盘中定位任何一个扇区?

先定位在哪一个磁道(柱面)cylinder,然后定位在哪一个盘面head,最后定义在哪一个扇区sector。所以磁盘中定位任何一个扇区采用的硬件级别的方式为CHS定位法。 

磁盘的逻辑结构

我们应该都见过磁带这个东西,就是将一条磁带卷成了一盘磁带,而那一整条磁带其实本质上就是线性的存储结构,那么我们的磁盘其实也可以看成和磁带相似,也是一个线性的存储结构。

那么假设我们有一个有两个盘片的磁盘,那么一共就会有四个面:

d6251a8ea1a1474e972508eb078e3a6d.png

所以我们把整个磁盘逻辑上看成一个 sector arr[n];所以说对磁盘进行管理,就变成了对数据进行管理。也就是先描述,再组织。

那么将磁盘变为一个线性存储结构,要如何找到一个扇区呢?

 只要知道一个扇区的下标,就算定位了一个扇区,而这个下标所对应的地址我们称之为LBA地址。

如果说有两个盘片,那么就有四个盘面,每个盘面有10个磁道,每个磁道有100个扇区,那么LBA地址所能表示的下标范围就是 4*100*10。

而如何将LBA地址找到对应的CHS地址呢?

我们这里来举个例子,如果LBA地址为123,磁盘信息和上面描述的一样,那么其所在的盘面就是123 / (10 * 100)= 0;也就是在0号盘面,其所在的磁道就是(123 / 100)% 10 = 1;而其所在的扇区就是123 % 100 = 23;其所在的扇区就是23号扇区。 

为什么OS要对存储结构进行逻辑抽象呢?

  • 便于管理
  • 不想让OS的代码与硬件强耦合,如果硬件发生变化,也不会影响操作系统 

我们上面知道了磁盘的访问的基本单位是512字节,但是这对于操作系统来说太小了,如果操作系统需要写入或读取数据,过小的基本单位就会导致磁头和盘片需要画更长的时间来定位内容所在的扇区,所以这样就导致了IO的时间会边的很长,所以操作系统内的文件系统就会定制多个扇区进行读取,可以采用1KB,2KB或4KB为单位进行IO。为什么呢?

局部性原理

局部性原理从原理上解释了在加载数据的时候,如果多加载一些,可以提高命中的效率,数据是先进入到内存中,之后被修改后再被写入到外设中,所以4KB进行IO,可以提高缓存命中的效率。

对磁盘的管理

我们每个人的电脑中都有一块固态硬盘或者是磁盘,那么操作系统需要对磁盘中的内容进行管理,如何进行管理,首先要对将磁盘进行分区。

e87fe44706c0486f8e290d8b4d87f7e6.png

 对磁盘的管理其实就是一种分治法。上面的图中表示了有一块500GB的磁盘,那么首先对磁盘进行分区,之后再将分区进行分组,将每组的内存管理好就可以实现对整个磁盘的管理。

super block

super block保存了整个文件系统结构信息,记录的信息主要有block和inode的总量,未使用的block和inode的数量,一个block和inode的大小等等。但是这里有一个疑问:为什么关于文件系统的内容会在磁盘的分组中出出现,这样的话磁盘有多个分区,而分区又会有多个组。其实存储这么多的文件系统结构信息目的是备份。

inode & inode bitmap & inode table

之前我们说过文件是由内容加属性组成的,这里的文件属性就是我们今天要说的inode,inode是固定大小的,并且一个文件对应一个inode。文件的几乎所有属性都存储在inode中,但是要注意文件名并不在inode中存储,inode为了彼此区分,每一个inode都有自己的ID。但是inode可以跨组但是不可以跨分区,这一点格外注意。

inode table中存放了所有可用的inode,包括已经使用的inode和没有被使用的inode。

当有一个新的文件的时候,总得有新的inode来存放文件的属性,那么如何知道哪个inode没有被使用可以作为新文件的属性存放的空间?那就是inode bitmap,这是一个inode对应的位图结构,可以知道哪个inode没有被使用,哪个inode被使用了。

data block & data block bitmap

文件是由属性和内容组成的,这里的data block就是存放内容的一些数据块,但是如何通过属性来定位这些数据块我们之后再说。

data block bitmap可以知道那块空间没有被使用,那块空间被使用了,和inode bitmap一样是一个位图结构。

GDT

GDT是一个块组描述的表结构:对应分组的宏观的属性信息。inode有多少,被使用了多少;数据块有多少,被用了多少。

当我们要查找某个文件的时候,我们只有inode编号,可以找到对应的文件属性。但是如何找到对应的数据块。

实际上在inode中有一个有一个block数组(有)存放了数据块的地址,前11个位置存放的是对应文件的数据块,但是由于空间有限,所以这十个块不能表示所有的文件的内容,而后面的空间可以存放地址所指向的空间中还可以存放其他空间的地址

那么创建一个文件的过程有那些?

首先将inode bitmap中的为0的位置变为1,再通过inode的编号去inode table中的找到对应的位置填写文件的属性,然后将数据写入到data block中,建立映射关系,返回inode编号,最终一个文件就被创建好了。

删除文件要做什么?

 实际上删除文件内容,只需要将inode bitmap中将对应inode的位图由1变为0。再将data block bitmap中的内容进行删除即可。实际上数据块中data block和inode中的内容是没有发生变换的。

软硬链接

硬链接

首先在一个目录下创建一个myfile.txt的文件:

a2b7bf1676c44087b4d578b7bfa77a31.png

之后使用指令ln [原文件] [目标文件]来创建硬链接

c0958da5371348f39a850f6fca6e2e76.png

e45e2732189e48c0ac7c8460a6683400.png 通过观察可以发现创建的两个文件的inode是一样的,那么硬链接到底是做了什么呢?

从inode上看硬链接并没有创建新的文件,将链接问价名与之前文件的inode又做了一份映射

在权限后的数字2是什么意思呢?

实际上这里使用了引用计数,表示有两个文件名对inode做了映射关系,当引用计数为0的时候,这个文件才真正被删除了

再次理解. ..文件

86cf64a154fd4bc2a947ea2c561900f3.png

 当我们在当前文件中创建一个目录文件,我们并没有让任何文件与之做硬链接,但是我们发现链接数为2,这是为什么?

dba97e3df9134e9da8462287453b7f97.png

我们发现 . 文件的inode和dir文件的inode是相同的,这是可以理解的, . 就是当前文件夹,这就是一个硬链接,其实 .. 文件的inode和dir所在文件的inode也是相同的,大家可以自己试一下

我们都知道. 和 .. 是Linux为目录文件建立的硬链接,那如果我们自己为目录文件建立一个硬链接呢

49dc870e678349d4b8bc6f5e428b10a4.png

我们发现是不能的,但这是为什么呢?

我们都知道Linux的目录结构是一颗以"/目录"为根节点的树,如果允许自定义硬链接,则很有可能会破坏这个结构,甚至形成循环;而一旦形成循环,对于需要遍历目录树的命令是致命的,为了避免对目录树结构的破坏,Linux不允许用户自定义硬链接在目录上

软链接

同样是上面创建的那个文件:

2c228f96fcdc48ac98d09dda5eb5926d.png

指令是 ln -s [原文件] [目标文件]

可以看到创建完成后就有一个蓝色的softlink指向了myfile.txt,但是可以发现与硬链接不同的是softlink是单独创建的一个文件,它拥有自己的inode,而它只有softlink这一个文件名与inode有映射关系

如果删除myfile.txt呢?

02d48b53e580417b8f482a8ee3f3570e.png

会发现链接文件变红并且被删除文件一直在闪烁,但我们都知道本质上hardlink和myfile.txt是同一个文件,那为什么还会报红呢?

这就说明软链接创建的文件的数据块中存放的是被链接文件的地址,所以查找的时候也是对照地址去寻找而不是寻找对应的inde,同样我再创建一个名为myfile.txt的问价,软链接也不会继续报红,尽管此时的myfile.txt已经不是之前的那个myfile.txt了

这就让我想到了windows下的快捷方式,其实软链接也可以理解为Linux下的一种快捷方式

ACM时间

291e92f08bec45c9aa8cd1c4ed892703.png

我们对之间创建的myfile.txt文件,使用stat命令可以查看其对应的ACM时间, Access就是访问文件的时间,但这个时间是周期性刷新而不是即时刷新

change是修改文件属性的时间:

2a67c5e27a9d42cbaa89a3293fadb8c9.png

那如果向这个文件中写内容进去呢?

9c581361191e444ba36c66e1060fa3fb.png

可以发现Modify的时间也变了,说明改变文件内容也会修改Modify的时间,但是可以发现这里Change的时间也被修改了,这是因为文件的大小也改变了,所以对应的属性也是被修改了

动态库和静态库

我们先来了解一下概念:

静态库:程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需静态库

动态库:程序在运行起来的时候才会去链接动态库的代码,多个程序共享使用库的代码

之前的时候我们提到过Linux操作系统默认使用动态链接,此外我们知道静态链接会导致形成的可执行程序的体积过大

这里我们知道Linux下动态库以.so为后缀,静态库以.a为后缀。在windows下动态库以.dll为为后缀,静态库以.lib为后缀

静态库

这里我们创建两个.c文件和两个.h文件,分别实现int整形的加法和减法运算,在上级目录我们创建一个test文件作为使用库的用户。

#include "Add.h"

int Add(int a, int b)
{
    printf("enter Add func, %d + %d = ?\n", a, b);
    return a + b;
}
#include "Sub.h"

int Sub(int a, int b)
{
    printf("enter Sub func, %d - %d = ?\n", a, b);
    return a - b;
}
#pragma once

#include <stdio.h>
extern int Add(int a, int b);
#pragma once

#include <stdio.h>
extern int Sub(int a, int b);

那么动态库的目的就是为了不想让对方知道源代码的前提下可以使用这些代码所实现的功能,我们使用汇编指令形成.o文件。

d8764fd1da9c4da0bbf0a9551bda8923.png

 在test文件中,我们创建一个main.c文件:

07d3e83414924f7eb1af4563c43910af.png

 那么我们认为将编译形成的二进程文件以及.h文件拷贝到test目录中,main.c就可以完成编译 ,那么这样真的可以吗?

98f7a2b4e0ae4dcea98f936fbc4d39d2.png

我们发现是可以编译的。那么接下来我们使用ar命令生成静态库:

5b37cccd311b49628ed49f6567b7667d.png

 生成的静态库的名称必须以lib开头,以.a为文件后缀,这里在Makefile中进行一些设置可以将文件进行打包

libmymath.a:Add.o Sub.o
	ar -rc $@ $^
Add.o:Add.c
	gcc -c Add.c -o Add.o
Sub.o:Sub.c
	gcc -c Sub.c -o Sub.o
.PHONY:putout
putout:
	mkdir -p mylib/include
	mkdir -p mylib/lib
	cp -f *.a mylib/lib
	cp -f *.h mylib/include 
.PHONY:clean
clean:
	rm -f libmymath.a *.o
	rm -rf mylib

我们可以将生成的文件夹使用tar命令打包,将打包好的文件拷贝到test文件中:

b8b737af09884c84933406ce2e8243d9.png

解压之后就会生成我们之前的那个目录,目录中有.h头文件,也有静态库文件,那么如何使用静态库呢?

这里直接说使用方法吧:

gcc -I + 路径 -->为gcc指定头文件所在的路径

gcc -L + 路径 --> 为gcc指定库文件所在的路径

gcc -l(小写L) + 路径 --> 为gcc指定所要链接的库名称

另外如果要链接库,必须指明库名称。(gcc没有我们相信的那么智能,如果路径中有多个库,gcc并不知道该如何进行链接,所以必须指明库名称才能链接成功)

efbb6dfb3dfa4647b2ea4d11a5612a9c.png

 我们都知道gcc默认的链接方式是动态链接,但是如果一个文件中既有动态库也有静态库,该如何编译呢?

实际上gcc会根据库的性质进行相应的链接,如果库是动态库就进行动态库的链接,如果库是静态库就静态链接。

动态库

我们知道使用动态库可以使形成的可执行文件体积更小,那么如何形成一个动态库呢?

首先在编译阶段,在编译形成.o文件的时候,需要添加一个特定的后缀:

889b6a6717c74ea1b32d67ccd4eee60c.png

-fPIC的目的是形成.o文件与位置无关码,这个与位置无关码是什么我们后面再说;动态库的形成与静态库不同,不需要使用ar命令,gcc就可以完成:

gcc -shared -o 库名称 依赖.o文件

358ac6baca1045fba3fb42e763c370f3.png

形成.so文件之后我们将库文件和.h文件进行打包后拷贝到上级目录,然后还是属性的方法形成可执行文件:

74247b83329a42238a00441b8fa7057d.png

但是当运行的时候就会报错:

84e93ae5b3bd462f96298a524d4a7fc3.png

查看mymath我们发现:

9ad2469a40ad4d789fb9de9fa0254828.png

libmylib.so显示是not found,这是因为程序编译gcc需要知道库所在的路径以及库的名称,但是程序运行起来就和gcc无关了,程序运行起来,操作系统和shell也是需要知道库在哪里。

那么如何链接成功呢?这里有很多的做法:

操作系统会在环境变量中搜索库路径:LD_LIBRARY_PATH

第一种方法就是将库路径添加到这个环境变量中,但是这种方法就是终端重启后之前的配置就会无效

修改系统配置文件:

18b6d0d3c47a4ec7a839db413d3e691c.png

这个配置文件的含义代表动态库进行搜索时可以在自己生成的.conf 配置文件中进行搜索,在创建的配置文件中添加库的路径,创建写入完成后还需要对更新一下config

ff0bca1ef2584c8cb4c93a12d7640e26.png

关掉终端再重新打开依然可以成功运行。

当然还有更简单的方法:

使用ln指令可以在当前目录下创建一个软链接链接到库文件所在的目录,搜索动态库的时候是可以直接在当前目录下搜索的。

动静态库的加载

当然这里静态库不存在加载,那么静态库是如何载入程序中最终完成程序的执行呢?

首先我们之前的学习知道了我们写好的程序在编译执行之前就拥有了自己的地址,那么静态库就是将库中代码直接拷贝到我们的程序中最终完成执行,库中的函数也必须通过相对确定的地址位置进行访问。所以形成的可执行文件比较庞大。

动态库的链接与静态库不同,我们的磁盘中有.exe和.so文件,动态链接的时候,就是将动态库中指定函数的地址,写入到我们的可执行程序中。但是这里的地址是什么地址?

还记得之前我们提到了一个与位置无关码,实际上这就是一种相对编址的方法,选定参照系之后进行的一个相对位置的编址!那么动态库中函数的地址就是start:偏移地址,start的地址是不确定的。当程序需要库之后,会将库加载到内存中,并通过页表映射到虚拟地址的共享区,一旦映射完毕就决定了这个库的起始地址,而之前代码段中使用库函数的时候就会用这个函数在库中的偏移量,所以这个时候就可以在共享区中找到这个函数,执行完成后返回代码段执行后面的代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Feng,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值