1. “BMC PSL-errno”到底是个什么需求
1.1 拆开三个词:BMC、PSL、errno
先说结论:我拿到的这个项目标题只有三个词,BMC、PSL、errno,看起来像是一个不完整的零散备注。如果你做过服务器固件或平台管理相关的工作,一眼就能看出它大概率是这样一个需求:在BMC侧接收处理器服务层上报的硬件错误,把错误原因翻译成上层系统能识别的errno,并完成日志记录和主动告警。
BMC是基板管理控制器,平时服务器断电、蓝屏,你还能远程开关机和看硬件状态,靠的就是这颗独立的小系统芯片。它自己带CPU、内存、网络口和Flash,跑一个裁剪过的Linux,典型实现就是OpenBMC。BMC和主CPU之间通过LPC、I2C、PCIe、JTAG之类的方式沟通,硬件出了问题,BMC往往是最后一个还醒着的东西。
PSL按项目里的实际语境理解,是处理器服务层,位于主CPU和外设加速部件、内存子系统之间。它既要处理地址翻译、一致性协议,也要负责把链路异常、超时、访问越界这一类的底层故障捕获下来,向上生成中断或错误事件。说得直白点,PSL就是主CPU和周边的“交通警察”,它发现有车闯红灯、行人乱穿,就会吹哨子并开一张罚单。而现在我们要做的,是把这张罚单里的交通违规代码,转换成Linux程序员熟悉的errno。
errno是Linux里一套经典的系统调用错误码体系,数值很小,但含义明确。EIO是I/O错误,ENODEV是没有这个设备,ETIMEDOUT是操作超时。系统调用的返回值只要小于0,调用者去看errno就能知道失败原因。硬件错误上报到软件层时,借用errno这套语义是成本最低的做法。
1.2 为什么这个项目值得写
做BMC的人经常遇到一类尴尬:用户在主机侧跑应用,突然报I/O错误,但应用怎么都查不到底层原因。而BMC日志里明明记了一条PSL错误,却只有一串十六进制寄存器数值,没有任何人能看懂。
这个项目的价值,就是把“PSL报上来的原始错误寄存器”和“系统侧能看懂的errno”桥接起来。做完之后,业务侧看到ENODEV,可以去BMC里查到对应的PSL链路状态,固件开发和运维也能用同一套错误码沟通,不用再拿着八位十六进制数互相猜。这类工作在数据中心可维护性、故障定位效率上,价值非常直接。
1.3 目标读者与前置技能
这篇内容主要写给两类人。第一类是BMC/固件开发工程师,尤其是正在基于OpenBMC做平台事件的盆友;第二类是负责服务器故障定位的运维工程师,你需要知道一条PSL错误最终会变成什么业主错误码,以及怎么从BMC日志里反查完整事件链。
前置技能并不复杂:懂一点Linux命令,知道errno是个什么东西,能看懂C语言伪代码就行。我会把关键寄存器解析、SEL记录结构、错误码映射表这些偏底层的细节都展开讲,所以即使你之前没接触过BMC开发,按着思路也能复现整个流程。
2. 整体设计:从PSL错误到errno的完整数据链路
2.1 事件源:PSL如何上报异常
在设计之前,必须先想明白事件从哪里来。PSL核心逻辑里有一组错误状态寄存器,每个bit位代表一类硬件故障,常见的有链路DMA超时、TLP格式错误、缓存一致性问题、访问越界、外部设备掉电、中断丢失等。当其中某个bit被置位,PSL会拉高一条中断线到BMC,有些平台也会同时让主CPU陷入一个错误中断。
BMC收到中断后,需要读PSL的错误寄存器快照,一般包括错误码主寄存器、错误详情寄存器、发生时间戳、以及相关地址信息。这里有一个容易忽略的细节:PSL错误寄存器通常是“写1清零”的。如果你读完一遍没有及时清除,后续新错误会被旧状态淹没;如果清得太早,记录还没写进日志,信息又会丢。最好的做法是先在BMC内存里把快照拷贝完整,确认已经落盘或写入SEL,再回头清零。
事件上报的数据链路方面,我采用这样一条通道:PSL中断线连接BMC的GPIO,BMC上的监控服务监听GPIO事件,在中断处理逻辑里通过sysfs或ioctl读取PSL寄存器,然后把解析结果送进OpenBMC的日志框架,最终同时产生SEL记录和主机侧可见的系统事件,比如写入D-Bus对象。这么做的好处是,BMC自带的服务栈和Redfish接口不需要额外开发,后面查询和展示都直接复用。
2.2 数据通路设计
读完寄存器不算完,数据还要在BMC内部正确流转。OpenBMC里这件事有一个完整链路:中断触发 -> 内核驱动把GPIO事件上报 -> phosphor-gpio-monitor等服务捕获 -> user空间程序读取PSL寄存器 -> 构造事件对象 -> 写入journal和SEL -> Redfish查询时动态呈现。
我在实施时没有在中断上下文里做任何寄存器读取和日志写入。中断里只做一件事:置一个flag,唤醒后台线程。后台线程再去执行慢速I2C或LPC读取,以及格式化日志内容。原因是中断上下文里调用延迟高的接口,轻则日志乱序,重则影响BMC主控的响应速度。这一条在整个设计里是硬约束。
日志记录路径上,我同时保留两类记录:内部完整记录和外部标准记录。内部记录写入BMC的journal,包含原始寄存器值、PSL版本号、固件版本,方便开发溯源;外部记录写入IPMI SEL,使用OEM传感器类型和自定义Sensor Number,保证带外管理工具也能收。后面我会给出具体的SEL记录设计。
2.3 错误码映射的取舍
把硬件错误映射到errno,这一步听起来简单,做起来全是细节。PSL报的可能只是“TLP header已损坏”“链路重放超时”这类硬件语义,而errno本身是操作系统语义,两者不在一个层次。举个例子,PSL检测到对应的外设设备已经被拔出,你可以报ENODEV,也可以报EIO。从业务侧看,应用最想知道的是“设备没了”还是“设备还在,但通信坏了”,所以这种区分必须做。
我采用的映射策略是“先分类,后细分”:先看错误类型是否属于链路物理层、数据链路层或事务层,再根据有没有伴随设备发现事件,决定最终errno。设备发现事件为假、且链路层报错,归为ENODEV;设备在位但事务层超时,归为ETIMEDOUT;数据校验失败或内存访问越界,归为EIO。这样映射出来,每一条errno都能被解读成硬件故障方向,而不是笼统的I/O错误。
另外要克制一点:errno映射表不是越全越好。错误种类太多反而难维护,我实际只维护了十来个常用映射,其余全部落到EIO兜底,并保留原始错误码。这个兜底设计很重要,能防止因为映射表漏了一个新错误导致整个处理链路白做。
3. 核心逻辑实现:OpenBMC上的PSL错误处理
3.1 定义错误状态寄存器与解析策略
我基于某开源BMC软件栈自研落地,项目内部把PSL错误寄存器虚拟成一个sysfs节点,节点名就是psl_errno。实际在硬件平台上可能走的是LPC或I2C访问,但抽象成sysfs之后,用户态程序就统一了。
下面是一段简化后的寄存器读取示意,重点是“读取、解析、清除”的顺序。
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #define PSL_ERRNO_SYSFS "/sys/class/psl/psl_errno" #define PSL_CLEAR_SYSFS "/sys/class/psl/psl_errno_clear" int psl_read_error(uint32_t *raw_err) { int fd = open(PSL_ERRNO_SYSFS, O_RDONLY); if (fd < 0) { return -errno; } char buf[32] = {0}; ssize_t n = read(fd, buf, sizeof(buf) - 1); close(fd); if (n <= 0) { return -EIO; } *raw_err = (uint32_t)strtoul(buf, NULL, 0); return 0; } int psl_clear_error(void) { int fd = open(PSL_CLEAR_SYSFS, O_WRONLY); if (fd < 0) { return -errno; } write(fd, "1", 1); close(fd); return 0; }解析策略上,我把寄存器字段拆成三层。第一层是主错误类型,占高16位;第二层是错误源编号,占中8位;第三层是保留字段和错误级别。解析时不是先转换成errno,而是先组装成一条结构化事件,后续映射时再使用主错误类型和错误源编号查表。
这里特别说明一下,sysfs节点路径和寄存器位宽是我按项目实际情况抽象出来的,你们真正落地时要按PSL规格书来。关键是结构不能乱:先读到原始值,再解析字段,最后做映射,三步不能合并。
3.2 errno映射表设计
映射表是整个功能的灵魂。我设计成一张静态二维映射表,第一维是主错误类型,第二维是错误源编号。错误源编号通常对应当前PSL管理的哪一个加速器或哪一个DMA通道,有助于精确上报到具体设备。
表格样例大致如此:
| 主错误类型 | 错误源编号 | 映射errno | 含义说明 |
|---|---|---|---|
| 0x0001 | 0x00 | EIO(5) | DMA读数据链路异常 |
| 0x0001 | 0x01 | ETIMEDOUT(110) | DMA读超时 |
| 0x0002 | 0x00 | ENODEV(19) | 外设掉电或链路断开 |
| 0x0003 | 0x00 | EFAULT(14) | 地址映射错误或越界 |
| 0x0004 | 任意 | EAGAIN(11) | 资源忙,重试可恢复 |
| 其他 | 任意 | EIO(5) | 统一兜底 |
映射表不是拍脑袋定的。每一条映射都经过一个内部评审流程:由硬件工程师确认错误含义,再由系统工程师确认errno语义,两边对齐后才写进代码。我还为每条映射配了一行解释文本,这样以后有人看到日志,能直接知道这条errno是从哪个寄存器状态变过来的。
实现上我没有用复杂的数据结构,数组加线性遍历就够了,因为整个表不到二十行。遍历时按“类型优先、源编号次之”的顺序查找,先匹配主类型,如果源编号是任意值就接受;如果找不到再落到兜底映射。
3.3 写SEL/OEM记录的关键代码
OpenBMC写SEL通常可以直接调IPMI相关的接口。为了展示核心逻辑,我这里给一个简化的记录生成代码,把PSL错误码和映射后的errno一起放进OEM数据段。
#include <stdio.h> #include <stdint.h> #include <string.h> #include <time.h> typedef struct { uint8_t rec_id[2]; uint8_t record_type; uint32_t timestamp; uint8_t generator_id[2]; uint8_t evm_rev; uint8_t sensor_type; uint8_t sensor_number; uint8_t event_dir_type; uint8_t event_data[3]; } ipmi_sel_record_t; void build_sel_record(ipmi_sel_record_t *sel, uint32_t psl_err) { memset(sel, 0, sizeof(*sel)); sel->record_type = 0x02; // system event record sel->timestamp = (uint32_t)time(NULL); sel->generator_id[0] = 0x20; // BMC sel->sensor_type = 0x0F; // OEM sensor type sel->sensor_number = 0x50; // 自定义 sensor number sel->event_dir_type = 0x6F; // assertion 事件 sel->event_data[0] = (uint8_t)((psl_err >> 16) & 0xFF); sel->event_data[1] = (uint8_t)((psl_err >> 8) & 0xFF); sel->event_data[2] = (uint8_t)(psl_err & 0xFF); }这段代码只是构造记录。真正写入SEL时,还要经OpenBMC的日志管理服务,把record写入持久化存储并分配Record ID。实际工程里我会把映射后的errno也塞进event_data的高字节里,比如event_data[0]的高4位存errno,低4位存原始错误类型子类。这样一条SEL记录既能被IPMITool直接读出,又不需要二次查表。
写SEL时有个动作必须做:写之前先判断当前错误码是否和上一条SEL重复。我加了一个去重逻辑,上一条SEL的event_data和当前完全一样,且时间差小于五分钟,就不再重复写入,只更新触发计数。防止一个坏掉的PSL从未清空状态反复触发事件,直接把SEL空间塞满。
3.4 主机侧同步上报
BMC写完日志还不够,主机侧最好能实时收到一个可识别的errno。我的方案是通过IPMI OEM命令下发:BMC开启一个监听线程,等待主机发送查询命令,命令号就定义为0x80,后面跟随PSL错误源编号。
主机侧的执行动作就简单了。一条命令查PSL状态,一条命令获取errno结果。
ipmitool raw 0x30 0x80 0x01返回内容里第一个字节就是映射后的errno值,第二个字节是原始PSL错误码低8位。这一步的好处是系统管理工具可以不解析SEL,直接得到程序化可用的错误码。这里有一个小坑:不同厂商的OEM命令号不能乱撞,要先用命令0x30 0x80前做一个能力探测,确认BMC支持这条OEM命令,再继续交互。
另外,主机侧内核模块也可以直接读BMC的sysfs映射结果。有些平台允许通过LPC映射一段BMC内存到主机总线,主机侧直接读内存地址得到最新PSL errno。这种方式延迟最低,但需要主机端具备对应的地址窗口配置,不是每个平台都能用。两条路子我都实现过,常规推荐OEM命令方式,通用性更好。
4. 测试与验证:如何证明这套链路是通的
4.1 环境准备与模拟错误注入
开发和测试时,不能每次都真去拔设备或者制造DMA超时,太伤硬件。我采用的方式是在PSL侧预留一个调试模式下发口,类似“错误注入寄存器”,往里写一个值,PSL就会像真实发生错误一样拉中断、更新错误寄存器。
模拟注入的基本流程是这样的:
- 先把BMC和主机侧的日志清理干净。
- 关掉可能需要真实硬件配合的链路判断,让PSL进入调试模式。
- 往错误注入寄存器写入0x00010000,对应“DMA读数据链路异常,错误源0”。
- 观察BMC的GPIO中断是否被拉起。
- 检查后台线程是否解析出主错误类型0x0001和源编号0x00。
- 确认SEL记录里event_data是否变成期望的值。
如果调试模式下没有触发中断,我一般先查GPIO中断配置是不是只配置了上升沿,而PSL是低有效中断,导致事件永远进不来。这是一个很容易踩的坑,调试了大半天结果只是极性配置反了。
4.2 用IPMI和Redfish核对日志
注入一次后,就可以用标准工具验证结果。先看SEL列表:
ipmitool sel list预期能看到类似这样的条目,注意各个字段要和设计一致:
45 | 03/12/2025 | 09:33:21 | BMC | PSL errno 0x00010005 | Assertion同样的SEL在Redfish接口里也应该能查到:
curl -k -u root:0penBMC https://<bmc-ip>/redfish/v1/Systems/system/Logs/Sel/45首次验证时容易发现一个问题:SEL里只有“PSL errno”这个笼统描述,具体errno值却没显示。原因是IPMI SEL的文本描述是事件目录生成的,不是自动解析event_data的。我后来在事件目录里针对自定义Sensor Number写了一段名称模板,把event_data解析出来的errno拼进描述文本,才让日志变得一眼可读。
Redfish那边同理,LogEntry的Message字段也需要定制映射模板。不要指望默认实现能懂你的OEM事件,该写代码就得写。
4.3 主机侧errno验证
BMC侧日志通过后,主机侧也要完整验证。假设主机侧加载好一个测试工具,先查询PSL错误码:
ipmitool raw 0x30 0x80 0x01返回值为0x05 0x00 0x64,第一个字节就是EIO,说明映射生效。第二个字节0x00是错误源编号,第三个字节0x64是时间戳的秒数。这个结果证明主机侧查询命令、BMC侧的映射表、以及上层的Display函数都正常。
再结合业务侧视角,一个应用在read或者mmap接口上收到EIO/ENODEV时,现在能直接说明设备是不是掉电、链路是不是断了、具体是哪一条DMA通道出错。这在现场连锁排障时非常有用,不用再去BMC里手翻寄存器。
4.4 边界条件测试
功能正常后,我还会专门花时间做边界条件测试,这是价值最大的部分。第一个用例是连续注入完全相同的错误,验证五分钟内的去重逻辑有没有生效。正常情况下SEL里只应该出现一条记录,而不是好几十条。
第二个用例是“错误注入后不读快照直接再次注入新错误”。设计里我在后台线程读取快照后才清一次中断,所以如果第一个错误的中断未及时处理,第二个错误的中断应该保持挂起,等待处理逻辑跑完再响应。测这个用例是为了确认没有因为边沿触发丢事件。
第三个用例是BMC重启后的恢复。模拟流程是:注入错误 -> 等待SEL写入完成 -> 立刻重启BMC服务或整机断电重启 -> 再查询SEL和Redfish,确认记录已经持久化,不会因为重启丢失。如果发现日志丢了,优先检查journal持久化配置,以及SEL存储有没有落到Flash分区。
5. 常见问题与排查技巧
5.1 日志不出现的五个原因
我在这个项目里测试出来的日志不出现原因,按频率排序如下:
原因一是中断极性配置反了,PSL中断大多低电平有效,而默认配置可能是高电平。修复方式是查原理图确认PSL中断脚连接的是不是BMC主控的一个下降沿/低电平GPIO。原因二是后台线程没有收到内核事件,OpenBMC的GPIO事件用的是libgpiod或sysfs轮询,服务起没起、GPIO编号对应错没对,都会导致中断消息根本没到user space。原因三是寄存器读失败,可能是I2C地址配错或LPC窗口没使能,这种情况日志里一般有read error,直接查读取函数返回值即可。原因四是SEL写入被过滤策略挡住,OpenBMC的event filter默认只放行部分事件,你的OEM传感器类型可能被丢弃。原因五是时间戳或事件类型被SEL工具认为非法,写入侧类型填了一个不受支持的Event Dir。
排查我通常按照“中断是否到达 -> 用户态是否读到 -> 映射逻辑是否执行 -> SEL写入是否成功”的顺序逐层走。不要一开始就去翻SEL,顺序搞反容易浪费时间。
5.2 errno对不上的排查思路
发现主机会话查询出来的errno和BMC SEL里预期的不一致,最常见原因是映射表查到了兜底值EIO,原因是原始错误类型没落到已定义的项。这种情况其实值得高兴,因为它发现了新PSL错误类型。排查时在BMC日志里找原始PSL错误码的高16位,然后对照PSL规格书确认新类型含义,及时补充映射关系再发布一版。
另一种情况是同一个错误码在SEL展示里是EIO,但通过OEM命令返回的是ENODEV。这通常是代码里用了两套映射逻辑,SEL侧和OEM命令侧各有一张映射表且没同步。我会建议整个工程只保留一份映射表,编译时同时生成SEL模板和OEM命令结构体引用,避免两处数据不一致。
还有一种隐蔽问题是errno数值错位。Linux里EIO是5,但有些裁剪内核或BSP把errno表重新编号过,少数平台里EIO可能是7或者别的值。解决方法是不要硬编码数值,用系统头文件里的EIO宏定义来生成映射表,这样换内核版本也不会错。
5.3 BMC日志持久化与去重
SEL持久化是OpenBMC里最容易翻车的地方。默认情况下journal写在内存tmpfs,BMC重启日志就没了,但这显然不行,生产环境下故障记录关机后必须还在。我最终把SEL存储设置到BMC的RW Flash分区,并且开启journal的同步刷盘。实验证明,在异常掉电前写入的SEL记录,重启后基本能完整保留。
去重逻辑方面,简单的“同码五分钟内不重复”方案,对偶发故障有效,但对高频故障却可能漏报。后来我改成“重复记录写计数,计数超过三次升级为Critical事件”,保证真正反复发生的硬件故障不会被静默吞掉。这个方案执行起来也不难:额外维护一个轻量HashMap,key是PSL错误码,value是触发时间和计数,定时清理超过十分钟的旧表项。
5.4 中断风暴控制
PSL如果出现硬件故障反复触发,中断风暴会让BMC的CPU占用飙到100%,连管理页面都打不开。我的控制策略分三层:第一层是中断线程里加时间戳门限,两次事件间隔低于10ms直接丢弃;第二层是内核中断层面配置检测,连续超过100次中断就暂停GPIO监听,直到用户态处理完成;最外层才是用户态频控。
这个案例我印象很深:第一次联调时PSL错误注入后,BMC的网络卡得SSH都连不上,查CPU占用发现监控进程接近100%。后来把这三层加上才稳住。所以强烈建议你在设计阶段就把中断风暴考虑进去,别等现场出了故障才手忙脚乱。
6. 最后再说点我的个人体会
这个项目看起来小,落地后价值却不小。做完后最大的感受是,BMC相关的开发和传统应用开发完全是两种节奏。应用开发是快速迭代、及时反馈;BMC开发则是“写代码一小时,查硬件半天”,因为很多问题无法从软件层直接观测,必须通过日志、寄存器、复位行为反推。
如果你也要做类似“BMC某个错误码上报”的功能,我建议你第一步先去把硬件规格书啃透,把PSL的寄存器位定义、中断极性、访问方式搞清楚,再开始写代码。不要像我第一次那样,代码写完了才发现中断线接的GPIO和驱动默认配置不一致。
另外,errno映射表的维护要当成一个长期资产来管理。每遇到一个新的原始错误码,先不要急着加映射,而是建一个Excel或者Wiki表格,记录“寄存器位取值、硬件含义、errno值、上游业务建议处理动作”四个字段。这张表不仅是你的工程文档,也是以后和硬件厂商、现场运维扯皮时的重要依据。
最后分享一个小技巧:SEL日志里一定要带上设备固件版本、BMC版本、PSL寄存器快照这三类信息。现场排查故障时,固件版本不同导致的错误码行为差异是最大的坑,没有版本上下文,任何错误码分析都是空中楼阁。我的做法是在每条SEL的OEM数据区域预留48字节,专门存放这些上下文信息。宁可日志长一点,也不能让现场工程师拿到记录后一头雾水。