【问题标题】:How does an extern variable work in a shared library外部变量如何在共享库中工作
【发布时间】:2016-10-14 17:33:26
【问题描述】:

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

lib.h

#pragma once

extern int x;
extern int p(void);

lib.c

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

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

交流

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

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

b.c

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

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

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

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

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

【问题讨论】:

  • 我认为链接器错误部分无关紧要,因为您必须做一些工作才能产生忽略未定义符号的可执行结果。对于运行时的情况,共享库在运行时单独链接到每个进程,可写页面是默认的写时复制。正常情况下 a 和 b 都有自己的 x 副本(大星号),没有单独的 lib 进程。在 Windows 上,如果显式请求,dll 可能包含进程之间共享的数据,但显式创建和使用共享内存段来共享内存更为常见和灵活。
  • @ArtYerkes AFAIK,如果链接器不知道lib 导出extern int x,则链接器将出错,这可能意味着,使用该链接器,库无法导出变量。至于你的其余评论,如果我理解正确的话:写信给例如extern int x 对其他进程不可见,但 lib 可见,仅当使用该进程调用时(很像案例 3,除了所有进程最初共享相同的 x )?是这样,还是您(使用“写入时复制”)的意思是当您写入变量时,它将创建一个lib 不使用的新变量?
  • @ArtYerkes 顺便说一句,把这个变成答案:)

标签: c shared-libraries extern


【解决方案1】:

这个问题有几个部分:

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

Linker error: x declared extern but never defined.

由于 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)

lib 在您的场景中不是一个进程,它是一个共享库。共享库在每个进程空间中单独加载和链接,其中某些内容以动态加载器(ld-linux.so,windows上的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)

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

All processes share the same x, including lib. (a.c, b.c and lib return random values)

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

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)

一些不支持单独地址空间的旧运行时系统确实以这种方式工作,尤其是 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.)?

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

在 cmets 中,还有其他几个问题:

Can windows dlls (or others) export non-function data.

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

Asterisk, see below?

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

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

【讨论】:

  • 谢谢,这有帮助。然而,虽然我知道lib 不是(技术上)一个进程,虽然我也从未声称它,但我确实认为将共享库与进程进行比较会使解释它们变得更容易。从共享库共享内存就像在进程之间共享内存,不是 C 标准定义的东西,而是其他大型标准(POSIX、Windows 等)通常实现的东西。事实上,AFAIK 在进程之间共享内存的工作方式与在共享库中的工作方式相同。
  • 还有一个问题:在共享库中返回指向静态变量的指针总是有效的(在写入时修改变量),对吧?
  • 是的,每个人都在进程空间中观察到相同的地址,因此您可以在共享库中返回指向静态和全局变量的指针,并且用户和您的库代码将观察到相同的值(在写入时修改,所以其他进程没有观察到相同的变化)。在进程之间共享内存在根本上是不同的,因为共享库没有自己的生命。它所做的一切,都代表它所在的进程。进程之间的共享内存是两个进程做出的选择,而共享库不能退出进程地址空间。
猜你喜欢
  • 1970-01-01
  • 2017-01-23
  • 1970-01-01
  • 2020-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-20
  • 1970-01-01
相关资源
最近更新 更多