V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
LeeReamond
V2EX  ›  编程

程序调用 dll 的本质是什么?

  •  
  •   LeeReamond · 2021-02-11 19:09:48 +08:00 · 4073 次点击
    这是一个创建于 1163 天前的主题,其中的信息可能已经有所发展或是发生改变。

    大家过年好,不知道这时间发技术问题还有没有人理我。

    我们做开发的在各种系统中编译和使用 dll 和 so 文件已经很多了,但是我最近发现自己完全不理解这个东西,以为它自然而然就是这样的,写好了自然能编译,编译完自然能引用,但是现在发现自己对细节完全不清楚,调用过程对我来说完全是个黑箱。

    举例来说,一般比如我们在后端实现诸如->操作图片、视频、音频这些解码编码操作时,在同样的内存占用的等级的情况下,我们往往倾向于通过程序“自身”实现这些功能,而不是通过 popen 调用 ffmepg,虽然后者也能达到完全一样的功能,并且系统的 pipe 通常情况下(?)是可靠的。

    但是这些所谓的自身实现,往往也不是程序员手写,还是要调用第三方类库,而第三方库中往往出现这种情况,比如你避开了 ffmpeg.exe ,实际上调用的却是 ffmpeg.dll 之类的东西。系统采用 dll 模式而不是进程管理模式这很好理解,不过从开发人员的角度出发,dll 具体来说又有那些好处,让我们抛弃 popen 呢?毕竟后者使用上只需要一行命令而已。

    18 条回复    2021-02-12 10:47:57 +08:00
    Jirajine
        1
    Jirajine  
       2021-02-11 19:12:02 +08:00 via Android
    ysc3839
        2
    ysc3839  
       2021-02-11 19:12:41 +08:00 via Android
    > 不过从开发人员的角度出发,dll 具体来说又有那些好处,让我们抛弃 popen 呢?

    跨进程通信麻烦,开销大。
    LeeReamond
        3
    LeeReamond  
    OP
       2021-02-11 19:14:03 +08:00
    @ysc3839 单个程序在源码阶段互相连接我能理解它是怎么工作的,但是我不理解编译好的程序怎么引入另外一个编译好的程序
    wevsty
        4
    wevsty  
       2021-02-11 19:17:14 +08:00
    1 、既然说到 DLL 那就是 Windows 平台了,Win 下面并没有 popen 这么一个东西,实际上你用的 popen 是别人帮你封装好的东西。
    2 、DLL 加载以后调用几乎跟调用函数一样,没有额外的开销。
    用匿名或者命名管道来做交互是需要额外的开销的,并且 Windows 下面启动进程实际上比较慢,如果是需要反复启动进程的任务,那么用 popen 这样的方式是不合适的。
    3 、你用管道之类的方法,出现意外是处理起来是更麻烦的,你可能需要去检查进程的状态,返回值,以及输出的内容。如果输出很复杂的话,肯定是比直接调函数更麻烦的。
    zacharyjia
        5
    zacharyjia  
       2021-02-11 19:17:16 +08:00   ❤️ 6
    《程序员的自我修养:链接、装载与库》
    LeeReamond
        6
    LeeReamond  
    OP
       2021-02-11 19:17:48 +08:00
    @ysc3839 说实话我的开发经验来讲,我遇到过因为爆内存导致管道崩溃的,但是在不触及硬件瓶颈的情况下其实我没遇到过 popen 有什么稳定性问题。所以发现自己没法回答为什么不直接用 popen
    ignor
        7
    ignor  
       2021-02-11 19:38:50 +08:00 via Android
    本质就是更直接地调用代码
    ahhui
        8
    ahhui  
       2021-02-11 19:53:45 +08:00 via iPhone   ❤️ 6
    dll 的主要作用有 2 个,一个是代码复用,另外一个是节省内存。

    第一个作用很容易理解,把相互关联的代码封装到一个 dll 里,就如同在源码层面封装到一个 cpp,或者其他语言的单元。需要使用的时候,import 后,你就可以直接调用里面的函数了。而 dll 就是这层逻辑的编译结果的封装。假设你需要开发一个功能,但不希望把源代码给下游调用者,你可以把 cpp 做成 dll,把函数导出成 lib,把声明写入 h 文件里。这样你只要分发上述 3 个文件就可以让下游开发直接使用了。另外,代码能复用意味着不需要把所有逻辑都编译到 exe 里。你想想,如果没有 windows 的 dll,你要创建一个程序,必须从窗口创建界面绘制鼠标交互等一系列代码逻辑开始写,既浪费时间,也导致编译出来的 exe 有 20g 这么大,一旦发现 bug,你要重新修改代码,重新分发 20g 的文件。而且这 20g 里用户用到的功能并不是 100%,但加载到内存中却需要 20g 物理内存。所以很不现实也没有益处。当拆分 dll 后,你的程序需要 ui 交互的只要链接相应的 dll,你就可以工作了;你要网络的,只要链接对应 dll 就可以了;以此类推。

    上面已经给出一定的节约内存解释,但还需要进一步。windows 是运行在 ring0 层的系统,它可以使用调配任何无理资源,如内存硬盘等。如果每个 exe 都不拆 dll,那内存再多也会撑爆。所以 windows 对运行在 ring3 层的 exe 做了许多工作。加载 exe 后,系统会加载所有 exe import table 里需求的 dll,然后执行初始化。同一路经的 dll 如没被加载过,会被 windows 开出一块物理内存将其加载进去,同时在 exe 的线性地址里的高位(如 32 位程序的 2g 高位内存地址)做一个映射,把 dll 和这个地址关联起来。当第二个程序也加载同一个 dll 的时候,由于内存中已经存在了,于是只需要给 dll 引用计数加 1,再把 dll 映射到第二个 exe 的线性地址的高位处。使得两个程序使用了同一个 dll,但物理内存中只有一份拷贝的结果,这样就节省了内存(实际过程比这个复杂,以上只是简述)。

    dll 的加载分静态和动态,静态是指,在编译 exe 的时候,import table 里,引入要使用的 dll,windows 在加载 exe 的时候立刻就去加载 dll,如果 dll 找不到,你就会看到经常见到的“找不到 dll,无法启动的提示”。

    那么 dll 的动态加载,则是并不存 dll 的名称到 exe 的 import table 里,而是在代码里,动态使用 LoadLibrary 函数加载。这样程序启动的时候,系统不会查找加载这个 dll,当程序需要用到 dll 中的逻辑的时候,通过 LoadLibrary 加载进来,如果 dll 不存在,程序也不会崩溃,可以从容处理加载失败的逻辑。

    那动态加载有什么用呢?很多用法,其中之一是,你可以开发一份同时支持个人版和商业版的程序,其中把商业逻辑封装到 enterprise.dll 里,把个人版逻辑封装到 person.dll 里,通过你分发的配置或者其他逻辑,来动态加载对应逻辑的 dll,实现不同版本的分发。

    当然有 dll 的存在也使得你有机会在不全部更新所有文件的情况下对大型系统打补丁。试想如果没有 dll,windows 的更新,可能每次都要 20-30g 吧。
    seeker
        9
    seeker  
       2021-02-11 20:03:50 +08:00
    level 1: 理解程序是可以存储在磁盘上的,比如 exe
    level 2: 理解程序也是可以没有 main 方法的,比如 dll
    level 3: 理解程序在执行的时候可以去调用其他地方预先写好的代码的,比如 exe 运行时载入 dll 执行 dll 里面的程序

    也许你自己没有直接用 dll 但实际上你可能已经在用 windows/linux/mac 系统的各种 dll/so/dylib 了。
    paradoxs
        10
    paradoxs  
       2021-02-11 20:05:48 +08:00
    dll 就是个后缀,你喜欢的话改为 XXX 也一样用。

    分割成不同的模块,按需调用,就不用一次过全部加载啊,减轻压力。

    不然一个 exe 几个 G 你受得了。
    IgniteWhite
        11
    IgniteWhite  
       2021-02-11 20:13:38 +08:00 via iPhone
    windows 的 dll 和 mac 的 dylib 在多大程度上能对应呢
    3dwelcome
        12
    3dwelcome  
       2021-02-11 21:55:20 +08:00 via Android
    so 可以做到 native 代码的热更新,这是无与伦比的优势。
    dll 这点还是欠缺了一些。微软一般不靠 dll 而是用 com interface 多。
    dll 只能说,可以代码复用。给别人可以不开放源码,只给 lib 文件。
    Mutoo
        13
    Mutoo  
       2021-02-11 23:43:40 +08:00
    最近看了这本书,关于编译、装载与库的,讲解得挺不错:
    https://book.douban.com/subject/3652388/
    jim9606
        14
    jim9606  
       2021-02-12 01:05:19 +08:00
    如果不用动态库 /静态库,就意味着只能通过 IPC 的方式进行交互和控制,交互方法会严重受限。除了个别简单场景,大部分时候会提高开发难度。

    例如交换数据需要序列化为字节流再通过 pipe 传递,收回来再反序列化。可能只能使用同步调用。如果被调用方出错你还得解析返回码甚至没有 API 稳定性保证的 stderr 输出来了解出错原因。

    如果需要多线程操作,还得开多个子进程,并各自维护管道,还得正确分发收到的信号量到各个控制流中。一个控制流被分散到若干个相互依赖的处理函数中,维护很麻烦。

    对于有性能需求的场景,IPC 增加的多次上下文切换 /进程调度 /内存复制等开销会降低性能。

    如果用库操作,你可以通过 C 函数调用直接传递结构化的数据,免去上下文切换和内存复制的开销,用库提供的 handler 结构体控制任务和资源所归属的控制流,还能支持异步调用。

    缩小依赖也是好处之一,例如 ffmpeg 包括 avformat 、avcodec 、avfilters 、avdevice 等子库,如果只是做个编解码只需依赖 avcodec,如果需要媒体容器分离 /混流需要 avformat,剩下没用掉的部分可以省掉。
    kaedea
        15
    kaedea  
       2021-02-12 03:57:20 +08:00 via Android
    ELF 文件的 Lazy Load
    imdong
        16
    imdong  
       2021-02-12 09:11:34 +08:00
    exe 与 dll 额关系有点类似于 程序与 class (包,库,类)的概念.

    而 exe 与 exe 相互调用的话,就有点 API (接口,微服务) 之间相互调用的关系了.

    显然 dll so class 包 之类的使用上更便捷, 调用 API 会有更多额外的开销.

    至于 exe 与 dll 之间相互调用的原理,只知道是底层支持,与 include 一样?更多的我也不熟悉了.
    learningman
        17
    learningman  
       2021-02-12 10:07:28 +08:00 via Android
    换个角度,你不觉得拼接字符串再 popen 很傻吗。。。
    直接一个函数调用参数舒服的多
    adofsauron
        18
    adofsauron  
       2021-02-12 10:47:57 +08:00 via Android
    程序员的自我修养
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2586 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 04:21 · PVG 12:21 · LAX 21:21 · JFK 00:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.