Reading the Keyboard Queue ala Networking

A network program monitors one or more file descriptors (such as sockets) for activity. It reacts when information is ready to process. This technique can also be applied to keyboard input, though not as elegantly as the kbhit() function shown in last week’s Lesson.

Networking programming is a complex booger of a topic, primarily because the code deals with multiple input and output sources as once. Each connection is monitored for activity in an endless loop. When activity is detected on one of the sources, the program acts.

One way to monitor the several network connections is to use the select() function and its associated macros. This function scans a set of file descriptors for activity, then reports which one is active and ready for reading or writing.

The details for using the select() function are complicated, so I’m not going to dive too deep here. To recreate last week’s program, I used three of the function’s macros: FD_ZERO, FD_SET, and FD_ISSET. These macros work on an fd_set structure that builds a collection of file descriptors that the select() function monitors.

The select() function returns a value based on activity in the file descriptor set. When the return value isn’t zero, it means one of the file descriptors is ready for action, reading or writing.

Here is an update to the code presented last week using the select() function and its macros to check for keyboard activity:

2024_04_06-Lesson.c

#include <stdio.h>
#include <sys/time.h>
#include <sys/select.h>

int main()
{
    char a;
    fd_set keyread;
    struct timeval timeout;
    int r;

    /* preset values */
    a = 'A';
    timeout.tv_sec = 0;        /* timeout structure */
    timeout.tv_usec = 0;

    /* loop until keyboard input available */
    while(1)
    {
        /* initailize file descriptor set */
        FD_ZERO(&keyread);
        FD_SET(0,&keyread);
        /* check standard input status */
        r = select(1,&keyread,NULL,NULL,&timeout);
        if( r==-1 )
        {
            perror("select");
            return 1;
        }

        /* output the alphabet loop */
        printf(" %c",a++);
        if( a=='Z' )
            a = 'A';
        /* terminate on keyboard input */
        if( r>0 )
        {
            /* confirm that it's standard input */
            if( FD_ISSET(0,&keyread) )
            {
                getchar();
                break;
            }
        }
    }

    return 0;
}

Before the while loop begins, a timeout structure is filled. The timeout is set to zero. (Refer to this post for details about this structure.) This structure is referenced in the select() function call.

Within the while loop, fd_set variable keyread is zeroed with the FD_ZERO macro. The FD_SET macro adds the standard input device (zero) to the set:

FD_ZERO(&keyread);
FD_SET(0,&keyread);

The select() statement configures the file descriptor set keyread for action:

r = select(1,&keyread,NULL,NULL,&timeout);

Immediately, a test is performed for an error, if( r==-1 ). When true, the program quits. Otherwise, an alphabet character is output in the endless while loop. This output shows the program stays busy as it monitors standard input.

Next, the return value from select(), r, is compared with zero: if( r>0 ). If true, the FD_ISSET macro tests standard input to see whether something is waiting to be read: if( FD_ISSET(0,&keyread)) When TRUE, input is available, and the loop terminates. The program ends.

For output, the program spews forth the alphabet repeatedly until you press the Enter key. Yes, unlike last week’s example with the kbhit() function, the input buffer is released only when you press Enter.

Granted, this technique wouldn’t be my first choice to check the keyboard for input. It does monitor standard input, and the loop continues during the monitoring, but it’s not the same as checking to see whether a single character is waiting to be read.

For next week’s Lesson I update this code with the epoll() family of functions.

3 thoughts on “Reading the Keyboard Queue ala Networking

  1. While I have used select() to efficiently wait for socket data (with an additional FD_SET(STDIN_FILENO, …); to make sure no keyboard input is missed), I admit that I wouldnʼt have thought of using select to—efficiently—wait for the availability of only keyboard data: thatʼs a neat idea!

    I have a stylistic remark, however: the first argument to select(), as the man pages note, should be set to “the highest-numbered file descriptor plus one”—with stdin (file descriptor 0) being the highest-numbered one, select(1, …); is, of course, correct.

    I think the code would be more readable, however, if select(STDIN_FILENO+1, …); was used instead. The same applies to FD_ISSET(): I think FD_ISSET(STDIN_FILENO, &keyread) would be more readable.

    Also, with timeout being set to zero itʼs ok to initialize ‘timeout.tv_sec’ and ‘timeout.tv_usec’ only (once) outside the loop and be done with it… in general, however, it would be necessary to (re-)initialize ‘timeout’ before every select() call. The reason being, that select() usually updates its ‘timeout’ argument to indicate how much time was left when the syscall returned. (In case of a timeout, all structure members will be zero.) As the above program sets ‘.tv_sec’ and ‘.tv_usec’ to zero anyway, this doesnʼt matter… in general, it does matter though.

    Lastly, Iʼve tried the above solution, and found that select() only immediately returns if the next character in line is '\n', i.e. if one presses Enter. In my view this happens, because with the above the terminal is left in canonical mode (often called “cooked mode” or what one could call line-by-line mode) where programs get informed of new input only after the user has entered a complete line⁽¹⁾.

    In my view, it is therefore necessary to switch the terminal over to “raw mode” (i.e. byte-by-byte mode), for the above to be of any real use. Iʼve to adapt your code and fix this “raw” vs. “cooked” mode issue (as I see it): SelectKeyboard.tar.xz

    ⁽¹⁾ A somewhat related issue exists with regards to the above ‘printf(" %c", a++);’ calls—because of “output buffering” these characters wonʼt get output immediately, but only after the respective output buffer has filled up. Iʼve included a setvbuf (stdout, NULL, _IONBF, 0); call in my variant of the above program to make sure that everything gets drawn to the screen immediately (without having to call fflush(stdout); after every print call.)

  2. You’re correct about the first argument to select(). I should use the constant plus one, which should never break anything. Bad habits.

    I did toy with the idea of turning off buffering for standard input and reading it raw. I might have actually done it as well, but don’t remember it working right. Regardless, it’s a silly solution that’s more about outside-the-box thinking. If I truly want to check the keyboard buffer, I’ll use kbhit().

    Another thing I thought of was trying to use ioctl() instead of select() to read network sockets. If only I had the time . . .

  3. «it’s a silly solution that’s more about outside-the-box thinking.»

    For sure, I was aware of the intent and agree that it’s a wonderful example of “outside-the-box thinking”!

    «If only I had the time . . .»

    I feel the same way… to much to do, not enough time. That being said: contrasting ioctl(socket, FIONREAD, &available); with non-blocking I/O, relying on select() or epoll() to efficiently wait for socket connections to get ready, does sound like an interesting idea!

Leave a Reply