外部变量如何在共享库中工作

YoY*_*nnY 5 c shared-libraries extern

假设我写了一个像这样的简单动态库:

库文件

#pragma once

extern int x;
extern int p(void);
Run Code Online (Sandbox Code Playgroud)

库文件

#include <lib.h>
#include <stdio.h>

x = 0;
int p(void) {
    printf("lib: %d\n", x++);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

交流电

#include <lib.h>
#include <stdio.h>

int main(void) {
    for (; !p(); x--) printf("a.c: %d\n", x);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

公元前

#include <lib.h>
#include <stdio.h>

int main(void) {
    for (; !p(); x = 0) printf("b.c: %d\n", x);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

a 和 b 会打印什么?我可以想到可能发生的几件事:

  • 链接器错误:x已声明extern但从未定义。
  • 每个进程都有自己的进程x,包括lib. (bc 始终为 0,ac 向下计数,lib 向上计数)
  • 每个进程都有自己的进程x可以与 共享lib。(ac 和 bc 始终为 1,lib 始终为 0)
  • 所有进程共享相同的内容x,包括lib. (ac、bc 和 lib 返回随机值)
  • 所有进程共享相同的x,包括lib,直到除了lib写入之外的其他人,然后该进程将获得它自己的 版本x而不是与之共享lib(在某处在线阅读此内容)。(lib 总是递增,bc 总是打印 0,ac 递减)

通常会发生什么?我们应该了解的编译器/平台之间是否存在任何不一致之处?我们可以强制一种行为(我在想__declspec(dllexport),编译器标志等)?

Art*_*kes 3

这个问题有几个部分:

a 和 b 会打印什么?我可以想到可能发生的几件事:

Linker error: x declared extern but never defined.
Run Code Online (Sandbox Code Playgroud)

由于 a 和 b 可能尚未构建到可执行文件中,因此不会打印任何内容。当然,您需要链接 lib.so、lib.a 或导入库 lib.lib,以将可执行文件公开给 x 的可链接定义,否则其他任何方法都不起作用(大多数情况下,如果努力尝试,它可能会比这更复杂)。

Each process gets it's own x, including lib. (b.c is always 0, a.c counts down, lib counts up)
Run Code Online (Sandbox Code Playgroud)

lib 不是您场景中的进程,它是一个共享库。共享库在每个进程空间中单独加载和链接,其中某些内容以动态加载器(windows 上的 ld-linux.so、ntdll.dll)理解的方式引用它。每个进程都会在其地址空间中观察到已加载库的副本,并且库本身也会看到相同的副本,因此运行 a 应该永远打印 0 后跟 1。p() 运行并测试,x 被打印,x 递减回 0。b 也将永远打印 0,后面跟着 1。p() 运行并测试,x 被打印,x 设置为 0。请注意,p() 打印 x++,因此增量发生在捕获 printf 参数的值之后。包含 a 和 b 的程序所引用的 x 变量特定于 a 或 b 的每次运行。这通常是在操作系统级别实现的,方法是将实际可加载库的页面从磁盘映射到内存并将它们设置为“写入时复制”,其中主机进程尝试进行的更改会导致操作系统分配新页面并复制旧页面内容放在第一位。结果是加载的库中未修改的部分占用更少的实际内存。

Each process gets it's own x to share with lib. (a.c and b.c are always 1, lib is always 0)
Run Code Online (Sandbox Code Playgroud)

Lib 不是一个单独的进程。在 a 中执行 p() 会看到与 a 链接的 x 相同的 x。

All processes share the same x, including lib. (a.c, b.c and lib return random values)
Run Code Online (Sandbox Code Playgroud)

通常情况并非如此(另见下文)。

All processes share the same x, including lib, until someone other than lib writes to it, then that process gets it's own version of x, not shared with lib (Read this online somewhere). (lib always increments, b.c always prints 0, a.c counts down)
Run Code Online (Sandbox Code Playgroud)

一些不支持单独地址空间的旧运行时系统确实以这种方式工作,特别是amigados。你不太可能遇到这样的人。

What typically happens? Are there any inconsistencies between compilers/platforms we should know about? Can we force one behaviour (I am thinking __declspec(dllexport), compiler flags, etc.)?
Run Code Online (Sandbox Code Playgroud)

在绝大多数情况下,每个进程都与该进程中加载​​的给定库的一个实例共享外部变量。除非您采取具体行动,否则这是预期的。

评论中,还有一些其他问题:

Can windows dlls (or others) export non-function data.
Run Code Online (Sandbox Code Playgroud)

是的。构建导入库时,在 .def 文件中使用 DATA 限定符。对于其他人来说,这与导出函数没有什么不同。然而,您将收到一个指向目标变量的指针,而不是绑定到所占用的空间。

Asterisk, see below?
Run Code Online (Sandbox Code Playgroud)

在 Windows 上,节具有 SHARED 属性,该属性会导致加载程序在使用 DLL 的每个进程中分配相同的页面。这不是默认设置,您必须跳过障碍并使用特定于平台的编译指示来完成此操作。有很多理由不使用这个。

大多数时候,当 dll 想要在多个进程中加载​​的自身副本之间共享状态时,它会使用主机系统的共享内存 API(通常是 CreateFileMapping 或 mmap)。这提供了灵活性(例如,所有 a 进程可以共享 x 的一个版本,与具有 x 的另一个副本的所有 b 进程分开)。请注意,使用 SHARED 很容易意味着运行 a 可能会使 b 崩溃,并且加载另一个长时间运行的用户 c 可能会阻止 a 或 b 再次启动,直到重新启动。