如何在从串口读取时实现read()的超时(C / C ++)

15

我正在使用文件描述符和 POSIX/Unix read() 函数从串行端口读取字节。在这个例子中,我从串行端口读取 1 字节(波特率设置等类似内容已被省略以保持清晰):

#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
   int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
   char buf[1];
   int bytesRead = read(fd, buf, 1);
   close(fd);
   return 0;
}
如果连接到/dev/ttyS0的设备没有发送任何信息,程序将会挂起。我该如何设置超时时间?
我尝试过这样设置超时时间:
struct termios options;
tcgetattr(fd, &options);
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 10;
tcsetattr(fd, TCSANOW, &options);

我原以为它应该提供1秒的超时,但是它没有任何作用。我认为我误解了VMIN和VTIME。VMIN和VTIME有什么用途?

后来我搜索了一下网络,发现有人谈论select()函数。那是否是解决方法,如果是,如何将其应用到上面的程序中以使超时时间为1秒?

非常感谢您的帮助。提前致谢 :-)


使用 tcsetattr()VTIME 并不直接,它需要其他模式设置,而一些串行驱动程序并不支持。请参见我的答案以获取通用解决方案。 - wallyk
这是我在网络上找到的关于VMIN和VTIME的最好解释[http://unixwiz.net/techtips/termios-vmin-vtime.html]。根据文章,当ICANON位关闭时,它会启用“原始模式”,改变VMIN和VTIME的解释方式。设置ICANON位将使代码按预期工作。 - arpl
通常情况下,当设置 options.c_cc[VMIN] = 0;options.c_cc[VTIME] = 10; 时,read() 函数应该在有一个或多个字节可用时返回,或者在超时时返回。在后一种情况下,read() 应该指示已读取 0 字节。 - hetepeperfan
5个回答

24

可以使用select(2)。将只包含您想读取的文件描述符的fd集传递到读取集中,并传递适当的超时时间,而写入/异常集为空。例如:

int fd = open(...);

// Initialize file descriptor sets
fd_set read_fds, write_fds, except_fds;
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
FD_ZERO(&except_fds);
FD_SET(fd, &read_fds);

// Set timeout to 1.0 seconds
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;

// Wait for input to become ready or until the time out; the first parameter is
// 1 more than the largest file descriptor in any of the sets
if (select(fd + 1, &read_fds, &write_fds, &except_fds, &timeout) == 1)
{
    // fd is ready for reading
}
else
{
    // timeout or error
}

非常感谢。那似乎正好符合我的要求。我还有三个问题:我可以使用select函数在打开端口时设置超时吗?我可以通过设置FD_SET(fd, &write_fds)来使用相同的代码进行写操作的超时吗?最后:except_fds用于什么? - pvh1987
2
poll() 通常比 select() 更好。 - Martin C. Martin
1
非常好!如果我在循环中运行它,我需要在每次迭代中进行FD_ZERO和FD_SET操作,还是在循环之前只需要一次就可以了? - Andy

4
VMIN和VTIME分别用于什么?
如果MIN>0且TIME=0,则在读取满足之前,MIN设置要接收的字符数。因为TIME为零,所以计时器未使用。
如果MIN=0且TIME>0,则TIME作为超时值。如果仅读取单个字符或超过时间(t=TIME * 0.1秒),则读取将被满足。如果超时,则不会返回任何字符。
如果MIN>0且TIME>0,则TIME作为字符间定时器。如果接收到MIN个字符或两个字符之间的时间超过了TIME,则读取将被满足。计时器在每次接收到字符后重新启动,在第一个字符接收后才变为活动状态。
如果MIN=0且TIME=0,则立即满足读取。将返回当前可用字符数或请求的字符数。根据Antonino(请参见贡献),您可以在读取之前发出fcntl(fd,F_SETFL,FNDELAY);以获得相同的结果。
来源:http://tldp.org/HOWTO/Serial-Programming-HOWTO/x115.html

1

您可以尝试捕获信号以停止读取操作。在读取之前使用alarm(1),如果读取函数没有返回,alarm将发送SIGALRM信号,然后您可以创建信号处理函数来捕获此信号,就像这样:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>

static jmp_buf env_alarm;

static void sig_alarm(int signo)
{
    longjmp(env_alarm, 1);
}

int main(void)
{
   int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
   char buf[1];

   if (signal(SIGALRM, sig_alarm) == SIG_ERR)
   {
       exit(0);
   }

   if (setjmp(env_alarm) != 0)
   {
      close(fd);
      printf("Timeout Or Error\n");
      exit(0);
   }

   alarm(1);
   int bytesRead = read(fd, buf, 1);
   alarm(0);

   close(fd);
   return 0;
}

但是如果你的程序很大,使用select或poll或epoll会更好。


有一个关于select的有趣案例,它检查fd是否可以写入,但当写入时会被阻塞,所以我认为在我的情况下select方法不能正常工作,但是你的方法可以很好地工作。这是一个好方法。 - kangear

0

0

有几种可能的方法。如果程序最终将计时多个i/o操作,则select()是明显的选择。

然而,如果唯一的输入来自于这个i/o,则选择非阻塞i/o和计时是一种简单直接的方法。我已经将其从单字符i/o扩展到多字符,以使其成为一个更完整的例子:

#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/time.h>

int main(void)
{
   int   fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);  // sometimes "O_NONBLOCK"
   char  buf[10];
   int   done = 0, inbuf = 0;
   struct timeval start, now;

   gettimeofday (&start, NULL);
   while (!done)
   {
       int bytesRead = read(fd, &buf[inbuf], sizeof buf - inbuf);
       if (bytesRead < 0)
       {
            error_processing_here();
            continue;
       }
       if (bytesRead == 0)  // no data read to read
       {
            gettimeofday (&now, NULL);
            if ((now.tv.sec  - start.tv_sec) * 1000000 + 
                 now.tv.usec - start.tv_usec > timeout_value_in_microsecs)
            {
                done = 2;    // timeout
                continue;
            }
            sleep(1);  // not timed out yet, sleep a second
            continue;
       }
       inbuf += bytesRead;
       if (we have read all we want)
           done = 1;
   }
   if (done == 2)
        timeout_condition_handling();
   close(fd);
   return 0;
}

这比使用select()要多得多,可以等待一秒钟后才读取数据。 - Martin C. Martin
1
@MartinC.Martin:这可能是因为我的代码是一个完整的工作示例,包括读取和错误检查,而选择示例仅显示所需内容的片段。 - wallyk
在1秒的延迟期间可能会涌入大量数据(尤其是在高波特率下),因此使用 sleep 不是一个好主意。更短的睡眠时间也可能会浪费处理器时间,这在低功耗设备中更为重要。通过使用 select 采用非轮询方法可以改进此代码。 - Isaac

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接