Почему мое приложение Go не читает из sysfs, как команда busybox `cat`?

Перейти 1.12 на Linux 4.19.93 armv6l. Аппаратное обеспечение представляет собой raspberypi zero w (BCM2835), на котором запущен образ yocto linux.

У меня есть датчик приближения SRF04, управляемый gpio, управляемый драйвером srf04 linux.

Он отлично работает с sysfs и оболочкой busybox.

# cat /sys/bus/iio/devices/iio:device0/in_distance_raw
1646

Раньше я использовал Go с устройствами IIO, поддерживающими триггеры и буферизованный вывод с высокой частотой дискретизации на этой аппаратной платформе. Однако для этого приложения драйвер srf04 не не реализовывать эти функции IIO. Драт. Мне не очень хочется добавлять в драйвер поддержку буфера/триггера (в настоящее время), так как мне не нужна «высокая» частота дискретизации. Для моей цели должно хватить нескольких пингов в секунду. Думаю, я рассчитаю среднее значение и стандартное значение. разв. для скользящего окна точек данных и «выделить» сигнал из шума.

Итак, я был бы совершенно счастлив прочитать байты из опубликованного файла sysfs с помощью Go.

Что подводит меня к сути этого поста. Когда я открываю файл для чтения и пытаюсь выполнить Read() любое количество байтов, я всегда получаю общую ошибку -EIO.

func (s *Srf04) Read() (int, error) {
    samp := make([]byte, 16)

    f, err := os.OpenFile(s.readPath, OS.O_RDONLY, os.ModeDevice)
    if err != nil {
        return 0, err
    }
    defer f.Close()

    n, err := f.Read(samp)
    if err != nil {
        // This block is always executed.
        // The error is never a timeout, and always 'input/output error' (-EIO aka -5)
        log.Fatal(err)
    }
    ...
}

Мне это кажется странным поведением. Поэтому я решил возиться с использованием io.ReadFull. Это дало ненадежные результаты.

func (s *Srf04) Read() (int, error) {
    samp := make([]byte, 16)

    f, err := os.OpenFile(s.readPath, OS.O_RDONLY, os.ModeDevice)
    if err != nil {
        return 0, err
    }
    defer f.Close()

    for {
        n, err := io.ReadFull(readFile, samp)
        log.Println("ReadFull ", n, " bytes.")
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Println(err)
        }
    }

    ...

}

Я закончил тем, что добавил его в цикл, так как обнаружил изменения в поведении от «однократного» чтения до нескольких вызовов чтения, следующих друг за другом. У меня он выходит, если он получает EOF, и неоднократно пытается прочитать иначе.

Результаты откровенно сумасшедшие, ненадежные, по-видимому, возвращающие случайные результаты. Иногда я получаю -5, иногда я читаю от 2 до 5 байтов с устройства. Иногда я получаю байты без файла eof перед EOF. Байты, по-видимому, представляют собой символьные данные для чисел (каждая руна - это руна между [0-9]) - чего я и ожидал.

В стороне: я ожидаю, что это связано с опросом файлов и реализацией ввода-вывода с блокировкой, но я не могу сказать точно.

В качестве временного обходного пути я решил попробовать использовать os.exec, и теперь я получаю ожидаемые результаты.

func (s *Srf04)Read() (int, error) {
    out, err := exec.Command("cat", s.readPath).Output()
    if err != nil  {
       return 0, err
    }
    return strconv.Atoi(string(out))
}

Но Йик. os.exec. Фу.


person bvarner    schedule 27.04.2020    source источник
comment
Почему вы открываете его как устройство?   -  person Volker    schedule 27.04.2020
comment
Поведение не отличалось, когда я этого не делал, поэтому я решил попробовать это. Я также пробовал с os.ModeDevice | os.ModeCharDevice.   -  person bvarner    schedule 27.04.2020


Ответы (2)


Я бы попытался запустить заклинание cat whatever под strace, а затем посмотреть, что на самом деле удается сделать read(2), вызывая cat (включая количество фактически прочитанных байтов), а затем я попытался бы воссоздать это поведение в Go.

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

Для начала учтите, что GNU cat — это не простое байтовое перелопачивание, а довольно хитрая часть программного обеспечения, которое, среди прочего, учитывает оптимальные размеры блоков ввода-вывода как для устройств ввода, так и для устройств вывода (если они доступны), вызывает fadvise(2) и т. д. Это не то, чтобы любой из этого фактически используется, когда вы запускаете его в файле, экспортированном sysfs, но это может повлиять на то, как работает полный стек (начиная со слоя sysfs) в случае использования cat и с вашим кодом соответственно.

Отсюда мой совет: начните с strace cat, а затем попробуйте воссоздать шаблон его использования в своем коде Go; затем попытайтесь придумать минимальное подмножество того, что работает; затем глубоко прокомментируйте свой код ;-)

person kostix    schedule 27.04.2020
comment
Отличные предложения, kostix. Как только у меня будет возможность попробовать это, я сообщу. - person bvarner; 28.04.2020
comment
Таким образом, похоже, что он выдает первоначальный Read(), который возвращает ошибку, затем сразу же следует за ним Read() и целевой буфер на 4096 байт, что завершается успешно. Между двумя Ридами не было абсолютно ничего объявлено. Это похоже на то, что он ожидал, что первое чтение завершится ошибкой, или что, если он получит -EIO, он просто автоматически повторит попытку. Я продублирую это в Go и посмотрю, что получится. - person bvarner; 29.04.2020
comment
@bvarner, я правильно понял, что первый read() сразу вернул -EIO? Интересно, какое значение имел 3-й аргумент для read(), count. - person kostix; 29.04.2020
comment
На самом деле это делается так: openat(AT_FDCWD, /sys/bus/iio/devices/iio:device2/in_distance_raw, O_RDONLY|O_LARGEFILE) = 3 sendfile64(1, 3, NULL, 16777216) = -1 EIO (вход /output error) read(3, 1367\n, 4096) = 5 write(1, 1367\n, 51367 ) = 5 read(3, , 4096) = 0 close(3) = 0 ``` Насколько я понимаю, это sendfile является копией в ядре. Вот что бомбит. Затем он успешно читает 5 байтов, записывает их на вывод и получает EOF при следующем чтении. - person bvarner; 30.04.2020
comment
@bvarner, да, sendfile позволяет в основном подключить два FD напрямую, не перелопачивая данные из одного из них в другой через пользовательское пространство (что дорого). - person kostix; 30.04.2020

Я уверен, что слишком долго смотрел на это сегодня вечером, и этот код, вероятно, ужасен. Тем не менее, вот фрагмент того, что я придумал, который работает так же надежно, как и busybox cat, но в Go.

Структура Srf04 несет в себе несколько вещей, важные части приведены ниже:

type Srf04 struct {
    readBuf     []byte `json:"-"`
    readFile    *os.File `json:"-"`
    samples     *ring.Ring `json:"-"`
}


func (s *Srf04) Read() (int, error) {
/** Reliable, but really really slow.
    out, err := exec.Command("cat", s.readPath).Output()

    if err != nil {
        log.Fatal(err)
    }

    val, err := strconv.Atoi(string(out[:len(out) - 2]))
    if err == nil {
        s.samples.Value = val
        s.samples = s.samples.Next()
    }
 */
    // Seek should tell us the new offset (0) and no err.
    bytesRead := 0
    _, err := s.readFile.Seek(0, 0)

    // Loop until N > 0 AND err != EOF && err != timeout.
    if err == nil {
        n := 0
        for {
            n, err = s.readFile.Read(s.readBuf)
            bytesRead += n
            if os.IsTimeout(err) {
                // bail out.
                bytesRead = 0
                break
            }
            if err == io.EOF {
                // Success!
                break
            }
            // Any other err means 'keep trying to read.'
        }
    }

    if bytesRead > 0 {
        val, err := strconv.Atoi(string(s.readBuf[:bytesRead-1]))
        if err == nil {
            fmt.Println(val)
            s.samples.Value = val
            s.samples = s.samples.Next()
        }
        return val, err
    }

    return 0, err
}
person bvarner    schedule 29.04.2020