前文简单介绍了Linux中的信号产生和信号捕捉的初步认识,这一篇文章我们将进一步了解Linux信号中的阻塞信号,并深入理解信号捕捉的具体过程。

阻塞信号

概念解释

在介绍阻塞信号之前,我们需要了解一些信号相关的概念:

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生带递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞(Block)某个信号
  • 被阻塞的信号产生时将保持未决状态,直到进程解除对此信号的阻塞,才执行递达动作
  • 递达和忽略(Ignore)是不同的概念,只要信号被阻塞就不会被递达,而忽略是递达之后可选的一种处理动作

信号在Linux内核中的表示

image.png 从上图中可以看到,信号的表示是在对应的进程task_struct结构下的,每个信号都由两个位图信息和一个处理函数组成,第一个位图信息用于标识信号是否被阻塞,第二个位图信息用于标识信号是否处于未决状态,而处理函数则由一个函数指针数组组成,下标就是对应的信号的处理函数;

sigset_t(信号集)

由上图可以发现,阻塞信号和递达信号的标识结构都是类似的,所以可以使用同一种数据结构(sigset_t)表示它们:<br>

sigset_t

是一个数据类型,用于表示一个信号集合。它通常用位图(bitmask)来表示多个信号,每个位代表一个信号的状态(存在或不存在),常用在信号的阻塞、等待等场景中,来表示哪些信号已经阻塞,哪些信号正在等待处理。

typedef struct {
    unsigned long sig[__SIGSET_WORDS];
} sigset_t;
  • 用途:
    • 管理信号阻塞集(signal mask),表示哪些信号被阻塞。
    • 挂起信号集(pending signals),表示哪些信号已经发送给进程但尚未处理。 这里,阻塞信号集也被称为当前进程的信号屏蔽字(Signal Mask);

信号集操作函数

虽然上述的演示说明信号集是通过对应位置上的bit位的有无标识信号的有效或无效状态的,但是其内部的具体实现则更为复杂,因此不可通过简单的位操作进行更改; 为此,信号库提供了一系列的操作函数用于对信号集进行处理:

#include <signal.h>
int sigemptyset(sigset_t* set); // 初始化set指向的信号集,使所有bit清零,表示该信号集不包含任何有效信号;
int sigfillset(sigset_t* set);  // 初始化set指向的信号集,使所有bit置位,表示该信号集包含所有支持的信号;
int sigaddset(sigset_t* set, int signo); // 在set指向的信号集中向指定位置写入信号;
int sigdelset(sigset_t* set, int signo); // 在set指向的信号集中删除指定位置的信号;
int sigismember(const sigset_t* set, int signo);

注意,在使用sigset_t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,是信号集处于确定的状态;

四个函数函数的返回值都是成功返回 0,失败返回 -1;sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,包含返回 1,不包含返回 0,出错返回 -1;

sigprocmask

该函数用于读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);

解释一下这里的参数含义:

  1. 如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出;
  2. 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;
  3. 如果上述二者均为非空指针,则先将原来的信号屏蔽字备份到oldset中,然后根据set和how参数更改信号屏蔽字;

下面以mask作为当前信号屏蔽字为例,说明how参数的可选值:

参数可选值 操作
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask\|set
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于maks = set

注意,如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmaks返回前,至少将其中一个信号递达;

sigpending

#include <signal.h>
int sigpending(sigset_t* set);

读取当前进程的未决信号集,通过set参数传出;成功返回 0,失败返回 -1, 下面进行实验演示:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <vector>

// 定义最大处理信号个数
#define NUM_SIG 32
// int sigemptyset(sigset_t *set);
// int sigfillset(sigset_t *set);
// int sigaddset(sigset_t *set, int signum);
// int sigdelset(sigset_t *set, int signum);
// int sigismember(const sigset_t *set, int signum);

// int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
// int sigpending(sigset_t* set);


std::vector<int> sig_vec = {2, 3};

void print_sig(const sigset_t& set){
  for(int i = NUM_SIG; i >= 1; --i){
    // 检查信号是否存在于当前信号集
    if(sigismember(&set, i)) std::cout << '1';
    else std::cout << '0';
  }
  std::cout << std::endl;
}

void handle(int signo){
  std::cout << "信号编号:" << signo << " has been called." << std::endl;
}

void set_signal(sigset_t set, sigset_t out_set){
  // 设置
  for(const auto& it : sig_vec) sigaddset(&set, it);
  for(const auto& it : sig_vec) signal(it, handle);
  sigprocmask(SIG_SETMASK, &set, &out_set); // 设置阻塞信号
}

int main(){
  sigset_t set, out_set;

  // 清空信号
  sigemptyset(&set);
  sigemptyset(&out_set);
  
  // 设置阻塞信号
  // 设置自定义处理函数
  set_signal(set, out_set);

  // for(const auto& it : sig_vec) sigaddset(&set, it);
  // for(const auto& it : sig_vec) signal(it, handle);
  // sigprocmask(SIG_SETMASK, &set, &out_set); // 设置阻塞信号

  int cnt = 10;
  int ans = 0;
  while(1){
    sigpending(&set);
    print_sig(set);
    if(!(cnt--)){
      sigprocmask(SIG_SETMASK, &out_set, &set); // 清除阻塞信号
      // 再次设置阻塞信号,用于下一次循环的正常进行
      set_signal(set, out_set);
      cnt = 10;
      ans++;
      if(ans == 3) kill(getpid(), 9);
      std::cout << std::endl << "ans = " << ans << std::endl;
    }
    else std::cout << std::endl << "cnt = " << cnt << std::endl;
    sleep(1);
  }
  return 0;
}

上述代码演示了通过signal提供的库函数对信号集内数据进行操作,实现了信号的阻塞、传递、递达等操作,至此我们便完成了Linux内核中有关信号的大部分操作,下一篇文章我将重点介绍Linux中的信号捕捉问题以及一种特殊的信号; 上面的代码可以在我的GitHub主页看到,位于signal文件夹中;