65.9K
CodeProject 正在变化。 阅读更多。
Home

将大型项目从 Windows 移植到 Linux。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (19投票s)

2022年7月4日

GPL3

14分钟阅读

viewsIcon

18919

downloadIcon

245

功能类似,差异细微,编译器众多……

引言

要将软件移植到新平台,强烈建议使用一个抽象层,将平台特定头文件的使用限制在少数几个 .cpp 文件中。然后可以为这些 .cpp 文件开发并行版本以支持新平台,而无需更改软件的其余部分。实现这些 .cpp 文件需要为已支持的平台找到对应的平台特定函数。然而,抽象层也可能需要演进,以处理平台之间的细微差异。并且,新平台可能需要使用新的编译器,这也可能意味着必须使用新的构建工具。

背景

本文讨论的代码取自健壮服务核心(RSC)。如果您是第一次阅读有关 RSC 的文章,请花几分钟阅读这篇前言

平台抽象层

RSC 是在 Windows 上开发的,但目标是最终也能支持 Linux。因此,从一开始就定义了一个平台抽象层,以便将特定于 Windows 的头文件限制在一组有限的文件中使用。Windows 的目标实现被放在 *.win.cpp 文件中,总计约 3.5K 行代码,在移植开始时不到项目的 1.5%。该抽象层由以下头文件组成,移植工作需要为它们开发 Linux 目标实现:

标题 描述
SysConsole.h 控制台函数
SysFile.h 目录函数
SysHeap.h 默认 C++ 堆的 Heap 子类
SysLock.h 低开销互斥锁
SysMemory.h 内存分配与保护
SysMutex.h 互斥锁
SysSignals.h POSIX 信号
SysThread.h 线程
SysThreadStack.h 堆栈跟踪
SysTickTimer.h 高精度时钟
SysTime.h 日历时钟
SysIpL2Addr.h IP 第 2 层地址和函数
SysIpL3Addr.h IP 第 3 层地址和函数
SysSocket.h 套接字函数
SysTcpSocket.h TCP 套接字函数
SysUdpSocket.h UDP 套接字函数
RscLauncher.h 失败时自动重启

Linux 的目标实现被放在 *.linux.cpp 文件中,首先开发的是 IP 网络部分。这相当直接,因为 Windows 和 Linux 在套接字类型和函数方面非常相似。但它们报告错误的方式不同,这导致了一些重构,改进了 RSC 的套接字错误报告,而这方面无论如何都需要改进。

接着,工作转向了内核函数。RSC 最初开发时,C++11 尚未问世。到移植开始时,编译器已经实现了 C++20。但正是 C++11 增加了大部分可用于移植的头文件。与其实现 Linux 的目标实现,不如直接用 C++11 中已经处理了平台差异的头文件来取代部分平台抽象层。为此,采用了以下 C++ 库头文件:

标题 描述
<chrono> 高精度时钟和日历时钟
<condition_variable> 允许线程等待直到收到通知或超时
<filesystem> 目录函数 (C++17)
<mutex> 互斥锁
<ratio> <chrono> 使用
<thread> 线程

采用 <chrono> 也让 RSC 能够移除 Duration.hTimePoint.h。尽管这两个文件都与平台无关,但它们支持时间间隔和时间戳的方式与 <chrono> 不兼容。

<chrono> 不同,<thread> 几乎没有减少代码量。它可以创建线程,但无法指定它们的堆栈大小。它的线程标识符是不透明的,并且它提供了 join 函数,这在玩具示例中很流行,但在其他地方用途有限。不过,它的 sleep_for 函数证明了其用处。创建一个线程对象时的一个主要问题是,底层线程通常在其线程对象完全构造好之前就开始运行,因为它不知道正在创建一个对象来包装它。RSC 有一个功能丰富的 Thread 类,包含一个 Pause 函数,但在 Thread 对象构造完成之前无法使用。之后,应该只使用 Pause,而不要使用 sleep_for。但是 sleep_for 允许一个线程短暂休眠,直到其 Thread 对象完全构造好之后再继续执行。

在采用新的 C++ 库头文件后,平台适配层的内核部分看起来是这样的:

标题 描述
SysConsole.h 保留
SysFile.h 由使用 <filesystem>FileSystem.h 替代
SysHeap.h 保留
SysLock.h 由使用 <mutex>Lock.h 替代(此头文件后来被删除)
SysMemory.h 保留
SysMutex.h 由使用 <mutex>Mutex.h 替代
SysSignals.h 保留
SysThread.h 保留,但部分被使用 <condition_variable><mutex>Gate.h 替代
SysThreadStack.h 重命名为 SysStackTrace.h,因为 C++23 将引入 <stacktrace>
SysTickTimer.h <chrono> 和简单的 SteadyTime.h 替代
SysTime.h <chrono> 和简单的 SystemTime.h 替代
RscLauncher.h 保留

静态分析工具

第一个问题是 RSC 有一个C++ 静态分析工具,经常用来清理代码。但它不仅仅是报告问题,还提供了一个编辑器,可以交互式地修复三分之二的问题。这变得非常有用,以至于该工具必须支持 RSC 使用的任何 C++ 语言特性。这就成了问题,因为该工具在很大程度上是一个 C++ 编译器,而支持一个完全兼容的 C++ 编译器所处理的所有语言特性是不现实的。

然而,采用 <chrono> 迫使该工具进化。问题与其说是 <chrono> 本身,不如说是它对 <ratio> 的使用,后者是一个模板元编程的“老鼠窝”,在编译时将一个比率转换为另一个。

ratio 使用整型字面量作为模板参数,这是该工具不支持的。工具希望模板参数是一个“类型”。它用 TypeSpec 表示一个类型,这是一个抽象类,其派生类有 DataSpec(几乎所有类型)和 FuncSpec(函数类型)。模板名称由 TypeName 表示,它有一个 vector 存放其后的模板参数 TypeSpec。然而,字面量位于类层次结构的另一个分支,在 CxxToken 之下。因此,创建了一个 TemplateArg 类。它包装一个 TypeSpec 或一个 Expression(可以是一个整型字面量),并且其几乎所有函数都简单地转发给它包装的这两个类之一。

接下来,工具需要知道 std::ratio 的一个实例可以转换为另一个实例。它已经支持类型转换、转换构造函数和转换运算符。但这些都不适用于两个不同的 ratio,它们是两个不同的模板实例。为解决此问题,修改了 StackArg::MatchWith,使其在处理 std::ratio 实例时剥离模板参数,以检测可转换性。

最后,用于处理 POSIX 信号的 Linux 目标实现需要使用 sigaction 来注册其信号处理程序。这为处理程序提供了有关信号的附加信息,以便在调试时,当有问题的代码写入一个有效但写保护的地址时,能将 SIGSEGV 映射为 RSC 专有的 SIGWRITE,从而简化调试。然而,某个有特殊需求的开发者也用 sigaction 命名了一个传递给 sigaction 函数的 struct。这意味着工具必须支持详述类型说明符:在类型名前加上关键字 classstructunionenum,以区别于其他同名事物,例如函数。支持这个相当容易:修改了 Parser::GetTypeSpec,使其在类型名前查找这些关键字之一。

改进工具的过程中也发现了一些需要纠正的错误和奇怪行为。最终,花在增强工具上的时间几乎和实际移植到 Linux 的时间一样多。

Windows 和 Linux 之间的差异

在移植过程中,必须解决 Linux 和 Windows 之间的各种细微差异。

文本文件。 如果在 Linux 上读取在 Windows 上创建的文本文件,会出现问题。Windows 在行尾使用 \r\n (CRLF),而 Linux 只使用 \n (LF)。因此,添加了 FileSystem::GetLine 函数,其签名与 std::getline 相同,用于移除 \r,否则当 Linux 读取在 Windows 上创建的文件时,std::getline 返回的每个字符串末尾都会出现 \r

目录分隔符。 Linux 使用 / 来分隔路径中的目录,而 Windows 使用 \。因此,常量 PATH_SEPARATOR 使用 std::filesystem::path::preferred_separator 进行初始化,该函数知道自己正在运行的平台。

。Windows 允许你创建与默认 C++ 堆功能相同的附加堆。然而,Linux 不支持附加堆。RSC 曾使用 Windows 的堆接口来创建附加堆,但它也开发了自己的 BuddyHeap 来支持写保护内存,因为 Windows 堆如果被写保护会崩溃。因此,移植到 Linux 意味着所有附加堆都必须使用 BuddyHeap。然而,BuddyHeap 的大小是固定的,只能通过重启来扩展,这对于其中一个堆来说是不可接受的。因此,RSC 必须实现可扩展的 SlabHeap 来补充 BuddyHeap

POSIX 信号。C++ 标准定义了 SIGABRTSIGFPESIGILLSIGINTSIGSEGVSIGTERM。Windows 增加了与 SIGINT 类似的 SIGBREAK,而 Linux 增加了与 SIGSEGV 类似的 SIGBUS

Gate.h。它提供一个对象,线程可以在其上等待,直到被通知或超时。它取代了 Windows 的 CreateEventWaitForSingleObjectSetEvent,其平台无关的对应物是 Gate::Gate(构造函数)、Gate::WaitForGate::Notify。尽管 Gate 的代码不多,但要正确实现它需要进行大量的研究,以及修复一些细微的错误。值得注意的是,Gate 需要三个临界区对象来实现其接口:一个 condition_variable、一个 mutex 和一个 atomic_bool

线程标识符。当一个线程退出时,RSC 不会从其 ThreadRegistry 中移除其标识符(包括原生标识符和 RSC 标识符)。它只将它们标记为 Deleted,这样调试工具仍然可以找到原生标识符,并在显示线程退出前捕获的跟踪记录时将其映射到 RSC 的标识符。这在 Windows 上运行良好,但在 Linux 上不行。当一个线程退出时,Linux 上的 POSIX 线程实现会立即将其标识符重新分配给它创建的下一个线程。因为该标识符仍在 ThreadRegistry 中,新线程会阻塞,永远等待其 Thread 对象被标记为 Constructed。必须修改代码来处理这种情况。

线程优先级。尽管 POSIX 线程支持优先级,但 Linux 调度器在标准构建中并不支持它们。这是一个问题,因为尽管 RSC 的大多数线程以相同优先级运行,但它有两个线程以较高优先级运行,以模拟健康检查和时钟中断。结果发现,这两个线程可以和其他所有线程以相同的优先级运行,并且仍然能完成它们的工作。这是因为 Windows 和 Linux 都以非常高的频率进行上下文切换,因此那些本应是高优先级的线程也能被频繁调度。

堆栈跟踪。虽然在 Linux 上捕获线程的堆栈很容易,但显示出来的函数名看起来像是受到了传输线路噪声的影响。为了与 C 兼容,C++ 编译器在将函数名添加到包含调试信息的目标文件时会对其进行“名字修饰”(mangling)。将地址映射到函数名的 Windows 函数会“反修饰”(demangle)该名称,但等效的 Linux 函数不会。必须使用一个单独的工具来完成此操作。

套接字解除阻塞。RSC 支持重启,这是系统的部分重启。在重启期间,除非线程的数据需要保留,否则线程会退出。然而,这对被阻塞的线程构成了问题。它们如何解除阻塞以便退出?没有办法解除从 cin 读取的阻塞,所以 CinThread 总是存活。对于阻塞在 recvfrom 上的 UdpIoThread 实例,在 Windows 上的方法是删除该线程的套接字,这会立即导致 recvfrom 返回错误。在 Linux 上,什么也没发生:recvfrom 继续阻塞该线程。有些人贬低 Windows,但我发现它是一个很好的操作系统。无论如何,现在必须找到一个不同的解决方案,即让 UdpIoThread 向自己的套接字发送一条消息来解除其 recvfrom 的阻塞。

类似功能的函数

如果您正在将软件从 Windows 移植到 Linux,或者反之,您通常需要找到某个函数在新平台上的等效函数。搜索“当前函数名”和“新平台名”通常会返回一个网站链接,在那里您的问题已经得到了解答。如果这不起作用,Windows 函数的文档在这里,而 Linux 函数的文档在这里。至少 Windows 会按主题对其函数进行分组,不像 Linux。看到这一切,我更加确信,一个大型操作系统应该使用面向对象的语言,这将有助于组织其庞杂的函数。

下表列出了 RSC 平台抽象层中使用的 Windows 和 Linux 的类似函数。首先是内核函数:

标题 Windows Linux
SysConsole.h SetConsoleTitle 一个奇怪的转义序列
SysHeap.h HeapAlloc
HeapSize
HeapFree
HeapValidate
malloc
malloc_usable_size
免费
mcheck(非线程安全)
SysMemory.h VirtualAlloc
VirtualFree
VirtualLock
VirtualProtect
VirtualUnlock
mmap
munmap
mlock
mprotect
munlock
SysThread.h SetPriorityClass
_beginthreadex
CloseHandle
signal
SetThreadPriority
setpriority
pthread_create
退出线程时无对应函数
sigaction(更佳)
pthread_setschedprio
SysStackTrace.h RtlCaptureStackBackTrace
SymFromAddr
SymGetLineFromAddr64
backtrace
backtrace_symbols
__cxxabiv1::__cxa_demangle
RscLauncher.h CreateProcessA
WaitForSingleObject
GetExitCodeProcess
posix_spawnp
waitpid
 

以及 IP 网络函数,它们的名称相同或非常相似:

标题 Windows Linux
SysIpL2Addr.h gethostname gethostname
SysIpL3Addr.h getaddrinfo
freeaddrinfo
getnameinfo
getaddrinfo
freeaddrinfo
getnameinfo
SysSocket.h socket
bind
getsockopt
setsockopt
ioctlsocket
closesocket
socket
bind
getsockopt
setsockopt
ioctl
close
SysTcpSocket.h connect
listen
accept
WSAPoll
getsockname
getpeername
recv
send
关闭
connect
listen
accept
poll
getsockname
getpeername
recv
send
关闭
SysUdpSocket.h recvfrom
sendto
recvfrom
sendto

使用多个编译器进行构建

在移植开始时,RSC 是使用通过 VS2022 的属性表创建的 .vcxproj 文件构建的。为了支持在多个平台上构建,Visual Studio 建议采用 CMake,而我对此一无所知。它似乎是一个流行的构建系统,有很好的文档,而且在 stackOverflow 上有很多关于它的问题和答案。然而,所有这些信息都是零散的,示例通常很简单。没有一个能解决如何将一个大型项目从 VS2022 构建迁移到 CMake 构建的问题。

幸运的是,我搜索到了一个开源工具 CMakeConverter,它可以分析 .vcxproj 文件来生成 CMake 文件。使用这个工具很简单,它很好地创建了 CMake 需要的文件。但仍有一些工作要做。RSC 有一个大多数项目共享的属性文件,而该工具不知道如何处理。但它生成的文件让我很好地了解了如何在 CMake 中构建一个大型项目。其中一个文件 GlobalSettingsInclude,显然是为了起到与那个共享属性文件相同的作用。

一旦有了整体的 CMake 框架,就可以在找到如何设置编译器和链接器选项等问题的答案后,逐步修改其文件。VS2022 也已经发展到与 CMake 集成得相当好。你不再需要从解决方案或项目文件打开 VS2022。你可以删除那些文件,直接打开包含源代码的文件夹。VS2022 随后会分析其 CMakeLists 文件来配置解决方案。点击“管理配置…”会打开一个选项卡,允许你添加新的构建配置(编译器加平台的组合),VS2022 会将其保存在一个 JSON 文件中。

没过多久就成功地进行了一次构建。这令人惊讶,因为我讨厌学习需要大量配置的工具,而且 C++ 的构建过程本身就很荒谬。另一个惊喜是 x64 构建速度快得多,因为它们现在使用 Ninja 而不是 VS2022。

现在构建可以使用 gcc 或 clang 编译器以及 MSVC。gcc 和 clang 都生成了新的编译器警告。一些通过更改代码解决,另一些则通过在 CMake 文件中添加编译器选项来抑制。然而,使用 clang 时出现了一个问题。RSC 有一套测试,以查看在发生坏事时它是否能存活。其中一个测试是除以零。如果用 MSVC 或 gcc 编译,这个测试会通过,但 clang 会进入一个无限循环,不断地除以零。在阅读了一些 Windows 文档后,我在 RSC 的 Windows 结构化异常处理程序中添加了对 _fpreset 的调用,解决了这个问题。

为了在 Linux 上测试,我选择了 WSL2 和 Ubuntu。当通过“调试”菜单启动 RSC 时,VS2022 会创建一个 Linux 控制台窗口选项卡。但 VS2022 在这方面还需要一些改进,因为它没有将所有键盘事件转发到该窗口。我发现只按 enter 键不会导致 std::getline(cin, str) 返回;enter 前必须先按一个空格。同样,ctrl-C 也没有任何作用。为了确认不是 RSC 的问题,这些操作不得不在一个真正的 Ubuntu 控制台上进行测试。

直到最近,为这次移植更改构建过程还需要付出更多的努力。但是 VS2022 已经发展到可以很好地支持 Linux 和其他 C++ 编译器。我最初以为即使我能让一切都正常工作,我最终也会得到一个降级的 IDE 体验。但这次过渡比预期的要容易得多,而且我实际上更喜欢使用 CMake。

历史

  • 2022年7月7日:扩展了关于静态分析工具的部分
  • 2022年7月4日:初始版本
© . All rights reserved.