Is there any way to change sigaction flags during execution?

Viewed 301

I have this child process in infinite loop and i want it to stop the loop when recive SIGUSR1 from parent pid.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

int GameOver = 0;
jmp_buf here; // <------- After Joshua's Answer

void trataSIGUSR1(int sig, siginfo_t *info, void *extra);

int main(int argc, char** argv){
    int someNumber = 0, score = 0;
    char word[15],c;
    struct sigaction new_action;

//  new_action.sa_flags = SA_SIGINFO;              // <------- Before Joshua's Answer
    new_action.sa_flags = SA_SIGINFO | SA_RESTART; // <------- After Joshua's Answer
    new_action.sa_sigaction = &trataSIGUSR1;

    sigfillset(&new_action.sa_mask);

    if (sigaction(SIGUSR1, &new_action, NULL) == -1){
        perror("Error: cannot handle SIGUSR1"); // não deve acontecer
        return EXIT_FAILURE;
    }
    
    FILE *f;
    f = fopen("randomfile.txt", "r");
    if (f == NULL){
        printf("Errr Opening File!\n");
        return EXIT_FAILURE;
    }
//  setjmp(here); // <------- After Joshua's Answer
    sigsetjmp(here,1); // <-- After wildplasser's Answer
    while (!GameOver){
        fscanf(f, "%s", word);
        printf("\nWord -> %s\n", word);
        if(!scanf("%d", &someNumber)){
            puts("Invalid Value!");
            while ((c = getchar()) != '\n' && c != EOF);
            continue;
        }
        if(someNumber == strlen(word) && !GameOver)
            score ++;

        if(feof(f)){
            printf("\nEnd of file.\n");
            break;
        }
    }

    if( GameOver )
        puts("\nAcabou o tempo!"); // <-- After wildplasser's Answer

    fclose(f);

    return score;
}

void trataSIGUSR1(int sig, siginfo_t *info, void *extra){
    if (info->si_pid == getppid()){ // only end when parent send SIGUSR1
//      puts("\nAcabou o tempo!"); // <-- Before  wildplasser's Answer
        GameOver = 1;
//      longjmp(here,1); // <------- After Joshua's Answer
        siglongjmp(here,1); // <---- After wildplasser's Answer
    }
}

It works fine but if i send SIGUSR1 to child pid from another process scanf get interupted... I want to interupt the scanf and automaticly stop the loop only when signal come from parent, in other case just ignore. Is there any way to change the flag to new_action.sa_flags = SA_RESTART; when signal comes from other process?!

2 Answers

There are several possibilities, ranging from a huge hack, to proper (but complicated).

The simplest thing is to have the SIGUSR1 from parent reopen standard input to /dev/null. Then, when scanf() fails, instead of complaining and retrying, you can break out of the loop if feof(stdin) is true. Unfortunately, freopen() is not async-signal safe, so this is not a standards (POSIX, in this case) compliant way of doing things.

The standards-compliant way of doing things is to implement your own read input line into a dynamically allocated string -type of function, which detects when the signal handler sets the flag. The flag should also be of volatile sig_atomic_t type, not an int; the volatile in particular tells the compiler that the value may be changed unexpectedly (by the signal handler), so whenever referenced, the compiler must re-read the variable value, instead of remembering it from a previous access. The sig_atomic_t type is an atomic integer type: the process and the signal handler will only ever see either the new, or the old value, never a mix of the two, but might have as small valid range as 0 to 127, inclusive.

Signal delivery to an userspace handler (installed without SA_RESTART) does interrupt a blocking I/O operation (like read or write; in the thread used for signal delivery – you only have one, so that will always be used), but it might occur between the flag check and the scanf(), so in this case, it is not reliable.

The proper solution here is to not use stdin at all, and instead use the low-level <unistd.h> I/O for this. Note that it is imperative to not mix stdin/scanf() and low-level I/O for the same stream. You can safely use printf(), fprintf(stdout, ...), fprintf(stderr, ...), and so on. The reason is that the C library internal stdin stream structure will not be updated correctly by our low-level access, and will be out-of-sync with reality if we mix both (for the same stream).

Here is an example program showing one implementation (licensed under Creative Commons Zero v1.0 International – do as you wish with it, no guarantees though):

// SPDX-License-Identifier: CC0-1.0
#define  _POSIX_C_SOURCE  200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

/* Maximum poll() timeout, in milliseconds, so that done flag is checked often enough.
*/
#ifndef  DONE_POLL_INTERVAL_MS
#define  DONE_POLL_INTERVAL_MS  100
#endif

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum, siginfo_t *info, void *context)
{
    /* This silences warnings about context not being used. It does nothing. */
    (void)context;

    if (signum == SIGUSR1 && info->si_pid == getppid()) {
        /* SIGUSR1 is only accepted if it comes from the parent process */
        done = 1;
    } else {
        /* All other signals are accepted from all processes (that have the necessary privileges) */
        done = 1;
    }
}

static int install_done(const int signum)
{
    struct sigaction   act;
    memset(&act, 0, sizeof act);
    sigemptyset(&(act.sa_mask));
    act.sa_sigaction = handle_done;
    act.sa_flags = SA_SIGINFO;
    return sigaction(signum, &act, NULL);
}

/* Our own input stream structure type. */
struct input {
    int           descriptor;
    char         *data;
    size_t        size;
    size_t        head;
    size_t        tail;
};


/* Associating an input stream with a file descriptor.
   Do not mix stdin use and input stream on descriptor STDIN_FILENO!
*/
static int  input_use(struct input *const in, const int descriptor)
{
    /* Check that the parameters are not obviously invalid. */
    if (!in || descriptor == -1) {
        errno = EINVAL;
        return -1;
    }

    /* Set the descriptor nonblocking. */
    {
        int  flags = fcntl(descriptor, F_GETFL);
        if (flags == -1) {
            /* errno set by fcntl(). */
            return -1;
        }
        if (fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) == -1) {
            /* errno set by fcntl(). */
            return -1;
        }
    }

    /* Initialize the stream structure. */
    in->descriptor = descriptor;
    in->data       = NULL;
    in->size       = 0;
    in->head       = 0;
    in->tail       = 0;

    /* Success. */
    return 0;
}


/* Read until delimiter from an input stream.
 * If 'done' is set at any point, will return 0 with errno==EINTR.
 * Returns 0 if an error occurs, with errno set.
 * Returns 0 with errno==0 when end of input stream.
*/
static size_t  input_getdelim(struct input *const in,
                              int const           delim,
                              char **const        dataptr,
                              size_t *const       sizeptr,
                              const double        timeout)
{
    const clockid_t  timeout_clk = CLOCK_BOOTTIME;
    struct timespec  then;

    /* Verify none of the pointers are NULL. */
    if (!in || !dataptr || !sizeptr) {
        errno = EINVAL;
        return 0;
    }

    /* Record current time for timeout measurement. */
    clock_gettime(timeout_clk, &then);

    char   *line_data = *dataptr;
    size_t  line_size = *sizeptr;

    /* If (*sizeptr) is zero, then we ignore dataptr value, like getline() does. */
    if (!line_size)
        line_data = NULL;

    while (1) {
        struct timespec  now;
        struct pollfd    fds[1];
        ssize_t          n;
        int              ms = DONE_POLL_INTERVAL_MS;

        /* Done flag set? */
        if (done) {
            errno = EINTR;
            return 0;
        }

        /* Is there a complete line in the input buffer? */
        if (in->tail > in->head) {
            const char *ptr = memchr(in->data + in->head, delim, in->tail - in->head);
            if (ptr) {
                const size_t  len = ptr - (in->data + in->head);
                if (len + 2 > line_size) {
                    /* Since we do not have any meaningful data in line_data,
                       and it would be overwritten anyway if there was,
                       instead of reallocating it we just free an allocate it. */
                    free(line_data);  /* Note: free(null) is safe. */
                    line_size = len + 2;
                    line_data = malloc(line_size);
                    if (!line_data) {
                        /* Oops, we lost the buffer. */
                        *dataptr = NULL;
                        *sizeptr = 0;
                        errno = ENOMEM;
                        return 0;
                    }
                    *dataptr = line_data;
                    *sizeptr = line_size;
                }

                /* Copy the line, including the separator, */
                memcpy(line_data, in->data + in->head, len + 1);

                /* add a terminating nul char, */
                line_data[len + 1] = '\0';

                /* and update stream buffer state. */
                in->head += len + 1;
                return len + 1;
            }

            /* No, we shall read more data.  Prepare the buffer. */
            if (in->head > 0) {
                memmove(in->data, in->data + in->head, in->tail - in->head);
                in->tail -= in->head;
                in->head  = 0;
            }
        } else {
            /* Input buffer is empty. */
            in->head = 0;
            in->tail = 0;
        }

        /* Do we need to grow input stream buffer? */
        if (in->head >= in->tail) {
            /* TODO: Better buffer size growth policy! */
            const size_t  size = (in->tail + 65535) | 65537;
            char         *data;
            data = realloc(in->data, size);
            if (!data) {
                errno = ENOMEM;
                return 0;
            }
            in->data = data;
            in->size = size;
        }

        /* Try to read additional data.  It is imperative that the descriptor
           has been marked nonblocking, as otherwise this will block. */
        n = read(in->descriptor, in->data + in->tail, in->size - in->tail);
        if (n > 0) {
            /* We read more data without blocking. */
            in->tail += n;
            continue;
        } else
        if (n == 0) {
            /* End of input mark (Ctrl+D at the beginning of line, if a terminal) */
            const size_t  len = in->tail - in->head;
            if (len < 1) {
                /* No data buffered, read end of input. */
                if (line_size < 1) {
                    line_size = 1;
                    line_data = malloc(line_size);
                    if (!line_data) {
                        errno = ENOMEM;
                        return 0;
                    }
                    *dataptr = line_data;
                    *sizeptr = line_size;
                }
                line_data[0] = '\0';
                errno = 0;
                return 0;
            }
            if (len + 1 > line_size) {
                /* Since we do not have any meaningful data in line_data,
                   and it would be overwritten anyway if there was,
                   instead of reallocating it we just free an allocate it. */
                free(line_data);  /* Note: free(null) is safe. */
                line_size = len + 1;
                line_data = malloc(line_size);
                if (!line_data) {
                    /* Oops, we lost the buffer. */
                    *dataptr = NULL;
                    *sizeptr = 0;
                    errno = ENOMEM;
                    return 0;
                }
                *dataptr = line_data;
                *sizeptr = line_size;
            }
            memmove(line_data, in->data, len);
            line_data[len] = '\0';
            in->head = 0;
            in->tail = 0;
            return 0;
        } else
        if (n != -1) {
            /* This should never occur; it would be a C library bug. */
            errno = EIO;
            return 0;
        } else {
            const int  err = errno;
            if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR)
                return 0;
            /* EAGAIN, EWOULDBLOCK, and EINTR are not real errors. */
        }

        /* Nonblocking operation, with timeout == 0.0? */
        if (timeout == 0.0) {
            errno = ETIMEDOUT;
            return 0;
        } else
        if (timeout > 0.0) {
            /* Obtain current time. */
            clock_gettime(timeout_clk, &now);
            const double  elapsed = (double)(now.tv_sec - then.tv_sec)
                                  + (double)(now.tv_nsec - then.tv_nsec) / 1000000000.0;
            /* Timed out? */
            if (elapsed >= (double)timeout / 1000.0) {
                errno = ETIMEDOUT;
                return 0;
            }

            if (timeout - elapsed < (double)DONE_POLL_INTERVAL_MS / 1000.0) {
                ms = (int)(1000 * (timeout - elapsed));
                if (ms < 1) {
                    errno = ETIMEDOUT;
                    return 0;
                }
            }
        }
        /* Negative timeout values means no timeout check,
           and ms retains its initialized value. */

        /* Another done check; it's cheap. */
        if (done) {
            errno = 0;
            return EINTR;
        }

        /* Wait for input, but not longer than ms milliseconds. */
        fds[0].fd = in->descriptor;
        fds[0].events = POLLIN;
        fds[0].revents = 0;
        poll(fds, 1, ms);
        /* We don't actually care about the result at this point. */
    }
    /* Never reached. */
}

static inline size_t  input_getline(struct input *const in,
                                    char **const        dataptr,
                                    size_t *const       sizeptr,
                                    const double        timeout)
{
    return input_getdelim(in, '\n', dataptr, sizeptr, timeout);
}

int main(void)
{
    struct input  in;
    char         *line = NULL;
    size_t        size = 0;
    size_t        len;

    if (install_done(SIGINT) == -1 ||
        install_done(SIGHUP) == -1 ||
        install_done(SIGTERM) == -1 ||
        install_done(SIGUSR1) == -1) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    if (input_use(&in, STDIN_FILENO)) {
        fprintf(stderr, "BUG in input_use(): %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    while (!done) {
        /* Wait for input for five seconds. */
        len = input_getline(&in, &line, &size, 5000);
        if (len > 0) {
            /* Remove the newline at end, if any. */
            line[strcspn(line, "\n")] = '\0';
            printf("Received: \"%s\" (%zu chars)\n", line, len);
            fflush(stdout);
            continue;
        } else
        if (errno == 0) {
            /* This is the special case: input_getline() returns 0 with
               errno == 0 when there is no more input. */
            fprintf(stderr, "End of standard input.\n");
            return EXIT_SUCCESS;
        } else
        if (errno == ETIMEDOUT) {
            printf("(No input for five seconds.)\n");
            fflush(stdout);
        } else
        if (errno == EINTR) {
            /* Break or continue works here, since input_getline() only
               returns 0 with errno==EINTR if done==1. */
            break;
        } else {
            fprintf(stderr, "Error reading from standard input: %s.\n", strerror(errno));
            return EXIT_FAILURE;
        }
    }

    printf("Signal received; done.\n");
    return EXIT_SUCCESS;
}

Save it as e.g. example.c, compile using e.g. gcc -Wall -Wextra -O2 example.c -o example, and run using ./example. Type input and enter to supply lines, or Ctrl+D at the beginning of a line to end input, or Ctrl+C to send the process a SIGINT signal.

Note the compile-time constant DONE_POLL_INTERVAL_MS. If the signal is delivered between a done check and poll(), this is the maximum delay, in milliseconds (1000ths of a second), that the poll may block; and therefore is roughly the maximum delay from receiving the signal and acting upon it.

To make the example more interesting, it also implements a timeout on reading a full line also. The above example prints when it is reached, but that messes up how the user sees the input they're typing. (It does not affect the input.)

This is by no means a perfect example of such functions, but I hope it is a readable one, with the comments explaining the reasoning behind each code block.

Historically we solved this problem by always setting SA_RESTART and calling longjump() to get out of the signal handler when the condition is met.

The standard makes this undefined but I think this does the right thing when stdin is connected to the keyboard. Don't try it with redirected handles. It won't work well. At least you can check for this condition with isatty(0).

If it doesn't work and you are bent on using signals like this, you'll need to abandon scanf() and friends and get all your input using read().

Related