1. Overview

System calls allow user-space applications to get service from the Linux kernel. Memory and file system management, process control, and inter-process communication are typical services provided by the kernel.

Some system calls may return an error if the call is interrupted by a signal. These system calls are known as interrupted system calls.

In this tutorial, we’ll discuss the interrupted system calls in Linux. After a brief introduction to the topic, we’ll see examples of using an interrupted system call, namely wait(), and how to deal with an interruption.

2. Interrupted System Calls

System calls are the interfaces between processes in the user space and the Linux kernel. Processes in the user space get service from the operating system (OS) using the system calls. We don’t actually invoke system calls directly, but rather indirectly using system call wrappers like glibc. For example, we use the exec() family of functions to spawn another process.

Some system calls may take a long time to complete or even block forever. For example, reading from a network socket using the read() system call might last long if there’s no data available. Another example is the wait() system call. A parent process may wait for a status change in its child process forever. We name this type of system call that may block forever as a slow system call.

If a process catches a signal while it’s blocked in a slow system call, then the system call is interrupted. The call returns an error and the OS sets the errno variable to EINTR. Therefore, we name these slow system calls as interrupted system calls.

However, instead of failing the system call and setting errno to EINTR, the OS can allow the system call to succeed and continue its execution. We’ll see examples of both cases in the subsequent sections.

3. An Example

We’ll use the following program, int_sys_call.c, to analyze the behavior of interrupted system calls:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h> 
void signal_handler(int signum, siginfo_t *info, void *extra)
{
    printf("Handler thread ID: %ld\n", syscall(SYS_gettid));
} 
void set_signal_handler(void)
{
    struct sigaction action;
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = signal_handler;
    sigaction(SIGINT, &action, NULL);
}
void main()
{
    pid_t pid;
    int wstatus;

    set_signal_handler();
    printf("Thread ID (main thread): %ld\n", syscall(SYS_gettid));

    pid = fork();
    if (pid == 0) {
        printf("Thread ID (main thread of child): %ld\n", syscall(SYS_gettid));
        for(;;) {
            sleep(1);
        }
    }

    pid = wait(&wstatus);
    if (errno == EINTR) {
        printf("wait() exited with errno set to EINTR\n");
    }
}

This program spawns a child process and then waits for the status updates of the child process using the wait() system call. Additionally, it installs a signal handler for the SIGINT signal. We’ll examine how wait() behaves when we send a SIGINT signal to the parent process.

3.1. Dissecting the Code

We’ll break down the code to understand it better:

void signal_handler(int signum, siginfo_t *info, void *extra)
{    
    printf("Handler thread ID: %ld\n", syscall(SYS_gettid));
}

The signal_handler() function just prints the process ID (PID) using the syscall(SYS_gettid) system call. As we’ll see shortly, this function is called when the process receives a SIGINT signal.

We install the signal handler using the set_signal_handler() function:

void set_signal_handler(void)
{
     struct sigaction action;
     action.sa_flags = SA_SIGINFO;
     action.sa_sigaction = signal_handler;
     sigaction(SIGINT, &action, NULL);
}

The sigaction() system call specifies the action of a process on receiving a signal. Normally, a process terminates when it receives SIGINT. However, we change the default behavior for SIGINT by setting the sa_sigaction field of the second parameter of sigaction() to signal_handler. Therefore, the OS calls the signal_handler() function when the process receives a SIGINT signal. The SA_SIGINFO flag provides additional information to the signal handler.

We call set_signal_handler() in main() to set the signal handler and print the PID of the parent process:

set_signal_handler();
printf("Thread ID (main thread): %ld\n", syscall(SYS_gettid));

Then, we spawn the child process using fork():

pid = fork();
if (pid == 0) {
    printf("Thread ID (main thread of child): %ld\n", syscall(SYS_gettid));
    for(;;) {
        sleep(1);
    }
}

fork() returns the PID of the spawned child process in the parent and 0 in the child process. Therefore, the code snippet in the if block above runs in the child process. It prints the PID of the child process using printf() and sleeps for one second at a time using sleep(1) in an infinite for loop.

The parent process, on the other hand, waits for the child process using wait():

pid = wait(&wstatus);
if (errno == EINTR) {
    printf("wait() exited with errno set to EINTR\n");
}

If the state of the child process changes, the wait() call returns. We check whether wait() returns due to receiving a signal by testing the equality of errno to EINTR.

3.2. Building and Running the Example

Let’s use gcc to build the application:

$ gcc -o int_sys_call int_sys_call.c

The -o option of gcc specifies the name of the executable, which is int_sys_call in our example.

Having built the executable, it’s time to run it:

$ ./int_sys_call
Thread ID (main thread): 2932
Thread ID (main thread of child): 2933

The parent and child processes start with PIDs 2932 and 2933, respectively. Now, let’s send a SIGINT signal to the parent process from another terminal using the kill command:

$ kill -SIGINT 2932

Let’s check the behavior of the running process after sending the SIGINT signal:

$ ./int_sys_call
Thread ID (main thread): 2932
Thread ID (main thread of child): 2933
Handler thread ID: 2932
wait() exited with errno set to EINTR

As we can see from the output, the signal_handler() function is called, printing Handler thread ID: 2932. Then, the wait() call returns. Since errno is equal to EINTR, the wait() exited with errno set to EINTR message is printed, and the process exits.

4. Modified Example

As we saw in the previous section, the OS interrupts the wait() system call when the process receives a signal. If we want to go on waiting for the status changes of the child process, we have to handle it manually. For example, we can recall wait() using an infinite loop:

for(;;) {
    pid = wait(&wstatus);
    if (errno == EINTR) {
        printf("wait() exited with errno set to EINTR\n");
        continue;
    }
    else {
        break;
    }
}

Once the execution of wait() is interrupted because of receiving SIGINT, we skip the rest of the infinite for loop and jump to the beginning of the loop using the continue statement. Therefore, we recall wait(). However, if wait() returns due to any reason other than an interruption by a signal, then we exit from the infinite loop using the break statement. An example of this case might be the termination of the child process. It’s possible to prevent applications from having to handle certain interrupted system calls by automatically restarting them. We can use the SA_RESTART flag of the sigaction() system call for this purpose:

void set_signal_handler(void)
{
    struct sigaction action;
    action.sa_flags = SA_SIGINFO | SA_RESTART;
    action.sa_sigaction = signal_handler;
    sigaction(SIGINT, &action, NULL);
}

The only difference from the set_signal_handler() function in the previous section is the action.sa_flags = SA_SIGINFO | SA_RESTART; statement. We use the SA_RESTART flag in addition to SA_SIGINFO. This flag controls the behavior of certain system calls such as read(), write(), and wait() when a process receives a signal during these calls. The signal handler returns normally with the system call automatically restarted.

4.1. Running the Example

Having updated and built int_sys_call.c, let’s run it again:

$ ./int_sys_call
Thread ID (main thread): 8284
Thread ID (main thread of child): 8285

The parent and child processes have PIDs 8284 and 8285, respectively. Let’s send a SIGINT signal to the parent process from another terminal:

$ kill -SIGINT 8284

Let’s check the behavior of the running process after sending SIGINT to it:

$ ./int_sys_call
Thread ID (main thread): 8284
Thread ID (main thread of child): 8285
Handler thread ID: 8284

Notably, the OS calls the signal_handler() function, printing Handler thread ID: 8284. However, wait() doesn’t return and set errno to EINTR. Therefore, the parent process doesn’t exit and continues running, as expected.

5. Conclusion

In this article, we discussed the interrupted system calls in Linux. First, we saw that interrupted system calls are system calls that may be blocked forever. The reception of a signal by a process interrupts the execution of interrupted system calls. We also learned that the OS may either fail the system call, with errno set to EINTR, or allow the system call to succeed, restarting the call. The latter case is possible using the SA_RESTART flag of the sigaction() system call.

Finally, we saw two examples using the wait() system call. In the first example, we didn’t use the SA_RESTART flag of sigaction(). wait() exited, with errno set to EINTR, and the process exited. In the second example, we used the SA_RESTART flag. wait() didn’t return EINTR, and the process continued running.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments