本文详细讨论了在 windows 和 linux 平台使用不同编译器以及 cmake 中关于使用共享库和静态库的相关内容

后文提到了如下几个编译器:

  • linux-gcc 就是 linux 下最常用的编译器了
  • windows-msvc-cl 指 windows 的官方编译器,VS 中默认使用它,其中 cl 是命令行中的编译器命令
  • windows-mingw-gcc 指 windows 下开源的 mingw-64 gcc 编译器实现,某些 windows IDE 会内置这种编译器,比如 CLion 或 QTCreator
    • 绝大多数默认行为与 linux-gcc 保持一致,但不可将二者完全是为行为一致
  • clang 编译器用的少,不熟悉,暂不讨论

共享库符号导出

符号导出是接触 windows 共享库开发之后才了解的概念,以前一直在 linux 下开发,linux 的默认行为比较开放,不特殊设置就会执行导出符号的行为,所以没意识到过这个问题

之所以要有符号导出,是基于共享库的原理做出的设计,共享库文件是在可执行程序运行时才加载的,这有多个好处:

  • 减小了可执行程序文件的大小
  • 共享库可以加载一次,然后在多个可执行程序进程间共享,减少了内存占用
  • 减小更新包大小,当只有共享库的代码改动时,只需要重新编译更新共享库即可应用新代码,不用重新编译整个项目

有这么些好处,那符号导出这个操作到底意味着什么?是个什么原理呢?

注意:处理导入导出符号是编译过程中链接阶段链接器的工作,这里为了方便起见,后文仍然将其视为编译器的功能,以便行文流畅思路清晰

大体上来说是把共享库里被导出的符号与其对应的代码实现拆分,先让可执行程序只拿到其需要的符号,这样就足够完成编译过程,生成可执行程序文件了,然后在运行时把符号对应的代码实现填充给可执行程序,如此一来可执行程序就完整了,可以正常运行了。有点类似于 C 语言中的 extern 关键字的作用,先用 extern 声明一个符号临时占位拿来用着,而这个符号对应的定义却在其他地方。

阅读全文 »

比如在程序中链接使用了第三方的 Release 动态链接库,如果在 VS 中使用 Debug 模式编译,一般情况下都会报错,反之亦然

在 VS2010 之前,这种做法是可以编译通过的,但在运行阶段可能会出现奇怪的错误

从 VS2010 开始 Windows 增加了这种混用的检测,并在编译阶段进行报错

那么 Windows 这样做是为了避免什么问题呢?什么原因导致的问题呢?

这就提到运行时库(一般是指 C 运行时库:CRT),这是系统提供的基础库,Windows 一个有别于 Linux 的重要区别是:
在 Windows 下可以为一个进程加载多个 CRT,在 Linux 下则不支持这个行为

在 VS 中项目属性中可以查看和设置项目的 CRT,可以发现 exe/dll/lib 项目都可以设置自己要使用的 CRT,有多种 CRT 可以选择:

  • /MT Multi-threaded
  • /MTd Multi-threaded Debug
  • /MD Multi-threaded DLL
  • /MDd Multi-threaded DLL Debug

不带 Debug 的则表示 Release 版本的 CRT,不带 DLL 的表示使用静态库,这意味着甚至可以把 CRT 静态链接到目标中

默认情况下,程序在 Debug 模式下就会默认使用 Debug 模式的 CRT,除非手动修改了此选项,Release 模式下同理

那么这就很清楚了,如果第三方的 dll 库是 Release 版的,那么可以认为其使用的 CRT 是 Release 版的

阅读全文 »

1. 概述

在 Linux 中生成进程时会执行几个步骤。如果程序依赖于共享库,则除了可执行二进制程序之外,还会将动态链接器加载到内存中。

在本教程中,我们将讨论动态链接器 /lib64/ld-linux-x86-64.so.2。(译者注:动态链接器名字不是固定的,也可能是 /lib/ld-linux.so.2 等等,但一般都是以 ld- 开头的一个 so 文件或符号链接)

首先,我们将简要讨论运行程序的内部结构以及动态链接在此过程中发生的位置。然后,我们将看到一个显示动态链接器用法的示例。

2. 动态链接器简介

在了解动态链接器之前,我们首先简要讨论程序的执行,以了解动态链接器的作用。

2.1. 可执行二进制文件是如何执行的?

当我们从命令行启动一个进程时,shell 首先调用系统调用 execve() 来执行程序。它打开可执行文件,并经过一些准备后,将可执行文件加载到内存中。

如果可执行文件具有共享库依赖项,则还会加载并运行解释器来组装依赖的共享库。这个解释器就是动态链接器。

一旦动态链接器完成其工作,控制权就会传递给用户程序。

阅读全文 »

基础

  1. 客户端可以在服务端没有监听端口 (bind)时,直接创建连接,这会在服务端开始监听端口时自动连接并传输消息
  2. 已经创建过的连接,如果中途中断,会自动重连
  3. 一个服务端 socket 可以监听多个不同的地址,端口不同甚至协议不同
  4. socket 内部有 IO 线程,send 函数返回时,消息并不一定已经发出了,有可能还在队列里等待
  5. 每创建一个 context 对象都会隐式创建一个 IO 线程
  6. C 需要库中处理字符串时 不/不应该 发送空终止字符,所以需要在接收字符串数据后,手动追加空终止字符
  7. zmq_send/zmq_recv 和 zmq_msg_send/zmq_msg_recv,主要区别在于是否需要调用者提供 msg 结构体对象,前者不需要,会自动将数据封装到一个默认的 msg 对象中
  8. 使用 ZMQ_SNDMORE 参数发送多块消息体,当对端接收到数据时,其实肯定已经接收到了所有消息体,但仍旧需要调用多次 get_socketopt + zmq_recv 接口,读取出来所有消息体数据
  9. 使用 zmq_poll(理解参考 linux poll 或 python 协程 异步 IO)相关接口,可以实现同时处理多个 socket 的 send/recv

上下文 Context 对象

  1. 程序一般都以创建 context 开始
  2. 一个进程通常应该创建并使用一个 context 对象
  3. 也有创建多个 context 对象的场景,这相当于拥有多个独立的 zqm 实例,并且要注意销毁所有 context 对象

请求-响应 模式(REQ-REP)

  1. 服务端一般使用 响应 REP 类型的 socket
  2. 客户端一般使用 请求 REQ 类型的 socket
  3. REQ 必须遵守 send-recv 配对规则,先请求并且得到响应后,再发起下一个请求,打乱顺序会报错;REP 同样但它先 recv 再 send
  4. 理论上可以有 N 多个客户端连接到服务端,同时连接都没问题
  5. 这个模式和常见的 CS/BS 模式很类似
  6. 多个 REQ 连接到一个 REP 时,从逻辑上来看 REP 端的消息处理流程是同步的,必须要等到当前已接收消息处理完毕并回复对端后,才能去接收处理下一个 REQ 请求,其他 REQ 请求都在阻塞
  7. 一个 REQ 可以连接到多个 REP 对端,请求会被负载均衡得发送到多个 REP 端
  8. REQ 和 REP 在概念上可以认为是基于 Router-Dealer 实现的,内部自动处理了信封,上层应用不会看到信封(包括空帧),只会看到数据
  9. 当 REQ 直接与 REP 连接时,底层/内部使用的信封是最简单的格式,没有 地址标识帧,只有空帧+数据帧
  10. 当 REQ 和 REP 中间加入了 Router-Dealer 作为代理层时,REP 收到的数据包含了 地址标识帧 + 空帧 + 数据帧
  11. REP 会认为从第一帧开始直到空帧为止都是信封帧,所以当 REQ 直接与 REP 连接时即便没有地址帧,也需要有空帧,这是为了兼容 Router-Dealer 作为代理时的情况
  12. REP 在收到消息时,会保存此消息的信封,在回复消息时会自动在把信封加在数据帧前面;由于 REP 必须遵守 请求-响应 配对规则,所以在回复消息时肯定是知道需要使用的信封的

Router-Dealer

  1. 多用于在 REQ 和 REP 中间作为代理层: REQ(可以是多个) <–> Router(一个) <–> Deader(一个) <–> REP(可以是多个)
  2. 可以成对得出现多个 Router-Dealer 来实现多层代理
  3. 异步的,调用完 recv/send 接口之后不用必须调用反向的 send/recv 接口
  4. 需要显式处理信封,信封用于标识请求来源,同时回复消息时也需要用信封来标识回复到哪一个具体的请求端
  5. 信封可以嵌套,用于表示数据被多个 Router 转发
  6. 需要自己借助信封处理 路由规则(消息发送到哪些对端)
  7. Router 在收到消息时,会跟踪每一个 TCP 连接,并自动在消息前面追加 地址标识帧,以此告诉上层应用此数据的来源
  8. Router 在发送消息时,根据 地址标识帧 判断需要使用哪个 TCP 连接,发送消息给正确的目标,但真正发送消息前,会移除掉 地址标识帧
  9. Dealer 在逻辑上几乎不处理任何东西,只会接收和发送所有帧,不管是信封帧还是数据帧,不会擅自增加或删除任意帧,就像是一个异步的收发中转站
  10. Router 只关心 地址标识帧,它不知道也不关心后面的帧,包括空帧
  11. 当业务层直接使用 Router 时,可以自己处理 地址标识帧,换句话说,可以更灵活得控制消息的路由规则
  12. 当业务层直接使用 Dealer 与 REP 连接时,Deader 应该按照如下规则实现:Deader 发送消息时,必须先发送一个空帧(send-more),来模拟 REQ 的消息信封,否则 REP 会丢弃掉没有空帧的消息;在接收数据时,应该判断第一帧是否是空帧,是的话跳过并取得数据帧,不是的话则丢弃掉整个消息(包括数据帧)
  13. 类似的,当业务层直接使用 Dealer 与 REQ 连接时,Deader 应该按照如下规则实现:Deader 在接收到的消息时,应该判断第一帧是否是空帧,是的话跳过并取得数据帧,不是的话则丢弃掉整个消息(包括数据帧);在发送消息时,同样需要先发送空帧
  14. Router 一般会自动为一个新的连接生成标识,也就是 地址标识帧,但如果对端使用 setsockopt 明确设置了标识,Router 则不再自动生成

发布-订阅 模式(PUB-SUB)

  1. 没有订阅者的话,发布者会把所有消息都丢弃掉
  2. 订阅者可以订阅多个发布者,消息将会以轮次获取的方式公平得从每个发布者处获取
  3. 如果订阅者使用 TCP 创建连接,但处理消息比较慢,发布者会缓存一定量的消息
  4. 在不同版本和连接协议下,filtering 消息过滤这一操作可能在发布者一侧,也可能在订阅者一侧
  5. 一个消息会被所有订阅者获取到
  6. 订阅者必须要设置 SUBSCRIBE 否则收不到任何数据
  7. 订阅者可以设置多个 SUBSCRIBES,匹配到 任何一个 都会收到数据
  8. 消息的读写都是异步的
阅读全文 »

这个问题不只是在 windows wsl2 + docker-desktop 中存在,在 linux 系统中也会出现,解决方法是一样的,都是添加内核启动参数,linux 下一般是修改 grub 配置文件,windows 下的 wsl2 需要修改:

1
2
3
4
%userprofile%\.wslconfig

[wsl2]
kernelCommandLine = vsyscall=emulate

参考地址:https://github.com/microsoft/WSL/issues/4694

windows 不能发现 linux 下基于 samba 的共享,原因是从 windows 10 某些版本开始禁用了 smb1.0,不过尝试从 windows 系统中启用 smb1.0 后,仍然不能正常工作,

最佳解决方案是,在 linux 系统中安装 wsdd,项目地址为:https://github.com/christgau/wsdd

安装后启动服务:

1
systemctl start wsdd

效果立竿见影

很久以前写过一篇文章介绍如何在 linux 下的 qtcreator 自动切换输入法,当时使用的方法比较简陋,而且只支持 linux,时过境迁,虽然 qtcreator 的 fakevim 插件功能仍然有限,但我搞出来了个跨平台的自动切换方案。

qtcreator fakevim 的配置文件中增加如下内容:

1
2
3
4
5
6
7
8
9
10
" 自动切换输入法
" 需要将可执行脚本文件放到标准可执行目录下,如:/usr/local/bin/
inoremap <ESC> <ESC>:!fakevim-smartim en<CR>
nnoremap i :!fakevim-smartim re<CR>i
nnoremap I :!fakevim-smartim re<CR>I
nnoremap a :!fakevim-smartim re<CR>a
nnoremap A :!fakevim-smartim re<CR>A
nnoremap s :!fakevim-smartim re<CR>s
nnoremap S :!fakevim-smartim re<CR>S

上述配置映射了些按键,在切换输入模式前,调用可执行程序 fakevim-smartim,这是我使用 golang 编写的一个小工具,源码:https://github.com/listenerri/scripts/tree/master/bin-srcs/fakevim-smartim

有编译好的二进制可以直接下载:

将下载或自己编译好的二进制重命名为 fakevim-smartim 并放到系统的可执行程序目录下,也就是 PATH 环境变量中的任意目录。

其他依赖:

  • linux 仅支持小企鹅输入法 fcitx,且依赖可执行程序 fcitx5-remote,这是较新版本的 fcitx 包中带有的命令,如果你使用的是旧版本的 fcitx,那么对应的应该是 fcitx-remote,可以修改 fakevim-smartim 的源码,将调用命令修改为 fcitx-remote,或者直接在 fcitx-remote 命令所在位置,创建一个符号链接 ln -sf fcitx-remote fcitx5-remote(未测试)。
  • windows 依赖另一个小工具:im-select,下载地址: https://github.com/daipeihust/im-select/tree/master/win/out
  • macos 同样依赖 im-select,下载地址: https://github.com/daipeihust/im-select/tree/master/macOS/out

Cygwin

Cygwin 旨在通过一些 动态链接库(DLL),以 C 标准库的形式作为 API 兼容性层(转换为 Windows API 调用),提供一个完整的 POSIX 层,包括主流 Unix 的系统调用及库实现,以实现在 Windows 上直接编译和运行 Unix 程序,其重视兼容性优先于性能(这里的兼容性主要是 Unix 程序源码在此环境下编译时的兼容性)。

由于这样的实现方式,在 Cygwin 上开发/编译的程序,在运行时会依赖那些提供了兼容层的 DLL,也就是说需要将 Cygwin 的 DLL 随程序一起发布,才能在其他机器上运行。(这些程序不能完全算是 Windows 原生程序,因为需要 Cygwin 这个调用转换层。)(同时也由于需要转换层,所以说程序性能一般)

MingGW

MinGW 由 Cygwin 发展而来,着重简化与性能(主要提供 GUN 编译套件 gcc g++,而不是近 Unix/Linux 体验?),基于 MinGW 的程序是直接调用 Windows API 编译的(特指 C 语言?其他语言仍然依赖 GUN 运行时库?比如 libstdc++),得到的程序算是完全 Windows 原生的。

但 MinGW 只提供了 Win32 的 API,且不提供 POSIX API,所以大多数 GNU 软件无法在不修改源代码的情况下用 MinGW 编译。MinGW 也在 Linux 下提供了用于交叉编译到 Windows 的环境。

Mingw-w64

Mingw-w64 是从 MinGW 分支出来的,主要用于扩展 MinGW 的特性,比如提供 64 位的开发、Win64 API、pthreads,但是同样不提供完整的 POSIX 兼容。

很多 Windows 下的 IDE 使用其作为默认的编译套件/工具链,比如 Clion、QtCreator 等等(这些 IDE 说什么也是不可能在内部打包一套微软的编译套件的)。

MSYS2

阅读全文 »

msys2 为 windows 平台提供了大量 unix 命令,其基于 cygwin 同时带来了更多更强大的特性,是个很赞的项目。

msys2 默认提供了几个不同的环境:

  • msys
  • mingw64
  • mingw32
  • ucrt64
  • clang64
  • clangarm64

其中 msys 环境是其他几个环境共用的基础环境,出于兼容性考虑,我一般使用 mingw64 环境,安装新的软件包时,也是找 mingw-w64-x86_64- 开头的包,这个软件包的前缀很重要,如果在 mingw64 环境下,安装了 mingw-w64-ucrt-x86_64- 开头的包,是找不到新装包的命令的,必须要到 ucrt64 环境下才能找到。

关于各个环境的主要区别,可以阅读官方文档:https://www.msys2.org/docs/environments/

书回正传,这里先假设我的 windows 账户名称是 ri,那么我在 windows 系统中的账户主文件夹/主目录会是 C:\Users\ri 目录。后续出现 ri 时,请注意替换理解。

msys2 使用了自己的 unix 风格目录结构,其 unix 根目录 / 默认是 msys2 在 windows 下载的安装目录,如果安装 msys2 过程中没有主动修改的话,那就是 C:\msys64 目录。

另外,msys2 会把 windows 不同的本地磁盘,以盘符为名字,“挂载”到 unix 根目录 / 下,比如 windows 的 C 盘,会被挂载在 unix 的 /c 目录上,windows D 盘同理。

而 msys2 默认的 unix 家(HOME)目录则是 /home/ri,注意这里的 /home/ri 并不是 C:\Users\ri 目录。

启动 mingw64 环境时,默认是从 unix 家目录启动的,这对我来说有些不方便,我希望每次启动后所在目录都是 windows 账户的主目录 C:\Users\ri。同时还要保留 msys2 的 unix 家目录,以便将 msys2 相关的各种配置文件,独立放置在 msys2 自己的目录体系内。

阅读全文 »