【嵌入式Linux应用开发基础】进程间通信(4):信号

目录

一、信号概述

1.1. 定义

1.2. 特点

二、常见信号及含义

2.1. SIGINT(2)

2.2. SIGTERM(15)

2.3. SIGKILL(9)

2.4. SIGSTOP(19)

2.5. SIGCONT(18)

2.6. SIGSEGV(11)

三、信号的发送

3.1. 使用 kill 命令

3.2. 使用 kill 函数

3.3. 使用 raise 函数

3.4. 使用 alarm函数

四、信号的处理

4.1. 默认处理动作

4.2. 忽略信号

4.3. 捕获信号并处理

五、信号阻塞与未决信号

5.1. 信号阻塞

5.2. 未决信号

六、信号处理函数设计要点

七、关键注意事项

八、典型应用场景

九、总结

十、参考资料


信号(Signal)是Linux系统中用于进程间异步通知的一种机制。当一个进程接收到信号时,它可以执行相应的信号处理函数,从而实现进程间的简单通信或事件通知。信号机制是Unix系统中最为古老的进程间通信机制之一,由操作系统内核提供。

一、信号概述

1.1. 定义

在嵌入式 Linux 系统中,信号(Signal)是一种异步的进程间通信机制,用于通知进程发生了某种特定的事件。信号可以由系统内核产生,也可以由一个进程发送给另一个进程。信号的本质是一种软件中断,它可以打断进程当前的执行流程,转而执行信号处理函数,处理完信号后再返回到原来的执行点继续执行。

信号在嵌入式系统中具有重要的作用,例如用于处理程序的异常情况(如除零错误、段错误等)、实现进程间的异步通知(如父进程通知子进程终止)等。

1.2. 特点

轻量级、不可靠(可能丢失)、无优先级,适用于简单事件通知。

二、常见信号及含义

Linux 系统定义了一系列的信号,每个信号都有一个唯一的编号和名称。以下是一些常见的信号及其含义。

2.1. SIGINT(2)

  • 名称:中断信号
  • 产生方式:当用户在终端按下 Ctrl+C 组合键时,会向当前前台进程发送该信号。
  • 默认处理动作:终止进程

2.2. SIGTERM(15)

  • 名称:终止信号
  • 产生方式:通常由系统或其他进程发送,用于请求进程正常终止。
  • 默认处理动作:终止进程

2.3. SIGKILL(9)

  • 名称:强制终止信号
  • 产生方式:由系统或其他进程发送,用于强制终止一个进程,该信号不能被捕获或忽略。
  • 默认处理动作:立即终止进程

2.4. SIGSTOP(19)

  • 名称:停止信号
  • 产生方式:可以由系统或其他进程发送,用于暂停一个进程的执行。
  • 默认处理动作:暂停进程

2.5. SIGCONT(18)

  • 名称:继续信号
  • 产生方式:用于恢复一个被暂停的进程继续执行。
  • 默认处理动作:恢复进程执行

2.6. SIGSEGV(11)

  • 名称:段错误信号
  • 产生方式:当进程访问了非法的内存地址时,系统会发送该信号。
  • 默认处理动作:终止进程,并生成核心转储文件(如果系统配置允许)

三、信号的发送

在嵌入式 Linux 中,可以使用以下几种方式发送信号。

3.1. 使用 kill 命令

在终端中,可以使用 kill 命令向指定的进程发送信号。例如,要向进程 ID 为 1234 的进程发送 SIGTERM 信号,可以使用以下命令:

kill -15 1234

其中,-15 表示信号编号,也可以使用信号名称,如 kill -TERM 1234

3.2. 使用 kill 函数

在 C 语言程序中,可以使用 kill 函数向指定的进程发送信号。kill 函数的原型如下:

#include 
#include 

int kill(pid_t pid, int sig);
  • 参数说明

    • pid:目标进程的 ID。如果 pid > 0,则表示向指定 ID 的进程发送信号;如果 pid == 0,则表示向调用进程所在的进程组中的所有进程发送信号;如果 pid == -1,则表示向所有有权限发送信号的进程发送信号;如果 pid < -1,则表示向 ID 为 -pid 的进程组中的所有进程发送信号。
    • sig:要发送的信号编号。
  • 返回值:成功时返回 0,失败时返回 -1,并设置 errno 变量。 

3.3. 使用 raise 函数

raise 函数用于向调用进程自身发送信号。其原型如下:

#include 

int raise(int sig);
  • 参数说明sig 为要发送的信号编号。
  • 返回值:成功时返回 0,失败时返回非零值。

3.4. 使用 alarm函数

设置定时器(发送SIGALRM)。

unsigned int alarm(unsigned int seconds);

四、信号的处理

进程可以对信号采取不同的处理动作,主要有以下三种。

4.1. 默认处理动作

每个信号都有一个默认的处理动作,如上述常见信号中提到的终止进程、暂停进程等。当进程接收到信号后,如果没有对该信号进行特殊处理,就会执行默认处理动作。

4.2. 忽略信号

进程可以选择忽略某些信号,即不做任何处理。可以使用 signal 或 sigaction 函数将信号的处理动作设置为 SIG_IGN。例如,忽略 SIGINT 信号的代码如下:

#include 

void ignore_signal() {
    signal(SIGINT, SIG_IGN);
}

4.3. 捕获信号并处理

进程可以捕获信号,并执行自定义的信号处理函数。可以使用 signal 或 sigaction 函数来设置信号处理函数。

①使用 signal 函数

signal():简单注册信号处理函数(不推荐,兼容性差)。

signal 函数的原型如下:

#include 

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
  • 参数说明

    • signum:要处理的信号编号。
    • handler:信号处理函数的指针。可以是自定义的信号处理函数,也可以是 SIG_IGN(忽略信号)或 SIG_DFL(执行默认处理动作)。
  • 返回值:返回之前的信号处理函数指针,失败时返回 SIG_ERR

以下是一个使用 signal 函数捕获 SIGINT 信号的示例:

#include 
#include 
#include 

void sigint_handler(int signum) {
    printf("Received SIGINT signal. Exiting...\n");
    // 可以在这里进行一些清理工作
    _exit(0);
}

int main() {
    // 设置信号处理函数
    signal(SIGINT, sigint_handler);

    while (1) {
        printf("Running...\n");
        sleep(1);
    }

    return 0;
}

 ②使用 sigaction 函数

sigaction 函数比 signal 函数更强大和灵活,它可以更精确地控制信号的处理。其原型如下:

#include 

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数说明
    • signum:要处理的信号编号。
    • act:指向 struct sigaction 结构体的指针,用于设置新的信号处理动作。
    • oldact:指向 struct sigaction 结构体的指针,用于保存旧的信号处理动作。

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);
};

 以下是一个使用 sigaction 函数捕获 SIGINT 信号的示例:

#include 
#include 
#include 

void sigint_handler(int signum) {
    printf("Received SIGINT signal. Exiting...\n");
    // 可以在这里进行一些清理工作
    _exit(0);
}

int main() {
    struct sigaction act;
    act.sa_handler = sigint_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    // 设置信号处理动作
    if (sigaction(SIGINT, &act, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    while (1) {
        printf("Running...\n");
        sleep(1);
    }

    return 0;
}

五、信号阻塞与未决信号

5.1. 信号阻塞

进程可以选择阻塞某些信号,即暂时不处理这些信号。被阻塞的信号不会立即被处理,而是处于未决状态,直到进程解除对该信号的阻塞。可以使用 sigprocmask 函数来设置信号掩码,从而实现信号的阻塞和解除阻塞。

sigprocmask 函数的原型如下:

#include 

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  • 参数说明
    • how:指定如何修改信号掩码。可以取值为 SIG_BLOCK(将 set 中的信号添加到当前信号掩码中)、SIG_UNBLOCK(从当前信号掩码中移除 set 中的信号)或 SIG_SETMASK(将当前信号掩码设置为 set)。
    • set:指向 sigset_t 类型的信号集的指针,用于指定要操作的信号。
    • oldset:指向 sigset_t 类型的信号集的指针,用于保存旧的信号掩码。

5.2. 未决信号

未决信号是指已经发送给进程,但由于进程对该信号进行了阻塞,而暂时未被处理的信号。可以使用 sigpending 函数来获取当前进程的未决信号集。

#include 

int sigpending(sigset_t *set);
  • 参数说明set 指向 sigset_t 类型的信号集的指针,用于保存当前进程的未决信号集。
  • 返回值:成功时返回 0,失败时返回 -1,并设置 errno 变量。

六、信号处理函数设计要点

  • 保持简洁:避免复杂操作(如调用mallocprintf等非异步安全函数)。

  • 使用volatile变量:确保信号处理函数与主程序共享变量时可见性。

  • 阻塞相关信号:通过sa_mask防止处理函数被嵌套调用。

  • 原子操作:修改全局变量时使用原子操作或锁机制。

七、关键注意事项

  • 不可重入函数:避免在信号处理函数中使用如printfmalloc等。

  • 实时信号:使用SIGRTMINSIGRTMAX的信号支持排队(通过sigqueue发送)。

  • 多线程环境:信号处理是进程级的,需谨慎处理线程与信号的交互。

八、典型应用场景

  • 硬件中断模拟:如通过信号响应外部事件。

  • 进程管理:监控子进程退出(SIGCHLD)。

  • 超时控制:结合SIGALRM实现任务超时机制。

九、总结

信号是嵌入式 Linux 系统中一种重要的进程间通信机制,它提供了一种异步的事件通知方式。通过合理地使用信号的发送、处理、阻塞和未决等机制,可以实现进程间的高效通信和异常处理。在实际开发中,需要注意信号处理函数的编写规范,避免在信号处理函数中调用不可重入函数,以确保系统的稳定性和可靠性。同时,要根据具体的应用场景选择合适的信号和处理方式,以满足系统的需求。

十、参考资料

  • 《Unix 环境高级编程(第 3 版)》
    • 作者是 Richard A. Stevens 和 Stephen A. Rago。这本书是 Unix 和 Linux 系统编程的经典之作,其中深入且全面地讲解了信号机制。详细阐述了信号的概念、各种信号的含义、信号的发送与处理方法,还涉及到信号与进程控制、线程等方面的交互。
  • 《Linux 内核设计与实现(第 3 版)》
    • 作者是 Robert Love。该书从内核层面剖析了 Linux 系统的工作原理,对于信号部分,介绍了信号在内核中的实现机制,包括信号的产生、传递、内核如何处理信号等底层细节。
  • 《嵌入式 Linux 应用开发完全手册》
    • 专门针对嵌入式 Linux 应用开发,书中结合嵌入式系统的特点,介绍了信号在嵌入式环境下的具体应用和优化方法。涵盖了如何在资源受限的嵌入式设备中合理使用信号进行进程间通信,以及如何处理信号与硬件中断等相关问题。
  • Linux 官方手册页
    • 在 Linux 系统中,可以通过 man 命令查看信号相关的手册页,如 man 7 signal 可以获取 Linux 系统中信号的详细信息,包括信号编号、名称、默认动作等。这些手册页是最权威和准确的参考资料,反映了当前系统所支持的信号机制。
  • GNU C 库文档
    • GNU C 库是 Linux 系统中广泛使用的 C 语言库,其官方文档对信号相关的函数(如 signalsigactionkill 等)进行了详细的说明,包括函数的原型、参数解释、返回值以及使用示例等。文档地址为:Top (The GNU C Library)。
  • Linux 内核源代码
    • Linux 内核源代码是了解信号实现机制的最直接资料。可以通过在线的内核源代码仓库(如 Kernel.org git repositories)查看内核中与信号处理相关的代码文件,如 include/linux/signal.hkernel/signal.c 等,深入研究信号在内核中的具体实现细节。

你可能感兴趣的:(#,嵌入式C语言开发,c语言,开发语言,linux,嵌入式软件开发)