Linux 下信号的保存和处理
信号的几个状态
- 信号抵达: 当接收到的信号被处理时, 此时就成为信号的抵达
- 信号的未决: 从信号的产生到信号抵达这个时间段之间, 称为信号未决
- 信号阻塞: 当进程设置了某个信号为阻塞后, 这个进程就不会在接收到这个信号
- 信号忽略: 将信号设置为忽略后, 接收到这个信号, 对这个信号的处理就是忽略
阻塞和忽略:
阻塞是进程没有收到对应的信号, 所以没有任何处理.
忽略接收到了信号, 但是对这个信号的处理就是不做任何处理, 进行忽视
虽然两者的行为是一样的, 没有任何处理, 但是本质还是有差别的
信号的保存
进程要对对接收到的信号处理, 那么就需要存在一块空间来存储接收到的信号.
在进程 PCB 中会存在三张表
1. block 表
block 表实际上是一个位图, 每一个比特位都对应一个信号.
信号对应比特位上要么是0, 要么是1.
1则代表在这个进程中, 这个比特位对应的信号设置为阻塞. 进程不接受此信号.
所以这张表也称为信号屏蔽字
2. pending 表
pending 表也是一个位图, 用来记录进程是否接收到了比特位对应的信号.
比特位为0表示没有接收到了对应位的信号. 为1表示接收到对应的信号.
这张表也可以称为未决表, 因为其中记录的信号都还未处理.
3. handler 表
handler 表和前两个表不同, handler 表是一个数组. 准确来说是一个函数指针数组.
这个表中记录着对应信号的处理函数地址. 当接收到信号时就会去调用对应的处理函数.
在上面的信号状态中, 有一个信号忽略, 实际上就是将 handler 表中的对应信号的函数指针设置为"SIG_IGN". SIG_IGN 是一个宏, 本质还是一个函数指针.
信号的一些操作
上面讲了信号存在阻塞状态, 还存在忽略状态. 除了系统默认的初始配置, 我们也可以手动进行设置.
1. 阻塞信号的设置
上面了解到阻塞表 (block) 是一个位图. 所以需要先来了解一下位图的使用.
sigset_t 类型就是位图
int sigemptyset(sigset_t *set) // 将每一个比特位都设置为 0
int sigfillset(sigset_t *set); // 将每一个比特位都设置为 1
int sigaddset(sigset_t *set, int signum); // 将第 signum 比特位设置为 1
int sigdelset(sigset_t *set, int signum); // 将第 signum 比特位设置为 0
int sigismember(const sigset_t *set, int signum); // 判断第 signum 比特位是0还是1并返回
当设置好了位图后, 使用函数 sigprocmask 来设置阻塞表
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
:指定如何修改信号阻塞集,可以取以下值:
SIG_BLOCK
:将set
中的信号添加到当前信号阻塞集中。SIG_UNBLOCK
:将set
中的信号从当前信号阻塞集中删除。SIG_SETMASK
:将当前信号阻塞集设置为set
。set
:指向sigset_t
类型的指针,表示要修改的信号集。oldset
:指向sigset_t
类型的指针,用于存储修改前的信号阻塞集。如果不需要,可以传NULL
。返回值:
- 成功时返回 0。
- 失败时返回 -1.
int main()
{
sigset_t myset;
sigemptyset(&myset);
sigaddset(&myset, 1);
sigprocmask(SIG_BLOCK, &myset, NULL);
// 将本进程的一号信号设置为阻塞
return 0;
}
2. 信号处理函数设置
接下来我们来设置对于信号的处理函数.
我们需要使用 signal 函数.
signum: 要设置几号信号的处理函数, 可以使用数字, 也可以使用信号名如: SIGINT
SIG_DFL
:恢复信号的默认处理行为。SIG_IGN
:忽略信号。- 自定义处理函数:用户定义的处理函数指针。
handler: 接收到信号时, 要调用的处理函数
void func(int signum) // 参数不能省略
{
cout << "接收到信号: " << signum << endl;
}
int main()
{
signal(1, func);
// 当进程接收到 1 号信号时, 就会调用函数 func, 进程就会打印信息
return 0;
}
信号的处理时机
从内核态返回用户态时, 会进行信号的处理
内核态和用户态都是操作系统的一种运行状态.
用户态拥有的权限较低, 只能访问自己的地址空间, 不能直接访问内核空间.
内核态拥有最高的权限, 可以访问系统的任何资源, 执行所有操作.
区分用户态和内核态是对操作系统的一种保护方式, 可以防止用户程序对系统资源的非法访问. 保护系统的稳定和安全.
那么什么时候会发生用户态和内核态之间的转换
系统调用, 如: 使用 read, write 函数, 这些就是系统提供的接口, 当调用这些系统调用时, 就会发生用户态到内核态的转换, 当执行完系统调用后, 就会从内核态转换为用户态.
此时, 就会对进程接收到的信号处理.