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,需要转成 2:
wsl --set-version Ubuntu 2转换过程会花几分钟,取决于发行版大小。转完之后再--list --verbose确认一下。另外建议把默认版本设成 2,这样以后新装的发行版默认就是 WSL2:
wsl --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::vector<std::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++ -std=c++17 -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++ 标准(C++17 及以上),而某些 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 找OpenSSL,apt 里是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-check=full --show-leak-kinds=all ./build/test/你的测试可执行文件--leak-check=full会详细报告每一处泄漏的调用栈,--show-leak-kinds=all把各种类型的泄漏都列出来。输出里重点看definitely lost和invalid read/write这两类——前者是确定的内存泄漏,后者是越界访问,都是必须修的。
有个细节要注意:valgrind 跑起来比正常执行慢很多,所以不要拿它跑整个测试套件,挑单个失败的测试用例跑就行。另外 valgrind 对多线程程序的检测能力有限,数据竞争这类问题它不一定能抓到,那种情况需要别的工具或者靠代码审查。
注意:valgrind 报告里有时会出现一些来自系统库或者第三方库的"泄漏",这些不是你代码的问题。判断方法是看调用栈,如果栈顶是你的代码,那就是你的问题;如果整条栈都在系统库里,可以忽略。
5.2 单元测试的跑法与失败定位
课程工程用 Google Test,编译后会生成测试可执行文件。跑全部测试直接执行那个文件即可,但更常用的是按测试套件或者单个用例过滤,尤其是调试阶段。
# 跑全部 ./build/test/测试可执行文件 # 只跑某个测试套件 ./build/test/测试可执行文件 --gtest_filter=BufferPoolManagerTest.* # 跑单个用例 ./build/test/测试可执行文件 --gtest_filter=BufferPoolManagerTest.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] memory=8GB processors=4 swap=2GBmemory限制 WSL 最多用多少内存,processors限制用几个核心,swap是交换空间大小。改完这个文件后要wsl --shutdown再重新进入才生效。对这套实验来说,8GB 内存和 4 个核心一般够用,编译和跑测试都不会太吃力。如果你的机器配置高,可以适当放宽。
这里有个权衡:限制太紧会导致编译慢、测试跑不动;限制太松又影响宿主机。建议先按默认跑,观察一下任务管理器里 WSL 的内存占用,再决定要不要限制。编译高峰期内存占用会明显上升,留够余量比较稳妥。
7. 从环境搭建到实验推进的衔接建议
环境通了之后,建议先别急着写实验代码,花点时间把工程结构和构建流程摸熟。具体做法是:找到每个实验模块对应的目录,看看里面的头文件和源文件组织方式,跑一遍该模块的单元测试,确认测试框架能正常工作。这一步能帮你建立对工程的整体认知,后面写代码时知道该改哪个文件、测试怎么加。
另外建议在开始每个实验前,先完整跑一遍该实验的测试,记录下初始通过率。课程工程通常会给一些未实现的接口留空,初始状态下部分测试是失败的,这是正常的。你后续的工作就是让这些失败的测试逐个通过。有了初始基线,你就能清楚知道自己改动的效果。
调试习惯上,我个人的经验是:遇到测试失败,先用--gtest_filter单独跑那个用例,确认能稳定复现;然后挂调试器断到失败点,看变量状态;如果涉及内存问题,再上 valgrind。这个顺序能覆盖绝大多数问题,比盲目改代码试错高效得多。
最后说一个心态上的事。环境搭建这一步看起来繁琐,但它其实是整个实验过程中最"标准化"的部分——所有问题都有确定的解法,不像后面的算法设计那样需要反复权衡。把这一步走扎实,后面写代码时就不会被环境问题干扰,能专心在数据结构和管理策略上。我见过不少人环境没搭利索就急着写代码,结果每次编译报错都分不清是环境问题还是代码问题,白白浪费大量时间。磨刀不误砍柴工,这话在这套实验上体现得特别明显。