
Windows 上折腾 CMU15445 这门数据库系统课的实验环境是很多人入门阶段第一个真正的拦路虎。课程官方给的是 Linux 环境下的构建流程而大部分人的主力机是 Windows直接硬上要么编译工具链对不上要么某些系统调用行为不一致跑着跑着就卡在莫名其妙的链接错误上。我前后在几台不同配置的机器上搭过这套环境也帮身边同学远程排查过不少次最后稳定下来的方案就是走 WSL 这条路——在 Windows 11 里跑一个轻量的 Linux 子系统把课程需要的编译工具链、依赖库、调试器全部装进去Windows 这边只负责编辑代码和开终端。这篇内容面向的是准备做 2024Spring 版本实验、但卡在环境搭建这一步的同学不管你之前有没有用过 WSL、熟不熟悉 CMake都能照着走通。我会把每一步为什么这么做讲清楚把那些官方文档里不会写、但实际一操作就会撞上的坑提前摆出来包括 WSL 版本选择、目录放在哪、依赖装哪些、编译报错怎么定位这些具体问题。整套流程走完你应该能得到一个能正常cmake、能跑单元测试、能挂调试器的开发环境后面专心写 Buffer Pool 和 B 树就行。1. 为什么这套实验在 Windows 原生环境下容易翻车先把这件事的底层逻辑说清楚不然很多人会误以为是自己的操作问题。CMU15445 的实验代码是一套 C 工程依赖 CMake 构建、Google Test 做单元测试、还有若干第三方库比如用于 JSON 解析的库、用于并发的组件。这套东西的设计前提就是 POSIX 环境也就是类 Unix 系统的那套文件、进程、线程语义。Windows 原生环境的问题不在于能不能装编译器而在于语义差异。举个最典型的例子课程代码里大量使用std::thread、互斥锁、条件变量来做并发控制这些在 Linux 上的 pthread 实现和 Windows 上的线程模型虽然标准库层面做了抽象但底层行为、调度时机、甚至某些边界条件下的表现并不完全一致。你在 Windows 上跑单元测试可能出现 Linux 上根本不会出现的偶发失败排查起来极其痛苦因为你分不清是自己代码写错了还是平台差异导致的。再一个是构建工具链。课程用的 CMake 脚本里有一些平台相关的判断逻辑默认按 Linux 的路径和库名去找依赖。你在 Windows 上用 MSVC 或者 MinGW 去编译很可能在find_package或者链接阶段就报找不到库。有人试过用 Visual Studio 直接打开结果 CMake 配置阶段就一堆警告和错误改起来没完没了。还有一个容易被忽略的点文件系统大小写敏感性。Linux 是大小写敏感的Windows 默认不敏感。课程代码里如果有#include buffer_pool_manager.h而实际文件名大小写有出入在 Windows 上可能蒙混过关但换到评测环境就炸。这种问题在本地开发时完全看不出来属于埋雷。所以结论很明确与其在 Windows 原生环境里跟工具链和语义差异搏斗不如直接用一个真正的 Linux 环境。WSL 就是成本最低的选择——不用装双系统、不用开虚拟机吃内存Windows 11 上的 WSL2 底层是轻量虚拟机性能和兼容性都够用文件互通也方便。提示不要试图用 Cygwin 或者 MSYS2 来凑合。这两个方案确实能提供一部分 POSIX 兼容层但面对课程这种涉及线程、内存管理、系统调用的工程兼容层的坑比 WSL 多得多而且社区里几乎没有针对这套实验的 Cygwin 经验可参考出问题只能自己啃。1.1 WSL1 和 WSL2 到底选哪个这是个必须一开始就定下来的决策因为两者切换虽然不算麻烦但装好的依赖和配置在切换后可能需要重来。WSL1 是系统调用翻译层文件系统直接走 Windows 的跨系统访问文件快但系统调用兼容性不完整WSL2 是真正的轻量虚拟机跑的是完整 Linux 内核兼容性几乎和原生 Linux 一致但跨系统访问文件尤其是从 Linux 侧访问 Windows 盘符下的文件性能较差。对这套实验来说答案很明确选 WSL2。原因在于课程代码涉及大量并发和内存操作WSL1 的翻译层在某些系统调用上行为不完整可能出现难以定位的运行时问题。而 WSL2 的完整内核能保证 pthread、内存映射这些机制的行为和原生 Linux 一致单元测试结果可信。代价是文件访问性能。这里引出一个非常关键的实操建议代码一定要放在 WSL 自己的文件系统里不要放在 Windows 的盘符下。很多人习惯在C:\Users\xxx\projects下建工程然后从 WSL 里访问/mnt/c/...这样编译速度会慢到让你怀疑人生——因为每次文件读写都要跨虚拟机边界。正确做法是在 WSL 的家目录下建工程比如~/cmu15445编译速度能差出好几倍。1.2 环境搭建的整体路线图在动手之前先把整条链路理清楚避免东一榔头西一棒子。整体分四步第一步在 Windows 侧启用 WSL 并装好一个 Linux 发行版第二步进入 Linux 环境更新包管理器、装编译工具链和依赖库第三步拉取课程代码配置 CMake 并完成首次构建第四步验证环境跑通单元测试、确认调试器可用。这四步里第一步和第二步是纯环境准备第三步和第四步才是真正验证这套环境能不能支撑后续实验。很多人卡在第三步的 CMake 配置或者编译报错上本质是第二步的依赖没装全。所以下面我会把依赖清单列清楚并且解释每个依赖是干什么用的这样你遇到报错时能自己判断缺了什么。2. Windows 侧启用 WSL 与安装发行版的具体操作Windows 11 上启用 WSL 比早期版本简单很多一条命令基本搞定但有几个前置条件需要确认。首先确认你的系统版本WSL2 要求 Windows 10 版本 1903 以上或者 Windows 11这个一般没问题。其次确认 CPU 虚拟化在 BIOS 里是开着的WSL2 依赖虚拟化功能如果 BIOS 里关了装完也启动不了。最省事的做法是直接用管理员权限打开 PowerShell执行安装命令。这里有个细节早期需要手动启用适用于 Linux 的 Windows 子系统和虚拟机平台两个可选功能然后重启再设置默认版本为 WSL2。较新的 Windows 11 版本已经把这一步合并了一条命令会自动处理启用功能和安装默认发行版。wsl --install执行完这条命令系统会自动启用所需功能、下载并安装 WSL2 内核、装一个默认的 Ubuntu 发行版然后提示你重启。重启后首次进入会要求设置 Linux 用户名和密码这个用户名密码是 Linux 系统内部的和 Windows 账户无关记好就行。如果你已经装过 WSL 但不确定版本或者想手动控制装哪个发行版可以用下面这组命令。先看当前状态wsl --status wsl --list --verbose--list --verbose会列出已安装的发行版和它们各自跑的 WSL 版本。如果某个发行版显示 VERSION 是 1需要转成 2wsl --set-version Ubuntu 2转换过程会花几分钟取决于发行版大小。转完之后再--list --verbose确认一下。另外建议把默认版本设成 2这样以后新装的发行版默认就是 WSL2wsl --set-default-version 22.1 发行版选择与首次进入的注意事项默认装的是 Ubuntu对这套实验来说完全够用不需要纠结 Debian、Fedora 之类的。Ubuntu 的包管理生态最成熟网上遇到问题搜到的解决方案也最多。版本上较新的 LTS 版本都行编译器版本足够支撑课程代码的 C 标准要求。首次进入 Linux 环境先做两件事更新包索引、确认基础工具在。更新这一步别省很多人装完就直接装依赖结果因为索引太旧导致某些包版本对不上。sudo apt update sudo apt upgrade -yapt upgrade可能会提示有一些服务需要重启或者配置文件冲突一般直接回车用默认选项即可。升级完确认一下几个基础工具gcc --version g --version make --version cmake --version如果cmake提示找不到说明还没装后面装依赖时会一起处理。这里先确认 gcc/g 在因为 Ubuntu 一般自带。如果连 gcc 都没有说明这个发行版比较精简需要手动装build-essential。注意WSL 里的 Linux 用户默认不是 root装软件要用sudo。如果你在首次设置时跳过了密码设置某些情况下会出现可以用sudo passwd重新设一个否则后面所有sudo操作都会卡住。2.2 目录规划工程放哪里决定了你的编译体验前面提过代码要放在 WSL 文件系统里这里具体说怎么放。WSL 的家目录在 Windows 侧可以通过资源管理器访问路径大概是\\wsl$\Ubuntu\home\你的用户名所以放在家目录下并不影响你用 Windows 的编辑器打开。推荐在 home 下建一个专门放课程的目录mkdir -p ~/projects cd ~/projects后续所有工程都放这个目录下。这样做的另一个好处是备份和迁移方便——WSL 的整个文件系统可以导出成镜像换机器时直接导入环境原样带过去。反过来说如果你非要把工程放在/mnt/c/...下编译时会明显感觉到慢尤其是这套实验代码量不小、模板和头文件依赖多增量编译的差距会非常明显。我实测过同一个工程在两个位置的完整构建时间差距能到数倍日常开发体验完全不是一个级别。3. Linux 侧编译工具链与依赖库的完整清单这一步是整个环境搭建的核心依赖装不全后面 CMake 配置和编译阶段会以各种看似不相关的报错形式暴露出来。我按必需和建议两档来列并且说明每个东西的作用方便你按报错反查。先装编译工具链和构建工具sudo apt install -y build-essential cmake gitbuild-essential是个元包包含 gcc、g、make、libc 开发头文件等一整套基础编译环境。cmake是构建系统课程工程用它来生成 Makefile。git用来拉代码虽然你也可以在 Windows 侧拉好再拷进来但直接在 Linux 里操作更顺。然后是这套实验会用到的第三方依赖。课程代码依赖几个库最典型的是用于 JSON 解析的库和用于测试的 Google Test。Google Test 课程工程一般会通过 CMake 的 FetchContent 或者子模块方式引入但有些版本需要系统里预装。稳妥起见把常见的开发库都装上sudo apt install -y libssl-dev libreadline-dev zlib1g-devlibssl-dev提供加密库的开发头文件某些依赖会用到libreadline-dev和zlib1g-dev属于常见的基础库装上避免链接阶段报缺库。这几个包体积不大装了不亏。调试工具单独说。这套实验后期排查内存问题和并发问题调试器几乎是必需品sudo apt install -y gdb valgrindgdb是标准调试器配合 VS Code 的调试插件可以图形化断点调试。valgrind用来检测内存泄漏和越界访问Buffer Pool 那部分实验里非常有用——你管理的内存页如果释放逻辑写错valgrind 能直接指出来。3.1 依赖装完后如何验证工具链完整装完不要急着拉代码先做一轮验证把问题挡在编译之前。验证分三层编译器能不能正常编译一个带线程的 C 程序、CMake 能不能正常配置一个最小工程、调试器能不能挂上。第一层写个最小测试程序验证编译器和线程库cat /tmp/test_thread.cpp EOF #include iostream #include thread #include vector int main() { std::vectorstd::thread threads; int counter 0; for (int i 0; i 4; i) { threads.emplace_back([counter]() { for (int j 0; j 1000; j) counter; }); } for (auto t : threads) t.join(); std::cout counter counter std::endl; return 0; } EOF g -stdc17 -pthread /tmp/test_thread.cpp -o /tmp/test_thread /tmp/test_thread能编译通过并输出结果说明编译器和 pthread 链接没问题。注意-pthread这个参数课程工程的 CMake 里会处理线程库链接但你手动测试时要带上否则会报std::thread相关的链接错误。第二层验证 CMake 能跑通一个最小工程。这个其实拉完课程代码配置时自然会验证但如果你想提前确认 CMake 本身没问题可以建个空目录放个最小CMakeLists.txt试一下。第三层验证调试器随便编译一个带-g的程序用 gdb 跑一下看能不能断住这个后面结合 VS Code 配置时再细说。3.2 一个容易被忽略的坑默认编译器和 C 标准课程代码用的是较新的 C 标准C17 及以上而某些 Ubuntu 版本自带的 g 版本偏老可能对某些特性支持不完整。装完build-essential后确认一下 g 版本g --version如果版本低于 9建议装个更新的版本。Ubuntu 的软件源里通常有多个 g 版本可选比如g-11、g-12。装完之后可以用update-alternatives切换默认版本或者直接在 CMake 配置时指定编译器。sudo apt install -y g-12 sudo update-alternatives --install /usr/bin/g g /usr/bin/g-12 100这里update-alternatives的最后一个数字是优先级数字大的优先。装多个版本时可以用--config交互式切换sudo update-alternatives --config g提示不要盲目追最新版本。课程代码在某个特定编译器版本上验证过太新的编译器有时会因为更严格的标准检查报出一些警告甚至错误。如果遇到莫名其妙的编译错误先确认是不是编译器版本太新导致的必要时降回课程推荐的版本区间。4. 拉取课程代码与首次 CMake 构建代码来源这里不展开具体地址你按课程官方渠道获取即可。拿到代码后放进前面建好的~/projects目录。进入工程根目录先看一眼目录结构确认有CMakeLists.txt和各个实验模块的子目录。首次构建的标准流程是建一个独立的构建目录然后在里面跑 CMake。这个习惯一定要养成——不要在源码目录里直接cmake .那样生成的文件会和源码混在一起清理起来麻烦而且容易污染 git 状态。cd ~/projects/你的工程目录 mkdir -p build cd build cmake ..cmake ..这一步会做依赖检查、生成构建文件。如果前面依赖装全了这一步应该顺利通过输出一堆配置信息。如果报错重点看报错信息里提到的找不到的包或者变量名基本能对应到缺哪个依赖。配置成功后开始编译make -j$(nproc)-j$(nproc)表示用所有可用核心并行编译能显著加快速度。nproc会输出当前环境的 CPU 核心数。WSL2 默认能用上宿主机的核心所以并行度一般够。4.1 CMake 配置阶段常见报错与对应处理配置阶段最常见的几类报错我按出现频率排一下并给出处理思路。第一类是找不到某个包报错形如Could NOT find XXX。这种直接对应到缺依赖用apt install装对应的-dev包即可。关键是看懂包名——CMake 报的包名和 apt 的包名不一定完全一致比如 CMake 找OpenSSLapt 里是libssl-dev。遇到不认识的搜一下CMake 包名 apt 安装基本能找到对应关系。第二类是编译器版本或者标准不满足报错里会提到C standard或者某个特性不支持。处理方式是升级编译器或者调整 CMake 里的标准设置。课程工程一般会在CMakeLists.txt里指定标准如果它指定的标准你的编译器不支持就得升级编译器。第三类是路径或者权限问题比如某个目录不可写、某个文件找不到。这种多半是构建目录建错了位置或者从 Windows 侧拷贝文件时权限丢了。WSL 里从/mnt/c拷过来的文件可能带着奇怪的权限必要时用chmod修一下或者干脆在 Linux 侧重新拉一份代码。4.2 编译阶段的链接错误定位思路配置通过但编译报错最常见的是链接错误报错形如undefined reference to XXX。这类错误的本质是编译器找到了函数声明头文件里有但链接阶段找不到函数实现对应的库没链上或者库路径不对。定位思路分三步。第一步看报错里提到的符号属于哪个库比如pthread_create属于 pthread 库SSL_new属于 OpenSSL。第二步确认这个库在系统里装了没有用dpkg -l | grep 库名查。第三步确认 CMake 里有没有正确链接这个库看CMakeLists.txt里的target_link_libraries。课程工程一般已经把链接配置写好了如果你遇到链接错误大概率是系统缺库而不是工程配置问题。少数情况下是编译器版本差异导致某些符号的 mangling 不一致这种比较难搞通常换回课程推荐的编译器版本能解决。5. 编辑器与调试器的打通在 Windows 侧高效开发环境搭好之后日常开发的体验取决于编辑器配置。推荐用 VS Code 配合 WSL 插件这样你在 Windows 侧用熟悉的界面编辑代码实际的文件操作和终端都在 WSL 里兼顾体验和性能。装好 VS Code 后安装 WSL 扩展。然后在 WSL 终端里进入工程目录执行code .VS Code 会自动以远程模式打开这个目录左下角会显示当前连的是 WSL 环境。这时候你装的 C 扩展、CMake 扩展都会在 WSL 侧生效代码补全、跳转、格式化都能正常工作。调试配置是重点。VS Code 的 C 调试需要launch.json针对这套实验关键是让调试器能找到编译出来的可执行文件、能正确设置工作目录因为单元测试可能依赖相对路径的资源文件。一个可用的配置大概长这样{ version: 0.2.0, configurations: [ { name: Debug Test, type: cppdbg, request: launch, program: ${workspaceFolder}/build/test/你的测试可执行文件名, args: [], stopAtEntry: false, cwd: ${workspaceFolder}, environment: [], externalConsole: false, MIMode: gdb, setupCommands: [ { description: Enable pretty-printing for gdb, text: -enable-pretty-printing, ignoreFailures: true } ] } ] }program指向编译出来的测试可执行文件具体路径看你工程的构建输出位置。cwd设成工程根目录这样测试里用相对路径找资源文件时不会出错。-enable-pretty-printing让 gdb 输出 STL 容器时更可读调试时看std::vector、std::unordered_map的内容会方便很多。5.1 用 valgrind 排查内存问题的实操方式Buffer Pool 那部分实验里内存管理是重头戏页的分配、回收、替换逻辑写错很容易造成泄漏或者越界。valgrind 是排查这类问题的利器但用法有讲究。基本用法是直接用它跑你的测试程序valgrind --leak-checkfull --show-leak-kindsall ./build/test/你的测试可执行文件--leak-checkfull会详细报告每一处泄漏的调用栈--show-leak-kindsall把各种类型的泄漏都列出来。输出里重点看definitely lost和invalid read/write这两类——前者是确定的内存泄漏后者是越界访问都是必须修的。有个细节要注意valgrind 跑起来比正常执行慢很多所以不要拿它跑整个测试套件挑单个失败的测试用例跑就行。另外 valgrind 对多线程程序的检测能力有限数据竞争这类问题它不一定能抓到那种情况需要别的工具或者靠代码审查。注意valgrind 报告里有时会出现一些来自系统库或者第三方库的泄漏这些不是你代码的问题。判断方法是看调用栈如果栈顶是你的代码那就是你的问题如果整条栈都在系统库里可以忽略。5.2 单元测试的跑法与失败定位课程工程用 Google Test编译后会生成测试可执行文件。跑全部测试直接执行那个文件即可但更常用的是按测试套件或者单个用例过滤尤其是调试阶段。# 跑全部 ./build/test/测试可执行文件 # 只跑某个测试套件 ./build/test/测试可执行文件 --gtest_filterBufferPoolManagerTest.* # 跑单个用例 ./build/test/测试可执行文件 --gtest_filterBufferPoolManagerTest.SampleTest--gtest_filter支持通配符用起来很灵活。测试失败时输出会告诉你哪个断言挂了、期望值和实际值分别是什么结合调试器断到失败的那一行基本能定位问题。一个实操心得测试失败时先别急着改代码先确认这个失败是不是稳定复现。并发相关的测试有时会因为调度时机偶发失败多跑几次看是否稳定。如果时好时坏大概率是并发逻辑有问题而不是测试本身的问题。6. 环境稳定性维护与几个反复踩到的坑环境搭好只是开始用一段时间后可能会遇到一些退化问题这里集中说几个我反复踩到的。第一个是 WSL 磁盘占用越来越大。WSL2 的虚拟磁盘文件会随着使用增长但删除文件后它不会自动收缩。时间长了可能占掉几十 GB。处理方式是定期手动压缩先关掉 WSL然后用 Windows 侧的 diskpart 或者 PowerShell 命令压缩虚拟磁盘。具体命令网上能查到核心是先wsl --shutdown再对对应的 vhdx 文件做 compact 操作。第二个是依赖库版本漂移。Ubuntu 的软件源会更新某次apt upgrade之后某个库的大版本变了可能导致原本能编译的工程突然报错。避免方式是在环境稳定后不要频繁做全量升级尤其是临近实验截止日期时。如果确实需要升级先在一个副本环境里验证。第三个是文件权限问题。从 Windows 侧往 WSL 里拷文件或者用某些工具同步文件可能导致文件权限变成所有人可读写甚至可执行git 会把这些权限变化当成修改。处理方式是配置 git 忽略文件模式变化git config core.fileMode false这样 git 就不再把权限变化当成文件修改避免一堆莫名其妙的 diff。6.1 编译缓存与增量构建的清理策略CMake 工程的增量构建有时会因为缓存不一致出问题典型表现是改了代码但编译结果没变或者报一些奇怪的文件已存在错误。遇到这种情况最干脆的办法是清掉构建目录重新来rm -rf build mkdir build cd build cmake .. make -j$(nproc)这套操作虽然要重新全量编译但能排除掉绝大多数缓存导致的问题。日常开发中如果只是改了源码正常make增量编译就行不需要每次清缓存。判断标准是如果增量编译的结果和预期不符或者报错信息明显不合理就清缓存重来。另外 CMake 有个--fresh参数较新版本支持可以在保留构建目录的同时重新配置比整个删掉温和一些。不过对这套实验来说构建目录里没什么需要保留的东西直接删了重建最省心。6.2 内存与 CPU 资源分配的调整WSL2 默认会占用宿主机相当一部分内存如果宿主机内存不大可能影响 Windows 侧的日常使用。可以在 Windows 用户目录下建一个.wslconfig文件来限制资源[wsl2] memory8GB processors4 swap2GBmemory限制 WSL 最多用多少内存processors限制用几个核心swap是交换空间大小。改完这个文件后要wsl --shutdown再重新进入才生效。对这套实验来说8GB 内存和 4 个核心一般够用编译和跑测试都不会太吃力。如果你的机器配置高可以适当放宽。这里有个权衡限制太紧会导致编译慢、测试跑不动限制太松又影响宿主机。建议先按默认跑观察一下任务管理器里 WSL 的内存占用再决定要不要限制。编译高峰期内存占用会明显上升留够余量比较稳妥。7. 从环境搭建到实验推进的衔接建议环境通了之后建议先别急着写实验代码花点时间把工程结构和构建流程摸熟。具体做法是找到每个实验模块对应的目录看看里面的头文件和源文件组织方式跑一遍该模块的单元测试确认测试框架能正常工作。这一步能帮你建立对工程的整体认知后面写代码时知道该改哪个文件、测试怎么加。另外建议在开始每个实验前先完整跑一遍该实验的测试记录下初始通过率。课程工程通常会给一些未实现的接口留空初始状态下部分测试是失败的这是正常的。你后续的工作就是让这些失败的测试逐个通过。有了初始基线你就能清楚知道自己改动的效果。调试习惯上我个人的经验是遇到测试失败先用--gtest_filter单独跑那个用例确认能稳定复现然后挂调试器断到失败点看变量状态如果涉及内存问题再上 valgrind。这个顺序能覆盖绝大多数问题比盲目改代码试错高效得多。最后说一个心态上的事。环境搭建这一步看起来繁琐但它其实是整个实验过程中最标准化的部分——所有问题都有确定的解法不像后面的算法设计那样需要反复权衡。把这一步走扎实后面写代码时就不会被环境问题干扰能专心在数据结构和管理策略上。我见过不少人环境没搭利索就急着写代码结果每次编译报错都分不清是环境问题还是代码问题白白浪费大量时间。磨刀不误砍柴工这话在这套实验上体现得特别明显。