Подтвердить что ты не робот

Ошибка с записью в файл в linux/sys/class/gpio

У меня самая странная ошибка, с которой я когда-либо сталкивался с системой linux, и, кажется, есть только два возможных объяснения -

  • Либо добавление sudo делает запись файлов мгновенно
  • Или добавление sudo вызывает небольшую задержку при выполнении операторов
  • Или я не знаю, что происходит с моей программой.

Хорошо, позвольте мне рассказать вам немного. В настоящее время я пишу программу С++ для малины pi gpio. Насколько я знаю, в программе нет видимой ошибки, так как она работает с 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/С++ или любого другого языка ввода-вывода, поскольку в 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. Забудьте об отсутствии здесь, поскольку он обрабатывается деструктором и не играет здесь никакой роли.

Если вам интересно, эти функции обрабатывают 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;
}

означает базовый С++ файл /io. Теперь позвольте мне объяснить ошибку.


Во-первых, здесь три прогона одного теста -

Normal run FAILS

[[email protected] 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 ПРОСМОТРЫ

[[email protected] 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 имеет раскомментированный // auto b = sleep(1);

[[email protected] 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.

Единственное различие между задержкой и стандартным запуском - единственная строка без комментирования - // auto b = sleep(1); Все одинаково, включая устройство, структуру каталогов, сборку conf и все. Единственное, что объясняет это: 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, я использовал std::flush после каждой записи, сделанной мной в операциях ввода-вывода. Все еще не работает.


Strace на помощь


Посмотрите на выполнение команды сбоя -

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)

Окаай, вот что-то, что объясняет, почему он проходит с судо. Но почему это происходит с задержкой? Пусть также проверьте,

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

Нет, подождите, 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
4b9b3361

Ответ 1

Существует гонка между udev и вашей программой. Когда вы пишете на /sys/class/gpio/export, запись не вернется, пока GPIO не будет полностью создан. Однако, как только он был создан, у вас есть два процесса, которые одновременно принимают меры на новом устройстве:

  • Функция hotplug/uevent запускает udev для оценки своих правил. В рамках этих правил он изменит права собственности и разрешения /sys/class/gpio/gpio17/value.
  • Ваша программа продолжается. Он немедленно попытается открыть /sys/class/gpio/gpio17/value.

Таким образом, есть вероятность, что ваша программа откроет файл value до того, как udev изменит права собственности и разрешения. Это на самом деле очень вероятно, потому что ваш обработчик udev выполняет команду оболочки, которая затем выполняет chown и chmod. Но даже без этого планировщик, как правило, отдаст приоритет заданию, которая уже была запущена при возврате из системного вызова, поэтому ваша программа обычно открывает файл value до того, как udev даже проснулся.

Вставляя сон, вы разрешаете udev выполнять свою работу. Чтобы сделать его надежным, вы можете опросить файл с доступом(), прежде чем открывать его.

Это также помогло бы, придав udev более высокий приоритет. Например. chrt -f -p $(pidof systemd-udevd) 3. Это дает приоритет udev в реальном времени, что означает, что он всегда будет работать до вашей программы. Это также может сделать вашу систему невосприимчивой, поэтому будьте осторожны.

Ответ 2

Из вывода 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' также работает, но делая явную флеш-память делает код более надежным. Как бы то ни было, теперь вы полагаетесь на факт, что поток закрывается сразу после записи, что может и не быть, если позже вы решите сохранить поток открытым до конца вашей программы.)

Недопустимая конечная новая строка может объяснить, почему она работает с задержкой: после этой задержки драйвер может интерпретировать данные так, как если бы была новая строка, и предполагает, что в потоке не ожидается больше писем.