传递参数给信号处理函数

79

我可以为信号处理程序提供/传递任何参数吗?

/* Signal handling */
struct sigaction act;
act.sa_handler = signal_handler;
/* some more settings */

现在,handler看起来像这样:

void signal_handler(int signo) {
    /* some code */
}

如果我想做一些特殊的事情,比如删除临时文件,我能否将这些文件作为参数提供给此处理程序?

编辑0:谢谢回答。我们通常避免/不鼓励使用全局变量。在这种情况下,如果你有一个庞大的程序,不同的地方可能出现问题,你可能需要做很多清理工作。为什么API是这样设计的?

8个回答

71

在信号处理程序中,你不能将自己的数据作为参数传递。相反,你需要将参数存储在全局变量中。(如果安装信号处理程序后需要更改这些数据,请非常小心)。

对编辑0的回应: 历史原因。信号是一种非常古老和低级别的设计。基本上,你只需给内核提供一个指向某些机器代码的单个地址,并要求它在发生某种情况时转到这个特定地址。我们回到了“可移植汇编语言”的思维模式,其中内核提供了一个简单的基线服务,而用户进程可以合理地期望其自己完成任何操作。

此外,通常反对全局变量的论点在这里并不适用。信号处理程序本身就是一个全局设置,因此没有相关的可能性为它提供几个不同的用户指定参数集。(嗯,实际上它并不完全是全局的,而只是线程全局的。但线程API将包括一些机制来进行线程本地存储,在这种情况下正好符合你的要求)。


3
信号处理程序不是针对每个线程的。更改信号的处理程序会应用于所有线程。 - R.. GitHub STOP HELPING ICE
3
实际上,我不确定从信号处理程序访问TLS是否有效。使用glibc / NPTL,通常情况下通常无效,因为在线程创建后但在控制传递到启动函数之前,信号可能会立即被传递,而线程的TLS副本仍在初始化中。如果您在此时小心地阻止信号,则可能是可以的,但这仍然是一个可疑的做法... - R.. GitHub STOP HELPING ICE
顺便说一下,即使没有TLS,也有一种方法可以以异步信号安全的方式识别信号处理程序正在运行的线程:&errno对于每个线程是唯一的!但是,我认为由于刚才描述的相同原因,在glibc / NPTL中可能不安全。如果不是这样,我相信这是符合性错误,但它可能是足够罕见的竞争,以便未被检测到... - R.. GitHub STOP HELPING ICE
@R.. 信号处理程序可以基于每个线程。POSIX提供了功能,您可以使用它来向同一进程中的单个线程发送信号。 - Curious
1
@Curious:你的两个句子没有关联。是的,你可以向特定线程发送信号,但处理程序是全局状态。除非实现自己的调度程序作为主信号处理程序,否则无法在每个线程上设置不同的处理程序,这既难以在可能需要设置每个线程处理程序的不同库之间协调,也在技术上不可能,因为没有 AS 安全的方法让信号处理程序确定它正在运行的线程。 - R.. GitHub STOP HELPING ICE
显示剩余2条评论

19

信号处理程序的注册已经是一个等价于全局变量的全局状态。因此,在其中使用全局变量传递参数并不会有更大的问题。然而,从信号处理程序执行任何操作都是一个巨大的错误(除非你是专家,否则几乎肯定会导致未定义的行为!)。如果你改为仅阻塞信号并从主程序循环中轮询它们,就可以避免所有这些问题。


5
除非你是信号方面的专家,否则从信号处理程序中执行任何操作都非常危险。相反,你可以使用信号处理程序来保存一个标志表示发生了信号,并在信号处理程序外的各个点检查它,或者你可以阻塞信号并使用 sigpendingsigwait 来轮询信号。 - R.. GitHub STOP HELPING ICE
2
哇,这对我来说完全是新的。我以前看过代码进行清理,例如删除临时文件或子进程清理,然后主程序终止。另外,当你说要检查标志时,如果你收到SIGTERM信号,那么这怎么可能起作用呢?如果你没有在处理程序中处理它,你的程序不会立即终止吗?为什么在处理程序内部做事情是“危险”的? - hari
5
信号处理程序可以在任何时候中断程序。这意味着,除非您已经采取措施确保这种情况不会发生,否则它可能在与各种数据对象一起工作时处于不一致状态。例如,FILE内的缓冲区指针可能仅部分更新,或者打开的FILE的链接列表可能有一个半插入或半删除的项部分链接到它。或者内部的malloc数据结构可能会参考已经释放了一半的内存... 如果信号处理程序调用依赖于该状态保持一致的函数,则会发生非常糟糕的事情! - R.. GitHub STOP HELPING ICE
5
顺便提一句,使用信号处理程序的一个经典而安全的方法是打开一个指向自己的管道,并将文件描述符存储在某个全局位置。信号处理程序可以向管道写入信息(这是安全的),你可以在主 select/poll 循环中等待来自管道的输入,以及其他等待输入的文件描述符。 - R.. GitHub STOP HELPING ICE
2
@R.. 在Linux上有signalfd()可以做到这一点。仍然是一个好主意。很少有人考虑使用管道在进程内部通信。这使得线程之间的通信更加容易和明确。 - MauganRa
显示剩余5条评论

11

这是一个很老的问题,但我认为我可以向您展示一个很好的技巧来解决您的问题。不需要使用sigqueue或其他方法。

我也不喜欢使用全局变量,所以我不得不找到一种聪明的方式,在我的情况下,发送void指针(稍后可以将其转换为适合您需要的任何类型)。

实际上,您可以这样做:

signal(SIGWHATEVER, (void (*)(int))sighandler); // Yes it works ! Even with -Wall -Wextra -Werror using gcc

那么你的 sighandler 应该是这个样子:

int sighandler(const int signal, void *ptr) // Actually void can be replaced with anything you want , MAGIC !
你可能会问:如何获得*ptr?以下是方法:在初始化时
signal(SIGWHATEVER, (void (*)(int))sighandler)
sighandler(FAKE_SIGNAL, your_ptr);

在您的 sighandler 函数中:

int sighandler(const int signal, void *ptr)
{
  static my_struct saved = NULL;

  if (saved == NULL)
     saved = ptr;
  if (signal == SIGNALWHATEVER)
     // DO YOUR STUFF OR FREE YOUR PTR
   return (0);
}

6
不错的技巧,但不幸的是它在标准中没有定义。请参见6.3.2.3(8):“如果使用转换后的指针调用与所引用类型不兼容的函数,则行为是未定义的”。 - eush77

10

1

我认为最好在sa_flags中使用SA_SIGINFO,这样处理程序将在siginfo_t中获取参数:void signal_handler(int sig, siginfo_t *info, void *secret)

Ty:HAPPY code


2
你可以在 sigval 联合体中设置一些指针或其他数据,然后将其传递给 sigqueue() 以排队实时(用户定义)信号,然后通过 siginfo_t 参数中的 sigval 联合体获取信号处理程序中指定的任何内容。但是,对于非实时信号,您无法指定用户定义数据。此外,早先已经提到过使用 sigqueue()... - Wad

1

将文件名存储在全局变量中,然后从处理程序中访问它。信号处理程序回调只会传递一个参数:导致问题的实际信号的ID(例如SIGINT、SIGTSTP)

编辑0:“不允许向处理程序传递参数必须有一个非常充分的理由。” <-- 存在中断向量(基本上是每个可能信号的例程跳转地址集)。根据触发中断的方式,调用特定函数。不幸的是,不清楚与变量相关联的内存将在哪里被调用,并且根据中断的不同,该内存实际上可能会被破坏。有一种方法可以解决这个问题,但是那样就无法利用现有的int 0x80汇编指令(某些系统仍在使用)


0
尽管这个问题非常古老,但它今天仍然存在。基本上,为了正确关闭临时文件、终止线程等,可以使用以下逻辑:
volatile sig_atomic_t sig_received = 0;

void sigterm_handler(int signum)
{
    printf("SIGTERM. PID: %d\n", getpid());
    sig_received = 1;
}
    void sigint_handler(int signum)
{
    fprintf(stderr, "SIGINT. PID: %d\n", getpid());
}

...

int main()
{
    struct sigaction action;
    action.sa_handler = sigterm_handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    sigaction(SIGTERM, &action, NULL);

    action.sa_handler = sigint_handler;
    sigaction(SIGINT, &action, NULL);

    pthread_t writer_thread, reader_thread;
    struct master_argv writer_args, reader_args;

    buffer_init(&(writer_args.buffer));
    buffer_init(&(reader_args.buffer));

    writer_args.pipename = PIPE_CAPOSC;
    reader_args.pipename = PIPE_CAPOLET;

    if (pthread_create(&writer_thread, NULL, master, (void *)&writer_args) != 0)
    {
        exit(1);
    }

    if (pthread_create(&reader_thread, NULL, master, (void *)&reader_args) != 0)
    {
        exit(1);
    }

    while (!sig_received)
    {
        sleep(1);
    }

    pthread_join(writer_thread, NULL);
    pthread_join(reader_thread, NULL);

    buffer_destroy(&(writer_args.buffer));
    buffer_destroy(&(reader_args.buffer));

    return 0;
}

基本上,在信号管理器中,设置了一个sig_atomic_t标志,它保证对该变量的原子访问,并使用volatile来向编译器发出信号,表明该变量不应被优化,因为它可能会遭受意外更改,例如被信号修改。
使用此标志,您可以通过不使用全局变量以安全的方式处理关闭,例如一系列线程的示例。

-1

你可以使用一个类的方法作为信号处理器。然后,该处理器可以访问该类的成员数据。我不完全确定Python在C signal()调用周围的操作,但它必须重新定义数据的作用域吧?

我很惊讶这个方法能够运行,但它确实可以。运行此代码,然后从另一个终端杀死该进程。

import os, signal, time

class someclass:
    def __init__(self):
        self.myvalue = "something initialized not globally defined"
        signal.signal(signal.SIGTERM, self.myHandler)
    def myHandler(self, s, f):
        # WTF u can do this?
        print "HEY I CAUGHT IT, AND CHECK THIS OUT", self.myvalue


print "Making an object"
a = someclass()

while 1:
    print "sleeping.  Kill me now."
    time.sleep(60)

5
这个问题涉及到C语言...虽然Python很可能会在后面记住信号并尽快处理它(在信号处理程序中进行操作是很危险的)。可能会使用全局变量来实现。 - MauganRa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接