ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ubuntu 20.04 下 gem5 构建与配置脚本实战:从 SCons 到模拟器运行

Ubuntu 20.04 下 gem5 构建与配置脚本实战:从 SCons 到模拟器运行 刚开始接触gem5的时候我差点被它的构建流程劝退。这东西不像普通软件那样./configure make就能搞定它有一套基于SCons的构建系统依赖库又多编译时间还特别长。更麻烦的是网上很多教程都是针对Ubuntu 18.04或者旧版gem5的放到Ubuntu 20.04 gem5 20.x这个组合上就会出现各种莫名其妙的问题。这篇文章就围绕“在Ubuntu-20.04上构建gem5并实现一个简单的配置脚本”这件事把环境准备、源码编译、配置脚本编写、常见问题排查全部走一遍。无论你是做体系结构研究的学生还是想在模拟器上跑自定义工作负载的工程师只要能把这套流程跑通后面做什么实验都会顺手很多。1. 环境准备Ubuntu-20.04 下的依赖安装与踩坑1.1 前置依赖不要凭感觉装包很多人拿到gem5源码第一步就是敲sudo apt install gcc g python然后发现编译到一半报错缺少头文件。其实gem5对系统环境的要求挺明确的我先说一下我实际用到的依赖清单sudo apt update sudo apt install build-essential git m4 scons zlib1g-dev \ pkg-config libprotobuf-dev protobuf-compiler \ libboost-dev libboost-system-dev libboost-filesystem-dev \ libboost-thread-dev python3-dev python3-six \ libeigen3-dev libgoogle-perftools-dev这里面有几个容易忽略的点m4是编译一些底层组件时要用的宏处理器装系统时如果选了最小化安装大概率没有。scons是gem5指定的构建工具千万别自作主张换成make或者cmake。libprotobuf-dev和protobuf-compiler必须配对安装版本不一致会在编译proto文件时报错。libboost-*那三个包处理线程和文件系统操作缺失的话编译报错信息非常隐晦。如果你是在干净的Ubuntu 20.04服务器上操作我建议先把build-essential装好再装上面那一串。因为build-essential会带来gcc、g、make等基础工具后面所有编译步骤都依赖它们。1.2 Python 版本检查3.8 是分水岭Ubuntu 20.04 自带的Python 3.8正好满足gem5 20.x版本的要求。但要注意一点系统里可能存在多个Python版本比如你之前装过Anaconda那python3命令指向的可能不是/usr/bin/python3。检查方法很简单python3 --version which python3gem5的SCons构建脚本会调用Python来解释执行SConscript文件如果它找到了错误版本的Python会在构建最开始就给出提示。我的建议是如果是纯gem5实验环境直接用系统自带Python就好不要额外安装Python 3.10或更高版本。gem5 20.x对Python新版本的支持并不友好有些API变了之后会直接报错。注意不要在gem5的构建目录里设置PYTHONHOME或PYTHONPATH环境变量这会导致Python找不到标准库报错信息看起来像系统坏了其实是环境变量干扰。2. 源码获取与构建别瞎选版本先从 scons 构建参数说起2.1 版本选择考量为什么我推荐 gem5 20.0 或 21.2gem5的版本更新非常频繁20.x和21.x的构建方式有细微差别。如果只是为了学习配置脚本和基础模拟流程我建议选择带有稳定分支标签的版本比如v20.0.0或v21.2.1。为什么这么说因为gem5的开发分支几乎每天都在变昨天能编译通过的代码今天可能就挂了。而发布版经过了完整测试至少在Ubuntu 20.04上是可以正常编译运行的。我自己用的是v21.2.1后面所有的配置文件也都是基于这一版写的。获取源码git clone https://gem5.googlesource.com/public/gem5 cd gem5 git checkout v21.2.1如果你的网络环境访问 googlesource 比较慢可以试试GitHub的镜像仓库git clone https://github.com/gem5/gem5.git cd gem5 git checkout v21.2.1两个源拿到的代码是一样的选一个方便的就行。2.2 构建流程指定 CPU 架构和编译优化等级首次构建gem5时最核心的命令是scons build/X86/gem5.opt -j$(nproc)这里有几个参数需要解释一下build/X86/gem5.opt中的X86表示目标架构。如果你只是想跑x86程序这个就够了。ARM、RISC-V等架构需要单独编译它们会生成不同的可执行文件。.opt表示优化级别。gem5有四种构建类型debug、opt、fast、prof。debug版本有完整符号和断言跑起来非常慢fast版本性能最高但没有断言检查opt是折中选择也是大多数人使用的配置。-j$(nproc)是并行编译参数。nproc返回CPU核心数如果你的机器有8个核心就相当于8个编译任务同时跑。注意内存要够每个编译任务大约占用1GB内存8核机器建议至少16GB内存。我自己第一次编译时用了-j8编译时间大概40分钟。中途如果因为内存不足出现cc1plus: out of memory错误可以降低并行度比如-j4。提示编译过程中会生成大量中间文件确保磁盘空间至少20GB。可以在编译前用df -h看一下别等磁盘满了才发现。构建完成之后会在build/X86/目录下生成gem5.opt可执行文件。可以验证一下./build/X86/gem5.opt --version如果能看到gem5的版本信息说明这一步成功了。3. 一个最小可运行的配置脚本从 hello 对象到模拟器跑通3.1 配置脚本的本质用 Python 描述模拟器硬件结构gem5的一个设计特点就是模拟器的硬件结构不是写在C代码里的而是通过Python脚本在运行时动态构建的。你写一个Python脚本创建CPU、内存、缓存等对象然后把它们连接起来gem5会理解这组Python对象并生成对应的C模拟对象最后在模拟器中执行。适合初学者的第一个实验是运行gem5自带的hello示例。在gem5/configs/example/gem5_library/x86-ubuntu-run-with-kvm.py这类文件之外gem5还提供了一个更简单的配置示例。不过那个文件依赖完整系统镜像涉及KVM等机制对新手来说太重了。我更推荐自己写一个最小的配置脚本只包含三个要素一个RiscvCPU或X86KvmCPU之类的处理器模型一块内存一个可以让CPU执行的工作负载让我先展示一个最简单的配置脚本然后逐步拆解它。3.2 手写第一个配置脚本HelloConfig.py在gem5根目录下新建my_tests/HelloConfig.pyimport m5 from m5.objects import * # 创建系统对象这是所有模拟组件的最顶层容器 system System() # 设置时钟频率和电压域 system.clk_domain SrcClockDomain() system.clk_domain.clock 1GHz system.clk_domain.voltage_domain VoltageDomain() # 设置内存 system.mem_mode timing system.mem_ranges [AddrRange(512MB)] # 创建CPU system.cpu X86TimingSimpleCPU() # 创建系统级内存总线 system.membus SystemXBar() # 连接CPU的指令和数据端口到内存总线 system.cpu.icache_port system.membus.cpu_side_ports system.cpu.dcache_port system.membus.cpu_side_ports # 创建中断控制器x86CPU必需 system.cpu.createInterruptController() # 连接内存控制器 system.mem_ctrl MemCtrl() system.mem_ctrl.dram DDR3_1600_8x8() system.mem_ctrl.dram.range system.mem_ranges[0] system.mem_ctrl.port system.membus.mem_side_ports # 创建工作负载 binary /usr/bin/hello # 创建一个进程对象 process Process() process.cmd [binary] system.cpu.workload process system.cpu.createThreads() # 实例化系统 root Root(full_systemFalse, systemsystem) # 开始模拟 m5.instantiate() print(Beginning simulation!) exit_event m5.simulate() print(Exiting tick {} because {}.format(m5.curTick(), exit_event.getCause()))这段代码看起来很长但核心逻辑只有几行system对象是gem5模拟世界的根所有组件都挂在它下面。SrcClockDomain定义CPU频率为1GHz。这里设置的是模拟时钟不是真实时钟1GHz只是模拟粒度。X86TimingSimpleCPU是gem5里最简单的CPU模型它不做流水线模拟每个指令按固定时序执行。好处是速度快、容易调试缺点是不够精确。SystemXBar是内部交叉开关用来连接CPU、内存和其他设备。MemCtrl DDR3_1600_8x8是一个时序内存模型DDR3_1600_8x8表示内存类型和时序参数这些参数直接来自真实DDR3芯片的规格。这里有一个比较关键的细节system.mem_ranges [AddrRange(512MB)]。模拟的内存大小是512MB如果你只分配128MB后面的程序跑起来可能会因为内存不足而报错。要根据工作负载的实际需求来设置。3.3 运行配置脚本的两种方式运行这个脚本有两种方式一种是直接指定脚本路径另一种是用gem5自带的configs/example/se.py作为通用入口。方式一直接运行自定义脚本./build/X86/gem5.opt my_tests/HelloConfig.py输出结果会保存在m5out/目录下其中有config.ini、config.json、stats.txt等文件。stats.txt里有完整的模拟统计信息比如运行了多少指令、多少周期、缓存命中率等。方式二使用gem5通用配置脚本./build/X86/gem5.opt configs/example/se.py --cmd/usr/bin/hello --cpu-typeTimingSimpleCPU --mem-typeDDR3_1600_8x8 --cachesse.py是gem5自带的系统调用模拟模式SE mode脚本它封装了很多配置参数适合快速测试。但它本质上也是调用了类似我们手写的那些组件理解了手写脚本再看se.py就会非常轻松。注意如果你的/usr/bin/hello不存在可以用/bin/ls或/bin/echo代替。系统调用模拟模式不需要真的启动操作系统它只是把程序的系统调用“翻译”给宿主机执行。4. 配置脚本核心机制拆解接收参数、绑定事件与内存映射4.1 Simulation 对象之间的参数传递gem5的Python配置脚本不是简单的“声明变量”它背后有一套参数绑定机制。每个C对象在Python侧都有一个对应的SimObject你设置Python属性时实际上是在初始化C对象内部的参数结构。比如system.cpu X86TimingSimpleCPU()这行代码做了两件事在Python侧实例化一个X86TimingSimpleCPU对象。在C侧生成一个对应的CPU对象并将Python对象作为它的引用。你设置system.cpu.clock 2GHz时这个字符串会被解析成时钟周期数值并传递给C对象。如果你给了一个不合法的频率值gem5会在模拟开始前给出非常详细的错误提示告诉你哪个参数不合法。这种“Python壳C核”的设计让gem5具备了很强的可扩展性。你可以写一个Python脚本在一个模拟中创建多个CPU、多块内存并随意连接它们。4.2 端口连接不要把端口接错gem5的端口连接是新手最容易出错的地方也是错误信息最难懂的地方。端口分为三类Master端口新版本叫cpu_side_ports主动发起请求的一端比如CPU的指令端口和数据端口。Slave端口新版本叫mem_side_ports接收请求的一端比如内存控制器的端口。Crossbar端口既有master也有slave的一种复合端口。在我上面的脚本里system.cpu.icache_port system.membus.cpu_side_ports system.cpu.dcache_port system.membus.cpu_side_ports system.mem_ctrl.port system.membus.mem_side_ports这三行分别把CPU的指令/数据端口连到总线的master端把内存控制器的端口连到总线的slave端。如果接反了gem5会提示“端口不兼容”或“连接无效”。从gem5 20.0开始端口命名做了一次大调整原来的master和slave改成了cpu_side_ports和mem_side_ports。网上很多老教程还在用旧名字直接套用到新版本上就会报错。遇到这种情况记住新名字就行。4.3 内存映射与工作负载加载配置脚本里最容易被忽略的是system.mem_ranges和process.cmd之间的关系。CPU执行的程序会被加载到指定的内存区域中加载地址由gem5内部决定。如果程序比较大而mem_ranges设置得太小加载时会直接报错。我建议设置成AddrRange(1GB)或AddrRange(2GB)这个不会显著拖慢模拟速度但能避免很多没必要的内存不足问题。工作负载这块process.cmd [binary]表示要运行的程序及参数。如果想传递参数给被模拟程序可以这样process.cmd [/usr/bin/python3, /path/to/your_script.py]这样gem5就会在模拟环境中运行Python解释器并让解释器执行目标脚本。不过SE模式下的系统调用翻译并不完整有些依赖特定设备驱动的程序会跑不起来。5. 常见问题与排查实录5.1 编译阶段常见报错报错1fatal error: boost/shared_ptr.hpp: No such file or directory原因缺少Boost库。解决sudo apt install libboost-dev libboost-system-dev libboost-filesystem-dev libboost-thread-dev报错2fatal error: protobuf/service.h: No such file or directory原因缺少protobuf库。解决sudo apt install libprotobuf-dev protobuf-compiler报错3scons: *** [build/X86/gem5.opt] Error 1这是通用错误结尾真正的原因在它上面的日志里。需要往上翻找到第一个error。最常见的有两种内存不足cc1plus: out of memory解决方法是降低-j并行度。磁盘空间不足编译到一半报No space left on device。报错4Python版本不对症状是SCons执行到一半报找不到m5.objects或类似的Python模块错误。解决方法是确认python3指向的是系统自带的3.8版本。5.2 模拟运行时常见报错报错5fatal: Attempted to instantiate a SimObject that was not completely constructed这个错误通常是因为漏掉了某个对象的必需参数比如创建CPU后没有设置时钟或创建中断控制器后没有连接。报错6panic: Tried to execute unrecognized instruction这说明被模拟程序包含了当前CPU模型不支持的指令。比如用X86 CPU跑ARM程序或者使用了新指令集扩展。解决方法是确认程序和CPU架构匹配。报错7模拟运行后stats.txt中指令数为0这种情况一般是工作负载没有正确加载或者CPU模型没有正确启动线程。检查脚本中是否调用了createThreads()并确认process.cmd路径正确。5.3 性能太慢怎么办gem5的模拟速度比真实硬件慢好几个数量级。TimingSimpleCPU模型大约能跑到每秒几百万条指令而真实CPU每秒是几十亿条指令。如果你要跑比较大的程序建议先把被模拟程序用-O2编译优化这样指令数会少很多同时尽量用--caches开启缓存模型因为无缓存模型在某些情况下反而更慢。另外gem5支持模拟多线程但核心模拟循环是单线程的不能在多个CPU核心上并行。唯一能加速的办法是减少模拟的细节精度比如把CPU换成KvmCPU让程序直接在宿主机上跑但这属于KVM加速模式需要额外配置。6. 从简单到实用写给新手的几个进阶建议6.1 用 gem5 自带的 example 脚本做对照当你写的配置脚本遇到问题最好的排查方法是运行gem5自带的configs/example/se.py然后对比两者生成的config.ini文件。config.ini里记录了所有SimObject的最终参数值和连接关系你可以在m5out/目录下阅读它。./build/X86/gem5.opt configs/example/se.py --cmd/bin/ls --cpu-typeTimingSimpleCPU --mem-typeDDR3_1600_8x8 --caches grep -A 20 system.cpu m5out/config.ini通过对比你可以清楚地看出自己的脚本里哪些参数没设置、哪些连接不对。6.2 从 SE 模式起步暂缓 FS 模式gem5支持两种模拟模式SESystem call Emulation模式不加载操作系统内核只模拟用户程序。系统调用被翻译给宿主机执行启动快、调试容易。FSFull System模式模拟完整操作系统需要内核镜像和磁盘镜像启动慢、配置复杂但更接近真实。初次上手的人不要一开始就碰FS模式否则会被各种“启动到一半卡死”“控制台无输出”的问题搞到怀疑人生。先把SE模式跑通再考虑FS。6.3 在配置脚本里加入统计信息与断点当你需要分析一个程序在gem5中的行为时除了看stats.txt还可以在配置脚本中添加m5.stats.dump()和m5.stats.reset()来分段统计。比如m5.instantiate() print(Running simulation...) exit_event m5.simulate(100000) # 模拟十万个周期 print(First 100k cycles done tick {}.format(m5.curTick())) m5.stats.dump() m5.stats.reset() exit_event m5.simulate(100000) # 继续模拟十万个周期 m5.stats.dump()这样可以分阶段观察程序执行行为定位性能瓶颈。7. 我踩过的最深的一个坑把配置脚本本身当成模拟对象7.1 一段痛苦的调试经历有一次我试图让gem5运行一个复杂的多核配置脚本修改了CPU核心数、缓存大小、内存通道数等参数后模拟器直接报segmentation fault。我查了很久也没找到原因最后发现居然是我在配置脚本中错误地复用了同一个缓存参数对象给多个CPU核心。gem5对同一个SimObject的复用是非常严格的如果一个对象被挂载到两个不同的父节点下模拟器就会崩溃。而且有些对象在Python侧看起来能创建两个引用实际指向的是同一个C对象。后来我把每个CPU的缓存都单独创建为一个对象实例问题就解决了。这个经验告诉我配置脚本里每个硬件组件都应该有独立的SimObject实例不要为了省代码而共用对象。7.2 版本变更带来的配置差异gem5 20.x和21.x之间有一些配置参数改了名字。比如X86TimingSimpleCPU这个类在20.x和21.x中都能用但某些内部参数可能不同。写配置脚本之前去src/目录下搜一下类定义确认参数名是否正确。搜索方法grep -r class X86TimingSimpleCPU src/cpu/simple/ grep -r createInterruptController src/cpu/这样能快速定位配置脚本中引用类的真实定义路径避免因为版本差异导致参数设置无效。7.3 后续可以扩展的方向跑通一个简单的配置脚本后接着可以做几件事在配置脚本中加入两级缓存L1 L2观察缓存参数对性能的影响。将CPU替换为MinorCPU体验流水线级模拟。尝试多核配置创建多个CPU核心并连接到共享L2缓存。使用configs/example/fs.py进入全系统模拟。每做一步都回到配置文件本身看看参数变化如何影响模拟器行为。这种“脚本驱动模拟”的方式是gem5最核心也最强大的能力——它让你不需要修改任何C代码就能构建出各种复杂的硬件模型。最后再分享一个小技巧在gem5配置脚本里多写注释。不是给编译器看的注释而是给三天后的自己看的。gem5的SimObject层级结构很深你今天觉得理所当然的写法隔几天再看可能完全不记得当时为什么这么写。留好注释能省下大量重复思考的时间。
返回列表