信号-进程间通信

发布时间:2024年01月10日

信号

1. 信号概述

在 Linux 操作系统中,信号是一种进程间通信的机制,用于通知进程发生了某个事件。信号可以由内核、其他进程,或者进程自身发送。每个信号都对应一个特定的事件或异常,例如进程终止、Ctrl+C 中断等。

本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很大的信息量,但是信号在系统中的优先级是非常高的。非常不建议使用信号进行进程间通信。

1.1 信号编号

通过 kill -l 命令可以察看系统定义的信号列表:

# 执行shell命令查看信号
$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

常见的 Linux 信号:

  1. SIGHUP (1): 终端挂起或控制进程终止。
  2. SIGINT (2): 用户发送中断信号(Ctrl+C)。
  3. SIGQUIT (3): 用户发送退出信号(Ctrl+\)。
  4. SIGILL (4): 检测到非法指令。
  5. SIGABRT (6): 调用 abort 函数,用于异常终止进程。
  6. SIGFPE (8): 浮点异常,如除以零。
  7. SIGKILL (9): 强制终止进程,无法被捕获或忽略。
  8. SIGSEGV (11): 段错误,试图访问未分配的内存。
  9. SIGPIPE (13): 向没有读取端口的管道写入数据。
  10. SIGALRM (14): 定时器超时。
  11. SIGTERM (15): 终止信号,用于请求进程正常终止。
  12. SIGUSR1 (10): 用户定义信号1。
  13. SIGUSR2 (12): 用户定义信号2。
1.2 查看信号信息

通过Linux提供的 man 文档可以查询所有信号的详细信息:

# 查看man文档的信号描述
$ man 7 signal

在信号描述中介绍了对产生的信号的五种默认处理动作,分别是:

  • Term:信号将进程终止

  • Ign:信号产生之后默认被忽略了

  • Core:信号将进程终止, 并且生成一个core文件(一般用于gdb调试)

  • Stop:信号会暂停进程的运行

  • Cont:信号会让暂停的进程继续运行

关于对信号的介绍有一句非常重要的描述:

The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

9号信号和19号信号不能被 捕捉, 阻塞, 和 忽略

  • 9号信号: 无条件杀死进程
  • 19号信号: 无条件暂停进程
1.3 信号的状态

在 Linux 中,信号可以有三种基本状态:未决(Pending)、阻塞(Blocked)、以及默认状态。这些状态描述了信号的当前处理情况。

  1. 未决状态(Pending):
    • 当一个信号产生时,它首先进入未决状态,表示该信号已经发生但尚未被处理
    • 如果多次产生相同的信号,未决状态只记录一次,不会累积。
  2. 阻塞状态(Blocked):
    • 进程可以选择阻塞(屏蔽)某些信号,使得这些信号在阻塞状态时不会被处理,而是保持在未决状态。
    • 阻塞状态是一种对信号的暂时性忽略,当信号被解除阻塞后,它会根据相应的处理方式处理。
  3. 默认状态:
    • 对于每种信号,都可以设置默认的处理方式,包括终止进程(Terminate)、忽略信号(Ignore)或执行用户自定义的处理函数(Handler)。
    • 默认状态是在信号产生时,如果未设置其他处理方式,则采用的方式。

2.信号相关函数

2.1 kill/raise/abort

kill 函数:

  • 用于向指定进程或进程组发送信号。

  • #include <signal.h>
    int kill(pid_t pid, int sig);
    
  • 参数 pid 表示进程或进程组的 ID,sig 表示要发送的信号编号。

raise 函数:

  • 用于向当前进程自身发送信号。

  • #include <signal.h>
    int raise(int sig);
    
  • 参数 sig 表示要发送的信号编号。

abort函数:

  • 给当前进程发送一个固定信号 (SIGABRT)
// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
#include <stdlib.h>
void abort(void);
2.2 定时器(信号捕捉)
alarm函数

alarm 函数是一个定时器函数,用于在指定时间后产生 SIGALRM 信号。它允许程序员设置一个定时器,当定时器超时时,内核将向进程发送 SIGALRM 信号。通常,SIGALRM 的默认操作是终止进程。

以下是 alarm 函数的基本形式:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
  • seconds 参数表示设置的定时器时间,以秒为单位。
  • 函数返回上一次设置的剩余时间(如果有),或者0(如果没有之前的定时器)。

使用 alarm 函数的一个常见用途是在程序中设置一个超时时间,以便在等待某个事件发生时避免无限期地阻塞。

下面是一个简单的例子,演示了如何使用 alarm 函数:

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

// SIGALRM 信号处理函数
void alarm_handler(int signum) {
    printf("Received SIGALRM\n");
}

int main() {
    // 设置 SIGALRM 的处理函数
    // 当程序接收到 SIGALRM 信号时,将调用 alarm_handler 函数来处理这个信号。
    signal(SIGALRM, alarm_handler);

    // 设置定时器,5 秒后发送 SIGALRM 信号
    unsigned int remaining_time = alarm(5);
    printf("Alarm set. Remaining time: %u seconds\n", remaining_time);

    // 模拟一些工作,等待超时
    sleep(7);

    // 如果在超时前收到了 SIGALRM,处理函数将被调用
    printf("Work done\n");

    return 0;
}
setitimer函数

setitimer () 函数可以进行周期性定时,每触发一次定时器就会发射出一个信号

// 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include <sys/time.h>

struct itimerval {
	struct timeval it_interval; /* 时间间隔 */
	struct timeval it_value;    /* 第一次触发定时器的时长 */
};
// 举例: luffy有一个闹钟, 并且使用这个闹钟定时:
// 早晨7点中起床, 第一次闹钟响起时可能起不来, 之后每隔5分钟再响一次
//  - it_value: 当前设置闹钟的时间点 到 明天早晨7点 对应的总秒数
//  - it_interval: 闹钟第一次响过之后, 每隔5分钟响一次

// 这个结构体表示的是一个时间段: tv_sec + tv_usec
struct timeval {
	time_t      tv_sec;         /* 秒 */
	suseconds_t tv_usec;        /* 微妙 */
};

int setitimer(int which, const struct itimerval *new_value, 
              struct itimerval *old_value);

参数:

  • which: 定时器使用什么样的计时法则, 不同的计时法则发出的信号不同
    • ITIMER_REAL: 自然计时法, 最常用, 发出的信号为SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间(从进程的用户区到内核区切换使用的总时间)
    • ITIMER_VIRTUAL: 只计算程序在用户区运行使用的时间,发射的信号为 SIGVTALRM
    • ITIMER_PROF: 只计算内核运行使用的时间, 发出的信号为SIGPROF
  • new_value: 给定时器设置的定时信息, 传入参数
  • old_value: 上一次给定时器设置的定时信息, 传出参数,如果不需要这个信息, 指定为NULL

例子:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>

void timer_handler(int signum) {
    printf("Timer expired!\n");
}

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

    // 设置定时器初始值和溢出后的处理方式
    struct itimerval timer;
    timer.it_value.tv_sec = 2;      // 初始值为2秒, 2s后第一次触发
    timer.it_value.tv_usec = 0;
    timer.it_interval.tv_sec = 1;   // 间隔为1秒,即每秒触发一次
    timer.it_interval.tv_usec = 0;

    // 设置定时器
    if (setitimer(ITIMER_REAL, &timer, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }

    // 让程序持续运行,观察定时器的触发
    while (1) {
        sleep(5);
    }

    return 0;
}

在这里插入图片描述

3. 信号集

3.1 信号集概述

信号集(Signal Set)是用于表示一组信号的数据结构。在 Unix/Linux 操作系统中,信号集被广泛用于进程管理中,主要用于设置、查询和修改进程的信号状态。信号集通常是一个二进制位图,每个位表示一个信号,位的状态表示信号的状态(屏蔽或未屏蔽)。

信号集的主要目的是允许进程选择性地阻塞或解除阻塞一组特定的信号,以实现对信号的灵活控制。以下是在 C 语言中使用信号集的相关函数和数据结构:

sigset_t 数据类型:

  • sigset_t 是用于存储信号集的数据类型,通常是一个整数或类似的位图结构。
  • 在头文件 <signal.h> 中定义,具体实现可能因系统而异。

在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集体现在内核中就是两张表。

在阻塞信号集中,描述这个信号有没有被阻塞

  • 默认情况下没有信号是被阻塞的, 因此信号对应的标志位的值为 0
  • 如果某个信号被设置为了阻塞状态, 这个信号对应的标志位 被设置为 1

在未决信号集中, 描述信号是否处于未决状态

  • 如果这个信号被阻塞了, 不能处理, 这个信号对应的标志位被设置为1
  • 如果这个信号的阻塞被解除了, 未决信号集中的这个信号马上就被处理了, 这个信号对应的标志位值变为0
  • 如果这个信号没有阻塞, 信号产生之后直接被处理, 因此不会在未决信号集中做任何记录
3.2 信号集函数

阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作。

读/写阻塞信号集的函数:

#include <signal.h>
// 使用这个函数修改内核中的阻塞信号集
// sigset_t 被封装之后得到的数据类型, 原型:int[32], 里边一共有1024给标志位, 每一个信号对应一个标志位
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

how:

  • SIG_BLOCK: 将参数 set 集合中的数据追加到阻塞信号集中
  • SIG_UNBLOCK: 将参数 set 集合中的信号在阻塞信号集中解除阻塞
  • SIG_SETMASK: 使用参 set 结合中的数据覆盖内核的阻塞信号集数据

oldset: 通过这个参数将设置之前的阻塞信号集数据传出,如果不需要可以指定为NULL

返回值:函数调用成功返回0,调用失败返回-1

sigprocmask() 函数有一个 sigset_t 类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数:

#include <signal.h>
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节

// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);

读未决信号集的操作函数:

#include <signal.h>
// 这个函数的参数是传出参数, 传出的内核未决信号集的拷贝
// 读一下这个集合就指定哪个信号是未决状态
int sigpending(sigset_t *set);

下面举一个简单的例子,演示一下信号集操作函数的使用:

需求: 
在阻塞信号集中设置某些信号阻塞, 通过一些操作产生这些信号, 然后读未决信号集, 最后再解除这些信号的阻塞
假设阻塞这些信号: 
  - 2号信号: SIGINT: ctrl+c
  - 3号信号: SIGQUIT: ctrl+\
  - 9号信号: SIGKILL: 通过shell命令给进程发送这个信号 kill -9 PID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

int main()
{
    // 1. 初始化信号集
    sigset_t myset;
    sigemptyset(&myset);
    // 设置阻塞的信号
    sigaddset(&myset, SIGINT);  // 2
    sigaddset(&myset, SIGQUIT); // 3
    sigaddset(&myset, SIGKILL); // 9 测试不能被阻塞

    // 2. 将初始化的信号集中的数据设置给内核
    sigset_t old;
    sigprocmask(SIG_BLOCK, &myset, &old);

    // 3. 让进程一直运行, 在当前进程中产生对应的信号
    int i = 0;
    while(1)
    {
        // 4. 读内核的未决信号集
        sigset_t curset;
        sigpending(&curset);
        // 遍历这个信号集
        for(int i=1; i<32; ++i)
        {
            int ret = sigismember(&curset, i);
            printf("%d", ret);
        }
        printf("\n");
        sleep(1);
        i++;
        if(i==10)
        {
            // 解除阻塞, 重新设置阻塞信号集
            //sigprocmask(SIG_UNBLOCK, &myset, NULL);
            sigprocmask(SIG_SETMASK, &old, NULL);
        }
    }
    return 0;
}
文章来源:https://blog.csdn.net/f593256/article/details/135496425
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。