4.5 KiB
4.5 KiB
动态库的加载与卸载
1. 动态库加载的基本原理
当程序依赖动态库时,操作系统会使用 动态链接器(loader)来加载这些库到内存中。通常,动态库的代码段(即 .text 部分)会被多个进程共享,因为它们是只读的,且可以被多个程序使用。这是内存管理的一个重要优化,避免多个进程重复加载同样的代码段。
代码段常驻内存
- 代码段(text segment) 是机器指令,是共享的部分。它包含了库的执行代码,因为它是只读的,不会改变,所以可以被多个进程共享。
- 数据段(data segment)通常包含了库的全局变量、静态变量等,每个进程会有自己独立的副本。
2. 动态库的卸载
动态库的卸载主要发生在两个场景下:
- 程序退出时:当程序终止时,操作系统会释放所有它加载的共享库资源,包括代码段和数据段。这时,所有与程序相关的库(包括它们的代码段和数据段)都会被卸载。
- 显式卸载:如果程序使用了
dlclose(通过dlfcn.h库),可以显式卸载已经加载的动态库,释放库占用的资源。
代码段的卸载
对于 代码段,在大多数情况下,代码段是常驻内存的,而不会在程序结束时被卸载。原因是操作系统通常会选择将这些只读的代码段保留在内存中,以便其他程序可以复用它们。如果操作系统认为没有程序再需要某个共享库,它才会将库的代码段卸载。
- 在 Linux 系统中,
mmap被用来将共享库映射到内存,代码段通常会使用共享映射(MAP_SHARED),这样多个进程就可以共享这部分内存。 - 数据段则通常在进程退出时会被清理,或者通过
dlclose显式卸载。
3. 共享库的生命周期
动态库的生命周期包括以下几个重要的点:
- 加载:当程序开始执行时,操作系统通过动态链接器加载所有依赖的共享库(包括 glibc 等)。这些库的代码段会驻留在内存中,供所有进程共享。
- 使用:所有依赖该动态库的程序都会复用这个库的代码段。这也意味着,多个程序在运行时共享相同的代码段,避免了重复加载。
- 卸载:动态库的卸载通常发生在以下两种情况:
- 程序退出:操作系统会卸载所有程序的共享库资源,包括代码段和数据段。
- 显式卸载:使用
dlclose等 API,程序可以显式卸载已加载的动态库,释放它占用的内存资源。
4. 代码段是否永远常驻内存?
理论上,代码段会常驻内存,并且可以被多个程序共享,但操作系统通常会在以下两种情况下卸载这些代码段:
- 没有程序使用该共享库:如果操作系统发现没有进程再依赖某个动态库,它可能会选择卸载该库的代码段。操作系统会跟踪每个共享库的引用计数(即有多少进程在使用该库),当引用计数为 0 时,它会卸载库的代码段。
- 内存压力:在某些情况下,操作系统可能会由于内存压力而决定卸载不再使用的共享库的代码段,尤其是在系统内存紧张时。
5. 动态库的加载与 ld-linux.so
当程序依赖于动态库时,操作系统会通过 动态链接器(ld-linux.so) 来加载这些库。ld-linux.so 是一个负责将库加载到内存中的程序,它会负责执行以下操作:
- 加载动态库的代码段:
ld-linux.so会将动态库的代码段映射到内存,并将其交给程序使用。这个过程使用 内存映射技术,因此多个进程可以共享这些库的代码段。 - 符号解析与重定位:
ld-linux.so还负责将程序中的外部符号(例如printf)解析并连接到实际的库函数地址。
6. 总结
- 动态库的代码段通常是常驻内存的,并且可以被多个进程共享。这是一种内存优化的方式,可以避免重复加载相同的库代码。
- 动态库的卸载主要发生在两种情况下:程序退出时,或者通过显式调用
dlclose卸载。 - 在大多数情况下,操作系统会尽量保持动态库的代码段在内存中,直到没有进程再使用该库,或者内存压力需要卸载它。
- 所有共享库(不仅仅是 glibc)都会采用类似的机制,即共享代码段并为每个进程提供私有的数据段。