我现在有一个我曾经见过的最糟糕的bug系统,而且似乎只有两种可能的解释 -
好吧,让我给你一些背景知识。我目前正在为树莓派gpio操作编写一个c ++程序。据我所知,程序中没有明显的错误。因为它成功地与sudo一起工作并且也成功延迟了。所以这里是rp的gpio工作原理 -
首先,您要导出一个,以保留它进行操作,它会创建一个新目录
gpio+number
,其中包含多个文件。
echo 17 > /sys/class/gpio/export
然后设置它的方向(意思是读取和输出意味着写入)
echo "out" > /sys/class/gpio/gpio17/direction
然后写入值(0或1表示关闭和打开)
echo 1 > /sys/class/gpio/gpio17/value
最后,将其取消导出,该目录将被删除。
echo 17 > /sys/class/gpio/unexport
无论您是通过bash命令还是通过c / c ++或任何其他语言IO执行此操作都无关紧要,因为在unix中这些只是文件,您只需要读取/写入它们即可。到目前为止一切正常。我手动测试了这个并且它可以工作,所以我的手动测试通过了。
现在,我为我的程序编写了一个简单的测试,看起来像这样 -
TEST(LEDWrites, LedDevice)
{
Led led1(17, "MyLED");
// auto b = sleep(1);
EXPECT_EQ(true, led1.on());
}
Led班级constructor
执行导出部分 - echo 17 > /sys/class/gpio/export
,而.on()
调用设置方向 - echo "write" > /sys/class/gpio/gpio17/direction
并输出值 - {{1} }。忘掉这里的devport,因为它是由析构函数处理的,在这里不起作用。
如果您有点好奇,这些函数会像这样处理I / O -
echo 1 > /sys/class/gpio/gpio17/value
表示基本的c ++文件/ io。现在让我解释一下这个错误。
首先,这里有3次相同的测试 -
{
const std::string direction = _dir ? "out" : "in";
const std::string path = GPIO_PATH + "/gpio" + std::to_string(powerPin) + "/direction";
std::ofstream dirStream(path.c_str(), std::ofstream::trunc);
if (dirStream) {
dirStream << direction;
} else {
// LOG error here
return false;
}
return true;
}
失败
Normal run
[isaac@alarmpi build]$ ./test/testexe
Running main() from gtest_main.cc
[==========] Running 2 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 1 test from LEDConstruction
[ RUN ] LEDConstruction.LedDevice
[ OK ] LEDConstruction.LedDevice (1 ms)
[----------] 1 test from LEDConstruction (1 ms total)
[----------] 1 test from LEDWrites
[ RUN ] LEDWrites.LedDevice
../test/test.cpp:20: Failure
Value of: led1.on()
Actual: false
Expected: true
[ FAILED ] LEDWrites.LedDevice (2 ms)
[----------] 1 test from LEDWrites (3 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 2 test cases ran. (6 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] LEDWrites.LedDevice
1 FAILED TEST
通行证
run with sudo
[isaac@alarmpi build]$ sudo ./test/testexe
[sudo] password for isaac:
Running main() from gtest_main.cc
[==========] Running 2 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 1 test from LEDConstruction
[ RUN ] LEDConstruction.LedDevice
[ OK ] LEDConstruction.LedDevice (1 ms)
[----------] 1 test from LEDConstruction (2 ms total)
[----------] 1 test from LEDWrites
[ RUN ] LEDWrites.LedDevice
[ OK ] LEDWrites.LedDevice (2 ms)
[----------] 1 test from LEDWrites (2 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 2 test cases ran. (5 ms total)
[ PASSED ] 2 tests.
PASSES 已取消注释wtf delay run
// auto b = sleep(1);
唯一的区别黑白延迟和正常运行是单个未注释的行 - [isaac@alarmpi build]$ ./test/testexe
Running main() from gtest_main.cc
[==========] Running 2 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 1 test from LEDConstruction
[ RUN ] LEDConstruction.LedDevice
[ OK ] LEDConstruction.LedDevice (1 ms)
[----------] 1 test from LEDConstruction (2 ms total)
[----------] 1 test from LEDWrites
[ RUN ] LEDWrites.LedDevice
[ OK ] LEDWrites.LedDevice (1001 ms)
[----------] 1 test from LEDWrites (1003 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 2 test cases ran. (1005 ms total)
[ PASSED ] 2 tests.
一切都是相同的,包括设备,目录结构,构建conf和一切。 解释这一点的唯一因素是linux可能会创建该文件及其朋友有时甚至需要一些时间?在此之前我打电话给// auto b = sleep(1);
。那可以解释一下......
但是为什么没有延迟的sudo调用通过?它是否使这些写入更快/更快或者是否将延迟语句单独放置?这是否是某种缓冲的原因?请说不:/
如果重要,我使用以下开发规则获取非sudo访问gpio目录 -
.on()
编辑 - 正如@charles所提到的,我在每次I / O操作写入后都使用了SUBSYSTEM=="bcm2835-gpiomem", KERNEL=="gpiomem", GROUP="gpio", MODE="0660"
SUBSYSTEM=="gpio", KERNEL=="gpiochip*", ACTION=="add", PROGRAM="/bin/sh -c 'chown root:gpio /sys/class/gpio/export /sys/class/gpio/unexport ; chmod 220 /sys/class/gpio/export /sys/class/gpio/unexport'"
SUBSYSTEM=="gpio", KERNEL=="gpio*", ACTION=="add", PROGRAM="/bin/sh -c 'chown root:gpio /sys%p/active_low /sys%p/direction /sys%p/edge /sys%p/value ; chmod 660 /sys%p/active_low /sys%p/direction /sys%p/edge /sys%p/value'"
。仍然失败。
拯救的目标
让我们看看失败的构建命令的执行 -
std::flush
open("/sys/class/gpio/export", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/unexport", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/export", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/gpio17/value", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = -1 EACCES (Permission denied)
open("/sys/class/gpio/gpio17/direction", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = -1 EACCES (Permission denied)
open("/sys/class/gpio/unexport", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
Okaaay,这里有一些东西,这解释了为什么它与sudo一起传递。但为什么它会延迟过去?我们也检查一下,
..., 0666) = -1 EACCES (Permission denied)
不等,wtf?这意味着如果当时没有创建文件,则拒绝的权限必须是。但是如何使用open("/sys/class/gpio/export", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/unexport", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/export", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/gpio17/value", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
open("/sys/class/gpio/gpio17/direction", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 4
open("/sys/class/gpio/unexport", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
解决这个问题?
这是sudo的相关输出 -
sudo
答案 0 :(得分:7)
udev与您的计划之间存在竞争。当您写入/sys/class/gpio/export
时,写入将不会返回,直到GPIO完全创建。但是,一旦创建,您就有两个进程同时对新设备执行操作:
/sys/class/gpio/gpio17/value
。/sys/class/gpio/gpio17/value
。因此,在 udev更改其所有权和权限之前,您的程序可能会打开value
文件。事实上这很可能,因为你的udev处理程序执行了一个shell的execve,然后执行chown和chmod。但即使没有这个,调度程序通常会优先考虑从系统调用返回时已经运行的任务,因此您的程序通常会在udev唤醒之前打开value
文件。
通过插入睡眠,你允许udev做它的事情。因此,为了使其健壮,您可以在打开文件之前使用access()轮询该文件。
通过给予udev更高的优先级也会有所帮助。例如。 chrt -f -p $(pidof systemd-udevd) 3
。这为udev提供了实时优先级,这意味着它将始终在您的程序之前运行。它还可以使您的系统无响应,所以要小心。
答案 1 :(得分:0)
从strace
输出
open("/sys/class/gpio/gpio17/value", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = -1 EACCES (Permission denied)
open("/sys/class/gpio/gpio17/direction", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = -1 EACCES (Permission denied)
您首先撰写value
,然后 direction
。
当然,您应该在写入值之前先设置正确的方向。
此外,您应该结束输出
if (dirStream) {
dirStream << direction;
} else {
// LOG error here
return false;
}
换行。
echo
命令还会附加换行符。
if (dirStream) {
dirStream << direction << std::endl;
} else {
// LOG error here
return false;
}
(在这种情况下,我会明确地使用std::endl
来刷新。当然只是添加'\n'
也可以,但是使flush更明确可以使代码更健壮。实际上,你是现在依赖于在写入之后流立即关闭的事实 - 如果您以后决定保持流打开直到程序结束,则可能不会。)
缺少的尾随换行符可能会解释为什么它会延迟工作:在延迟之后,驱动程序可能会将数据解释为,如果有换行符并且假设流中没有更多的字母在等待。