Linux软硬链接和动静态库

前言

不知你有没有想过,一般我们在windows下载程序的存放路径可是很深的,例如:C:\Software\vscode,但为什么我在点击桌面的快捷键就可以打开你的程序呢?其实这本质是软链接!什么是软连接,下面将揭晓!另外,我们在介绍gcc/g++的时候提过动静态库,但是没有深刻的理解,本期将进行详解!

本期内容介绍

• 软硬链接详解

• 静态库的制作与链接

• 动态库的制作与链接

• 动态库的加载

软硬链接

软链接介绍

软链接又称符号链接,他是一个单独存在的特殊文件拥有属于自己的inode属性以及相应的文件内容他的文件内容存的是另一文件的路径!所以,一般软链接文件较小!

软连接就类似于我们平时Windows桌面上用的快捷方式!

创建软链接的方式
ln -s 目标文件 软链接文件

OK,我当前在这样一个目录里建了一个文件log.txt,并在里面写入hello world

我们现在根据上面介绍的方式给log.txt创建一个软连接log_soft_link

此时直接操作软链接文件就相当于操作目标文件了:

这就和我们平时用的快捷方式很像,其实快捷方式就是软链接,他本质也是指向目标文件的:

当然我们可以将软链接建立在其他的目录下:

因为软链接是目标文件的快捷方式,所以删除软链接不会影响目标文件;但是删除目标文件,软连接已经不能再用了

软链接的特征

        • 文件类型:软连接是一种特殊的文件类型,在属性列表中是 开头

        • 文件类容:软链接存储的是目标文件的路径,不是实际类容

        • 跨文件系统:软链接可以跨不同的文件系统,可以链接不同的分区/网络共享中的文件/目录

软连接的使用场景

• 在不同文件系统或分区之间创建快捷方式,方便用户操作避免了频繁的路径切换

硬链接介绍

硬链接并不是一个单独的文件,没有独立的inode编号,本质是一个新文件名和目标文件的inode在指定的目录下新建立的映射关系!

硬链接创建方式
ln 目标文件 硬链接文件
//注意没有-s选项

虽然硬链接没有inode,但是他和源文件共用一个,所以他也可以访问到源文件,也可以执行源文件的各种操作:

硬链接数

不知有没有发现,我们创建软链接是权限后面的数字没变,但是创建完硬链接后那个数字变了

这个数字我们一直没有用介绍,其实这是硬链接数它的本质是inode对象中的一个属性项作用是引用计数(记录有多少个文件和当前inode有映射关系)!当有一个文件与当前文件的inode建立关系是这个数字就+1,当删除硬链接时,这个数字就-1,最后当为1时,再删除就真的删了!

struct inode
{
    int size; //文件大小
    mode_t mode;//权限
    int creater;//创作者
    int tmie;//ACM时间等
    int ref_count;//当前文件的硬链接数
    //...
    int inode_num;//文件对应的inode编号
    int datablocks[N];//N 一般是15,指向内容的块号,可以有多级
};

OK,我现在知道了这是硬链接数,也明白了一个文件建立硬链接后它的硬链接数会++;但是我现在不明白的是为什么新建目录的硬链接数是2呢?

不知你还记不记得我们以前介绍过的,每一个目录都会包含两个隐藏的目录文件,即 . 和  .. 前者指向当前的目录,后者指向当前目录的上级目录!其实这里当前目录的硬链接数是2就是因为 . 他其实就是一个当前目录的硬链接(OS特殊处理的)!也就是  和当前的目录共用一个inode所以他默认的硬链接数++就从1变成了2,而 . 和  ..  是每个目录默认有的所以一开始创建就有所以,我们看到的现象就是新建的目录硬链接数是2!

所以,基于上面的介绍,我们可以得出一个结论:

如果一个目录的硬链接数是A,则它里面有 A - 2个目!(除去自身和 .

验证一下:

硬链接的特征

• 文件类型 : 和目标文件的类型一样

• 文件类容 : 硬链接直接指向目标文件的数据块

• 跨文件系统:硬链接只能在同一个文件系统内创建,不能跨越不同的文件系统

软连接的使用场景

• 构建Linux的路径结构,让我可以使用. 和 .. 来进行定位路径

• 进行文件的备份

这里的文件备份主要是避免一些重要的文件被误删,所以会在一些其他的目录下建立一个当前文件的硬链接,即使那边把目标文件删了,但是只要硬链接在他的引用计数至少可以保证是1,就可以恢复,所以也就变相的进行了备份!!!

注意:可以给目录创建软链接,但是不能给目录创建硬链接(防止形成路径环)!!

这里有的伙伴就说了,你说的不对!上面你刚说一个目录里面有两个隐藏的目录文件 . 和 ..

他两刚说完是当前目录和上级目录的硬链接!是的!但是他两是OS为了方便用户切换路径特殊处理过的!

取消链接

取消链接的方式有两种:

1、直接删除链接文件

2、通过 unlink 取消链接关系

 

ACM时间

每一个文件都有三个时间:访问 Access、修改属性 Change 、修改内容 Modify,简称为ACM时间

可以通过 stat查看指定文件详细属性(包含ACM时间):

这三个的晒新策略如下:

Access : 最近一次查看内容的时间,具体实现取决于OS

Change : 最近一次修改属性的时间

Modify : 最近一次修改内容的时间(内容更改后,属性也会随着修改)

Access是高频操作,OS一般不会每次都更新,而是一般查看N次后再刷新

注意:修改内容一定会改变属性的时间,但是不一定改变访问的时间(例如,用chmod修改权限),原因是操作文件不一定要打开文件!

动静态库

在正式的谈动静态库之前,我们先来想一个问题:我们有没有使用过库呢?答案是我们肯定用过!比如说C/C++标准库!我们的程序要运行起来,除了要有申明还必须要有实现!但是我们平时写的代码只是调用人家的接口函数,例如:printfscanf等,并没有写具体实现呀!不是也跑的好好的吗?其实这就涉及链接了!

回忆程序的翻译过程

我们知道一个程序最重要跑起来,必须要经历以下四个阶段:

• 预处理:将头文件展开、去注释、宏替换、条件编译等,最后会形成 .i 文件;

• 编译:词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令最后会形成 .s 文件;

• 汇编:将汇编语言形成二进制指令,最后会形成 .o 文件;

• 链接:将生成的各个 .o 文件进行连接,最后会形成可执行文件;

什么是库?

其实在安装VS等时,顺便给你安装的那些.dll的库本质里面就是一堆打包好的的 .o 文件!即将众多的 .o 文件成包就可以将这个包称为库!(简单理解),所以你平时写的C语言虽然没有写printf的实现但是程序任然可以跑的原因是:你的VS在安装时一定为了开发,同步的安装了这些库,所以在执行链接的时候就可以找到!

库的分类

库又分为静态库动态库

静态库:程序在编译链接的时候把库中的代码链接(拷贝)到可执行文件中。

              程序运行起来的时候不再需要静态库!

动态库:程序在运行时才去链接动态库的代码。动态库的代码多个程序共享

Linux中,静态库的后缀为 .a ,动态库的后缀为 .so

Windows中,静态库的后缀为 .lib ,动态库的后缀为 .dll

ldd和file

为了方便直观的看到动静态库,我们先来介绍一下两条指令:ldd和file!

ldd : 查看当前的可执行程序链接了哪些态库

file:用来确定文件的类型,确定他是不是可执行程序或共享库,但是不会显示具体库的依赖关系!

OK ,先不管其他的,先拿两个可执行文件试一下:

注意:上述的动态库libc.so的真实库名是,去掉前缀lib和后缀.so

所以,libc.so的库名就是c;同理libstdc++.so的库名就是stdc++

为什么要有库?

提高开发的效率

如果你没有库,你写个C语言,就好还得自己手搓printf,关键是那你写的还不一定对哈哈!而有了库之后,你直接用接开发其他的就可以了,不用在为了输入输出担心了!

静态库的制作

再介绍如何使用库之前我们总得有个库吧!所以我们先来介绍如何制作库,了再来介绍如何使用(链接)库!我们这里就模拟的写一个数学加减的库!

Add.h

#pragma once

int Add(int, int);

 Add.c

#include "Add.h"

int Add(int x, int y)
{
    return x + y;
}

Sub.h 

#pragma once

int Sub(int, int);

 Sub.c

#include "Sub.h"

int Sub(int x, int y)
{
    return x - y;
}

第一步:将所有的源文件生成对应的目标文件

第二步:使用 ar 命令将所有的目标文件打包成库

先来介绍一下ar指令:

ar 指令是 gnu 的归档工具,常用于将目标文件打包为静态库!

选项:

-r (replace):如果静态库存在,则替换库中要更新的目标文件

-c (create) : 不存在这个库,则创建

-t :列出生成静态库中的文件

-v (verbose):显示详细的信息

OK ,先生成:

ar -rc 要生成库的名字 目标文件
//注意:ar 生成的静态库,所以哭的名字要以.a结尾

此时可以用-t和-v选项就可以查看一下库中的内容和详细信息了:

第三步:将头文件和静态库给组织(发布)起来

我们制作好了静态库后,如果要给别人用的话,我们得提供头文件和静态库!所以我们,得将头文件和库文件一并组织到一起。所以,可以将头文件全部放到mymath目录下的include的目录下,将libmymathc.a放到mymath目录下的lib目录下:

此时别人拿到mymath就可以快乐的使用啦~!

当然上述步骤,都可以写一个makefile来做:

mylib=libmymathc.a #方便后续的修改,类似于宏

CC=gcc #指定编译器

$(mylib):Add.o Sub.o #生成静态库
	ar -rc $@ $^

%.o:%.c #将所有的.c文件全部编译成同名的.o文件,这里的%就是一种占位符
	$(CC) -c $< 

.PHONY:clean #清理
clean:
	rm -rf *.o $(mylib)

.PHONY:output  #组织头文件和静态库
output:
	mkdir -p ../mylib/include
	mkdir -p ../mylib/lib
	cp ./*.h ../mylib/include
	cp ./*.a ../mylib/lib


这里唯一要解释的一个东西就是 $< 他的作用就是只依赖对应的第一个文件!

比如上面的 %.o:%c这里就只依赖.c,而如果%.o:%.c %.i此时也只是依赖第一个即.c

生成静态库

将头文件和静态库进行组织

此时你将完整的库可以利用tar等压缩软件打包给别人,别人一解压就可以用了!

静态库的使用

OK,现在有库了,我们现在是站在使用者角度的, 我们现在拿到了库!

将静态库安装到系统

我们现在想要使用库的话,我们就得将他安装到系统!头文件安装在 /usr/inlcude 下,库安装在 /usr/lib64或者 /usr/lib下!这就是编译器默认寻找库的路径!

OK,那我们就拷贝一下吧:

OK,安装好了,那我就在本地可以将该库删除了!直接创建一个测试文件跑一下:

#include "Add.h"
#include "Sub.h"
#include <stdio.h>

int main()
{
    int x = 3;
    int y = 5;
    printf("%d+%d=%d\n", x, y, Add(x, y));
    printf("%d-%d=%d\n", x, y, Sub(x, y));
    return 0;
}

为什么我们都将库安装了OS还是找不到库呢?原因很简单,编译器只认识标准库而不认识我们写的第三方库!所以我们得指定要链接那个库!如何指定呢?可以带gcc -l 选项指定!如下:

其实,我们一般是极其不推荐,直接将非官方的第三方直接安装到系统的!更何况这还是我们自己写的!如果是官方或者像C语言之父丹尼斯.里琦这样的大佬写的库,可以写安装到系统外,其他都不建议安装到系统!

不安装使用静态库

那我们不安装了咋使用呀?如果你是为了简单的测试你可以直接在当前路径下用,也就是将库拷贝到当前测试路径下:

这咋又找不到头文件了呢?我们使用的是 "" 包含的头文件呀,他不应该现在当前路径下找吗,他会到当前的路径下找,但是不进入到mylib目录寻找!所以就找头文件不到了,同理库也必定找不到!如何解决?可以只用gcc的选项

-I 指定头文件的路径

-L 指定库的路径

-l 指定链接哪一个库

当然如果你不想在命令行指定头文件的路径了,你还可以这样写:

#include "mylib/include/Add.h"
#include "mylib/include/Sub.h"
#include <stdio.h>

int main()
{
    int x = 3;
    int y = 5;
    printf("%d+%d=%d\n", x, y, Add(x, y));
    printf("%d-%d=%d\n", x, y, Sub(x, y));
    return 0;
}

OK,这就是静态库的制作和使用!下面我们来看一下动态库:

动态库的制作

动态库的制作和静态库基本上是一样的,但是还是有一点区别!OK,我们下面来看:

第一步:将所有的源文件生成对应的目标文件

此时就和静态库有区别了,动态库需要加上 -fPIC 选项(后面解释),另外动态库的结尾是 .so

第二步:使用gcc的 -shared 选项将所有的.o文件打包成库

注意:这里和静态库不一样了,不再使用ar -rc了!

第三步:将头文件和动态库给组织(发布)起来

还是和静态库一样,将头文件放在mylib/include目录下,将动态库放在lib目录下:

当然上面的也是可以用makefile来做的:

mylib=libmymathc.so #方便后续的修改

CC=gcc #指定编译器

$(mylib):Add.o Sub.o #生成动态库
	gcc -shared -o $@ $^

%.o:%.c #将所有的.c文件全部编译成同名的.o文件,这里的%就是一种占位符,注意 -fPIC
	$(CC) -c -fPIC $<  

.PHONY:clean #清理
clean:
	rm -rf *.o $(mylib)

.PHONY:output  #组织或发布
output:
	mkdir -p mylib/include
	mkdir -p mylib/lib
	cp ./*.h mylib/include/
	cp ./*.so mylib/lib/

OK,生成动态库:

将动态库发布

动态库的使用

OK,还是和静态库一样的测试代码:

#include "Add.h"
#include "Sub.h"
#include <stdio.h>

int main()
{
    int x = 3;
    int y = 5;
    printf("%d+%d=%d\n", x, y, Add(x, y));
    printf("%d-%d=%d\n", x, y, Sub(x, y));
    return 0;
}

 这里还是和静态库一样使用gcc -I -L-l 选项进行变异生产生成可执行!

OK,这里可执行程序是生成了,但是为什么在运行的时候说动态库找不到呢??gcc编译的时候不是已经指定了搜索路径了吗?其实,给gcc指定路径只是给编译器指定了,编译器编译时查找有这个库,就给你生成了可执行程序了!但是编译完就和编译器没有关系了,你运行的时候本质是OS在运行等操作系统执行到你依赖的动态库的实现的时候,去它的系统路径找的时候发现没有!这就报错了!

为什么同样的选项静态库没有这样的问题?静态库在编译的时候会将库中的实现代码拷贝到当前的可执行程序,所以后期运行的时候操作系统就不在依赖库了!

而我们的动态库是在操作系统运行的时候采取链接的,所以我们这里也要告诉系统动态库在哪里!

OK,如何做呢?这里有5种方式,下面我们来一一介绍:

1、将动态库安装到系统

这就和上面的静态库安装一样了:

//拷贝头文件
sudo cp ./mylib/include/*.h /usr/include/

注意:这里在安装动态库的时候,一定要明确自己的机器是X64还是X86的,如果是X86的OS查找动态库的默认路径是/usr/lib 如果是X64的OS默认查找的路径是/usr/lib64!

可以用 uname -m查看

 

我这里是X64所以我就拷贝到了lib64下面了: 

//拷贝库
sudo cp ./mylib/lib/*.so /usr/lib64/

OK,此时就可以不用担心找不到库和头文件了,只需要指定链接哪一个库即可:

这种方式,还是和上面的静态库一样是极其不推荐的!

2、在系统默认的路径下建立软连接

sudo ln -s 目标文件   链接文件

这种方式,很推荐!

3、命令行导入环境变量

操作系统在运行程序的时候回去查找,当前程序所用到的相关动态库!一般是先去OS的默认路径,除此之外,还会去 LD_LIBRARY_PATH 的环境变量中去找!所以,我们可以将我们依赖的库

的路径添加到 LD_LIBRARY_PATH 中!

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/cp/oslearn/mylib_test/mylib/lib

注意:系统是知道我们要链接哪一个库的,所以我们只需要告诉他在哪里照即可,所以主需要给他路径即可!

OK,这样就可以找到了,但是,我们以前介绍过,这样修改环境变量是内存级别的,等下次重新登陆的时候就与不行了!要想永久的生效就必须得把他写入到相关的配置文件里面!这种方法一般用于测试这个库时候有问题!

4、修改.bashrc配置文件

为了让我们的环境变量永久生效,我们可以到根目录下的.bash_profile文件,将我们的环境变量添加到配置文件即可!

注意:添加完之后,我们需要重启一下!

当然,这种方法除非很重要的库,要不然你也就被把它加到配置文件了!

5、新增动态库的搜索配置文件

在系统中存在着一个 /etc/ld.so.conf.d 的目录,这是系统管理所有的动态库加载相关的配置文件!

打开看到的这个路径就是,OS在运行时索要查找相关动态库的路径!所以我们要想自己的动态库永久有效,只要要在/etc/ld.so.conf.d 的目录下创建一个文件,然后将自己的动态库路径写到这个文件即可!

OK,创建好了!我们来试一下:

我们都有动态库的配置文件了,咋还是找不到呢?其实这是系统的还没有刷新,还不知道你的新增的文件!所以,我们就得刷新一下让系统知道!如何刷新?1、一处当前的xshell然后重新登陆 2、使用 ldconfig刷新!注意,这些都是系统文件,只有 root才有权限,所以要以root的身份执行!

OK,这就是动态库的使用!

动静态库的比较

动态库(Dynamic Libraries)

优点

  1. 节约磁盘和内存空间:由于多个程序可以共享同一个动态库文件,因此可以显著减少磁盘空间的使用。同时,在运行时,操作系统通常会将动态库文件的内容映射到进程的地址空间中,因此多个进程也可以共享同一份内存中的动态库代码,进一步减少内存使用。(这个将在下面介绍)

  2. 更新和维护方便:当动态库需要更新或修复时,只需要替换磁盘上的库文件,而无需重新编译或分发依赖该库的所有程序。这大大简化了软件的更新和维护流程。

  3. 支持模块化开发:动态库允许开发者将程序分解为更小的、可重用的模块,从而支持更加灵活的软件开发方式。

缺点

  1. 依赖性问题:如果程序运行环境中缺少必要的动态库文件,或者库文件的版本不兼容,程序将无法正常运行。因此不允许动态库丢失或出现版本问题。

  2. 加载时间:程序启动时,操作系统需要加载并初始化动态库,这可能会增加程序的启动时间。

静态库(Static Libraries)

优点

  1. 无需运行时依赖:静态库在编译时被直接链接到可执行文件中,因此运行时不需要额外的库文件。这简化了程序的部署和分发过程,尤其是在跨平台或嵌入式系统中尤为重要。

  2. 兼容性较好:由于静态库的内容被直接嵌入到可执行文件中,因此不受外部库文件版本变化的影响,减少了因版本不兼容而导致的问题。

缺点

  1. 增大可执行文件大小:每个使用静态库的程序都会在其可执行文件中包含库代码的副本,这会导致可执行文件的大小显著增加。

  2. 浪费内存:虽然每个程序都包含了静态库的副本,但这些副本在内存中仍然需要独立的空间,不能共享。这可能导致内存使用效率的降低。

  3. 更新和维护不便:当静态库需要更新时,所有依赖该库的程序都需要重新编译和分发。这增加了软件更新和维护的复杂性和成本

注意:gcc编译器不加-static默认使用的是动态库!-static是强制的让程序连接静态库,但前提是得提供相应的静态库!

动态库的加载

我们这里不考虑静态库,因为如果当前的程序依赖的是静态库的话,编译好之后可执行程序中已经把静态库中的内容拷贝了一份,运行的时候不在依赖库了!但是动态库不一样,动态库是在操作系统在运行时采去找的,而程序运行是要先加载到内存的,所以动态库也必然是要加载到内存的!具体怎么做,下面解释!这里我们先来谈一谈程序的加载!

程序的加载

当程序编译好之后就变成了二进制的可执行程序,其中,这些二进制程序每一行是有地址的!并且不是随便存储的,而是以特定的格式存储的;在Linux中,ELF的格式存储的!其中在ELF可执行程序的头部会存储着由加载器扫描得到的当前可执行程序的各个区域的开始和结束的地址,以及main函数的地址!其中,将编译好的二进制中的地址称为逻辑地址(组成原理的知识)!二进制可执行程序的编址方式有两种即绝对编址和相对编址!绝对编址就是在一个范围内从小到大逐一编址,例如,从[0...0, f..ff]依次编址;相对编址就是在一个范围内,从某一个基础地址开始按照偏移量编址!例如,在[00..0, f...f]的任意一个位置开始,按照一定的偏移量编址!而将基础地址+偏移量编址的方式称为平坦模式!(ELF就是平坦模式)

当程序运行起来的时候,当前程序就变成了进程!进程 = 内核数据结构 + 代码和数据!我们以前在初识虚拟地址空间的时候是介绍过的,OS会先给当前进程准备好各种内核的数据结构,例如:task_struct、mm_struct、页表等!然后才会加载代码和数据到内存!

这里你有没有想过mm_struct本质不就是一个结构体对象吗?它内部就是大量的begin和end!其中堆栈是由OS指定的!但现在的问题是,我们代码和数据都还没有加载到内存,你的正文代码区、已初始数据化区、未初始化数据区是谁初始化的?OK,刚刚介绍了,当程序编译成二进制之后他们也有地址,且在ELF的头部存储着由加载器扫描得到的各个区域的逻辑地址。其实,OS在将代码数据加载到内存前会先获取ELF二进制的头部信息,会用ELF的各个段的开始结束的信息去初始化虚拟地址空间的各个段!所以,我们可以简单的认为磁盘的逻辑地址就是虚拟地址空间的地址

然后当代码和数据加载到内存后,内存中也是有每一行代码的地址的,也就是物理地址!当有了物理地址,地址空间也有了虚拟地址,此时就可以映射到页表了!

这就和我们高考考上大学,没开学是一样,你在没有开学之前,学校的教务系统不会先给你分宿舍,而是先把你的档案从高中提取过来,然后给你在教务系统上注册,分学号!等你开学后,就会给你分宿舍,当你有了宿舍后就会有宿舍号,此时就可以用你的学号和宿舍号进行映射了!

程序真正的在运行的时候,是CPU在运行!现在的另一个问题是:主函数的地址OS已经在初始化虚拟地址空间,获取ELF头部的时候CPU就拿到了main的虚拟地址!但是执行完main函数,下一条执行的地址在哪里CPU如何知道呢?其实CPU中有一个叫PC的寄存器,是专门记录当前指令的下一条地址的!所以当CPU执行完当前指令后,就可以从PC寄存器获取下一条要执行指令的地址了!

此时,CPU执行程序的逻辑就 "转" 起来了!当CPU拿着当前要执行的虚拟地址时,会先到页表找到其对应的物理地址,然后根据物理地址到内存中执行相应的代码!执行完后,再去PC寄存器那下一条要执行的虚拟地址....!

OK,这就是程序的加载,下面我们有了这层理解之后,就可以理解动态库的加载了!

动态库的加载

我们上面介绍制作动态库的时候,说在使用gcc编译动态库的时候,需要加上-fPIC选项,当时说是位置无关码!到底什么意思呢?我们下面就来探索一下:动态库也是二进制代码,也是用ELF的格式存储起来的!它采用的也是相对地址的编址,当当前的程序在执行到调用库的方法的时候,发现使用了动态库,且这个不在内存(如何检测在不在下面介绍),就会去磁盘加载库的动态库的ELF头部的属性,然后按照属性将他映射在虚拟地址空间的共享区!完成后,加到内存,将物理地址和虚拟地址进行了页表的映射!因为是相对编址,所以用到当前库的时候就会拿着,当前依赖方法的地址,去共享区中找到当前依赖库的起始地址,然后库的起始的虚拟地址+方法(地址)偏移量就是该方法在库中的虚拟地址,然后通过页表映射就可以在内存中找到!

因为是相对编址,库里面存的是方法的偏移量,所以不管库被映射在虚拟地址空间的哪里,根本不重要,因为不管在栈上、堆上、还是在共享区,库中方法的地址是永远不变的!这就是与位置无关码

另外,我们有时候不止一个进程用这个库,可能很多进程都在用这个库/多个库!那后来的进程是如何知道当前以来的动态库已经被记载到内存了呢?其实因为OS内部有很多的进程,他们都可能依赖很多的库,所以这么多记载到内存的库,操作系统要不要把他们管理起来呢?当然是要的!如何管理?先描述,在组织!通过结构体描述,用双链表进行组织!此时当前进程以来一个动态库时,OS会先遍历动态库的双链表,如果找到了就直接映射到共享区!否则就发生缺页中断进行加载、然后构建映射,最后使用!

OK,本期分享就到这里,好兄弟,我们下期再见!

结束语:编程之路,虽长且艰,但我不曾后退!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值