Linux文件系统以及动静态库

目录

一、系统 I/O

1.1 接口介绍

1.2 系统调用和库函数

1.3 文件描述符

1.4 重定向

二、理解文件系统

2.1 inode

2.2 硬链接

2.3 软连接

三、动静态库

3.1 初识动静态库

3.2 静态库的打包与使用

3.2.1 打包

3.2.2 使用

3.3 动态库的打包与使用

3.3.1 打包

3.3.2 使用


一、系统 I/O

        对于文件的操作,之前的文章中有写有关于C的接口,其实我们还可以用系统接口进行文件访问,下面我们一起来学习。

1.1 接口介绍

open

pathname:要打开或创建的目标文件

flags:此处可以传入多个常量进行或运算,如下

  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开

上面三个常量,必须指定一个且只能指定一个

  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

mode:文件的权限

返回值:成功时返回新打开的文件描述符(下面会讲),失败时返回-1

        类比C接口,系统接口还包括write、read、close、lseek,其使用方式传参与open一致,就不详细介绍了。

1.2 系统调用和库函数

fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)

而刚刚介绍的 open close read write lseek 都属于系统提供的接口,称之为系统调用接口

我们来看一张操作系统的图:

从上图中可以明显发现,f#系列的函数,都是对系统调用的封装,方便二次开发

1.3 文件描述符

        文件描述符实质就是一个小的整数

        当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

下面我们来做一个实验:连续打开五个文件,看看这五个文件分配的文件描述符分别是多少:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main()
{
    int fd1 = open("log.txt1",O_RDONLY | O_CREAT,0666);
    int fd2 = open("log.txt2",O_RDONLY | O_CREAT,0666);
    int fd3 = open("log.txt3",O_RDONLY | O_CREAT,0666);
    int fd4 = open("log.txt4",O_RDONLY | O_CREAT,0666);
    int fd5 = open("log.txt5",O_RDONLY | O_CREAT,0666);
    
    printf("fd1: %d\n",fd1);
    printf("fd2: %d\n",fd2);
    printf("fd3: %d\n",fd3);
    printf("fd4: %d\n",fd4);
    printf("fd5: %d\n",fd5);
    
    return 0;
}

运行后发现打开的这几个文件的文件描述符是从3开始的一串连续数字,为什么是从3开始呢?

        因为上面之前说过,创建一个进程时,会默认打开标准输入、标准输出、标准错误,fd_array给他们三个分配的文件描述符分别是0、1、2,因此我们后面创建文件的时候分配的描述符会从3开始。      

如果我们使用close关闭0,1,2其中的一个会怎么样呢?

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main()
{
    close(0);
    int fd = open("log.txt",O_RDONLY | O_CREAT,0666);
    
    printf("fd: %d\n",fd);
    return 0;
}

  

可以发现当我们关闭0即标准输入时,分配给文件的文件描述符为1。

其实文件描述符的分配规则是:从fd_array中找一个最小的、未被使用的,作为这个文件的文件描述符(fd)

1.4 重定向

再来看段代码:

#include<stdio.h>
#include<string.h>
int main()
{
   const char* msg0="hello printf\n";
   const char* msg1="hello fwrite\n";
   const char* msg2="hello write\n";
   printf("%s",msg0);
   fwrite(msg1,strlen(msg1),1,stdout);
   write(1,msg2,strlen(msg2));
   fork();
   return 0;                                                                                                                                                        
}                                                                                                                        
     

我们再对输出进行重定向到file文件中:

        我们发现printf 和fwrite(库函数)都输出了2次,而write 只输出了一次 (系统调用) 。为什么呢? 肯定和fork有关! 

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲
  • printf 和fwrite库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲
  • 而我们放在缓冲区中的数据,甚至fork之后都不会被立即刷新,只有当进程退出之后,会统一刷新,写入文件当中
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据
  • write没有变化,说明没有所谓的缓冲
         printf 和fwrite  是库函数, write 是系统调用,库函数在系统调用的"上层",是对系统调用的"封装”,但是 write 没有缓冲区,而   printf 和fwrite  有,足以说明,该缓冲区是二次加上的,又因为是c,所以由C标准库提供。

二、理解文件系统

2.1 inode

inode(index node)是文件系统中的一个概念,用于存储和管理文件的元数据,包括文件的权限、大小、时间戳等信息,以及文件数据在磁盘上的物理位置。

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了

  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • i节点表:存放文件属性如文件大小,所有者,最近修改时间等
  • 数据区:存放文件内容

我们使用ls -l 命令时,还可以得到除了文件名外的元数据,其中一共包括7种数据,分别是:

模式、硬链接数、文件所有者、文件所属组、大小、最后修改时间和文件名

2.2 硬链接

        其实真正找到磁盘上文件的并不是文件名,而是inode。而在linux中可以让多个文件名对应于同一个inode

        硬链接它不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为它没有自己的inode,我们可以将它理解成源文件的别名

我们可以使用下面的指令来创建一个硬链接:

ln 源文件名称 硬链接文件名称

         使用ls -li 显示它们的 inode 号码和其他相关信息可以看到,硬链接文件和源文件的inode是一样的。

2.3 软连接

        硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。

        软链接它有自己独立的inode,是一个独立的文件,有自己的inode属性也有自己的数据块(保存的是指向文件的所在路径+文件名)。

我们可以使用下面的指令来创建一个软链接:

ln -s 源文件名称 硬链接文件名称

        可以看到我们的软链接的inode编号与源文件的inode编号是不一样的,并且软链接的文件大小要比源文件的文件大小要小很多,再来运行一下试试:

        我们可以看到这两个可执行程序运行起来之后打印的结果是一样的。但是软链接的文件大小却要比源文件的大小小很多,这是不是有一种熟悉的感觉呢?

        软链接就相当于是windows下的快捷方式。软链接它的数据块保存的是指向文件的所在路径+文件名,所以我们执行软链接的时候就相当于间接执行了这个源文件。

        但是快捷方式它是不能够单独存在的,当我们的源文件被删除后,尽管有文件名,但是这个软链接就不能再执行了:

三、动静态库

3.1 初识动静态库

我们写一段代码:

#include<stdio.h>

int main()
{
    printf("hello world\n");
    return 0;
}

然后使用ldd指令来查看一个可执行程序所依赖的库文件:

这里的libc.so.6就是当前可执行程序所依赖的库文件,我们通过 ls命令 查看后发现libc.so.6它其实就是一个软链接:

紧接着,我们通过file+文件名来查看一下libc-2.17.so的文件类型:

可以看到libc-2.17.so它其实是一个动态库。

注:

  • 在Linux下,以.so为后缀的文件是动态库,以.a为后缀的文件是静态库
  • 在Windows下,以.dll为后缀的文件是动态库,以.lib为后缀的文件是静态库

在Linux下,我们如何知道一个动静态库的名字呢?

比如libc.so,我们去掉前缀lib,去掉后缀.so,剩下的就是该动静态库的库名了。

在Linux下,gcc/g++编译器默认采用的是动态链接,若是想采用静态链接的话,我们需要在gcc/g++后面带上一个-static选项即可。

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库.
  • 动态库(.so):程序在运行的时候才去链接动态库的代码多个程序共享使用库的代码
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

总的来说,动静态库的优缺点如下:

   动态库

优点:

  • 使用动态库生成的可执行程序体积比较小,节省资源。当多个可执行程序同时运行时,若是要使用同一个库,库文件会通过进程地址空间进行共享,内存中不会存在重复代码。

缺点:

  • 比较依赖于第三方库。

   静态库

优点:

  • 使用静态库生成的可执行程序,它不需要依赖于第三方库,即使没有第三方库它也可以独立运行,可移植性高。

缺点:

  • 生成的可执行程序体积比较大,比较占资源。 当多个静态链接生成的可执行程序使用同一个库时,内存中会存在大量重复的代码。比如说:9个静态链接生成的可执行程序都要使用C标准库,那么当这些可执行程序同时运行的时候,内存中就会存在9份C标准库的代码。

3.2 静态库的打包与使用

3.2.1 打包

静态库打包的本质:把代码生成的二进制.o文件进行打包。

接下来举个例子帮助大家深刻了解:

首先创建四个文件,其中两个源文件myadd.c和mysub.c,两个头文件myadd.h和mysub.h:

myadd.h:

#pragma once
#include<stdio.h>

int myadd(int a,int b);

myadd.c:

#include"myadd.h"

int myadd(int a,int b)
{
    return a+b;
}

mysub.h:

#pragma once
#include<stdio.h>

int mysub(int a,int b);

mysub.c:

#include"mysub.h"

int mysub(int a,int b)
{
    return a-b;
}

接下来,根据静态库打包的本质,首先将所有的源文件变成对应的.o目标文件:

然后,使用ar命令将所有的目标文件打包生成静态库:

ar是gnu归档工具,rc表示(replace and create)
我们还可以使用ar命令的-t选项和-v选项来查看静态库中的目录列表:
  • -t:列出静态库重点文件
  • -v(verbose):详细信息

最后,我们将生成的静态库文件与目标文件对应的头文件组织起来:

当我们将我们自己的库给别人使用的时候,我们需要创建两个目录,一个用来存放所有的头文件一个用来存放静态库文件

因此,我们将myadd.h与mysub.h这两个头文件放到include这个目录下将静态库文件放到lib这个目录下。然后将这两个目录都放到mylib这个目录下,此时我们若是向把我们自己的库给别人使用,我们只需要将mylib这个文件发给别人就可以了

3.2.2 使用

我们首先创建一个mytest.c文件,然后写一段简单的代码用一下加法函数:

#include"myadd.h"
int main()
{
    int x = 10;
    int y = 20;
    printf("add: %d\n",myadd(x,y));
    return 0;
}

因为编译器它并不知道你所包含的myadd.h头文件在哪里,所以我们需要指定头文件的搜索路径。

因此我们需要在makefile里面的gcc后面带一个选项  -I(大写i) 就可以指定头文件的搜索路径了:

make后发现报错:

这是因为头文件myadd.h里面只有加法函数的声明,并没有函数的定义,因此我们还需要指定库文件的搜索路径。

因此我们只需要在makefile里面的gcc后面带一个选项  -L  就可以指定库文件的搜索路径了,与此同时,因为你只是告诉了编译器库文件的搜索路径在哪里,但是你并没有告诉你要去链接哪一个库,假如说这个库文件里面有很多个库,那编译器它又怎么知道你想要去链接哪个库呢。

所以我们还需要指定一下我们需要链接库文件中的哪一个库

这就需要在makefile里面的gcc后面再带一个选项  -l (小写L)去指明我们想要链接库文件中的哪一个库:

注意:前面说过怎么看一个库文件的名字(去掉前缀去掉后缀),因为我们是要告诉编译器我们想要链接库文件中的哪个库,所以我们只需要在-l选项后面带上它的名字即可。

大功告成。

除此之外,我们还可以将头文件和库文件放到系统路径下,来达到使用的目的。

如果我们不指定搜索路径的话,编译器它就找不到我们的头文件和库文件,除了上面指定搜索路径的法子我们还可以将我们的头文件和库文件放到系统路径下,这样的话编译器就能够找到了:

虽然说这种方法相较于第一种方法而言会简单一点,但是我不建议你使用这种方法,原因有两个:

  1. 将我们的头文件和库文件放到系统路径下,这样做有可能会对系统文件造成污染
  2. 使用第一种方法可以帮你熟悉gcc命令,同时使用第一种方法虽然麻烦但是不会对系统文件造成污染。

3.3 动态库的打包与使用

3.3.1 打包

有了静态库的基础,就可以很容易地理解动态库了:

首先,让所有的源文件生成对应的.o目标文件

​​​​​​​

其中有三个我们比较陌生的选项:

  • shared: 表示生成共享库格式
  • fPIC:产生位置无关码
  • < 表示当前规则中的第一个依赖文件(即myadd.c,第六行为mysub.c)

然后,将头文件和我们生成的动态库组织起来,这里我们换一种方式来将它们两个组织起来,我们在Makefile里面编写一段代码,通过 发布 然后将他们给组织起来:

3.3.2 使用

发布成功后,我们继续写一段测试代码吧:

test.c:

#include"myadd.h"
#include"mysub.h"

int main()
{
    int a = 20;
    int b = 10;
    printf("add: %d\n",myadd(a,b));
    printf("sub: %d\n",mysub(a,b));
    
    return 0;
}

结果发现运行时显示报错:

        但我们明明已经指定头文件的搜索路径,指定库文件的搜索路径,以及我们要链接库文件中的哪一个,为什么我们这里还是会报错呢?

        这是因为这只是告诉了编译器,但并没有告诉操作系统,而链接动态库的时候是在程序运行的时候链接的。

因此我们可以使用以下两种方法解决:

1. 拷贝.so文件到系统共享库路径下

2. 更改LD_LIBRARY_PATH

LD_LIBRARY_PATH是程序运行时帮我们找动态库的路径的,因此我们将该动态库所在目录路径添加到LD_LIBRARY这个环境变量中就可以了:

至此,本章的内容就介绍完啦,如果觉得对你有帮助的话可以点赞关注,谢谢啦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值