How to Wait for a Keystroke Interrupt with a Syscall on Linux

How do I wait for a keystroke interrupt with a syscall on Linux?

The necessary code for this is rather complicated; I eventually figured out how to check for F1 in C with raw ioctl, read, and write. The translation to nasm should be straightforward if you're familiar with assembly and Linux syscalls.

It's not exactly what you want, in that it only checks for F1, not the rest of them. F1's sequence is 0x1b, 0x4f, 0x50. You can find other sequences with od -t x1 and pressing the key. For example, F2 is 0x1b, 0x4f, 0x51.

The basic idea is that we get the current terminal attributes, update them to be raw (cfmakeraw), and then set them back. The ioctl syscall is used for this.

On a terminal in raw mode, read() will get any character(s) the user has typed, unlike the "cooked" mode where the kernel does line-editing with backspace and control-u until the user submits the line by pressing enter or control-d (EOF).

#include <unistd.h>
#include <sys/ioctl.h>
#include <termios.h>

struct ktermios {
tcflag_t c_iflag;
tcflag_t c_oflag;
tcflag_t c_cflag;
tcflag_t c_lflag;
cc_t c_line;
cc_t c_cc[19];
};

int getch() {
unsigned char c;
read(0, &c, sizeof(c));
return c;
}

int main(int argc, char *argv[]) {
struct ktermios orig, new;
ioctl(0, TCGETS, &orig);
ioctl(0, TCGETS, &new); // or more simply new = orig;

// from cfmakeraw documentation
new.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
new.c_oflag &= ~OPOST;
new.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
new.c_cflag &= ~(CSIZE | PARENB);
new.c_cflag |= CS8;

ioctl(0, TCSETS, &new);

while (1) {
if (getch() == 0x1b && getch() == 0x4f && getch() == 0x50) {
break;
}
}

write(1, "Got F1!\n", 8);
ioctl(0, TCSETS, &orig); // restore original settings before exiting!
return 0;
}

I based this on this answer, which was very helpful.

Reading a single-key input on Linux (without waiting for return) using x86_64 sys_call

Syscalls in 64-bit linux

The tables from man syscall provide a good overview here:

arch/ABI   instruction          syscall #   retval Notes
──────────────────────────────────────────────────────────────────
i386 int $0x80 eax eax
x86_64 syscall rax rax See below

arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────────
i386 ebx ecx edx esi edi ebp -
x86_64 rdi rsi rdx r10 r8 r9 -

I have omitted the lines that are not relevant here. In 32-bit mode, the parameters were transferred in ebx, ecx, etc and the syscall number is in eax. In 64-bit mode it is a little different: All registers are now 64-bit wide and therefore have a different name. The syscall number is still in eax, which now becomes rax. But the parameters are now passed in rdi, rsi, etc. In addition, the instruction syscall is used here instead of int 0x80 to trigger a syscall.

The order of the parameters can also be read in the man pages, here man 2 ioctl and man 2 read:

int ioctl(int fd, unsigned long request, ...);
ssize_t read(int fd, void *buf, size_t count);

So here the value of int fd is in rdi, the second parameter in rsi etc.

How to get rid of waiting for a newline

Firstly create a termios structure in memory (in .bss section):

termios:
c_iflag resd 1 ; input mode flags
c_oflag resd 1 ; output mode flags
c_cflag resd 1 ; control mode flags
c_lflag resd 1 ; local mode flags
c_line resb 1 ; line discipline
c_cc resb 19 ; control characters

Then get the current terminal settings and disable canonical mode:

; Get current settings
mov eax, 16 ; syscall number: SYS_ioctl
mov edi, 0 ; fd: STDIN_FILENO
mov esi, 0x5401 ; request: TCGETS
mov rdx, termios ; request data
syscall

; Modify flags
and byte [c_lflag], 0FDh ; Clear ICANON to disable canonical mode

; Write termios structure back
mov eax, 16 ; syscall number: SYS_ioctl
mov edi, 0 ; fd: STDIN_FILENO
mov esi, 0x5402 ; request: TCSETS
mov rdx, termios ; request data
syscall

Now you can use sys_read to read in the keystroke:

mov  eax, 0              ; syscall number: SYS_read
mov edi, 0 ; int fd: STDIN_FILENO
mov rsi, buf ; void* buf
mov rdx, len ; size_t count
syscall

Afterwards check the return value in rax: It contains the number of characters read.

(Or a -errno code on error, e.g. if you closed stdin by running ./a.out <&- in bash. Use strace to print a decoded trace of the system calls your program makes, so you don't need to actually write error handling in toy experiments.)


References:

  • What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?
  • Why does the sys_read system call end when it detects a new line?
  • How do i read single character input from keyboard using nasm (assembly) under ubuntu?
  • Using the raw keyboard mode under Linux (external site with example in 32-bit assembly)

How do system calls like select() or poll() work under the hood?

It depends on what the select/poll is waiting for. Let's consider a few cases; I'm going to assume a single-core machine for simplification.

First, consider the case where the select is waiting on another process (for example, the other process might be carrying out some computation and then outputs the result through a pipeline). In this case the kernel will mark your process as waiting for input, and so it will not provide any CPU time to your process. When the other process outputs data, the kernel will wake up your process (give it time on the CPU) so that it can deal with the input. This will happen even if the other process is still running, because modern OSes use preemptive multitasking, which means that the kernel will periodically interrupt processes to give other processes a chance to use the CPU ("time-slicing").

The picture changes when the select is waiting on I/O; network data, for example, or keyboard input. In this case, while archaic hardware would have to spin the CPU waiting for input, all modern hardware can put the CPU itself into a low-power "wait" state until the hardware provides an interrupt - a specially handled event that the kernel handles. In the interrupt handler the CPU will record the incoming data and after returning from the interrupt will wake up your process to allow it to handle the data.

Multiple interrupt vector tables for multiple processes

A process running in a user address space can not service interrupts and thus does not have a vector table. The interrupt vector table will reside in the kernels address space.

In the case of a keyboard, the kernel's vector table(s) will handle the interrupt and pickup the key press. The kernel then will send the character from the key press to the user application via a system call. In linux, the system call will most likely be abstracted as a file being read by the user processes.

Multiple user processes (applications in this example) can read from the same file, so the behavior depends on the specifics of the file/file like device. There is a good chance it will end up being an unpredictable race between processes to read the data first. In practice, its often a bad idea to have multiple processes concurrently accessing the same file.

Have ctrl+c interrupt a blocking system call

There's no getting around the fact that you need to know what you're interrupting, and that the code has to be designed to be interrupted.

In the particular example you've chosen, replace the sleep with a timed wait on an event. You can then interrupt it by setting the event.

If you're making a synchronous I/O call, you can use CancelSynchronousIo but of course the code handling the I/O then has to properly handle the cancellation.

If you're in an alertable wait you can queue an APC to the thread.

If you're in a message loop you can post a message or have the loop use MsgWaitForMultipleObjectsEx.

Additional: demo code showing demonstrating how to wake SleepEx

#include <Windows.h>
#include <stdio.h>
#include <tchar.h>

HANDLE mainthread;

VOID CALLBACK DummyAPCProc(
_In_ ULONG_PTR dwParam
)
{
printf("User APC\n");
return;
}

void SleepIntr()
{
printf("Entering sleep...\n");
SleepEx(10000, TRUE);
printf("Sleep done...\n");
}

static BOOL WINAPI ctrl_handler(DWORD dwCtrlType) {
printf("ctrl_handler pressed\n");
// Wake program up!
if (!QueueUserAPC(DummyAPCProc, mainthread, NULL))
{
printf("QueueUserAPC: %u\n", GetLastError());
return TRUE;
}
return TRUE;
}

int _tmain(int argc, _TCHAR* argv[])
{
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &mainthread, GENERIC_ALL, FALSE, 0))
{
printf("DuplicateHandle: %u\n", GetLastError());
return 0;
}

SetConsoleCtrlHandler(ctrl_handler, TRUE);
SleepIntr();
printf("awake!\n");
return 0;
}

What exactly happens when I hit the Enter button in terms of system_read interrupt, assembly?

There is a long way from inputting to the application:

  • Hardware
  • Driver layer
  • Console layer
  • reading functions

Somewhere therein happens the treatment of lines, I think it is at the console layer. There you can input data which is processed on in lines.

If an application comes along and reads, it gets as many characters as it asks for, the remaining ones are kept for the next reading call.

If there are none remaining, it will wait until the next line is complete - or if the user presses ^D, which means to terminate the current read() call. If no data were entered before, read() returns 0, denoting EOF. In all other cases, read() returns the number of bytes read so far.

Best way to monitor for a key press without stopping a loop

You can use a thread waiting for a key before calling popen, call popen, cancel the thread and check if any key was pressed before calling pclose.

An example using C (it is trivial to adapt it to C++), I'm ommiting error checks for brevity:

#define _XOPEN_SOURCE

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *handler(void *arg)
{
int *key = arg;
char c;

while (read(STDIN_FILENO, &c, 1))
{
if (*key == 0)
{
*key = c;
}
}
return NULL;
}

int main(void)
{
pthread_t thread;
int key = 0;

pthread_create(&thread, NULL, handler, &key);

// Simulate a long read using sleep 5
// to give you enough time to press a key + enter
FILE *cmd = popen("sleep 5; echo 'command finished'", "r");
char str[1024];

while (fgets(str, sizeof str, cmd))
{
printf("%s", str);
}
pthread_cancel(thread);
pthread_join(thread, NULL);
if (key != 0)
{
printf("%c was pressed\n", key);
}
pclose(cmd);
return 0;
}

EDIT:

Someone suggested in comments to use select instead of pthreads, I don't see much advantage in using select but here we go:

select() allows to wait until a file descriptor (in this case stdin) become ready, it can be connected to a timeout that specifies the interval that select() should block waiting for the file descriptor to become ready, in this case, since we want to read what is pending on stdin as soon as popen finishes, we set the timeout to 0, at the end we need to clean stdin, otherwise we end up poluting the terminal with garbage if the user starts typing but doesn't hit enter, or types more than one digit pressing enter.

IMHO threads are a cleaner alternative in this case. Same approach using select:

#define _XOPEN_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <termios.h>

void clear_stdin(void)
{
int stdin_copy = dup(STDIN_FILENO);

tcdrain(stdin_copy);
tcflush(stdin_copy, TCIFLUSH);
close(stdin_copy);
}

int keypress(int state)
{
int key = 0;

// state 0 = timeout (nothing to get)
if (state == 1)
{
char c;

if (read(STDIN_FILENO, &c, 1))
{
key = c;
}
}
clear_stdin();
return key;
}

int main(void)
{
fd_set set;

FD_ZERO(&set);
FD_SET(STDIN_FILENO, &set);

FILE *cmd = popen("sleep 5; echo 'command finished'", "r");
char str[1024];

while (fgets(str, sizeof str, cmd))
{
printf("%s", str);
}

struct timeval timeout;

timeout.tv_sec = 0;
timeout.tv_usec = 0;

int state = select(FD_SETSIZE, &set, NULL, NULL, &timeout);
int key = keypress(state);

if (key != 0)
{
printf("%c was pressed\n", key);
}
pclose(cmd);
return 0;
}


Related Topics



Leave a reply



Submit