在linux/sys/class/gpio中写入文件的错误

Abh*_*yal 10 c++ linux sudo file c++11

我现在有一个我曾经见过的最糟糕的bug系统,它似乎只有两种可能的解释 -

  • 附加sudo使文件写入即时
  • 或者追加sudo会在执行语句时产生短暂的延迟
  • 或者我不知道我的程序发生了什么

好吧,让我给你一些背景知识.我正在为raspberry pi gpio操作编写一个c ++程序.据我所知,程序中没有明显的错误,因为它成功地与sudo一起工作,并且也成功地延迟了.所以这里是rpi的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());
}
Run Code Online (Sandbox Code Playgroud)

Led类constructor执行导出部分 - echo 17 > /sys/class/gpio/export.on()调用设置方向时 - echo "write" > /sys/class/gpio/gpio17/direction并输出值 - echo 1 > /sys/class/gpio/gpio17/value.忘掉这里的devport,因为它是由析构函数处理的,在这里不起作用.

如果你很好奇,这些函数会像这样处理I/O -

{
    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;
}
Run Code Online (Sandbox Code Playgroud)

表示基本的c ++文件/ io.现在让我解释一下这个bug.


首先,这里有3次相同的测试 -

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 Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)

wtf delay run PASSES 没有注释// 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.
Run Code Online (Sandbox Code Playgroud)

b/w延迟和正常运行的唯一区别在于单个未注释的行 - // auto b = sleep(1);一切都相同,包括设备,目录结构,构建conf和一切.解释这一点的唯一原因是linux有时可能会创建该文件及其朋友,或者需要一些时间?我.on()之前打电话给他.那可以解释一下......

但是为什么sudo调用没有延迟通过?它是否使这些写入更快/更快或是否自己放置延迟语句?这是某种缓冲的原因吗?请说不:/

如果重要的话,我正在使用以下开发规则获取非sudo访问gpio目录 -

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'"
Run Code Online (Sandbox Code Playgroud)

编辑 - 正如@charles所提到的,我std::flush在I/O操作上每次写入后都使用过.仍然失败.


需要救援


让我们看看失败的构建命令的执行 -

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
Run Code Online (Sandbox Code Playgroud)

..., 0666) = -1 EACCES (Permission denied)

Okaaay,这是什么,这解释了为什么它与sudo一起传递.但为什么它会延迟过去?让我们检查一下,

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
Run Code Online (Sandbox Code Playgroud)

没等了,wtf?这意味着如果当时未创建文件,则拒绝权限必须是.但是如何使用sudo解决方案呢?

这是sudo的相关输出 -

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
Run Code Online (Sandbox Code Playgroud)

Arn*_*out 8

udev和你的程序之间有一场比赛.写入时/sys/class/gpio/export,写入将不会返回,直到GPIO完全创建.但是,一旦创建,您就有两个进程同时对新设备执行操作:

  • hotplug/uevent触发udev来评估其规则.作为这些规则的一部分,它将改变其所有权和权限/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提供了实时优先级,这意味着它将始终在您的程序之前运行.它还可以使您的系统无响应,所以要小心.