新闻  |   论坛  |   博客  |   在线研讨会
嵌入式Linux:进程如何处理信号
美男子玩编程 | 2024-09-27 09:27:59    阅读:262   发布文章

在Linux系统中,当进程接收到信号后,可以通过设置信号处理方式来决定如何响应信号。


通常,信号的处理方式可以是以下三种之一:

  • 忽略信号进程对该信号不做任何处理,直接忽略。

  • 捕获信号为该信号设置一个处理函数,当信号到达时执行该函数。

  • 执行系统默认操作采用系统预定义的信号处理方式。


本篇文章主要讲解进程如何处理信号。Linux 系统提供了两个主要的函数 signal() 和 sigaction() 用于设置信号的处理方式。


1


signal()函数

signal()函数的原型如下:


#include <signal.h> typedef void (*sig_t)(int); sig_t signal(int signum, sig_t handler);


函数参数和含义:

  • signum指定需要进行设置的信号。你可以使用信号的名称(如SIGINT)或者其对应的数字编号。不过,建议使用信号名称,因为这样可读性更强。

  • handler这是一个sig_t类型的函数指针,用于指向信号的处理函数。

  • handler可以设置为以下几种:

    • 用户自定义函数这是一个处理函数,在接收到信号时会自动调用这个函数。该函数的参数是一个int类型的值,表示触发该函数的信号编号。通过这个参数,你可以在一个函数中处理多个信号。

    • SIG_IGN表示忽略该信号,进程在接收到该信号时不会进行任何处理。

    • SIG_DFL表示采用系统的默认处理方式,系统会对信号进行其预定义的操作。


返回值:signal()函数的返回值是一个sig_t类型的函数指针。成功调用时,返回指向之前信号处理函数的指针,这意味着你可以保存这个指针,以便在将来恢复原来的信号处理方式。如果调用失败,则返回SIG_ERR,并设置errno以指示错误原因。


以下是一个简单的示例代码,展示如何使用signal()函数来捕获SIGINT信号,并执行自定义的信号处理函数:


#include <stdio.h>#include <signal.h>#include <unistd.h> // 自定义信号处理函数void handle_signal(int signal) {    printf("Caught signal %dn", signal);} int main() {    // 将 SIGINT 信号处理方式设置为自定义的 handle_signal 函数    signal(SIGINT, handle_signal);     // 无限循环,等待信号    while(1) {        printf("Running...n");        sleep(1);    }     return 0;}


在上述代码中,当用户按下CTRL+C(触发SIGINT信号)时,自定义的handle_signal()函数会被调用,并输出捕获的信号编号。程序会继续运行,而不会终止。如果要忽略SIGINT信号,可以将signal(SIGINT, handle_signal);改为signal(SIGINT, SIG_IGN);。


2


sigaction() 函数

sigaction() 函数是 Linux 系统中用于设置信号处理方式的一个更强大且灵活的系统调用。与 signal() 函数相比,sigaction() 提供了更详细的控制和更高的移植性,因此更推荐在实际开发中使用它。


sigaction() 函数原型如下:


#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);


函数参数:

  • signum指定要设置处理方式的信号编号。可以为除 SIGKILL 和 SIGSTOP 以外的任何信号。

  • act指向 struct sigaction 结构体的指针,用于指定信号的新的处理方式。如果 act 为 NULL,则不改变信号的处理方式。

  • oldact指向 struct sigaction 结构体的指针,用于存储信号先前的处理方式。如果不需要获取原来的处理方式,可将其设置为 NULL。


返回值:成功返回 0;失败返回 -1,并设置 errno。


struct sigaction 结构体用于描述信号的处理方式,定义如下:


struct sigaction {    void (*sa_handler)(int);    void (*sa_sigaction)(int, siginfo_t *, void *);    sigset_t sa_mask;    int sa_flags;    void (*sa_restorer)(void);};


成员变量如下:

  • sa_handler信号处理函数指针,与 signal() 函数中的 handler 参数相同。可设置为自定义函数、SIG_IGN(忽略信号)或 SIG_DFL(系统默认处理)。

  • sa_sigaction另一个信号处理函数指针,用于处理带有更多信息的信号。与 sa_handler 互斥,通常使用 sa_handler。选择使用 sa_sigaction 需设置 SA_SIGINFO 标志。

  • sa_mask定义在执行信号处理函数期间要阻塞的信号集合,以避免信号之间的竞争条件。

  • sa_flags标志位,用于控制信号的处理行为。常用标志包括:

    • SA_NOCLDSTOP:阻止当子进程停止或恢复时发送 SIGCHLD 信号。

    • SA_NOCLDWAIT:子进程终止时不变为僵尸进程。

    • SA_NODEFER:不阻塞自身的信号。

    • SA_RESETHAND:执行完信号处理后将信号恢复为默认处理方式。

    • SA_RESTART:被信号中断的系统调用在信号处理完成后重新发起。

    • SA_SIGINFO:使用 sa_sigaction 代替 sa_handler。

  • sa_restorer已过时,通常不使用。


siginfo_t 结构体用于在 sa_sigaction 处理信号时传递更多的上下文信息,结构体定义如下:


typedef struct siginfo {    int si_signo;       /* Signal number */    int si_errno;       /* An errno value */    int si_code;        /* Signal code */    pid_t si_pid;       /* Sending process ID */    uid_t si_uid;       /* Real user ID of sending process */    void *si_addr;      /* Memory location which caused fault */    int si_status;      /* Exit value or signal */    int si_band;        /* Band event */    // ... 其他成员} siginfo_t;


下面是一个使用 sigaction() 捕获 SIGINT 信号的示例代码:


#include <stdio.h>#include <signal.h>#include <unistd.h> void handle_signal(int signal, siginfo_t *info, void *ucontext) {    printf("Caught signal %dn", signal);    printf("Signal sent by process %dn", info->si_pid);} int main() {    struct sigaction act;    act.sa_sigaction = handle_signal;    act.sa_flags = SA_SIGINFO;  // 使用 sa_sigaction 而不是 sa_handler    sigemptyset(&act.sa_mask);        sigaction(SIGINT, &act, NULL);     // 无限循环,等待信号    while(1) {        printf("Running...n");        sleep(1);    }     return 0;}


在这段代码中,sigaction() 用来设置 SIGINT 信号的处理方式。当用户按下 CTRL+C 发送 SIGINT 信号时,程序会调用 handle_signal() 函数,该函数可以通过 siginfo_t 结构体获取信号的更多信息,比如发送信号的进程 ID。


3


注意事项

当一个应用程序刚启动时,或在程序中未调用 signal() 或 sigaction() 来显式设置信号处理方式时,进程对所有信号的处理方式通常为系统默认操作。这意味着大多数信号在未被特殊处理的情况下,都会执行默认的处理动作。


当一个进程使用 fork() 系统调用创建一个子进程时,子进程会继承父进程的信号处理方式。由于子进程是通过复制父进程的内存映像而创建的,所以信号捕获函数的地址在子进程中同样有效。这意味着子进程将会继承父进程的信号处理函数和其他相关的信号处理状态。


这种继承机制确保了子进程在初始状态下能够正确处理信号,避免因为未定义的信号处理而导致不可预测的行为。如果需要,子进程可以在运行过程中修改其信号处理方式,从而实现特定的行为需求。


在设计信号处理函数时,通常建议保持其简单性。这与设计中断处理函数的原则相似:处理函数应尽可能简短和高效,避免执行大量耗费 CPU 时间的操作。

主要原因如下:


  • 减少信号竞争条件信号竞争条件(Race Condition)指的是在多线程或多进程环境中,不同信号可能在不合适的时间内打断正在处理的代码,导致不可预测的结果。如果信号处理函数复杂且耗时较长,进程在执行处理函数时,可能会接收到相同或其他信号,增加竞争条件发生的风险。

  • 保证系统响应性信号处理函数应快速完成,以确保系统能够及时响应其他事件或信号。如果处理函数占用了大量的 CPU 时间,系统响应速度可能会受到影响,尤其是在实时性要求较高的系统中。

  • 减少对系统状态的影响复杂的信号处理函数可能会改变进程的全局状态(如修改全局变量),这可能会导致进程在信号处理完成后进入不一致的状态。因此,简单的处理函数可以减少这些副作用。


最佳实践:

  • 在信号处理函数中,只执行必要的操作,如设置一个标志或记录一个简单的状态。

  • 如果需要执行复杂的逻辑,可以在信号处理函数中设置一个标志,然后在主程序的主循环中检查该标志,并执行相应的复杂逻辑。

    这种方式可以有效分离信号处理与复杂逻辑,降低风险。


通过保持信号处理函数的简单性,你可以有效提高程序的稳定性和可靠性,减少潜在的问题和复杂的调试过程。

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
推荐文章
最近访客