C语言 什么是一个安全的方法来中断一个由fork()/system()创建的进程,而不是调用fork()/system()的线程?

k3bvogb1  于 2023-03-29  发布在  其他
关注(0)|答案(1)|浏览(134)

上下文:

我正在开发一个系统,用于通过以太网更新运行linux的设备。该设备托管一个更新服务器,当客户端连接时,客户端发送一个更新包,该更新包由服务器通过system()调用脚本在多个步骤中应用于设备。在此期间,第二个线程(pthread)向客户端发送进度更新并监视连接。
我希望服务器进程在检测到客户端断开连接时优雅地死亡,尽可能快(目的是如果客户端断开连接,防止更新完成并尽快重新启动更新服务器重试)。

问题:

我对此的问题是,如果脚本是运行时间较长的脚本之一,那么服务器当前将需要一段时间才能完成,在此期间,客户端可能会尝试再次连接并失败。(第二个线程检测到错误连接,设置原子布尔值以指示问题,并且在每个system()调用完成后,检查布尔值以查看过程是否应该继续)。

尝试次数:

我首先尝试将主线程的pthread TID存储在全局中,当连接失败时,第二个线程将在父TID上执行pthread_kill(),发送SIGINT并设置布尔值,我假设在终端上尝试这会导致system()产生的进程接收SIGINT并返回到主线程中执行,然后检查bool并退出,或者如果它在system()调用之间,它自己的SIGINT处理程序将捕获并以同样的方式退出。从我所知道的,这不起作用,因为system()禁用了父进程中的一些信号,包括SIGINT,而fork()system()调用中创建的进程将具有与调用它的主线程不同的PID/TID。

这导致我目前的尝试,我尝试重新创建system()的函数,以存储分叉的子PID,并且可以从第二个线程中断,而不是父TID,如本MVE所示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <sys/wait.h>
#include <stdatomic.h>

pthread_mutex_t lock;
pid_t forked_process = 0;
atomic_bool done = ATOMIC_VAR_INIT(false);

void *signal_thread(void * args){
    sleep(5);
    pthread_mutex_lock(&lock);
    if (forked_process != 0){
        kill(forked_process, SIGINT);
    }
    pthread_mutex_unlock(&lock);
    done = true;
}

int mysystem(const char * command){
    pid_t pid, res_pid;
    char *argp[] = {"sh", "-c", NULL, NULL};
    argp[2] = (char *)command;
    int status;
    
    switch (pid = fork()) {
    case -1:            /* error */
        return(-1);
    case 0:             /* child */
        execv("/bin/sh", argp);
        _exit(127);
    }
    pthread_mutex_lock(&lock);
    forked_process = pid;
    pthread_mutex_unlock(&lock);

    res_pid = waitpid(pid, &status, 0);

    pthread_mutex_lock(&lock);
    forked_process = 0;
    pthread_mutex_unlock(&lock);

    return status;
}

int main(int argc, char const *argv[])
{
    pthread_t child;
    int ret = 0;

    if (pthread_mutex_init(&lock, NULL)) {
        printf("Error: mutex init failed\n");
        return -1;
    }

    pthread_create(&child, NULL, &signal_thread, NULL);
    ret = mysystem("./script.sh");

    do {
        printf("Still in main, ret = %d\n", ret);
        sleep(1);
    } while (!done);
    return 0;
}

我知道这种方法本身可能有一些问题。例如,我可以看到一个竞争条件,其中forked child完成,但第二个线程首先获得互斥锁,并且向不再有效/属于不同进程的PID发送信号。(尽管我认为竞争条件和PID重用都不太可能以这种方式发生,如果我错了,请纠正我)。**我不明白的是为什么这个例子在目前的形式不工作。**正确的PID(如脚本和线程发送kill()所报告的)被发送,但继续完成。如果我发送SIGKILL,然而,这是不可取的,因为我希望脚本能够处理中断和清理后,自己。

**所以重申我的主要问题,**我应该研究哪些方法来实现所需的功能:

  • 主线程以可中断的方式执行脚本
  • 辅助线程可以中断在main或main中执行的脚本

我很高兴能被指出另一个方向来完成这一点,因为这似乎不是我用目前的方法可以轻松完成的事情,或者如果有人能指出一种方法来让我的示例以安全的方式按预期工作,我同样会很高兴。
为了完整起见,下面的脚本正在执行:

#!/bin/bash

for i in {1..20}
do
   echo "Sleeping in script PID = $$"
   sleep 1
done
echo "DONE"

它是用以下代码编译的:

gcc -pthread ./main.c -o main
kqhtkvqz

kqhtkvqz1#

我第一次尝试将主线程的pthread TID存储在全局中,当连接失败时,第二个线程将在父TID上执行pthread_kill(),并发送SIGINT [...]
这样做的主要问题不是SIGINT在调用system()的进程中被阻塞,而是在这种情况下需要发出信号的是 * 子进程 *。在运行system()的线程中发出信号并不能实现这一点。
您可以尝试kill(0, SIGINT)。这将向调用进程的进程组中的每个进程发送SIGINT,其中 * 可能 * 包括由system()启动的shell(但不一定是由该shell依次启动的进程)。它肯定包括调用进程本身,尽管SIGINT最初将被阻塞在那里。请注意,因为它也可以包括其他进程,例如更新服务器的父进程。
在我有限的测试中,system("./script.sh")kill(0, SIGINT)的组合似乎对我有效,但我建议注意确保产生的SIGINT不会杀死服务器进程,我现在不确定为什么它在我的测试中没有这样做,但它没有。
沿着同样的思路,您可以尝试kill(-1, SIGINT)。(除了一些未指定的系统进程)允许调用进程发出信号。这几乎肯定会包括与system()调用相关的所有进程。它将包括调用进程本身。而且它很可能也包括其他进程。当我测试这个时,我整个登录过程都被它毁了
我尝试重新创建system()的函数,以存储分叉的子PID,并且可以从第二个线程而不是父TID中断
[...]

我不明白为什么这个例子在目前的形式不起作用。

你似乎遇到了你的/bin/sh的一个行为。我发现如果我修改你的mysystem()函数来通过/bin/bash启动脚本...

execl("/bin/bash", "bash", "-c", command, (char *) NULL);

...然后它接收到另一个线程发送的SIGINT并中止。我无法根据the manual解释这一点。我尤其难以解释为什么当我从交互式环境执行相同的命令(/bin/bash -c ./script.sh)时,bash仍然忽略SIGINT

相关问题