【问题标题】:How to detect USB cable disconnect in blocking read() call?如何在阻塞 read() 调用中检测 USB 电缆断开连接?
【发布时间】:2021-08-08 08:25:22
【问题描述】:

我有一个智能电表,它每秒发送一次能耗数据。我编写的用于读取数据的守护程序(C++/C Arch Linux)在 USB 电缆断开时不会退出,并且在阻塞的 read() 调用中无限期停止。

如何中断阻塞的 read() 调用(即以 EINTR 失败而不是等待下一个字符)?

我在 Google 上进行了广泛的搜索,并在 SO 中查看了此处,但找不到此问题的答案。

详情:

  • Smartmeter Github 上的项目源码
  • 带有 FT232RL USB 转 UART 桥接器的红外加密狗
  • 数据报的固定长度为每秒发送 328 个字节
  • Read 方法检测到开头 \ 和结尾!数据报的标记
  • sigaction 捕获 CTRL+C SIGINT 和 SIGTERM 信号
  • termios 设置为使用 VMIN = 1 和 VTIME = 0 执行阻塞 read()。

试过了:

  • 使用 VMIN 和 VTIME
  • 已删除 SA_RESTART

可能的解决方案:

  • 使用非阻塞读取方法,可能使用 select() 和 poll()
  • 或 VMIN > 0(数据报超过 255 个字符,我需要以更小的块读取数据报)
  • 不确定如何处理数据报开始/结束检测以及非阻塞读取方法的数据报之间的一秒间隔

编辑:下面的代码现在将 read() 调用缓冲到一个 255 字节的中间缓冲区(VMIN = 255 和 VTIME = 5),改编自 here。这避免了为每个字符调用 read() 的小开销。实际上,与一次读取一个字符相比,这并没有什么不同。 Read() 仍然没有在电缆断开时正常退出。守护进程需要用kill -s SIGQUIT $PID 杀死。 SIGKILL 无效。

main.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;
}

非常感谢您的帮助。

【问题讨论】:

  • 本文修改后的代码真的有效吗? VMIN 和 VTIME 的设置是什么,即阻塞或非阻塞读取?我看过这个,但对我来说不是结论。
  • 这似乎也与您的问题不太直接相关,但还有另一篇文章。 Linux read() call not returning error when i Unplug serial cable [duplicate]
  • 我也看过这个。但是这个 SO 中的解决方案在哪里?如果出现错误,使用 termios 并执行阻塞 read() 的程序如何返回?我的程序仅在 read() 收到另一个字节后终止,而不是在阻塞读取调用中。如果电缆断开连接,它将无法接收另一个字节,因此永远停止。有什么解决办法吗?
  • 您可能会问一个 XY 问题。为什么“USB 电缆已断开” 对您的守护程序来说是个问题?与主机的稳定 USB 连接如何,但仪表失败并停止发送数据;那是怎么处理的?这真的是错误情况吗?

标签: serial-port timeout usb blocking termios


【解决方案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() 不断轮询缓冲区是!也许我会在read() 之前尝试ioctl(SerialPort, FIONREAD, &amp;bytes_available),尽管我不知道这是否真的会有所作为。

有什么建议吗?

【讨论】:

  • “这个解决方法是不断地轮询串行端口” -- 它轮询系统缓冲区(而不是硬件),正如您稍后提到的那样。 “我相信一次读取一个字符不是问题......” -- 这是一个系统调用,所以有开销,例如切换 CPU 模式。这是低效的。使用 FIONREAD ioctl 只是一个不同的系统调用,具有相似的开销。
  • 所以分块读取UART缓冲区会更好类似于How to handle buffering serial data?这就是为什么我更喜欢阻塞 read() - 它只是等到接收到一个字符。调用 read() 368 次并不像 10000 次那么糟糕。
  • @sawdust 6.7 % 非缓冲和非阻塞的单线程 CPU 时间vs. 非缓冲和阻塞的可忽略的 CPU 时间。阻止 read() 是明显的赢家!
  • "So reading the UART buffer ..." -- 不,read() 是从 UART 硬件中移除了几层,并获取来自串行终端缓冲区的字节。见Linux serial drivers 当然阻塞读效率更高。不要轮询事件并将调度留给操作系统。
  • @sawdust 那么您建议的读取 328 字节的固定长度消息的方法是什么,消息之间有 1 秒的延迟?我想我会坚持使用主题问题中发布的阻塞 read() 方法,但可能会以 100 个字节的块而不是一次一个字符的形式读取消息。我唯一缺少的是在 USB 电缆断开连接时优雅地退出守护程序,我还没有找到解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-07
  • 1970-01-01
  • 2017-03-16
相关资源
最近更新 更多