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

Bug with writing to file in linux /sys/class/gpio

本文关键字:文件 错误 gpio linux sys class      更新时间:2023-10-16

我现在遇到了我在linux系统中见过的最奇怪的错误,似乎只有两种可能的解释-

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

让我给你介绍一些背景知识。我目前正在编写一个c++程序的树莓派gpio操作。据我所知,程序中没有明显的错误&因为它可以成功地与sudo一起工作,也可以成功地与延迟一起工作。下面是rpi的gpio是如何工作的

  • 首先你必须导出一个,为了保留操作,它将创建一个新目录gpio+number,其中包含几个文件。

    echo 17 > /sys/class/gpio/export

  • 然后设置方向(in表示读,out表示写)

    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并输出值- echo 1 > /sys/class/gpio/gpio17/value。忘记这里的unexport吧,因为它是由析构函数处理的,在这里不起作用。

如果你很好奇,这些函数是这样处理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;
}

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


首先,这里有3个相同的测试运行-

Normal run FAILS

[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 PASSES

[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.

wtf delay run PASSES has uncomment // 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.

唯一的区别b/w延迟和正常运行是一个没有注释的行- // auto b = sleep(1);一切都是一样的,包括设备,目录结构,构建配置和一切。唯一能解释这一点的是linux可能会稍后创建该文件和它的朋友,或者需要一些时间?在此之前我调用.on()这就可以解释了…

但是为什么sudo调用没有延迟通过?它是使那些写更快/即时还是单独放置延迟语句?这是某种缓冲的原因吗?请说不:/

如果有关系,我使用以下dev规则来获得非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'"

EDIT -正如@charles所提到的,我在每次写I/O操作后都使用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

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

好的,这里有一些东西,解释了为什么它是用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

不,等等,怎么了?这意味着,如果当时没有创建文件,则必须拒绝该权限。但如何使用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

udev和您的程序之间存在竞争。当您写入/sys/class/gpio/export时,在GPIO完全创建之前,写操作不会返回。然而,一旦它被创建,你有两个进程同时对新设备采取行动:

    一个热插拔/uevent触发udev评估它的规则。作为这些规则的一部分,它将改变/sys/class/gpio/gpio17/value的所有权和权限。
  • 程序继续。它将立即尝试打开/sys/class/gpio/gpio17/value

因此,在 udev更改其所有权和权限之前,您的程序有可能打开value文件。这实际上是很有可能的,因为您的udev处理程序执行shell,然后执行chown和chmod。但是,即使没有这样做,调度程序通常也会在从系统调用返回时优先考虑已经在运行的任务,因此您的程序通常会在udev唤醒之前打开value文件。

通过插入一个sleep,你允许udev做它的事情。因此,为了使其健壮,您可以在打开文件之前使用access()轮询该文件。

给udev更高的优先级也会有所帮助。例:chrt -f -p $(pidof systemd-udevd) 3。这给了udev实时优先级,这意味着它总是在程序之前运行。

从您的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'也可以,但是显式刷新会使代码更加健壮。实际上,您现在依赖于这样一个事实,即流在写入后立即关闭——如果您以后决定将流保持打开状态直到程序结束,则可能不会关闭。

后面缺少换行符可以解释为什么它与延迟一起工作:在该延迟之后,驱动程序可能会将数据解释为好像有换行符,并假设流中没有更多的字母等待。