Как обнаружить отключение USB-кабеля при блокировке вызова read()?

У меня есть умный счетчик энергии, который отправляет данные о потреблении энергии каждую секунду. Программа-демон, которую я написал (C++/C Arch Linux) для чтения данных, не завершает работу при отключении USB-кабеля и останавливается на неопределенное время в блокирующем вызове read().

Как прервать блокирующий вызов read() (т.е. сбой с EINTR вместо ожидания следующего символа)?

Я тщательно искал Google и смотрел здесь, в SO, и не смог найти ответ на эту проблему.

Подробности:

  • Источник проекта Smartmeter на Github
  • ИК-ключ с мостом FT232RL USB-UART
  • Дейтаграммы имеют фиксированную длину 328 байт, отправляемых каждую секунду.
  • Метод чтения обнаруживает начало \ и конец! маркеры дейтаграммы
  • sigaction для перехвата сигналов CTRL+C SIGINT и SIGTERM
  • termios настроен на блокировку read() с VMIN = 1 и VTIME = 0.

Пытался:

  • Игра с VMIN и VTIME
  • Удалено SA_RESTART

Возможное решение:

  • Используйте неблокирующий метод чтения, возможно, с помощью select() и poll()
  • Или VMIN › 0 (датаграмма длиннее 255 символов, и мне нужно будет прочитать дейтаграмму более мелкими фрагментами)
  • Не знаете, как обрабатывать обнаружение начала/конца дейтаграммы и интервал в одну секунду между дейтаграммами для неблокирующего метода чтения.

EDIT: приведенный ниже код теперь буферизует вызов read() в промежуточный буфер размером 255 байт (VMIN = 255 и VTIME = 5), адаптированный из здесь. Это позволяет избежать небольших накладных расходов на вызов read() для каждого символа. На практике это не имеет значения по сравнению с чтением по одному символу за раз. Read() по-прежнему не завершается корректно при отключении кабеля. Демон должен быть убит с помощью kill -s SIGQUIT $PID. SIGKILL не имеет никакого эффекта.

основной.cpp:

volatile sig_atomic_t shutdown = false;

void sig_handler(int)
{
  shutdown = true;
}

int main(int argc, char* argv[])
{
  struct sigaction action;
  action.sa_handler = sig_handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags = SA_RESTART;
  sigaction(SIGINT, &action, NULL);
  sigaction(SIGTERM, &action, NULL);

  while (shutdown == false)
  {
      if (!meter->Receive())
      {
        std::cout << meter->GetErrorMessage() << std::endl;
      return EXIT_FAILURE;
      }
  }

Smartmeter.cpp:

bool Smartmeter::Receive(void)
{
  memset(ReceiveBuffer, '\0', Smartmeter::ReceiveBufferSize);  
  if (!Serial->ReadBytes(ReceiveBuffer, Smartmeter::ReceiveBufferSize)) 
  {
    ErrorMessage = Serial->GetErrorMessage();
    return false;
  }
}

SmartMeterSerial.cpp:

#include <cstring>
#include <iostream>
#include <thread>
#include <unistd.h>
#include <termios.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include "SmartmeterSerial.h"

const unsigned char SmartmeterSerial::BufferSize = 255;

SmartmeterSerial::~SmartmeterSerial(void)
{
  if (SerialPort > 0) {
    close(SerialPort);
  }
}

bool SmartmeterSerial::Begin(const std::string &device)
{
  if (device.empty()) {
    ErrorMessage = "Serial device argument empty";
    return false;
  }
  if ((SerialPort = open(device.c_str(), (O_RDONLY | O_NOCTTY))) < 0)
  {
    ErrorMessage = std::string("Error opening serial device: ") 
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }
  if(!isatty(SerialPort))
  {
    ErrorMessage = std::string("Error: Device ") + device + " is not a tty.";
    return false;
  }
  if (flock(SerialPort, LOCK_EX | LOCK_NB) < 0)
  {
    ErrorMessage = std::string("Error locking serial device: ")
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }
  if (ioctl(SerialPort, TIOCEXCL) < 0)
  {
    ErrorMessage = std::string("Error setting exclusive access: ") 
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }

  struct termios serial_port_settings;

  memset(&serial_port_settings, 0, sizeof(serial_port_settings));
  if (tcgetattr(SerialPort, &serial_port_settings))
  {
    ErrorMessage = std::string("Error getting serial port attributes: ")
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }

  cfmakeraw(&serial_port_settings);

  // configure serial port
  // speed: 9600 baud, data bits: 7, stop bits: 1, parity: even
  cfsetispeed(&serial_port_settings, B9600);
  cfsetospeed(&serial_port_settings, B9600);
  serial_port_settings.c_cflag |= (CLOCAL | CREAD);
  serial_port_settings.c_cflag &= ~CSIZE;
  serial_port_settings.c_cflag |= (CS7 | PARENB);
  
  // vmin: read() returns when x byte(s) are available
  // vtime: wait for up to x * 0.1 second between characters
  serial_port_settings.c_cc[VMIN] = SmartmeterSerial::BufferSize;
  serial_port_settings.c_cc[VTIME] = 5;

  if (tcsetattr(SerialPort, TCSANOW, &serial_port_settings))
  {
    ErrorMessage = std::string("Error setting serial port attributes: ") 
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }
  tcflush(SerialPort, TCIOFLUSH);

  return true;
}

char SmartmeterSerial::GetByte(void)
{
  static char buffer[SmartmeterSerial::BufferSize] = {0};
  static char *p = buffer;
  static int count = 0;   

  if ((p - buffer) >= count)
  {
    if ((count = read(SerialPort, buffer, SmartmeterSerial::BufferSize)) < 0)
    {
      // read() never fails with EINTR signal on cable disconnect   
      ErrorMessage = std::string("Read on serial device failed: ")
        + strerror(errno) + " (" + std::to_string(errno) + ")";
      return false;
    }
    p = buffer;
  }
  return *p++;
}

bool SmartmeterSerial::ReadBytes(char *buffer, const int &length)
{
  int bytes_received = 0;
  char *p = buffer;
  bool message_begin = false;
  
  tcflush(SerialPort, TCIOFLUSH);
  
  while (bytes_received < length)
  {
    if ((*p = GetByte()) == '/')
    {
      message_begin = true;
    }
    if (message_begin)
    {
      ++p;
      ++bytes_received;
    }
  }
  if (*(p-3) != '!')
  {
    ErrorMessage = "Serial datagram stream not in sync.";
    return false;
  }
  return true;
}

Большое спасибо за вашу помощь.


person apohl    schedule 19.05.2021    source источник
comment
Это дубликат этой статьи? Определение отключения символьного устройства в Linux с помощью termios api (c++)   -  person kunif    schedule 19.05.2021
comment
Действительно ли работает пересмотренный код в этой статье? Какова настройка VMIN и VTIME, то есть блокировка или неблокировка чтения? Я видел это, но это не было убедительным для меня.   -  person apohl    schedule 19.05.2021
comment
Это также, кажется, менее напрямую связано с вашим вопросом, но есть другая статья. Вызов чтения() в Linux не возвращает ошибку, когда я отключаю последовательный кабель [дубликат]   -  person kunif    schedule 19.05.2021
comment
Я тоже видел этот. Но где же решение в этом ТАК? Как программа, использующая termios и выполняющая блокировку read(), возвращает значение в случае ошибки? Моя программа завершается только после read() получения другого байта, а не в блокирующем вызове чтения. В случае, если кабель отключен, он не может получить еще один байт и, следовательно, останавливается навсегда. Есть ли какое-нибудь решение для этого?   -  person apohl    schedule 19.05.2021
comment
Возможно, вы задаете вопрос XY. Почему кабель USB отсоединен является проблемой для вашего демона? Как насчет стабильного USB-соединения с хостом, но счетчик не работает и перестает отправлять данные; как это обрабатывается? Это действительно состояние ошибки?   -  person sawdust    schedule 20.05.2021
comment
@sawdust Если счетчик выходит из строя, он в настоящее время не обрабатывается. Мой сетевой концентратор, где счетчик подключен к Odroid, не подключен к ИБП, поэтому в этом случае весь дом потеряет питание, и все равно все будет отключено. Но вы правы с вопросом XY, если нет простого способа определить, был ли ключ физически отключен, я оставлю это так и буду жить с этим. Однако в настоящее время, когда кабель отключен, я могу выйти только с помощью kill -s SIGQUIT $PID, что раздражает.   -  person apohl    schedule 20.05.2021


Ответы (1)


Хотя приведенный ниже код не является решением исходного вопроса о том, как прервать блокирующий вызов read(), по крайней мере, для меня это был бы действенный обходной путь. С VMIN = 0 и VTIME = 0 теперь это неблокирующий метод read():

bool SmartmeterSerial::ReadBytes(char *buffer, const int &length)
{
  int bytes_received = 0;
  char *p = buffer;
  tcflush(SerialPort, TCIOFLUSH);
  bool message_begin = false;
  const int timeout = 10000;
  int count = 0;
  char byte;

  while (bytes_received < length) 
  {
    if ((byte = read(SerialPort, p, 1)) < 0)
    {
      ErrorMessage = std::string("Read on serial device failed: ")
        + strerror(errno) + " (" + std::to_string(errno) + ")";
      return false;
    }
    if (*p == '/')
    {
      message_begin = true;
    }
    if (message_begin && byte)
    {
      ++p;
      bytes_received += byte;
    }
    if (count > timeout)
    {
      ErrorMessage = "Read on serial device failed: Timeout";
      return false;
    }
    ++count;
    std::this_thread::sleep_for(std::chrono::microseconds(100));
  }

  if (*(p-3) != '!')
  {
    ErrorMessage = "Serial datagram stream not in sync.";
    return false;
  }
  return true;
}

Тем не менее, мне все еще любопытно узнать, действительно ли возможно прервать блокировку `read()`, поскольку этот обходной путь постоянно опрашивает последовательный порт.

Я считаю, что чтение одного символа за раз не является проблемой, поскольку полученные байты от UART буферизуются ОС, но постоянный опрос буфера с помощью read() есть! Может быть, я попробую ioctl(SerialPort, FIONREAD, &bytes_available) перед read(), хотя я не знаю, будет ли это на самом деле иметь значение.

Какие-либо предложения?

person apohl    schedule 19.05.2021
comment
этот обходной путь постоянно опрашивает последовательный порт — он опрашивает системный буфер (а не аппаратное обеспечение), как вы позже упомянете. Я считаю, что чтение одного символа за раз не является проблемой... -- Это системный вызов, поэтому есть накладные расходы, например. переключение режимов процессора. Это неэффективно. Использование FIONREAD ioctl — это просто другой системный вызов с аналогичными накладными расходами. - person sawdust; 19.05.2021
comment
Таким образом, чтение буфера UART по частям будет лучше похоже на Как обрабатывать буферизацию серийные данные? Вот почему я предпочитаю блокировку read() - она ​​просто ждет, пока не будет получен символ. Вызов read() 368 раз не так плох, как 10000 раз. - person apohl; 19.05.2021
comment
@sawdust 6,7 % процессорного времени одного потока для небуферизованного и неблокирующего против незначительного процессорного времени для небуферизованного и блокирующего. Блокировка read() — явный победитель! - person apohl; 19.05.2021
comment
Итак, чтение буфера UART... -- Нет, read() удаляет несколько уровней из аппаратного обеспечения UART и извлекает байты из буфера последовательного терминала. См. последовательные драйверы Linux. Конечно, блокирование чтения более эффективно. Не опрашивайте события и оставьте планирование ОС. - person sawdust; 19.05.2021
comment
@sawdust Итак, какой вы предлагаете подход к чтению сообщения фиксированной длины в 328 байтов с задержкой в ​​одну секунду между сообщениями? Я думаю, что буду придерживаться подхода с блокировкой read(), опубликованного в вопросе темы, но, возможно, буду читать сообщение кусками, скажем, по 100 байтов, а не по одному символу за раз. Единственное, чего мне не хватает, - это изящно выйти из демона при отключении USB-кабеля, для которого я еще не нашел решения. - person apohl; 20.05.2021
comment
Откуда это чтение сообщения кусками, скажем, по 100 байт? При неканоническом чтении вы не можете надежно получить одну полную дейтаграмму за read(). Вы можете попробовать, но ваша программа все равно должна проверить сообщение и быть способной к повторной сборке фрагментов. Пример кода буферизации, на который вы ссылались, может сделать это. Попробуйте с VMIN=255 и VTIME=5. Сосредоточение внимания только на отсоединении USB-кабеля недальновидно; есть большая проблема. - person sawdust; 20.05.2021
comment
Пример кода буферизации, на который вы ссылались, может сделать это. Попробуйте это с VMIN=255 и VTIME=5: теперь это реализовано, и это не влияет на использование ЦП. Обе версии — с блокировкой и буферизацией (чтение 255 символов) по сравнению с блокировкой и небуферизацией (чтение 1 символа) потребляют 0,0 % циклов ЦП. Я останавливаюсь здесь и иду дальше - если кто-то не сможет опубликовать решение исходного вопроса. - person apohl; 20.05.2021