软硬链接

硬链接

ln a b,就是将目标文件a硬链接到文件b

ls -i 查看文件的inode,ls -li查看所有文件的inode

原理

Linux 文件由inode + 数据块组成:

  • inode:记录文件元信息(权限、大小、指向数据块指针),文件名只是 inode 的一个别名。
  • 硬链接本质:多个文件名指向同一个 inode

特点

  1. 同一个 inode 号,多个文件名,共享磁盘数据
  2. 只能对文件创建,不能给目录创建硬链接(系统目录本身有. .. 硬链接除外)
  3. 不能跨文件系统(分区),inode 只在当前文件系统有效
  4. 删除一个硬链接,只是删掉一个文件名;只要还有一个链接存在,文件数据不会删除
  5. 修改任意一个硬链接文件,所有链接文件都会同步变化(共享同一份数据)
  6. 不额外占用磁盘空间(仅增加目录项记录)

创建并写入内容到test.txt,观察inode

软连接

ln -s a b,将文件a软链接到文件b

test.txt 和 hard.txt inode 相同;soft.txt 是独立 inode

原理

软链接本身是独立的小文件,它的 inode 指向的数据内容保存的是源文件路径。类似 Windows 的快捷方式。

特点

  1. 软链接有自己独立的 inode,是一个单独文件
  2. 可以链接文件,也可以链接目录
  3. 支持跨分区、跨文件系统
  4. 如果源文件删除,软链接依然存在,但变成失效链接(红闪烁),访问会提示 No such file
  5. 修改软链接对应的文件,实际修改的是源文件数据
  6. 软链接文件本身占用少量磁盘空间(存放路径字符串)
  7. 相对路径创建软链接时,源路径是相对于软链接所在目录,不是当前执行命令目录,很容易踩坑

使用场景

  • ✅ 硬链接:重要文件多备份,防止误删(同一分区内)
  • ✅ 软链接:版本切换(python 多版本)、目录迁移、快捷访问、跨分区链接
特性硬链接软链接
inode和源文件相同独立 inode
本质文件名别名保存路径的独立文件
支持目录❌ 不支持✅ 支持
跨分区❌ 不支持✅ 支持
删除源文件文件还在链接失效
文件大小和源一致路径字符串长度
权限和源一致软链接权限几乎无效,以源文件为准

动静态库

动态库

库:库是写好的现有的,成熟的,可以直接复用的代码实现,不需要去写代码时需要应用到然后在去重写代码,直接调用库中的代码应用就可以。

动态库(.so)

原理:链接阶段只记录库的符号信息,不拷贝代码;程序运行时才加载动态库到内存。多个进程可以共享内存里同一份库代码。

特点:

✅ 优点

  1. 可执行文件小,内存中只加载一份库,节省内存
  2. 库升级:替换 .so 文件,程序不用重新编译(接口不变即可)

❌ 缺点

  1. 运行环境必须存在对应 .so,否则程序无法启动(经典:error while loading shared libraries)
  2. 运行时多一步加载,速度稍慢
# 1. 生成位置无关代码 -fPIC(必须!)
gcc -c -fPIC add.c -o add.o

# 2. 生成动态库 libxxx.so
gcc -shared add.o -o libadd.so

# 3. 编译程序链接动态库
gcc main.c -o main -L. -ladd

创建及使用

add.h

#ifndef __ADD_H__
#define __ADD_H__
int add(int a,int b);
#endif

add.c

#include "add.h"
int add(int a,int b)
{
    return a+b;
}

main.c

#include <stdio.h>
#include "add.h"
int main(void)
{
    printf("10+20=%d\n", add(10,20));
    return 0;
}
第 1 步:生成位置无关代码 -fPIC
gcc -c -fPIC add.c -o add.o
  • -c:只编译,不链接,生成目标文件 add.o
  • -fPIC:位置无关代码,动态库必须加这个参数!

动态库会被加载到内存任意地址,代码不能写死内存地址,所以要 PIC。不加这个编译会报错!

第 2 步:用 add.o 打包生成动态库 libadd.so
gcc -shared add.o -o libadd.so
  • -shared:告诉 gcc 生成共享库(动态库)
  • 命名规则:libxxx.so,xxx 是库名,后面 -lxxx 使用

执行完当前目录就有 libadd.so

③ 3-2 动态库使用(编译 main 程序链接动态库)
gcc main.c -o main_dynamic -L. -ladd

解释:

  • -L.:编译时,在当前目录找库文件(libadd.so)
  • -ladd:链接 libadd.so(自动省略 lib 和 .so)

✅ 到这里,编译成功,生成可执行文件 main_dynamic

问题

现在直接运行:

./main_dynamic

报错:error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

❗核心概念区分(很多人搞不懂) -L. 只是编译阶段给 gcc 看的:编译的时候去当前目录找库 程序运行阶段,操作系统的动态链接器 ld-linux.so 不会默认去当前目录找 so! 编译成功 ≠ 运行成功!

用 ldd 查看程序依赖的动态库:

ldd ./main_dynamic

你会看到:libadd.so => not found

4 种解决方案(由简单到永久)
方案 1:临时环境变量 LD_LIBRARY_PATH(课堂测试首选,临时生效)
# 把当前目录加入动态库搜索路径
export LD_LIBRARY_PATH=$PWD
# 再运行程序
./main_dynamic

✅ 输出:10+20=30

缺点:只在当前终端有效,新开终端就失效。

方案 2:编译时写入 rpath(推荐实验用,把路径写进程序内部)

重新编译 main,加上 -Wl,-rpath=$PWD

gcc main.c -o main_dynamic -L. -ladd -Wl,-rpath=$PWD
  • -Wl,-rpath:传递参数给链接器,把库路径硬编码到程序中 之后直接运行 ./main_dynamic,不用设置环境变量。
方案 3:修改系统配置文件(全局永久)
  1. 将库路径写入 /etc/ld.so.conf
sudo echo "/root/lesson10" >> /etc/ld.so.conf
  1. 更新系统动态库缓存
sudo ldconfig

所有用户、所有终端都可以找到这个 so,永久生效。

方案 4:直接拷贝 so 到系统库目录
sudo cp libadd.so /usr/lib

系统默认搜索 /usr/lib,拷贝完成即可运行。

静态库

原理

编译链接阶段,把库里面的代码完整复制,合并进可执行文件。运行时不再依赖原库文件。

特点

✅ 优点

  1. 运行独立,不需要系统上存在该库,移植方便
  2. 运行速度略快(运行时不用加载库)

❌ 缺点

  1. 可执行文件体积大,多个程序用同一个静态库,会有多份副本,浪费磁盘
  2. 库更新:必须重新编译链接程序才能生效
# 1. 源码编译成目标文件 .o
gcc -c add.c -o add.o

# 2. ar 打包成静态库 libxxx.a
ar rcs libadd.a add.o
# r:插入文件 c:创建库 s:建立索引

# 3. 使用静态库编译程序
gcc main.c -o main -L. -ladd
# -L 指定库搜索路径;-ladd 链接 libadd.a(省略lib和.a)

创建使用静态库

add.h

#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif

add.c

#include "add.h"
int add(int a, int b)
{
    return a + b;
}

main.c

#include <stdio.h>
#include "add.h"
int main(void)
{
    int a = 10;
    int b = 20;
    printf("10 + 20 = %d\n", add(10,20));
    return 0;
}
静态库生成
  1. 把 add.c 编译成目标文件 add.o
gcc -c add.c -o add.o
  1. 使用 ar 工具打包,生成静态库 libadd.a
ar rcs libadd.a add.o
  • r:插入 / 替换文件
  • c:创建库文件
  • s:生成符号索引,加快链接

编译 main,链接静态库:

gcc main.c -o main_static -L. -ladd
  • -L.:告诉编译器,库文件在当前目录查找
  • -ladd:链接 libadd.a,自动补全 lib 和 .a

运行:

./main_static

输出:10 + 20 = 30

✅ 静态库特点:编译完成后,删除 libadd.a,程序依旧可以正常运行。代码已经拷贝进可执行文件。

项目静态库 .a动态库 .so
链接时机编译链接期运行期
代码拷贝完整复制进程序只记录符号,不复制
文件体积大小
内存占用多进程多副本内存共享一份
更新库重新编译程序替换 so 即可
运行依赖不依赖原库必须存在 so 文件
扩展名.a.so

面试题

一、基础简答题(必背)

1. 什么是静态库?后缀?原理?

答: 静态库:.a(Linux),本质是一堆 .o 目标文件的归档包(ar打包)。 编译链接阶段,静态库里面的代码会直接复制拷贝到你的可执行程序中。 程序运行时不再依赖静态库文件。

优点:运行独立,不需要外部库;部署简单。 缺点:可执行文件体积大;多个程序同时使用会有多份代码副本,浪费内存;库更新后程序必须重新编译。

2. 什么是动态库(共享库)?后缀?原理?

答: 动态库:.so(Linux)。 链接阶段只记录符号信息,不拷贝代码;等到程序运行的时候,操作系统才把动态库加载进内存,多个进程可以共享同一份库代码。

优点:程序体积小;内存节省;库升级替换,程序不用重新编译。 缺点:运行时依赖 so 文件,缺少就无法运行;部署要带上库。

3. 静态库和动态库的区别(高频面试大题)

答:

  1. 链接时机不同 静态库:编译链接阶段,代码拷贝进可执行文件。 动态库:编译只做符号检查;运行时才加载库。
  2. 文件依赖 静态:编译完成后,.a库文件可以删除,程序正常运行。 动态:运行时必须存在 .so,否则程序启动失败。
  3. 内存占用 静态:多个进程运行,内存有多份库代码。 动态:内存中只保留一份库,多个进程共享。
  4. 程序大小 静态编译出来的可执行文件更大;动态更小。
  5. 更新方式 静态:库修改,应用必须重新编译。 动态:替换 so 文件,应用不用重新编译。
4. 静态库怎么生成、使用?

答:

#1.编译生成目标文件
gcc -c add.c -o add.o
#2.ar打包生成静态库libadd.a
ar rcs libadd.a add.o
#3.使用静态库编译程序
gcc main.c -o main_static -L. -ladd
  • -L.:编译时库搜索路径
  • -ladd:链接 libadd 库
5. 动态库怎么生成、使用?为什么要 -fPIC?

答:

#1.生成位置无关代码
gcc -c -fPIC add.c -o add.o
#2.生成动态库
gcc -shared add.o -o libadd.so
#3.链接程序
gcc main.c -o main_dynamic -L. -ladd

-fPIC:Position Independent Code,位置无关代码。 动态库可以被加载到内存任意地址,代码不能写死绝对内存地址,必须生成 PIC 代码;静态库不需要。

原理 & 坑点题(你实验踩过的坑,面试最爱问)

6. 编译时 -L 和环境变量 LD_LIBRARY_PATH 的区别?

答:

  • -L:编译链接阶段生效,告诉 gcc 去哪里找库文件。
  • LD_LIBRARY_PATH:程序运行阶段生效,告诉操作系统动态链接器去哪里找 so。

所以:编译成功 ≠ 运行成功。

7. 动态库编译成功,运行报错 cannot open shared object file,原因 + 解决方案?

答: 原因:编译时 -L 指定路径只给编译器用;运行时动态链接器不会默认去当前目录找 so。

四种解决办法:

  1. 临时设置环境变量:export LD_LIBRARY_PATH=$PWD(当前终端生效)
  2. 编译加 -Wl,-rpath,路径,把库路径写入程序内部
  3. 修改 /etc/ld.so.conf 添加库路径,执行 sudo ldconfig 更新缓存(全局永久)
  4. 将 so 拷贝到系统默认库目录 /usr/lib
8. 同一个目录下同时存在 libadd.a 和 libadd.so,执行 gcc main.c -L. -ladd,会链接静态还是动态库?怎么强制静态链接?

答: gcc 优先链接动态库 libadd.so。 强制静态链接:增加 -static 参数

gcc main.c -o main_static -L. -ladd -static
9. ldd 命令作用?

答: ldd 可执行文件名,查看这个程序依赖哪些动态库,以及库是否能找到。找不到会显示 not found。

10. ar 命令 rcs 参数含义?

答:

  • r:替换 / 插入文件到库中
  • c:创建库文件
  • s:生成符号索引,加快链接速度
11. 动态链接的两种方式:隐式链接、显式链接(dlopen)

答: 隐式链接:就是我们刚才实验这种,编译时 -lxxx 链接库,程序启动自动加载 so。 显式链接:代码里面用 dlopen()、dlsym()、dlclose() 在代码里手动打开动态库,运行时随时加载,不需要编译时链接。插件化框架常用。

12. 动态库的 rpath 是什么?

答: rpath 是写入可执行程序内部的库搜索路径。程序运行时优先从 rpath 指定目录查找 so,不需要配置环境变量。编译参数:-Wl,-rpath=/xxx/path

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐