【问题标题】:Best Practise to Read Desired Amount of Data from Serial?从串行读取所需数据量的最佳实践?
【发布时间】:2021-03-21 14:04:01
【问题描述】:

由于read 只会从串行读取所有可用数据,this answer 建议使用while 循环等待直到读取所需长度的数据。但是 AFAIK 系统调用很昂贵,因此,这种方法是不是有点粗糙,尤其是当所需的长度很大时?我知道这是否会导致性能问题取决于实际应用程序,我不应该过早优化。我只是好奇在这种情况下是否有更好的做法来避免密集的系统调用?

【问题讨论】:

    标签: c linux serial-port


    【解决方案1】:

    但是 AFAIK 系统调用很昂贵,...

    的确,系统调用比本地过程/函数调用消耗更多的 CPU 周期。系统调用需要在用户模式和(受保护的)内核模式之间进行 CPU 模式转换,然后再返回用户模式。

    ...因此,这种方法是不是有点粗糙,尤其是当所需的长度很大时?

    从串行终端(例如/dev/ttyXn设备)(而不是串行端口)读取时,您必须问自己的第一个问题) 是“将要接收什么样的数据,即(ASCII)文本的数据lines 是否以某种类型的 EOL(行尾)字符终止,或者是否需要将数据简单地视为二进制(或原始)数据?"

    应使用规范(又名熟)模式从串行终端读取文本行。操作系统将对您的程序接收到的数据执行词法扫描,并根据您指定的 EOL 字符分隔每个 line 文本。 然后,read() 可以返回一行,假设使用了阻塞 I/O,并且该行文本不长于所提供的缓冲区。

    应使用非规范(又名原始)模式从串行终端读取二进制数据。操作系统将忽略数据的值,并且(当使用阻塞 I/O 时)每个 read() 将根据时间和字节数的限制返回一定量的数据。

    this answer for more details

    请注意,您的问题实际上是关于阅读文本的帖子,但 OP 是(错误)使用非规范模式。如果 OP 使用了正确的模式来匹配输入,那么他可能永远不会遇到部分读取问题。


    我只是好奇在这种情况下是否有更好的做法来避免密集的系统调用?

    正确的 termios 配置对于使用串行终端的高效 I/O 至关重要。

    阻塞 I/O 模式应被视为首选模式。
    当进程更频繁地放弃控制时,操作系统可以更好地执行多任务调度。
    操作系统可以更有效地确定何时可以将数据返回给用户。
    另请注意,在使用阻塞模式时,termios 配置最有效,例如非规范模式下的 VMIN 和 VTIME 规范。

    例如,使用 select()poll() 然后使用 read() 比使用只是(阻塞)read()。但是您可以找到许多这样的代码示例,因为似乎存在一些流行的误解,即程序可以通过这种方式更快地从“UART”获取数据。
    但非阻塞和异步模式不一定更快(在多任务操作系统中),read() 只是从与实际硬件相隔数层的 termios 缓冲区中获取数据。

    如果您的程序使用非阻塞模式但在等待数据时没有执行有用的工作,而是使用 select()poll()(甚至更糟调用 sleep()),那么你的程序就会变得不必要地复杂和低效。见this answer
    阻塞模式 read() 可以完成所有等待您的程序的工作,使您的程序更简单、更易于编写和维护,并且运行时效率更高。

    但是,对于阻止非规范读取,您将不得不接受某种程度的低效率。您可以做的最好的事情是权衡延迟与系统调用的数量。一个可能的例子是this answer,它试图在每个系统调用中获取尽可能多的数据,但允许对接收到的二进制数据进行简单的逐字节词法扫描。


    请注意,读取串行终端时延迟的可能来源是配置不当的内核,而不是 termios API 和 read() 开销。
    例如,通过 ioctl() 设置 ASYNC_LOW_LATENCY 标志(例如,参见 High delay in RS232 communication on a PXA270)是改善 read() 延迟的一种方法。

    【讨论】:

    • 非常感谢您详细而深刻的回答。我必须处理的实际任务向我提出了这个问题,是读取具有特殊帧格式的二进制数据,它有一个尾字节来指示帧的结束。但是,帧的主体字节可以采用任何值,包括 0x00。我想知道在这种情况下使用将 VEOF 设置为尾字节的规范模式是否合适?
    • 这不是一个好主意;您必须正确取消配置所有其他规范功能,并且仍然必须检测并处理“短”读取(即不完整的消息)。 (当我写这个答案时,我想知道是否有人/你会考虑这样的方案。)更简单的 IMO 使用类似于我链接到的缓冲方案的东西。消息检测思路见stackoverflow.com/questions/16177947/…
    【解决方案2】:

    为避免密集的系统调用调用,您可以使用 sleep() 系统函数。

    从串口读取数据通常是线程业务。

    线程休眠 100 毫秒,然后从串口读取数据,并将它们放入 FIFO 队列中。

    【讨论】:

    • 这个解决方案对我来说毫无意义。 sleep() 不是必需的,因为 read() 可以实现相同的目的。 Linux 文件描述符(无论是文件、管道还是串行连接)已经提供了 FIFO。那为什么还要再复制一个 FIFO 呢?
    • 将应用程序拆分为 2 个线程。一个从串口读取数据,另一个是应用程序的其余部分。 2 个线程通过 OS FIFO 服务进行通信。
    • 如果您使用 Linux FIFO,应用程序的其余部分无论是从串行端口还是从 FIFO 读取,看起来都完全相同。如果是从串口读取,则可以简单的去掉串口线程和相关代码。我没有看到该线程的任何附加值。它当然不会减少 CPU 时间或内存消耗(OP 担心)。
    【解决方案3】:

    按照当今的标准,串行连接(源自 RS-232 标准)是非常慢的连接。它们通常不支持高于 1 Mbps 的数据速率,而 USB 4 的数据速率高达 20 Gbps。因此,在使用串行连接时,系统主要处于等待状态。关键是要避免忙等待,即如果没有数据到达,系统不应该在串行连接上花费任何时间。

    链接代码在此失败。由于它在打开端口时设置了O_NDELAY 标志,read() 不会阻塞,而是在没有可用数据时立即返回 0。这可能会吸收 100% 的单个 CPU。是否涉及系统调用无关紧要。循环尽可能快且频繁地运行,直到新数据到达。它缺少一种不浪费 CPU 时间的等待机制。

    最简单的解决方案是不设置O_NDELAY 标志。如果没有可用数据,read() 将阻塞。而且它完全不需要花费任何 CPU 时间。一旦有新数据到达,Linux 会唤醒线程或进程。

    如果阻止不是一个选项,还有许多其他选项,但它们取决于您的其余代码,我们对此一无所知。

    此外,我建议对像 系统调用很昂贵这样的语句要非常小心。在这种形式下,陈述是错误的。当然,任何事情在 CPU 时间、内存空间、有效时间等方面都是有代价的。但是没有任何数字或相对于替代操作而言,它弊大于利。

    【讨论】:

      猜你喜欢
      • 2016-03-04
      • 1970-01-01
      • 2013-04-21
      • 1970-01-01
      • 1970-01-01
      • 2019-01-06
      • 1970-01-01
      • 1970-01-01
      • 2016-08-26
      相关资源
      最近更新 更多