简体   繁体   中英

Catching ctrl-c in c and continuing execution

I am writing a simple shell program in c, and need to handle ctrl-c.

If a foreground process is running, I need to terminate it and continue the main shell loop. If not, I need to do nothing but print that the signal was caught.

Below is my code, based on this thread: Catch Ctrl-C in C

void inthandler(int dummy){
    signal(dummy, SIG_IGN);
    printf("ctrl-c caught\n");
}

and I call signal() right before entering my main loop

int main(int argc, char*argv[]){
    signal(SIGINT, inthandler)
    while(true){
        //main loop
    }
}

As of now, I am able to intercept ctrl-c and print my intended message, but any further input results in a segfault.

How can I return to execution of my main loop after I enter inthandler?

  1. Use sigaction() , not signal() , except when setting the disposition to SIG_DFL or SIG_IGN . While signal() is specified in C, and sigaction() POSIX.1, you'll need POSIX to do anything meaningful with signals anyway.

  2. Only use async-signal safe functions in signal handlers. Instead of standard I/O (as declared in <stdio.h> ), you can use POSIX low-level I/O ( read() , write() ) to the underlying file descriptors. You do need to avoid using standard I/O to streams that use those same underlying descriptors, though, or the output may be garbled due to buffering in standard I/O.

  3. If you change the signal disposition in the signal handler to ignored , only the first signal (of each caught type) will be caught.

Consider the following example program:

#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

/* Helper function: Write a string to a descriptor, keeping errno unchanged. 
   Returns 0 if success, errno error code if an error occurs. */
static inline int  wrs(const int fd, const char *s)
{
    /* Invalid descriptor? */
    if (fd == -1)
        return EBADF;

    /* Anything to actually write? */
    if (s && *s) {
        const int   saved_errno = errno;
        const char *z = s;
        ssize_t     n;
        int         err = 0;

        /* Use a loop to find end of s, since strlen() is not async-signal safe. */
        while (*z)
            z++;

        /* Write the string, ignoring EINTR, and allowing short writes. */
        while (s < z) {
            n = write(fd, s, (size_t)(z - s));
            if (n > 0)
                s += n;
            else
            if (n != -1) {
                /* This should never occur, so it's an I/O error. */
                err = EIO;
                break;
            } else
            if (errno != EINTR) {
                /* An actual error occurred. */
                err = errno;
                break;
            }
        }

        errno = saved_errno;
        return err;

    } else {
        /* Nothing to write. NULL s is silently ignored without error. */
        return 0;
    }
}

/* Signal handler. Just outputs a line to standard error. */
void catcher(int signum)
{
    switch (signum) {
    case SIGINT:
        wrs(STDERR_FILENO, "Caught INT signal.\n");
        return;
    default:
        wrs(STDERR_FILENO, "Caught a signal.\n");
        return;
    }
}

/* Helper function to install the signal handler. */
int install_catcher(const int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);

    act.sa_handler = catcher;
    act.sa_flags = SA_RESTART;  /* Do not interrupt "slow" functions */
    if (sigaction(signum, &act, NULL) == -1)
        return -1;  /* Failed */

    return 0;
}

/* Helper function to remove embedded NUL characters and CRs and LFs,
   as well as leading and trailing whitespace.  Assumes data[size] is writable. */
size_t clean(char *data, size_t size)
{
    char *const  end = data + size;
    char        *src = data;
    char        *dst = data;

    /* Skip leading ASCII whitespace. */
    while (src < end && (*src == '\t' || *src == '\n' || *src == '\v' ||
                         *src == '\f' || *src == '\r' || *src == ' '))
        src++;

    /* Copy all but NUL, CR, and LF chars. */
    while (src < end)
        if (*src != '\0' && *src != '\n' && *src != '\r')
            *(dst++) = *(src++);
        else
            src++;

    /* Backtrack trailing ASCII whitespace. */
    while (dst > data && (dst[-1] == '\t' || dst[-1] == '\n' || dst[-1] == '\v' ||
                          dst[-1] == '\n' || dst[-1] == '\r' || dst[-1] == ' '))
        dst--;

    /* Mark end of string. */
    *dst = '\0';

    /* Return new length. */
    return (size_t)(dst - data);
}


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

    if (install_catcher(SIGINT)) {
        fprintf(stderr, "Cannot install signal handler: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    printf("Type Ctrl+D, 'exit', or 'quit' on an empty line to exit.\n");

    while (1) {
        len = getline(&line, &size, stdin);
        if (len < 0)
            break;

        clean(line, len);
        printf("Read %zd chars: %s\n", len, line);

        if (!strcmp(line, "exit") || !strcmp(line, "quit"))
            break;
    }

    return EXIT_SUCCESS;
}

On most POSIXy systems, Ctrl + C in a pseudoterminal also clears the current line buffer, so pressing it in the middle of interactively supplying a line will discard the line (the data not sent to the process).

Note that the ^C you normally see in pseudoterminals when you press Ctrl + C is a terminal feature, controlled by the ECHO termios setting. That setting also controls whether keypresses in general are echoed on the terminal.

If you add signal(SIGINT, SIG_IGN) to catcher(), just after the wrs() line, only the first Ctrl + C will print "Caught INT signal"; any following Ctrl + C will just discard the the incomplete line.

So, if you type, say, Foo Ctrl + C Bar Enter , you'll see either

Foo^CCaught INT signal.
Bar
Read 4 chars: Bar

or

Foo^CBar
Read 4 chars: Bar

depending on whether the INT signal generated by Ctrl + C is caught by the handler, or ignored, at that point.

To exit, type exit or quit at the start of a line, or immediately after a Ctrl + C .

There are no segmentation faults here, so if your code does generate one, it must be due to a bug in your program.


How can I return to execution of my main loop after I enter inthandler?

Signal delivery interrupts the execution for the duration of executing the signal handler; then, the interrupted code continues executing. So, the strictly correct answer to that question is by returning from the signal handler .

If the signal handler is installed with the SA_RESTART flag set, then the interrupted code should continue as if nothing had happened. If the signal handler is installed without that flag, then interrupting "slow" system calls may return an EINTR error.

The reason errno must be kept unchanged in a signal handler -- and this is a bug many, many programmers overlook -- is that if an operation in the signal handler changes errno, and the signal handler gets invoked right after a system or C library call failed with an error code, the errno seen by the code will be the wrong one! Granted, this is a rare case (the time window where this can occur is tiny for each system or C library call), but it is a real bug that can occur. When it does occur, it is the kind of Heisenbug that causes developers to rip out their hair, run naked in circles, and generally go more crazy than they already are.

Also note that stderr is only used in the code path where installing the signal handler fails, because I wanted to be sure not to mix I/O and POSIX low-level I/O to standard error.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM