01. 信号产生
生活中的信号类比(交通信号灯、警报),当产生这些信号时,我们会立马想到对应的动作。在Linux中,信号是事件发生对进程的通知机制亦称软件中断,由操作系统内核、进程本身或者其他进程向目标进程异步事件发送机制(即收到某种信号,并不会立马去执行)。通知进程发生某种预定义的时间,要求进程作出相应响应。信号与硬件中断相似之处在于会打断程序执行流程。
1.1 常见信号
- 硬件异常:
- 如内存越界(
SIGSEGV
) - 除零错误(
SIGFPE
) - 由内核自动生成
- 如内存越界(
- 系统调用:
kill()
:向指定进程发送信号raise()
:进程向自身发送信号alarm()
:设置定时器,超时后发送SIGALRM
- 终端输入:
Ctrl+C
:SIGINT
(终止进程)Ctrl+Z
:SIGTSTP
(暂停进程)Ctrl+\
:
- 软件条件:
- 子进程退出时,父进程收到
SIGCHLD
- 定时器到期(如
alarm()
)触发信号
- 子进程退出时,父进程收到
- 核心转储 :
1.2 信号的分类
信号分为两大类。(编号1-31)为传统信号信号,内核向进程通知且递送一次,(编号34-64)为实时信号使信号按序递送。
1.3 信号处理方式
- 默认动作: 部分是终止自己,暂停等
- 忽略动作: 是一种信号处理的方式,只不过动作就是什么都不干
- 自定义动作: 使用signal方法修改信号的处理动作。(即用户程序员编写的函数:将默认动作转化为自定义动作)
1.4 改变信号处理方式
signal,sigaction
02. 信号阻塞
2.1 概念悉知
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号产生后会稍后递达给某进程,信号在产生和到达期间会一直处于pending等待状态。
2.2 信号在内核中的表示
每个进程都有一个信号屏蔽字亦称阻塞信号集,一个 sigset_t
类型的位图,每一位对应一个信号(如 SIGINT
、SIGQUIT
等)。用来标记哪些信号当前被阻塞。被阻塞的信号若已产生,会进入未决状态,直到阻塞被解除才会触发处理。位为1生效,0不生效。
2.2.1 block与pending之间联系
pending集是信号产生后的暂存区,block是控制信号是否能从pending暂存区递交给进程的开关
- block:决定哪些信号会被阻塞,近而将其放入未决信号集
- pending:记录已产生但未被处理的信号(因被阻塞或正在处理其他信号)
- 通过
sigpending()
査询挂起的信号集,通过sigprocmask()
控制阻塞状态。
其中block
位图用于表示进程是否阻塞。pending
位图用于是否有信号写入(信号是否产生,信号产生时,内核在进程控制块中设置该信号的pending
位图,直到信号抵达才消失)。handler
函数指针数组用于进程执行何种动作(其中存放的是动作函数指针,每个信号的编号就是其数组下标)。
2.3 关键系统调用
2.3.1 信号集操作函数
sigset_t 本质上是一个位图,每一位代表一个信号。当某一位被设置为 1 时,表示对应的信号被包含在信号集中;为 0 则表示不包含。使用者只能调用以下函数来操作sigset_t
变量,不用关注内部数据。
在使用sigset_t
类型的变量之前,一定要调用sigemptyset()
函数初始化一个未包含任何成员的信号集或者sigfillset()
函数则初始化一个信号集,使其包含所有信号(包括所有实时信号)。
#include <signal.h>
// 清空信号集,置0
int sigemptyset(sigset_t *set);
// 填充所有信号, 置有效状态1?
int sigfillset(sigset_t *set);
信号集初始化后,可以分别使用 sigaddset()
和sigdelset()
函数向一个集合中添加或者移除单个信号。使用sigismember()
测试信号是否是信号集set
的成员。
#include <signal.h>
// 添加单个信号
int sigaddset(sigset_t *set, int signo);
// 删除单个信号
int sigdelset(sigset_t *set, int signo);
// 判断信号是否存在
int sigismember(const sigset_t *set, int signo);
2.3.2 信号掩码(阻塞信号传递)
内核会为每个进程维护一个信号掩码(一组信号),阻塞其针对该进程的传递。如果将被阻塞的信号发送给某进程,那么对该信号的传递将延后,直至从进程信号掩码中移除该信号,从而解除阻塞为止。阻塞信号集内0
变1
只能由其触发。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数 :
how
:指定了sigprocmask()函数想给信号掩码带来的变化。SIG_BLOCK
:set包含了我们希望添加到当前信号屏蔽字的信号(逻辑或)SIG_UNBLOCK
:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号(与非)SIG_SETMASK
:设置当前信号屏蔽字为set所指向的值(直接赋值)
set
- 要操作的信号集,(为NULL则忽略)
oldset
- 保存旧的信号掩码(可为NULL)
返回值:成功返回 0,失败返回 -1(设置 errno
)
- 阻塞:信号暂存到未决信号集直到解除阻塞
- 忽略:直接丢弃
操作示例:
void handler(int no){
cout << "execcute user-defined actions" << " ";
}
int main(){
//更改2号信号执行动作
signal(2, handler);
//创建两个信号集
sigset_t set, old_set;
//清空信号集
sigemptyset(&set);
sigaddset(&set, SIGINT);//将2号信号添加进set集01000...000
//将set信号集中指定信号添加到当前阻塞信号集中,同时将原来的信号集保存到old_set中
sigprocmask(SIG_BLOCK, &set, &old_set);//阻塞set中包含的信号集
cout << "SIG_INT is blocked, ctrl+c has failed" << endl;
sleep(5);//5秒内ctrl+c不会有反应
//使用old_set替换当前所有阻塞信号集
sigprocmask(SIG_SETMASK, &old_set, NULL);//恢复之前阻塞信号集
cout << "SIG_INT has been resloved" << endl;
sleep(5);
return 0;
}
阻塞期间发送2号信号?
信号不会立马处理,而是将该信号加入pending信号集
(即pending位图
的SIG_INT
位从0-->1
)
信号阻塞接触后?
pending信号集
内该信号立即处理(处理方式鉴于默认动作,忽略(丢弃) 或 自定义动作),该位由1-->0
。
2.3.3 sigpending获取未决信号集
如果某进程接受了一个该进程正在阻塞的信号,那么会将该信号填加到进程的等待信号集中。当之后解除了对该信号的锁定时,会随之将信号传递给此进程。
#include <signal.h>
int sigpending(sigset_t *set);
系统调用返回后,用户空间的set变量
包含了当前的未决信号集,可以使用sigismember()
检查特定信号是否在未决信号集中。
作用: 返回处于等待状态的信号集,并将其置于 set
指向的sigset_t
结构中。
操作 | 是否进入未决? | 后续动作 |
---|---|---|
信号被阻塞,且期间被发送 | 是 | 该信号记录在未决信号集中,直到阻塞解除后被处理 |
信号被阻塞,期间未被发送 | 否 | 不记录该信号,解除阻塞后若收到信号则直接处理 |
信号未被阻塞,但被发送 | 否 | 信号直接调用对应函数即可 |
2.3.4 signal() - 设置信号处理
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
返回函数指针的函数:
代码示例:
void handler(int signo){
cout << "execcute user-defined actions" << " ";
}
...
//更改2号信号执行动作
signal(2, handler);
声明该函数类型是一个函数指针类型,signal
是函数名(如:int (*p)(int,int)
,p
是函数名,有两个int
类型参数,返回值是一个int
类型)。signal
返回的是一个函数指针,该函数指向的函数参数是一个int类型,返回值是void
。所以外层是指明这是一个函数指针类型。里面才是使用的函数本体。
signal
是一个函数- 它接受一个整数和一个函数指针作为参数
- 它返回一个与第二个参数类型相同的函数指针
返回函数指针的数组: 对?
2.3.5 sigaction() - 设置信号处理
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
signum
: 目标信号 -
act
新的信号处理方式sa_handler
:信号处理函数指针(或SIG_IGN
、SIG_DFL
)。sa_mask
:处理该信号时额外阻塞的信号集。sa_flags
:控制选项(如SA_RESTART
、SA_NODEFER
)NULL
-
oldact
- 保存旧的处理配置(可为
NULL
)
- 保存旧的处理配置(可为
返回值:成功返回 0,失败返回 -1(设置 errno
)
struct sigaction {
void (*sa_handler)(int); // 简单处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理函数
sigset_t sa_mask; // 处理期间屏蔽的信号集
int sa_flags; // 控制标志
void (*sa_restorer)(void); // 内部使用(已废弃)
};
现在我们使用sigprocmask
函数和sigpending
函数完成一个函数。思路是
- 对2号信号写入信号集
- 将该信号集使用
sigprocmask
函数将2号
信号block住 - 我们键盘输入
ctrl+c
,产生2号
信号,发送至该进程。 - 使用
sigpending
函数输出pending位图,
因为我们将2号
进程block
住了,该信号并不能抵达。我们再向其发送2号
信号,信号被block住了并不会影响信号的写入。因此我们再发送2号
进程之前pending
位图应该全为0
,发送2号
进程之后会看见pending
位图发送由0
至1
的变化。
执行流程:
- 前 5 秒内,
SIGINT
(Ctrl+C)被阻塞,信号进入未决状态但不触发处理。 - 5 秒后解除阻塞,若之前有未决的
SIGINT
,会立即触发handle
函数。
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;
static void handler(int signo){
cout << signo << " 号信号确实递达了" << endl;
//最终不退出进程
}
void DisplayPending(const sigset_t pending){
// 打印 pending 表
int i = 1;
while (i < 32)
{
if (sigismember(&pending, i)) cout << "1";
else cout << "0";
i++;
} cout << endl;
}
int main(){
// 更改 2 号信号的执行动作
signal(2, handler);
// 创建信号集
sigset_t set, oset;
// 信号集清空 0
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set, 2); //将2号信号写入set
// 设置当前进程的屏蔽信号集
sigprocmask(SIG_BLOCK, &set, &oset);//0给进程
// 死循环
int n = 0;
while (true){
if (n == 5){
// 采用 SIG_SETMASK 的方式,覆盖进程的 block 表
sigprocmask(SIG_SETMASK, &oset, NULL); // 不接收进程的 block 表
}
// 获取进程的 未决信号集
sigset_t pending;
sigemptyset(&pending);
int ret = sigpending(&pending);
assert(ret == 0);
(void)ret; // 避免 release 模式中出错
DisplayPending(pending);
n++;
sleep(1);
}
return 0;
}
03. 信号处理
3.1进程地址空间
为了进程可以通过访问虚拟地址来间接访问物理资源,我们需要建立虚拟地址空间与物理内存的映射关系。用户区和内核区都需要通过内核级页表或者用户级页表进行映射使用。
系统调用:
系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。以应用程序编程接口(API
)的形式,内核提供有一系列服务供程序访问。
3.2信号处理过程
用户态的时候执行用户代码,在中断/异常/系统调用的时候切换到内核态,査看进程的三张表。(信号捕捉过程)当需要执行自定义动作的时候,切回用户态执行自定义动作,自定动作完成之后,再切到内核态执行sys_sigretur()
函数,最后再切回用户态中用户代码的下一行继续执行。当执行默认动作或者忽略动作的时候,直接可以在内核中完成,完成之后切回用户态中的用户代码的下一行,继续执行。
sigaction()
volatile关键字
SIGCHLD