Search code examples
clinuxsocketsuartposix-select

C: Wait for n characters on a blocking socket with timeout


I need to wait for n bytes of data (count is known) on a serial port or socket on Linux. Currently I use a loop with poll, measure the time and decrement the timeout:

static int int_read_poll(int fd, uint8_t *buffer, size_t count, int timeout)
{
    struct pollfd pfd;
    int rc;

    pfd.fd = fd;
    pfd.events = POLLIN;

    rc = poll(&pfd, 1, timeout);
    if (rc < 0) {
        perror("poll");
        return 0;
    }

    if (rc > 0) {
        if (pfd.revents & POLLIN) {
            rc = read(fd, buffer, count);
            return rc;
        }
    }
    return 0;
}

static int int_read_waitfor(int fd, uint8_t *buffer, size_t count, int timeout)
{
    int rc;
    struct timespec start, end;
    int delta_ms;
    int recv = 0;

    do {
        clock_gettime(CLOCK_MONOTONIC_RAW, &start);
            rc = int_read_poll(fd, buffer + recv, count - recv, timeout);
        clock_gettime(CLOCK_MONOTONIC_RAW, &end);
        delta_ms = (end.tv_nsec - start.tv_nsec) / 1000000;

        if (!rc || (rc < 0)) return 0;
        recv += rc;
        timeout -= delta_ms;

        if (timeout <= 0)
            return 0;

    } while (recv != count);
    return recv;
}

On a serial port, poll returns on each single byte and causes many iterations.

Is there a more elegant way to solve that problem?

I am aware that depending on the baudrate, timeout might not decrement in that code portion. Counting nanoseconds might be a better approach.


Solution

  • Thanks to all for your valuable hints!

    After some testing, I finally decided not to use signals as they may interfere with the application once I port my functions into a library or publish them as source.

    I eventually found a neat solution which uses poll and termios (only four syscalls):

    static int int_read_waitfor(int fd, uint8_t *buffer, size_t count, int timeout)
    {
        struct termios tm;
        struct pollfd pfd;
        int rc;
    
        tcgetattr(fd, &tm);
        tm.c_cc[VTIME] = 1; // inter-character timeout 100ms, valid after first char recvd
        tm.c_cc[VMIN] = count; // block until n characters are read
        tcsetattr(fd, TCSANOW, &tm);
    
        pfd.fd = fd;
        pfd.events = POLLIN;
    
        rc = poll(&pfd, 1, timeout);
        if (rc > 0) {
            rc = read(fd, buffer, count);
            if (rc == -1) {
                perror("read");
                return 0;
            }
            return rc;
        }
    
        return 0;
    }
    

    Unlike network sockets which are usually packet based, serial ports (n.b.: in non-canonical mode) are character based. It is expected that a loop with poll is iterated for every arriving character, in particular at low baud rates.

    I my application I send a comand over a serial line to a device and wait for an answer. If no answer is received, a timeout will occur and maybe we'll do a retry.

    The termios option "VMIN" is handy as I can specify how many characters I like to reveive. Normally read would block until n chars have arrived.

    If there is no answer, the command will block forever.

    The termios option "VTIME" in conjunction with VMIN > 0 is specifying the intercharacter timeout in deciseconds (1 = 100ms). This is handy but the timeout will start only after reception of the first character. Otherwise an intercharacter timeout would make no sense.

    So if I would use only termios options, read would block of the slave serial device is dead.

    To circumvent that problem, I use poll in front of read. Once the first character has arrived (poll returns with rc=1), I start reading. "VTIME" is active as well and will enforce the intercharacter time of 100ms (the lowest possible setting).

    As a bonus the timeout handling is optimized:

    Lets assume a timeout of 400ms

    • If the slave device is dead, poll will return after 400ms
    • If the slave works and replies within 50ms (first character), poll returns and read starts. If the slave sends too few data, VTIME will kick in and stop reception after 50ms + 100ms. We don't have to wait the whole 400ms for the last (missing) byte to arrive.