文章目录
- 1.信号相关常见概念
- 2.管理信号的数据结构
- 3.初识sigset_t
- 4.信号集操作函数
- 4.1sigpending
- 4.2sigprocmask
- 4.2代码测试
- 1.测试1
- 2.测试2
- 3.测试3
- 4.3bash 脚本文件
1.信号相关常见概念
信号相关动作:产生 发送 接收 阻塞 递达(处理)
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态 称为信号未决(Pending)
进程可以选择阻塞 (Block )某个信号
信号产生后未被递达前 或 被阻塞的信号 将保持在未决状态 直到进程解除对此信号的阻塞 才执行递达的动作
阻塞信号和忽略信号的区别
阻塞和忽略是不同的 只要信号被阻塞就不会递达 而忽略是在递达之后可选的一种处理动作
忽略: 对信号已经进行了处理,处理方式是不做处理
阻塞:压根就没有对信号进行处理。
2.管理信号的数据结构
- 在进程PCB中,信号的pending位图、block位图和handler函数指针数组是用于管理信号处理的数据结构。
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending) 以及一个函数指针数组表示处理动作
- 信号产生未被递达时 内核在进程控制块中设置该信号的未决标志 直到信号递达才清除该标志
- a. 上图SIGHUP信号未阻塞也未产生过 当它递达时执行默认处理动作
b. SIGINT信号产生了 但正在被阻塞 暂时不能递达 虽然它的处理动作是IGN(忽略) 但在没有解除阻塞之前 这个信号是不能进行处理即去忽略的 【在解除阻塞前进程仍有机会改变处理动作】
c. SIGQUIT信号未产生过 一旦产生SIGQUIT信号将被阻塞 它的处理动作是用户自定义函sighandler
如果在进程解除对某信号的阻塞之前 这种信号产生过多次 将如何处理?
POSIX.1允许系统递送该信号一次或多次。
Linux是这样实现的:常规信号在递达之前产生多次只计一次 而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
解读相关数据结构
在Linux下,PCB(进程控制块)的pending位图与信号的处理紧密相关。pending位图主要表示已经收到但是还没有被递达的信号。换句话说,当一个进程接收到一个信号,但这个信号尚未被进程处理(例如,因为进程当前正在执行某个不允许被中断的操作,或者信号的处理函数尚未被执行),那么这个信号的状态就会被记录在pending位图中。
pending位图的作用在于跟踪哪些信号已经到达进程但尚未被处理。这允许操作系统在适当的时机递达这些信号,比如当进程处于可以安全处理信号的状态时。通过这种方式,pending位图确保了信号的可靠传递和处理,防止了信号的丢失或重复处理。
在更深入地理解pending位图时,还需要考虑其他与信号相关的PCB属性,如block位图(表示哪些信号不应该被递达,直到解除阻塞)和信号屏蔽字(handle函数指针数组,用于定义每个信号的处理方式)。这些属性与pending位图一起,共同构成了Linux中进程信号处理的复杂而精细的机制。
总的来说,pending位图是Linux进程控制块中用于跟踪和管理已接收但尚未处理信号的重要数据结构,它确保了信号的可靠性和高效性。
Pending信号集:每个进程都有一个pending位图,用于记录当前已经被该进程接收但尚未处理(递达)的信号(未决信号)。当一个信号被接收时,对应的位会被设置为1,表示该信号已产生处于未决状态,直到信号递达(处理)才清除该标志。进程可以通过检查pending位图来确定是否有已产生未处理的信号。在Linux中,可以使用sigpending函数来获取当前进程的pending位图。
Block信号屏蔽字:每个进程都有一个block位图,用于指定当前被阻塞的信号。当一个信号被阻塞时,对应的位会被设置为1,表示该信号被阻塞,暂时不会被递达。进程可以通过设置block位图来控制哪些信号被阻塞,以避免进程在关键时刻被中断。在Linux中,可以使用sigprocmask函数来设置和获取当前进程的block位图。
Handler函数指针数组:每个进程都有一个handler函数指针数组,用于管理信号处理函数。该数组的索引对应于信号的编号,数组的元素是函数指针,指向相应信号的处理函数。当进程接收到一个信号时,会根据信号的编号在handler函数指针数组中查找对应的处理函数,并调用该函数来处理信号。在Linux中,可以使用signal和sigaction函数来设置和获取信号处理函数。
之前我们的文章中signal函数自定义处理信号的原理:将信号处理函数的指针填入到handler数组对应信号编号的位置。
pending位图、block位图和handler函数指针数组是针对每个进程而言的,每个进程都有自己独立的位图和数组。这样可以实现不同进程对信号的独立管理和处理。
3.初识sigset_t
- 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
- 未决和阻塞标志可以用相同的数据类型sigset_t来表示 sigset_t称为信号集 这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
- 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
什么是sigset_t?
sigset_t 是 Linux 系统中用于表示信号集的数据类型。信号集是一组信号的集合,这些信号可以是未决信号(即已经发送但尚未处理的信号)或阻塞信号(即当前被阻塞不处理的信号)。sigset_t 可以看作是一个由二进制位组成的大整数,每一位对应一个信号,某一位为 1 表示信号集中包含该信号,为 0 则表示不包含。
在编程中,可以使用多种函数来操作 sigset_t 类型的信号集。例如,sigemptyset() 函数用于创建一个空的信号集,sigaddset() 函数用于向信号集中添加信号,sigdelset() 函数用于从信号集中删除信号。
在多线程编程中,pthread_sigmask() 函数特别有用,它允许你在主线程中控制信号掩码。这个函数接受三个参数:一个操作方式(SIG_BLOCK、SIG_UNBLOCK 或 SIG_SETMASK),一个指向信号集的指针(表示要设置或修改的信号集),以及一个可选的指向旧信号集的指针(用于保存之前的信号集状态)。
需要注意的是,直接修改 sigset_t 类型的内部数据可能并不安全或有效,通常建议使用专门的函数来操作信号集。同时,处理信号和信号集时应当小心谨慎,以避免潜在的竞态条件或错误。
总的来说,sigset_t 是 Linux 系统中用于处理信号的重要数据类型,它提供了一种方便的方式来表示和操作信号集。通过相关的函数,你可以轻松地创建、修改和查询信号集,以满足不同的编程需求。
计算机常识
- 语言会提供.h,.hpp 和自定义类型
- OS也会提供.h,和 OS自定义的类型(为了让用户能够用接口,接口的参数又不能是语言里的类型,或者语言里没有这种类型)
- 语言的一些库函数如读写函数封装了系统接口,那么读写函数的头文件中有可能就包含了系统的头文件
理解sigset_t
sigset_t:user是可以直接使用该类型 与 用内置类型 && 自定义类型 没有任何差别
4.信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
sigset_t:不允许用户自己进行位操作,OS提供了对应的操作位图的方法
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
- 在使用sigset_ t类型的变量之前,要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
- 初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
- 这四个函数都是成功返回0,出错返回-1。
sigset_t:一定需要对应的系统接口来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象 接下来的4.1/4.2即为系统接口
4.1sigpending
OS通过这个接口将set给用户
sigpending 是一个在 Unix 和 Linux 系统中用于获取当前进程挂起信号集的函数。挂起信号集是指那些已经发送到进程,但尚未被处理的信号。
函数的原型如下:
c
int sigpending(sigset_t *set);
参数说明:
set:指向一个 sigset_t 类型的变量,函数会将当前进程的挂起信号集存储在这个变量中。
返回值:
如果成功,则返回 0。
如果失败,则返回 -1,并设置 errno 以指示错误。
sigpending 函数允许进程查询哪些信号当前处于挂起状态,即哪些信号已经发送给进程但尚未被处理。这对于进程来说是非常有用的信息,因为它可以根据这些信息来决定如何响应这些挂起信号,或者进行其他相关操作。
注意,sigpending 函数返回的是挂起信号集,而不是当前进程的信号屏蔽字。挂起信号集是那些已经发送但尚未处理的信号,而信号屏蔽字则决定了哪些信号当前被进程阻塞。因此,即使某个信号在挂起信号集中,如果它也在信号屏蔽字中,那么进程也不会立即响应它。
使用 sigpending 时,通常需要与 sigprocmask 等函数结合使用,以便进程能够更全面地控制和处理信号。
4.2sigprocmask
sigprocmask 是一个在 Unix 和 Linux 系统中用于检查和/或更改当前进程的信号屏蔽字的函数。信号屏蔽字是一个位掩码,用于控制哪些信号当前应该被进程阻塞(即忽略)。
函数的原型如下:
c
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
how:指定如何修改当前的信号屏蔽字。它可以是以下三个值之一:
SIG_BLOCK:将 set 所指向的信号集中的信号添加到当前的信号屏蔽字中。mask |= set
SIG_UNBLOCK:从当前的信号屏蔽字中移除 set 所指向的信号集中的信号。mask = mask &~set
SIG_SETMASK:将当前的信号屏蔽字设置为 set 所指向的信号集。mask = set
set:指向一个 sigset_t 类型的变量,该变量包含了一组信号,这些信号将根据 how 参数的值被添加到、从或从当前信号屏蔽字中移除。
oldset:如果此参数不是 NULL,则函数会将调用前的信号屏蔽字存储在 oldset 所指向的 sigset_t 变量中。
返回值:
如果成功,则返回 0。
如果失败,则返回 -1,并设置 errno 以指示错误。
sigprocmask 函数允许进程精细地控制哪些信号应被阻塞。这对于实现多线程程序中的同步机制,或者在执行某些不能被信号中断的临界区代码时特别有用。通过阻塞某些信号,进程可以确保在关键代码段执行期间不会被这些信号中断。
注意,sigprocmask 修改的是调用进程的信号屏蔽字,因此它只对调用它的进程有效。此外,阻塞的信号仍然可以被进程接收,只是它们不会中断进程的执行,直到信号屏蔽字被修改以允许这些信号为止。
4.2代码测试
1.测试1
如果我们对所有的信号都进行了自定义捕捉 我们是不是就写了一个不会被异常或者用户杀掉的进程? 并不是,OS的设计者也考虑了
9号信号不可被捕获 ⇒ 管理员信号
2.测试2
如果我们将2号信号block,并且不断的获取并打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们就应该肉眼看到pending信号集中,有一个比特位0->1
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void showPending(sigset_t &pending)
{
for (int sig = 31; sig >=1; sig--)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
// 1. 定义信号集对象
sigset_t set, oldset, pending;
// 2. 初始化
sigemptyset(&set);
sigemptyset(&oldset);
sigemptyset(&pending);
// 3. 添加要进行屏蔽的信号
sigaddset(&set, 2 /*SIGINT*/);
// 4. 将set添加到当前的信号屏蔽字中(内核的进程内部)
//[默认情况进程不会对任何信号进行block]
int n = sigprocmask(SIG_BLOCK, &set, &oldset);
assert(n == 0);
(void)n;
std::cout << "block 2 signal success...., pid: " << getpid() << std::endl;
// 5. 重复打印当前进程的pending信号集
while (true)
{
// 5.1 获取当前进程的pending信号集
sigpending(&pending);
// 5.2 显示pending信号集中信号状态
showPending(pending);
sleep(1);
}
return 0;
}
- 如果解除2号信号的阻塞 会发生什么?
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void handler(int signum)
{
std::cout << "捕获信号:" << signum << std::endl;
// exit(1); 不终止进程
}
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
// 0. 测试 捕捉2号信号 不让其执行默认终止动作
signal(2, handler);
// 1. 定义信号集对象
sigset_t set, oldset;
sigset_t pending;
// 2. 初始化
sigemptyset(&set);
sigemptyset(&oldset);
sigemptyset(&pending);
// 3. 添加要进行屏蔽的信号
sigaddset(&set, 2 /*SIGINT*/);
// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
int n = sigprocmask(SIG_BLOCK, &set, &oldset);
assert(n == 0);
(void)n;
std::cout << "block 2 号信号成功...., pid: " << getpid() << std::endl;
// 5. 重复打印当前进程的pending信号集
int count = 0;
while (true)
{
// 5.1 获取当前进程的pending信号集
sigpending(&pending);
// 5.2 显示pending信号集中的没有被递达的信号
showPending(pending);
sleep(1);
count++;
if (count == 20)
{
// 默认情况下 解除2号信号的阻塞 确实会进行递达
// 2号信号的默认处理动作是终止进程 需要对2号信号进行捕捉
std::cout << "Unblock signal 2" << std::endl;
int n = sigprocmask(SIG_SETMASK, &oldset, nullptr);
assert(n == 0);
(void)n;
}
}
return 0;
}
上面0和1的顺序是可以通过输出格式控制的逆序遍历 ⇒ 正向输出
我们可以获取sigpending 貌似没有一一个接口用来设置pending位图(所有的信号发送方式,都是修改pending位图的过程)
3.测试3
如果我们对所有的信号都进行block 我们是不是就写了一个不会被异常或者用户杀掉的进程
监测指令
i=1; id=$(pidof mysignal);\
> while [ $i -le 31 ]; do echo "i: $i, id: $id";\
> let i++; sleep 1; done
i=1; id=$(pidof mysignal);\
> while [ $i -le 31 ]; do kill -$i $id; \
> echo "sending signal $i";\
> let i++; sleep 1; done
9号信号也不可以被屏蔽/阻塞
SIGKILL(9)信号 和 SIGSTOP(19)信号 不能被捕捉,也不能被阻塞。20号信号默认动作是忽略(猜测)
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void showPending(sigset_t &pending)
{
for (int sig = 31; sig >= 1; sig--)
{
if (sigismember(&pending, sig))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
}
static void blockSig(int sig)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, sig);
int n = sigprocmask(SIG_BLOCK, &set, nullptr);
assert(n == 0);
(void)n;
}
int main()
{
for(int sig = 1; sig <= 31; sig++)
{
blockSig(sig);
}
sigset_t pending;
while(true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
4.3bash 脚本文件
#!/bin/bash
i=1
id=$(pidof mysignal)
while [ $i -le 31 ]
do
if [ $i -eq 9 ];then
let i++
continue
fi
if [ $i -eq 19 ];then
let i++
continue
fi
kill -$i $id
echo "kill -$i $id"
let i++
sleep 1
done
#!/bin/bash
这一行称为 shebang 或 hashbang。它告诉操作系统该脚本应该使用哪个解释器来执行。在这个例子中,它指定了使用 /bin/bash,也就是 Bourne Again SHell 的路径,这是许多 Linux 和 Unix 系统中的默认 shell。当你尝试运行这个脚本时(例如,通过输入 ./scriptname.sh),操作系统会查看脚本的第一行,并使用 /bin/bash 来解释和执行脚本的内容。
2. if … then … fi
这是 Bash 中的条件语句结构。它用于根据某个条件是否满足来执行不同的代码块。
if [ 条件 ]
:检查后面的条件是否满足。如果满足,则执行then
后面的代码块。then
:表示如果前面的条件满足,则执行接下来的代码块。fi
:表示if
语句的结束。
在你的脚本中,有两个 if 语句,分别检查 $i 是否等于 9 或 19。如果等于这两个值,脚本会跳过发送信号的部分,并直接继续到下一个循环迭代。
- then
如上所述,then 关键字用于 if 语句中,表示如果前面的条件满足,则执行 then 后面的代码块。
为了更清楚地理解,让我们看一个简化的例子:
bash
#!/bin/bash
number=5
if [ $number -eq 5 ]; then
echo "The number is 5."
fi
在这个例子中,脚本会检查变量 number 是否等于 5。如果是,它会输出 “The number is 5.”。fi 表示 if 语句的结束。