Sending SIGINT to forked exec process which runs script does not kill it

Deadly 提交于 2021-02-08 09:19:01

问题


I'm writing shell application with C and encountered a problem that sending SIGINT to process running script won't stop it.

Sending the same signal to a normal executable works just fine.

Example:

Bash script which just imitates long working script (work.sh):

#! /bin/bash

COUNTER=0
while true
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

C code:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        sleep(5);
        kill(pid, SIGINT);
    }

    return 0;
}

After sending the signal main process ends but script proceeds printing.

Is signal handling in scripts are somehow different? How you need to stop child process ignoring what is running?


回答1:


A SOLUTION:

Add this line on top of your work.sh script trap exit SIGINT to have an explicit SIGINT handler:

#! /bin/bash

trap exit SIGINT

COUNTER=0
while true
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

Running work executable now prints:

#1 Working...
#2 Working...
#3 Working...
#4 Working...
#5 Working...

after which it returns back to shell.

THE PROBLEM:

I found this webpage linked in a comment to this question on Unix stackexchange (For the sake of completeness, here also the webpage linked in the accepted answer.) Here's a quote that might explain what's going on:

bash is among a few shells that implement a wait and cooperative exit approach at handling SIGINT/SIGQUIT delivery. When interpreting a script, upon receiving a SIGINT, it doesn't exit straight away but instead waits for the currently running command to return and only exits (by killing itself with SIGINT) if that command was also killed by that SIGINT. The idea is that if your script calls vi for instance, and you press Ctrl+C within vi to cancel an action, that should not be considered as a request to abort the script.

So imagine you're writing a script and that script exits normally upon receiving SIGINT. That means that if that script is invoked from another bash script, Ctrl-C will no longer interrupt that other script.

This kind of problem can be seen with actual commands that do exit normally upon SIGINT by design.

EDIT:

I found another Unix stackexchange answer that explains it even better. If you look at bash(1) man pages the following is also quite explanatory:

Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

especially when considering that:

Signals ignored upon entry to the shell cannot be trapped, reset or listed.

Basically, running work.sh runs it in a separate execution environment:

When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment.

This includes the signal handlers which (if not explicitly present) will ignore SIGINT and SIGQUIT by default.




回答2:


After trying your example, I came to these conclusions.

Even when run interactively in a terminal, the work.sh script does not react to SIGINT sent from another terminal; but it does when interrupted with [Ctrl][c] from its own terminal.
I guess there is some kind of magic from bash in the terminal management...

Adding an explicit management of SIGINT in your script seems to make it actually react to SIGINT (whatever the origin, even from your C code).

#! /bin/bash

must_stop=""
function sigint_handler()
{
    echo "SIGINT"
    must_stop="1"
}
trap sigint_handler SIGINT

COUNTER=0
while [ -z "${must_stop}" ]
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

As stated in the comments, if the only purpose is to stop this script, may be SIGHUP or SIGTERM should be considered; SIGINT is generally more related to interactively hitting [Ctrl][c] in the terminal than to explicitly sending a signal from a program.

Note also that capturing the signal with trap offers the ability to exit cleanly from the loop rather than violently in the middle of any instruction.




回答3:


Allow me to point out that the shutdown approach could be improved, since SIGINT is suboptimal when used alone and doesn't guarantee a shutdown.

In fact, SIGINT shouldn't be used as an exit signal.

SIGINT could be used to notify the child process of a soft shutdown (for example, when the running process is a server script).

This will allow the child process to shutdown gracefully.

A hard shutdown requires SIGKILL, which is a signal that cannot be ignored and which is enforced by the OS.

The C code could, for example, look like this:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        int result = 0;
        sleep(5);
        kill(pid, SIGINT); /* soft shutdown */
        sleep(1);
        if(waitpid(pid, &result, WNOHANG) == -1) {
           /* we didn't exit? perform a hard shutdown */
           sleep(4);
           kill(pid, SIGKILL);
        }
    }
    return 0;
}

This will notify the child process of both a soft and a hard shutdown, allowing time for both.

...

Of course, a better approach might use sigtimedwait.




回答4:


You could also:

  1. Change kill(pid, SIGINT); to killpg(getpid(), SIGINT); this is what pressing ctrl-c would do; but it will hit your main() as well.
  2. If you don’t want your main hit, do a setpgid(0,0) just before the execvp(), and change the kill(pid, SIGINT) to killpg(pid, SIGINT).

Then the shell script doesn’t have to “help”. To be a little more overt, leave your shell script alone, and change your program to:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        setpgid(0,0);
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        sleep(5);
        killpg(pid, SIGINT);
    }

    return 0;
}


来源:https://stackoverflow.com/questions/58473685/sending-sigint-to-forked-exec-process-which-runs-script-does-not-kill-it

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!