读取sysfs(hwmon)传感器数据偶发阻塞调用耗时过长问题排查
问题背景
我有一台运行Linux(BusyBox)的ARM设备,需要频繁读取存储电压值的文件 /sys/class/hwmon/hwmon0/device/in7_input,该文件位于 /sys/class/hwmon/ 路径下的虚拟文件系统 sysfs 中。
通过SSH连接到设备读取该文件,返回的数据格式示例如下:
root:~# cat /sys/class/hwmon/hwmon0/device/in7_input 10345 root:~# cat /sys/class/hwmon/hwmon0/device/in7_input 10250
通常读取该文件的耗时约为 1 ms,但偶发读取耗时会达到 1 sec。由于该问题仅在现场环境低概率出现,测试台架上始终无法复现。问题发生时核查过CPU利用率,通常低于 60%,无法确定为何偶发读取该文件会出现类似阻塞调用的现象,导致函数执行时长远超预期。
目前不确定该问题是源于虚拟文件系统 sysfs 的底层异常,还是编写的 readVoltage() 函数未实现完全非阻塞。
以下是调整后的代码片段:
/****************************************************************************** Online C++ Compiler. Code, Compile, Run and Debug C++ program online. Write your code in this editor and press "Run" button to compile and execute it. *******************************************************************************/ #include <iostream> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> using namespace std; uint64_t GetClockCount(void); float calculateLoopTime(uint32_t tUsec1, uint32_t tUsec2); void readVoltage() { static const char VoltageFile[] = "/sys/class/hwmon/hwmon0/device/in7_input"; static int VoltageFD = -1; if (-1 == VoltageFD) { VoltageFD = open(VoltageFile, O_RDONLY | O_NONBLOCK); } if (-1 == VoltageFD) { std::cout << "couldn't open FD for " << VoltageFile << std::endl; } else { static const size_t bufSize = 15; char buffer[bufSize]; fd_set input; FD_ZERO(&input); FD_SET(VoltageFD, &input); struct timeval to; to.tv_sec = 0; to.tv_usec = 0; int n = 0; n = select(VoltageFD + 1, &input, NULL, NULL, &to); if (n > 0) { ssize_t bytes_read = pread(VoltageFD, buffer, bufSize, 0); if (bytes_read > 0) { float voltage = (atof(buffer) / 1000.0f); std::cout << "voltage= " << voltage << std::endl; } } } } int main() { uint32_t start_time = GetClockCount(); readVoltage(); uint32_t end_time = GetClockCount(); float time_diff = calculateLoopTime(start_time, end_time); std::cout << "function took " << time_diff << " ms to execute" << std::endl; return 0; } uint64_t GetClockCount(void) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now)) return 0; return static_cast<uint64_t>(now.tv_sec) * 1000000 + now.tv_nsec / 1000; } float calculateLoopTime(uint32_t tUsec1, uint32_t tUsec2) { float time_diff = 0; if (tUsec1 != tUsec2) { uint32_t time_diff_temp = 0; if (tUsec2 > tUsec1) { time_diff_temp = (tUsec2 - tUsec1); } // Scale from microseconds to milliseconds time_diff = static_cast<float>(time_diff_temp) / 1000; } return time_diff; }
该代码可在在线IDE运行,但无法读取到有效电压值,因为在线IDE环境中不存在 /sys/class/hwmon/hwmon0/device/in7_input 文件。
根因分析
你写的非阻塞逻辑对sysfs文件完全无效,偶发1秒延迟不是用户态代码阻塞,是内核态硬件访问被卡住:
O_NONBLOCK、select()这套逻辑只对真正支持非阻塞操作的文件类型生效,比如socket、管道、普通块设备文件。sysfs是虚拟文件系统,文件读操作回调直接对接硬件驱动,根本不实现非阻塞语义。你给open加O_NONBLOCK、调用零超时的select()没有任何实际作用,只要调用pread触发读操作,内核就会直接执行驱动代码和ADC硬件通信,整个过程完全同步,不会因为非阻塞标记提前返回。- 1秒级延迟基本都是驱动超时重试逻辑触发的:hwmon对应的ADC驱动读取电压值时,如果I2C/SPI总线出现偶发电磁干扰、仲裁失败、硬件应答超时,驱动会按照预设超时时间重试,常见单次超时就是1秒量级。这个过程中进程处于内核态睡眠状态,CPU利用率不会升高,和你观察到的CPU低于60%的现象完全吻合。现场环境比测试台架电磁干扰强,硬件通信偶发出错,才会出现低概率复现的情况。
修复方案
- 删掉无意义的非阻塞冗余逻辑:不用给sysfs文件加
O_NONBLOCK,也不用调用select()判断可读,这两步对sysfs文件没有实际作用,直接调用pread读取即可,减少无意义的系统调用开销。 - 不要在时序敏感的高优先级业务线程里直接读sysfs:单独开一个低优先级后台线程轮询读取电压值,结果存在全局原子变量中,业务线程需要电压值时直接读原子变量的缓存值,就算驱动读取时卡1秒,也不会阻塞主业务流程。
- 定位根因直接抓内核日志:现场复现时抓取dmesg日志,查看是否有ADC驱动、I2C/SPI总线的超时报错信息,就能确认是否为硬件通信重试导致的延迟。如果是总线干扰问题,从硬件加屏蔽、调整驱动重试参数、降低总线速率几个方向优化即可,用户态代码无法解决内核驱动层的硬件访问阻塞。
- 修复现有代码的类型bug:
GetClockCount()返回值是uint64_t类型,但存储时间的start_time、end_time定义为uint32_t,运行时间长了会出现整数截断溢出,导致耗时计算错误,需要把这两个变量的类型改成uint64_t。
内容的提问来源于stack exchange,提问作者Programmer
相关产品推荐
相关产品推荐

