前文简单介绍了Linux中的信号产生和信号捕捉的初步认识,这一篇文章我们将进一步了解Linux信号中的阻塞信号,并深入理解信号捕捉的具体过程。
阻塞信号
概念解释
在介绍阻塞信号之前,我们需要了解一些信号相关的概念:
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生带递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号产生时将保持未决状态,直到进程解除对此信号的阻塞,才执行递达动作
- 递达和忽略(Ignore)是不同的概念,只要信号被阻塞就不会被递达,而忽略是递达之后可选的一种处理动作
信号在Linux内核中的表示
从上图中可以看到,信号的表示是在对应的进程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
类型的变量之前,一定要调用sigemptyset
或sigfillset
做初始化,是信号集处于确定的状态;
四个函数函数的返回值都是成功返回 0,失败返回 -1;
sigismember
是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,包含返回 1,不包含返回 0,出错返回 -1;
sigprocmask
该函数用于读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
解释一下这里的参数含义:
- 如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出;
- 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;
- 如果上述二者均为非空指针,则先将原来的信号屏蔽字备份到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文件夹中;