openbmc 之 entity-manager 是如何工作的(1)

openbmc 之 entity-manager 是如何工作的(1)

基于 1e19c030559e84a42c98effa19e35665c7ae4b7c (2.16.0-dev) ,即截止文章撰写时的最新代码版本。

这篇文章主要从我的视角记录了 entity manager 的工作过程,可能存在不准确的地方,还望补充修正。

这篇文章之所以产生,是因为发现了一个逻辑不通:就以其自带的 configurations/1ux16_riser.json 这样一个配置文件为例。在这个配置文件中,并没有指定这张卡挂在哪个总线上,所有总线相关的条目都是 $bus ,甚至连 eeprom 所对应的设备地址也都是 $address,而且,在相关 kernel devicetree 中,也没有任何与这张卡相关的总线和地址信息。那么,问题来了,是谁为 $address 和 $bus 填入了对应的地址? I2C 不是不可探测的总线吗?那设备是怎么被扫描出来的?完全未知不就意味着驱动都没有?那设备又是怎么被识别出来的?

在开始之前,先来介绍一下 openbmc 对于板卡管理的架构设计。

设计架构

总的来说,OpenBMC 的社区设计,将部件管理分为三大个模块:

探测器

配置管理器

执行器

“探测器”用于发现硬件设备并搜集这些设备的原始信息。这些原始信息将被推送至 dbus 上,供“配置管理器”之类的其它模块使用。

“探测器”的典型代表是接下来要介绍的 fru-device ,当然,社区也有其它的“探测器”实现,比如 peci-pcie 。

“配置管理器”负责将本地的配置文件与“设备原始信息”进行匹配,从而正确的识别这些设备并形成更加详细的设备配置信息。这些配置信息也将被推送至 dbus 上,供“执行器”之类的其它模块使用。

“配置管理器”的典型代表是 entity-manager 。

“执行器”有许多不同的种类。它们所做的事,是根据“配置管理器”挂在 dbus 上的信息,找出自己可以操作的设备,获取需要的信息,最后再将信息推回到 dbus 上。“执行器”的典型代表是 dbus-sensors 。

也许你对上面所说的过程并没有什么概念,但是可以举个更详细的例子:

“探测器”扫描到了一个硬件设备,但是只知道这个设备的 id 是 xxxx ,于是它将这个 id 推送到了 dbus 上。

“配置管理器”根据这个 id ,结合本地的配置文件,确定了这到底是哪一张板卡。于是“配置管理器”便可以往 dbus 上推送与这张板卡有关的信息,比如它的名称、描述,它所带有的传感器地址等等。

“执行器”根据“配置管理器”提供的传感器地址信息,读取传感器数据,最终又将数据推回到 dbus 上。

由于所有数据都在 dbus 上,因此,需要获取信息的东西,如 bmcweb 提供的 redfish 接口,便可以轻松的从 dbus 上拿到任何想要的东西。

用一张图来总结这个过程:

包结构

好了,接下来看看 entity-manager 这个包(配方)的结构。

来看看它的编译产物:

``

meson.build

......

executable(

'entity-manager',

'entity_manager.cpp',

'expression.cpp',

'perform_scan.cpp',

'perform_probe.cpp',

'overlay.cpp',

'topology.cpp',

'utils.cpp',

......

)

......

executable(

'fru-device',

'expression.cpp',

'fru_device.cpp',

'utils.cpp',

'fru_utils.cpp',

'fru_reader.cpp',

......

)

.....

可以看到,entity-manager 虽然是一个包,但却会构建两个可执行文件,一个是 entity-manager ,另一个是 fru-device 。这两个可执行文件分别是由 xyz.openbmc_project.EntityManager.service 和 xyz.openbmc_project.FruDevice.service 独立负责启动的,两者在启动时并没有直接的依赖关系。

从上面我们的架构分析可知,这两者,一个是“配置管理器”,另一个则是“探测器”。说句实在话,把 fru-device 放在这个包里其实并没有那么合适,倒是有种作为“探测器” demo 的感觉,它完全可以像 peci-pcie 一样自己起一个包。

接下来,看看它们分别是如何工作的。

fru-device

概述

先来概况一下这个可执行文件的工作内容:它需要在没有设备驱动的情况下扫描所有 i2c 总线下的每一个地址(实际上是指定范围的地址),如果发现地址上存在 FRU ,则解析 FRU 的内容并将其推到 dbus 上。

什么是 FRU ?说白了,它就是一种电子标签,用以存储部件信息。在物理上,它一般存储在板卡上的特有 EEPROM 里,与 BMC 通过 i2c 的方式取得连接。fru-device 在根本上所做的事,便是通过 i2c 来寻找 FRU ,从而发现各种各样的板卡。

扫描

接下来看一个简要的 fru-device 代码分析:

在 main() 函数中,其会调用 rescanBusses() ,开始第一轮的扫描:

src/fru_device.cpp

int main()

{

......

// run the initial scan

rescanBusses(busMap, dbusInterfaceMap, unknownBusObjectCount, powerIsOn,

objServer, systemBus);

......

}

rescanBusses() 会创建一个 FindDevicesWithCallback 回调。这个回调会在扫描完成后被调用,可以看到,扫描完成后,它就 addFruObjectToDbus() 来将扫描的结果挂到 dbus 上去了:

```cpp

src/fru_device.cpp

void rescanBusses(

BusMap& busmap,

boost::container::flat_map<

std::pair,

std::shared_ptr>& dbusInterfaceMap,

size_t& unknownBusObjectCount, const bool& powerIsOn,

sdbusplus::asio::object_server& objServer,

std::shared_ptr& systemBus)

{

......

......

auto scan = std::make_shared(

i2cBuses, busmap, powerIsOn, objServer, [&]() {

......

for (auto& devicemap : busmap)

{

for (auto& device : *devicemap.second)

{

addFruObjectToDbus(device.second, dbusInterfaceMap,

devicemap.first, device.first,

unknownBusObjectCount, powerIsOn,

objServer, systemBus);

}

}

});

scan->run();

......

}

接下来,我们主要聚焦于扫描的过程, scan->run() 最终会调用 findI2CDevices() ,我们直接来看这个函数:

```cpp

src/fru_device.cpp

static void findI2CDevices(const std::vector& i2cBuses,

BusMap& busmap, const bool& powerIsOn,

sdbusplus::asio::object_server& objServer)

{

for (const auto& i2cBus : i2cBuses)

{

int bus = busStrToInt(i2cBus.string());

......

auto& device = busmap[bus];

device = std::make_shared();

......

// fd is closed in this function in case the bus locks up

getBusFRUs(file, 0x03, 0x77, bus, device, powerIsOn, objServer);

......

}

}

在上方的这个函数中,它会遍历所有存在的 i2c 总线,(检测这些总线的特性,比如 I2C_FUNC_SMBUS_READ_BYTE 和 I2C_FUNC_SMBUS_READ_I2C_BLOCK,上面的代码里省略掉了 ),最后调用 getBusFRUs() 来扫描这一条总线。

传入的参数,0x03 代表扫描的起始地址,0x77 代表扫描的结束地址。

在这里, device 以引用的方式传递,是扫描结果的输出。device 以引用的方式被绑定到 busmap ,其整体结构是这样的:

using DeviceMap = boost::container::flat_map>;

using BusMap = boost::container::flat_map>;

如果用树状结构画出来:

busmap

├── device-map-i2c-0

│ ├── device-0x57-fru-content-vector

│ └── device-0x58-fru-content-vector

└── device-map-i2c-1

即,busmap 装有所有总线的所有设备的所有 FRU 信息。

busmap 由 DeviceMap 组成,代表单个总线上的所有 device 的所有 FRU 信息。

DeviceMap 里装的是一组 std::vector ,一个 vector 代表一个设备的 FRU 信息。

在这里,FRU 的信息仍然是原始未解析的,解析的过程是在后续 add to dbus 的时候进行的。

在明白了重要参数的含义后,我们进一步看看 getBusFRUs() 是如何获取各个设备的 FRU 的:

src/fru_device.cpp

int getBusFRUs(int file, int first, int last, int bus,

std::shared_ptr devices, const bool& powerIsOn,

sdbusplus::asio::object_server& objServer)

{

std::future future = std::async(std::launch::async, [&]() {

......

// Scan for i2c eeproms loaded on this bus.

std::set skipList = findI2CEeproms(bus, devices);

......

for (int ii = first; ii <= last; ii++)

{

......

if (skipList.find(ii) != skipList.end())

{

continue;

}

......

// Set slave address

if (ioctl(file, I2C_SLAVE, ii) < 0)

{

std::cerr << "device at bus " << bus << " address " << ii

<< " busy\n";

continue;

}

// probe

if (i2c_smbus_read_byte(file) < 0)

{

continue;

}

......

makeProbeInterface(bus, ii, objServer);

......

auto readFunc = [is16BitBool, file, ii](off_t offset, size_t length,

uint8_t* outbuf) {

return readData(is16BitBool, false, file, ii, offset, length,

outbuf);

};

FRUReader reader(std::move(readFunc));

std::string errorMessage = "bus " + std::to_string(bus) +

" address " + std::to_string(ii);

std::pair, bool> pair =

readFRUContents(reader, errorMessage);

......

devices->emplace(ii, pair.first);

}

return 1;

});

......

}

上面的代码中其实隐含了两种 FRU 的扫描读取方式。

第一种包含在 findI2CEeproms() 中:针对已经安装了设备驱动的设备,(即可以直接找到对应的设备目录以及 eeprom 文件),直接读取 eeprom 的内容并将其作为输出结果。既然通过这种方式已经能够拿到结果了,那么就可以将其加入到 skipList 避免下面货真价实的硬扫描了(1)。

第二种方式则是硬扫描:

首先的首先,什么是“硬扫描”?这里的硬扫描是指直接调用 i2c 驱动的接口,在 i2c 总线上发送原始的请求,并获取原始的结果。硬扫描是直接与 i2c 驱动交互的,并不依赖特定设备的设备驱动,因此我们可以通过其,在没有设备驱动的情况下与设备通信(其实就是手动模拟设备驱动的功能)。这种原始的通信方式是通过 /dev/i2c-xx 的字符设备进行,依赖在内核 config 中开启选项 CONFIG_I2C_CHARDEV 。

好,接下来照着上面的代码看看“硬扫描”的过程:

首先,其对该总线执行了一个 ioctl 来设置从机地址。

然后,它直接尝试从从机读取内容。这一步我有些疑问,因为照理说 i2c 应该还需要发送一个 command 从机才会响应(比如指定所读取的数据在从机内的地址),但是,有相关资料说 Some devices are so simple that this interface is enough,那我们也只能认为 FRU 所在的 eeprom 也是这样 simple 的设备。

假如成功从从机读取了内容,它便会认为此处有 i2c 设备,会调用 makeProbeInterface() 在 dbus 上挂一个 xyz.openbmc_project.Inventory.Item.I2CDevice 类型的接口,这个接口仅仅代表此处可能有 i2c 设备,并不包含 FRU 的信息。

假如成功从从机读取了内容,它便会调用 readFRUContents() 来读取 FRU 的原始数据,并将数据填入到输出结果中。

传入给 readFRUContents() 的 FRUReader 是一层简单的包装,能够将原来受 i2c 协议限制的指定大小读写转换为任意大小的读写,并附以缓存来提高性能。而上面作为 lambda 传入的 readData() 才是真正负责从 i2c 读取内容的函数,其最终通过调用 libi2c 库提供的接口从 /dev/i2c-xx 取得结果。

readFRUContents() 的详细过程此处不再展开,其主要由两个过程组成:

校验 FRU 的头部:它会按照 FRU 的标准,读取 eeprom 头部的数个字节进行校验,确保这个 i2c 设备真的是一个 FRU 。

结合头部的 offset 信息,获取 FRU 的完整原始内容:FRU 本身是由不同的区块组成的,FRU 的头部填写了这些区块在 FRU 本体中的偏移量,这个函数会结合这些偏移量信息,将 FRU 的完整原始内容读取出来。这中间会有许多跳来跳去,在不同 offset 读取内容的过程,这就该轮到 FRUReader 中的缓存发挥作用了。

到这里,上面关键的 busmap 就被填充完成了。也就是说,我们扫描了所有的 i2c 总线,对疑似有设备的地址进行了读取,在确定了设备可能是 FRU 后,将设备中的原始内容全部取出,暂时保存在 busmap 中,等待后续处理。

注释:

(1) 这种情况主要针对的是后触发的重扫描,毕竟刚开机的时候可没有什么设备驱动,至于设备驱动是如何安装的,则将在后续对 entity-manager 的介绍中再进行。当然,这种情况也可以是对于在 dts 中写死了驱动的设备,不过比较少见。对于已经安装了驱动的设备,直接进行硬扫描可能导致与驱动发生冲突,造成不可预期的后果,因此预先判断该设备有没有设备驱动十分重要。

解析与推送

接下来就是 FRU 的解析过程了,FRU 的解析发生在往 dbus 上推送对应 object 的时候。

接着上面的 FindDevicesWithCallback 中的 addFruObjectToDbus() 来看:

src/fru_device.cpp

void addFruObjectToDbus(

std::vector& device,

boost::container::flat_map<

std::pair,

std::shared_ptr>& dbusInterfaceMap,

uint32_t bus, uint32_t address, size_t& unknownBusObjectCount,

const bool& powerIsOn, sdbusplus::asio::object_server& objServer,

std::shared_ptr& systemBus)

{

boost::container::flat_map formattedFRU;

std::optional optionalProductName = getProductName(

device, formattedFRU, bus, address, unknownBusObjectCount);

......

std::string productName = "/xyz/openbmc_project/FruDevice/" +

optionalProductName.value();

......

std::shared_ptr iface =

objServer.add_interface(productName, "xyz.openbmc_project.FruDevice");

dbusInterfaceMap[std::pair(bus, address)] = iface;

for (auto& property : formattedFRU)

{

......

if (property.first == "PRODUCT_ASSET_TAG")

{

std::string propertyName = property.first;

iface->register_property(

key, property.second + '\0',

[bus, address, propertyName, &dbusInterfaceMap,

&unknownBusObjectCount, &powerIsOn, &objServer,

&systemBus](const std::string& req, std::string& resp) {

......

});

}

else if (!iface->register_property(key, property.second + '\0'))

{

......

}

......

}

// baseboard will be 0, 0

iface->register_property("BUS", bus);

iface->register_property("ADDRESS", address);

iface->initialize();

}

真正的解析过程发生在 getProductName() -> formatIPMIFRU() 里,解析结果存储在 formattedFRU 中,这个变量会全程以引用的形式被传进去,最后被作为结果读出来。接着,解析结果会被逐属性的推上 dbus 。在这里,PRODUCT_ASSET_TAG 属性会被特殊对待,它除了被推上去外,还被额外设置了一个 setter (也就是那个 lambda 表达式),这种 setter 会劫持如 busctl set-property 的过程,确保属性被别的程序设置时,fru-device 也能感知到这个变化,在这里,它被用于 updateFRUProperty() 也就是 FRU 回写,这里的内容就不再进一步展开了。

接下来简单看看 getProductName() 调用的 formatIPMIFRU() 的解析过程:

首先, 其会按照标准将 FRU 解析成键值对。看起来 FRU 的键主要有两种,一种是固定的,其值对应 FRU 中的特定偏移量。这些固定的键可以在 src/fru_utils.hpp 中找到。另一种是自定义的键,这一种键不可自定义名称,显示的键名统一为 const std::string fruCustomFieldName = “INFO_AM”; + 一个数字,而值则通过解析 FRU 来获得。

此外,在键的名称之前,还会加上一个该键所对应的区域的名称。比如 BOARD_PRODUCT_NAME 代表 BOARD Area 中的 PRODUCT_NAME Field, PRODUCT_MANUFACTURER 代表 PRODUCT Area 中的 MANUFACTURER Field 。

再接下来,回到 getProductName() 中,这个方法主要用于根据刚刚 formatIPMIFRU() 的解析结果来确定设备名称:其会按顺序尝试读取解析结果中的 BOARD_PRODUCT_NAME、PRODUCT_PRODUCT_NAME ,遇到有值的则将值作为设备名,如果没有值则将设备名设为 UNKNOWN 。

设备名会在推上 dbus 时被使用。解析出来的 FRU 会以 xyz.openbmc_project.FruDevice 的类型被推到 /xyz/openbmc_project/FruDevice/<设备名> 路径下。当然,对设备名也有一些针对重名的处理,比如在发生重名时增加数字后缀以避免 dbus throw exception ,这里就略过了。

以下是一个被推上 dbus 的 FRU 数据示例:

xyz.openbmc_project.FruDevice interface - - -

.ADDRESS property u xx emits-change

.BOARD_FRU_VERSION_ID property s "xxx" emits-change

.BOARD_INFO_AM1 property s "xxx" emits-change

.BOARD_INFO_AM2 property s "xxx" emits-change

.BOARD_LANGUAGE_CODE property s "x" emits-change

.BOARD_MANUFACTURER property s "xxx" emits-change

.BOARD_MANUFACTURE_DATE property s "xxx" emits-change

.BOARD_PART_NUMBER property s "xxx" emits-change

.BOARD_PRODUCT_NAME property s "xxx" emits-change

.BOARD_SERIAL_NUMBER property s "xxx" emits-change

.BUS property u x emits-change

.Common_Format_Version property s "xxx" emits-change

.PRODUCT_ASSET_TAG property s "xxx" emits-change writable

.PRODUCT_FRU_VERSION_ID property s "xxx" emits-change

.PRODUCT_LANGUAGE_CODE property s "x" emits-change

.PRODUCT_MANUFACTURER property s "xxx" emits-change

小结

fru-device 负责在没有设备驱动的情况下发现 i2c 总线上所有可能存在的 FRU ,读取解析 FRU 的内容并将其挂到 dbus 上。读取的过程可能有一些约定的依赖性,比如设备必须能够直接响应不带有 command 的 i2c read 。同时,这个读取过程是低效且可能超时的,其默认的超时处理是将超时的总线直接加入黑名单以避免下一次扫描。但是这样的过程提供了非常不错的灵活性,设备所在的总线和地址不再需要写死了,可以在扫描的过程中动态生成。

相关推荐

王者荣耀轮回活动究竟要搞多久?几个月?一年?还是永久?
淘宝网店官方版怎么下载?新手注册流程有哪些?
Bet体育365验证提款

淘宝网店官方版怎么下载?新手注册流程有哪些?

07-05 👁️ 1151
电饼铛烙饼的做法
好多假365平台

电饼铛烙饼的做法

07-10 👁️ 4302