56/10 shell脚本 后台启动 程序1 + “tail -f log“, ctrl +c 导致程序1中断

前言

接上一篇文章, node 程序后台执行加上 tail 命令, 中断 tail 命令, 同时也中断了 node 程序

我们来详细 参照对比一下 这个问题的各种情况 

主要的脚本如下类似, 第一条命令 后台启动 程序1, 然后 第二条命令 tail -f 查看日志 

然后 ctrl+c 中断 "tail -f" 第一条命令的进程 也收到了 SIGINT 的信号 

nohup node nodeProcess.js > ./logs/nodeNohup.log 2>&1 &
tail -f ./logs/nodeNohup.log

c/node/java 程序的 前台/后台 执行的对比 

1. c 程序, 测试程序如下 

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

/**
 * sign handler
 * @param dunno
 */
void sigHandler(int dunno) {
    switch (dunno) {
        case 1:
            printf("Get a signal-SIGHUP \n");
            break;
        case 2:
            printf("Get a signal - SIGINT \n");
            break;
        case 3:
            printf("Get a signal - SIGQUIT \n");
            break;
    }
    fflush(stdout);
}

int main() {

    printf("press id is %d \n", getpid());
    fflush(stdout);
    signal(SIGHUP, sigHandler);
    signal(SIGINT, sigHandler);
    signal(SIGQUIT, sigHandler);

    struct sigaction sigAction;
    sigAction.sa_sigaction = sigHandler;
    sigAction.sa_flags = SA_SIGINFO;
    struct sigaction existsSigHandler;
    sigaction(SIGINT, &sigAction, &existsSigHandler);

    int counter = 0;
    while (1) {
        counter ++;
    }

}

新建 cStartUp.sh 脚本如下 

master:11_singal2Parent jerry$ cat cStartUp.sh 

/Users/jerry/ClionProjects/HelloWorld/cmake-build-debug/HelloWorld > ./logs/cNohup.log 2>&1 
# /Users/jerry/ClionProjects/HelloWorld/cmake-build-debug/HelloWorld > ./logs/cNohup.log 2>&1 &

tail -f ./logs/cNohup.log

在 Clion 中直接 运行/调试 

// 调试启动, 分别发送 SIGHUP, SIGINT 到进程, 发现 SIGINT 的 handler 没有生效
press id is 11024 
Signal: SIGHUP (signal SIGHUP)
Get a signal-SIGHUP 
Signal: SIGINT (signal SIGINT)


// 正常启动, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
press id is 11047 
Get a signal-SIGHUP 
Get a signal - SIGINT 

分别切换 cStartUp.sh 的前台启动 和 后台启动, 查看现象 

# 前台启动时, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
master:11_singal2Parent jerry$ tail -f logs/cNohup.log 
press id is 11137 
Get a signal-SIGHUP 
Get a signal - SIGINT 

# 后台启动时, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
press id is 11166 
Get a signal-SIGHUP 
Get a signal - SIGINT 

cStartUp.sh 的前台启动 和 后台启动时, 中断 "tail -f" 是否向 c程序 发送 SIGINT 

# 前台启动时 cStartUp.sh 的日志信息, "^C" 为 ctrl + c 的操作
master:11_singal2Parent jerry$ ./cStartUp.sh 
^C

# 读取日志文件, 发现 c程序 收到了 SIGINT
master:11_singal2Parent jerry$ tail -f logs/cNohup.log 
press id is 11581 
Get a signal-SIGHUP 
Get a signal - SIGINT 
Get a signal - SIGINT 


# 后台启动时 cStartUp.sh 的日志信息, 下面为 tail -f 的输出, "^C" 为 ctrl + c 的操作
master:11_singal2Parent jerry$ ./cStartUp.sh 
press id is 11166 
Get a signal-SIGHUP 
Get a signal - SIGINT 
^Cmaster:11_singal2Parent jerry$ 

# 读取日志文件, 发现 c程序 收到了 SIGINT
press id is 11166 
Get a signal-SIGHUP 
Get a signal - SIGINT 
Get a signal - SIGINT 

2. node 程序, 测试程序如下 

process.on('SIGHUP', function() {
    console.log('SIGHUP');
});

process.on('SIGINT', function() {
    console.log('SIGINT');
});


console.log('PID: ', process.pid);

var http = require('http'); // HTTP server to keep the script up long enough
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\
');
}).listen(1337, '127.0.0.1');

console.log('Server running at http://127.0.0.1:1337/');

nodeStartUp.sh 脚本如下

master:11_singal2Parent jerry$ cat nodeStartUp.sh 
nohup node nodeProcess.js > ./logs/nodeNohup.log 2>&1
# nohup node nodeProcess.js > ./logs/nodeNohup.log 2>&1 &

tail -f ./logs/nodeNohup.log

在 Webstorm 中直接 运行/调试 

// 调试启动, 分别发送 SIGHUP, SIGINT 到进程, 发现 SIGINT 的 handler 没有生效
press id is 11024 
Signal: SIGHUP (signal SIGHUP)
Get a signal-SIGHUP 
Signal: SIGINT (signal SIGINT)


// 正常启动, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
press id is 11047 
Get a signal-SIGHUP 
Get a signal - SIGINT 

分别切换 nodeStartUp.sh 的前台启动 和 后台启动, 查看现象 

# 前台启动时, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
PID:  11461
Server running at http://127.0.0.1:1337/
SIGHUP
SIGINT

# 后台启动时, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
PID:  11538
Server running at http://127.0.0.1:1337/
SIGHUP
SIGINT

nodeStartUp.sh 的前台启动 和 后台启动时, 中断 "tail -f" 是否向 node程序 发送 SIGINT 

# 前台启动时 nodeStartUp.sh 的日志信息, "^C" 为 ctrl + c 的操作
master:11_singal2Parent jerry$ ./nodeStartUp.sh 
^C

# 读取日志文件, 发现 node程序 收到了 SIGINT
master:11_singal2Parent jerry$ tail -f logs/nodeNohup.log 
PID:  11606
Server running at http://127.0.0.1:1337/
SIGHUP
SIGINT
SIGINT


# 后台启动时 nodeStartUp.sh 的日志信息, "^C" 为 ctrl + c 的操作
master:11_singal2Parent jerry$ ./nodeStartUp.sh 
PID:  11617
Server running at http://127.0.0.1:1337/
SIGHUP
SIGINT
^Cmaster:11_singal2Parent jerry$ 

# 读取日志文件, 发现 node程序 收到了 SIGINT
PID:  11617
Server running at http://127.0.0.1:1337/
SIGHUP
SIGINT
SIGINT

3. java 程序, 测试程序如下 

/**
 * Test07Signal2ParentProcess
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2022-09-10 19:18
 */
public class Test07Signal2ParentProcess {

    // Test07Signal2ParentProcess
    public static void main(String[] args) throws Exception {

        String lines = "HUP\n" +
                "INT\n" +
//                "QUIT\n" +
//                "ILL\n" +
                "TRAP\n" +
                "ABRT\n" +
                "EMT\n" +
//                "FPE\n" +
//                "KILL\n" +
                "BUS\n" +
//                "SEGV\n" +
                "SYS\n" +
                "PIPE\n" +
                "ALRM\n" +
                "TERM\n" +
                "URG\n" +
//                "STOP\n" +
                "TSTP\n" +
                "CONT\n" +
                "CHLD\n" +
                "TTIN\n" +
                "TTOU\n" +
                "IO\n" +
                "XCPU\n" +
                "XFSZ\n" +
                "VTALRM\n" +
                "PROF\n" +
                "WINCH\n" +
                // todo, not work in hostpostVM9
//                "INFO\n" +
//                "USR1\n" +
                "USR2";
        for (String sigNo : lines.split("\n")) {
            Signal.handle(new Signal(sigNo), new SignalHandler() {
                @Override
                public void handle(Signal signal) {
                    System.out.println(sigNo);
                }
            });
        }

        Signal.raise(new Signal("ALRM"));
        Signal.raise(new Signal("INT"));
        Thread.sleep(300 * 1000);

    }

}

startUp.sh 脚本如下 

master:11_singal2Parent jerry$ cat startUp.sh 

# java -classpath /Users/jerry/IdeaProjects/HelloWorld/target/classes com.hx.test13.Test07Signal2ParentProcess > ./logs/nohup.log 2>&1 &

# java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -classpath /Users/jerry/IdeaProjects/HelloWorld/target/classes com.hx.test13.Test07Signal2ParentProcess

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -classpath /Users/jerry/IdeaProjects/HelloWorld/target/classes com.hx.test13.Test07Signal2ParentProcess > ./logs/nohup.log 2>&1 &


tail -f ./logs/nohup.log

在 Idea 中直接 运行/调试 

// 调试启动, 分别发送 SIGHUP, SIGINT 到进程, 发现 SIGINT 的 handler 没有生效
INT
ALRM


// 正常启动, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
INT
ALRM

分别切换 startUp.sh 的前台启动 和 后台启动, 查看现象 

# 前台启动时, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
master:11_singal2Parent jerry$ ./startUp.sh 
Listening for transport dt_socket at address: 5005
INT
ALRM
^CINT

# 后台启动时, 分别发送 SIGHUP, SIGINT 到进程, SIGINT 的 handler 不生效
# 使用 kill -2 $pid 同样不生效
master:11_singal2Parent jerry$ ./startUp.sh 
Listening for transport dt_socket at address: 5005
ALRM

startUp.sh 的前台启动 和 后台启动时, 中断 "tail -f" 是否向 java程序 发送 SIGINT 

没有 SIGINT 的日志输出 就表示该 进程没有收到 SIGINT 的信号吗? 不一定 

# 前台启动时 startUp.sh 的日志信息, "^C" 为 ctrl + c 的操作
master:11_singal2Parent jerry$ ./startUp.sh 
Listening for transport dt_socket at address: 5005
INT
ALRM
^CINT


# 后台启动时 startUp.sh 的日志信息, "^C" 为 ctrl + c 的操作
master:11_singal2Parent jerry$ ./startUp.sh 
Listening for transport dt_socket at address: 5005
ALRM
^Cmaster:11_singal2Parent jerry$ 

# 读取日志文件, 没有 SIGINT 的日志输出 
Listening for transport dt_socket at address: 5005
ALRM

后台启动时, 使用 gdb 连接后台进程, ctrl + c 的时候, 能够接收到 SIGINT 的信号 

Thread 1 "java" received signal SIGINT, Interrupt.
0x00007fb7f452098d in pthread_join (threadid=140428059076352, 
    thread_return=0x7fffb6cb9e08) at pthread_join.c:90
90	in pthread_join.c

ubuntu 中基于 openJdk 调试 Test07Signal2ParentProcess

这里是基于 jvm, linux 的一些调试, 因此 我们期望看到更细节的东西 

这里 hotspotVM 基于 openjdk8 

比如 注册 SignalHandler 的时候, 是否会有一些不同的处理, 是否注册上 

另外就是 发送了 SIGINT 之后的处理?

startUp.sh 的前台启动 和 后台启动时, 中断 "tail -f" 是否向 java程序 发送 SIGINT 

 在 Clion 中直接 运行/调试 

这里的现象和 上面 Clion 调试 HelloWord 的现象一致, 应该是 Clion 的相关代理处理, 导致的问题

// 调试启动, 分别发送 SIGHUP, SIGINT 到进程, 发现 SIGINT 的 handler 没有生效
ALRM


// 正常启动, 分别发送 SIGHUP, SIGINT 到进程, 两个 handler 都生效
INT
ALRM

Clion 中调试看一下 注册 SignalHandler, 以及 接收到 SIGINT 之后的处理 

SIGHUP 注册了 UserHandler 作为 handler 

 SIGINT 注册了 UserHandler 作为 handler 

通过 raise 准备发送 SIGHUP 的信号 

这里往 将 0xea 赋值到 eax, 然后执行 syscall, 0xea 对应的系统调用为 tgkill, 类似于 kill 命令

向给定的 进程发送了 SIGHUP 的信号 

resultvar 对应于 eax, 值为 0, 表示 tgkill 的系统调用调用成功 

如下是收到了 SIGHUP 的信号之后, 当前进程的处理, 注册的函数是 UserHandler 

通过 raise 准备发送 SIGINT 的信号 

这里往 将 0xea 赋值到 eax, 然后执行 syscall, 0xea 对应的系统调用为 tgkill, 类似于 kill 命令

向给定的 进程发送了 SIGINT 的信号 

resultvar 对应于 eax, 值为 0, 表示 tgkill 的系统调用调用成功  

但是后面没有 收到 SIGINT 之后的回调处理, 这个就很奇怪? 

这也是我没有搞明白的地方 

我们再确认一下 raise 的时候, SIGINT 对应的 handler, 执行 "print os::signal(2, os::user_handler())", sigAct 为新的 handler, oldSigAct 为已有的 handler, 可以发现都是 UserHandler 

那就说明我们这里的情况是 tgkill 发送了 SIGINT 成功, 但是 后面操作系统有什么其他的处理?

Clion 远程调试 gdbServer 前台启动的服务, startGdbServer.sh 如下 

root@ubuntu:~/Desktop/openJdk/HelloWorld# cat startGdbServer.sh 

mainClazz=Test07Signal2ParentProcess
classpath=.:./lib/jol-core-0.8.jar:


EXISTS_PID=`jps -lvm | grep $mainClazz | grep -v grep | awk '{print $1}'`
if [ "$EXISTS_PID" = "" ]; then
    echo " the main class $mainClazz does not startup "
else
    kill -9 $EXISTS_PID
    echo " the main class $mainClazz shutdown succeed, kill -9 $EXISTS_PID "
fi


gdbserver localhost:1234 /root/Desktop/openJdk/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java -XX:+AllowUserSignalHandlers -cp $classpath $mainClazz > ./logs/nohup.log 2>&1
# gdbserver localhost:1234 /root/Desktop/openJdk/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java -XX:+AllowUserSignalHandlers -cp $classpath $mainClazz > ./logs/nohup.log 2>&1 & 


tail -f ./logs/nohup.log

注册 SIGHUP, SIGINT 的 handler, 均为 UserHandler 

 

raise 发送信号之后, 和上面一样 SIGHUP 进入了 UserHandler 的处理, SIGINT 没有进入 UserHandler 的处理  

Clion 远程调试 gdbServer 后台启动的服务, startGdbServer.sh 如下  

注册 SIGHUP 的 handler, 为 UserHandler, 但是 SIGHUP 的 handler 的注册, 我们发现 居然没有了? 

另外就是 raise 发送信号的处理, 也是一样, SIGHUP 的正常发送, 然后 SIGINT 的没有了? 

呵呵 这是本文的另外的一个 细节的地方了, 如果 没有调试, 这种现场 估计你想都想不到 

看第一个 RegisterSignal, 可以看到 SIGINT 的 handler 为 SIG_IGN 

这里对于 SIGHUP, SIGINT, SIGTERM 如果 已有的 handler 是 SIG_IGN 的话, 会直接返回, 返回 1, 表示 handler 为 SIG_IGN 

raise 的时候处理如下, SIGINT 的 handler 为 SIG_IGN, 因此 直接 返回了 

Clion 远程调试 gdbServer 后台启动的服务, 中断 "tail -f" 是否向 java程序 发送 SIGINT 

手动执行 "kill -2 $pid" 是可以看到一个 SIGINT 触发了断点中断 

但是当我 ctrl + c 中断 "tail -f " 的时候

在 gdb server 上面接收到了这个 SIGINT 的中断 

mac 中基于 openJdk 调试 Test07Signal2ParentProcess

和上述 ubuntu 的相关结论基本一致 

是那个进程发送的 SIG_INT?

基于 siginfo_t.si_pid 来获取发送信号的进程的信息 


#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "signal.h"

void signalHandler(int sigNumber, siginfo_t *info, void *context) {
    int senderPid = info->si_pid;
    printf(" process %d, sender process pid %d, got signal %d \r\n", getpid(), senderPid, sigNumber);
    fflush(stdout);
}

void processHandler(int pid, char *processName) {
    while(1) {
        printf(" I'm %s, and pid is %d \r\n", processName, pid);
        fflush(stdout);
        sleep(2);
    }
}

int main(int argc, char** argv) {

    // echo -e "nohup /Users/jerry/ClionProjects/HelloWorld/cmake-build-debug/HelloWorld >> logs/nohup.log 2>&1 &\n tail -f logs/nohup.log" >> start.sh
    // chmod +x start.sh
    struct sigaction sa;
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = signalHandler;

    sigaction(2, &sa, NULL);
    sigaction(3, &sa, NULL);
    sigaction(4, &sa, NULL);

    int childPid = fork();
    if(childPid == 0) {
        processHandler(getpid(), "child1");
    } else {
        int child2Pid = fork();
        if(child2Pid == 0) {
            processHandler(getpid(), "child2");
        } else {
            processHandler(getpid(), "parent");
        }
    }

}

再 mac 上面, 无论是 前台启动进程, 还是 后台启动进程 + "tail -f logs/nohup.log", 得到的日志 均是类似于如下 

I'm child2, and pid is 2246
I'm child1, and pid is 2245
I'm parent, and pid is 2243
process 2243, sender process pid 488, got signal 2
process 2246, sender process pid 488, got signal 2
process 2245, sender process pid 488, got signal 2
I'm child2, and pid is 2246
I'm child1, and pid is 2245
I'm parent, and pid is 2243

进程树 大致如下 , 可以看到的是 signal 是由 Terminal 这个进程发出的 

488      1   0  10:37AM ??         0:27.76 /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
    489    488   0  10:37AM ttys000    0:00.02 login -pf jerry
        // bash 
        490    489   0  10:37AM ttys000    0:00.19 -bash
            // start.sh
            2242   490   0  3:51PM ttys000    0:00.00 -bash
                2243  2242   0  3:51PM ttys000    0:00.01 /Users/jerry/ClionProjects/HelloWorld/cmake-build-debug/HelloWorld
                    2245  2243   0  3:51PM ttys000    0:00.00 /Users/jerry/ClionProjects/HelloWorld/cmake-build-debug/HelloWorld
                    2246  2243   0  3:51PM ttys000    0:00.00 /Users/jerry/ClionProjects/HelloWorld/cmake-build-debug/HelloWorld
                2244  2242   0  3:51PM ttys000    0:00.00 tail -f logs/nohup.log

再 linux 上面, 若果是直接 中断 "tail -f", 似乎是拿不到的发送 SIGINT 的进程的进程号的 

 I'm child1, and pid is 7521
 I'm parent, and pid is 7519
 I'm child2, and pid is 7522
 process 7522, sender process pid 0, got signal 2
 I'm child2, and pid is 7522
 process 7519, sender process pid 0, got signal 2
 I'm parent, and pid is 7519
 process 7521, sender process pid 0, got signal 2
 I'm child1, and pid is 7521
 I'm child2, and pid is 7522

但是手动 kill 发送信号, 是可以拿到 发送 SIGINT 的进程的进程号的

 I'm child2, and pid is 7522
 I'm child1, and pid is 7521
 I'm parent, and pid is 7519
 process 7521, sender process pid 7336, got signal 2
 I'm child1, and pid is 7521
 I'm child2, and pid is 7522
 I'm parent, and pid is 7519
 I'm child1, and pid is 7521
 process 7519, sender process pid 7336, got signal 2
 I'm parent, and pid is 7519
 I'm child2, and pid is 7522
 I'm child1, and pid is 7521
 process 7522, sender process pid 7336, got signal 2
 I'm child2, and pid is 7522
 I'm parent, and pid is 7519
 I'm child1, and pid is 7521

结论

1. 在后台启动了 c/node/java 程序之后, 添加了一个 "tail -f", 当 ctrl + c 中断 "tail -f" 的时候, 会发送 SIGINT 到对应的 c/node/java 进程 

2. 在后台启动 java 进程的场景中, 注册 SIGINT 实际上最终是没有注册成功, 会直接被忽略掉, 发送 SIGINT 信号到给定的进程, 也会被 忽略掉 

3. 在后台启动 java 进程的场景中, SIGINT 的 handler 在 java 层面 和 jvm 层面存放的逻辑 handler 不匹配, java 层面拿到的是注册的自定义的 handler, jvm 拿到的的是 SIG_IGN 

4. Clion 调试启动程序时候, 对于 SIGINT 信号的支持存在问题 ?

这里大概可以解释 node 程序后台执行加上 tail 命令, 中断 tail 命令, 同时也中断了 node 程序 中的第三个, 第四个问题 

3. java 程序这边通过 "startUp.sh" 启动, 进程到底收到 SIGINT 没有? 

4. java 程序这边程序中注册的 SIGINT 的 handler 为什么没有生效?, 替换成了什么?

java 程序这边通过 "startUp.sh" 启动, 然后 ctrl + c 中断了 "tail -f" 是收到了 SIGINT 的信号了的 

java 程序这边程序中注册的 SIGINT 的 handler

        如果是前台启动, 则会为 SIGINT 注册正确的 UserHandler/Test07Signal2ParentProcess$1, raise, kill 也能正确的发送信号, 但是因为神秘的原因, 没有处理这个信号 

        如果是后台启动, 则 SIGINT 默认注册的 handler 是 SIG_IGN, 因此后面用户手动注册的 handler 是没有注册上的, 然后 raise 的时候, 直接忽略了这个信号请求, 甚至还没有发送信号 

这里大概可以解释 node 程序后台执行加上 tail 命令, 中断 tail 命令, 同时也中断了 node 程序 中的第一个问题 

1. 但是是 哪一个进程呢 向 node进程 发送的 SIGINT ?

2. 这个进程 为什么要发送 SIGINT 到 node 进程呢 ?

在 mac 中就是 bash 对应的 父进程 发送的 SIGINT 信号

在 linux 中不确定

猜你喜欢

转载自blog.csdn.net/u011039332/article/details/126810608