信号
- 在Linux中什么是信号
- 信号的产生方式
- 硬件产生的信号
- 软件产生的信号
- 异常产生的信号
- 进程对信号的处理
- 信号的保存
- 信号方法更改函数signal
- 信号处理的更改
- 恢复默认
- 信号忽略
- 信号的管理
- 信号集 sigset_t
- 对信号集的操作
- 信号的捕捉过程
在Linux中什么是信号
在 Linux 系统中,信号是一种进程间通信的基本机制,用于通知进程发生了某种事件。
信号是一种软件中断,用于通知进程发生了某种事件。
这些事件可能包括硬件异常、用户输入、系统调用请求等。信号是异步发生的,即进程无法预测信号何时发生,但当信号发生时,系统会将信号发送给相应的进程。
在Linux中,信号一般被分为三大类:可以在bash使用kill -l 命令直接产看
-
标准信号(Standard Signals):由内核或进程向进程发送的信号,上图中的1-31号新号即为标准信号
-
实时信号(Real-time Signals):在标准信号的基础上引入了实时性概念,允许信号排队和按优先级传递,上图中的34-64即为实时信号
-
自定义信号(User-defined Signals):用户可以定义自己的信号类型,用于特定应用或通信需求。
信号的产生方式
无论信号有多少种产生方式,永远只能让OS向目标进程发送
硬件产生的信号
硬件产生的信号是由硬件设备或操作系统内核生成的,用于通知进程发生了某种事件。例如,当我们按下键盘上的^C键时,硬件会生成中断信号(SIGINT)来通知操作系统,然后操作系统将其传递给相应的进程
软件产生的信号
软件产生的信号是由进程自身或其他进程通过系统调用(如 kill )发送给目标进程的。这种方式允许进程之间进行通信和协作,例如向目标进程发送中断信号(SIGINT)以请求其终止执行:kill -9 pidnum
异常产生的信号
异常产生的信号是由硬件或操作系统检测到的异常事件引起的。例如,当进程执行非法指令、访问越界内存或发生浮点数异常时,硬件或操作系统会生成相应的信号(如 SIGILL、SIGSEGV、SIGFPE)来通知进程发生了异常情况。
进程对信号的处理
信号的保存
每个进程的PCB中都有一张自己的函数指针数组,一般被称为信号处理函数表,这个数组中的每个元素对应一个特定信号的处理函数。
当进程接收到信号时,操作系统会根据进程的PCB中的信号处理函数表找到对应的处理函数,并执行相应的逻辑。
以下是对信号处理的几个概念:
- 信号递达:实际信号的处理动作,一般有三种:默认,忽略,自定义
- 信号未决(pending):信号从产生到递达之间的状态,即在信号的位图中时,一斤产生但未被处理时的状态
- 信号阻塞(block):信号时允许被阻塞的,信号产生了,但暂时不进行递达的信号就是阻塞信号
因此操作系统会在进程PCB中创建三个表,用于对信号的状态进行记录,
其中,block表记录的是对应位置信号是否被屏蔽
pending表记录的是对应位置信号是否未决
handler表记录的事对应位置信号的执行方式
信号方法更改函数signal
信号处理的更改
在Linux中,信号处理函数 (signal) 是一种用来处理异步事件的方法。
信号是一个软件中断,通常由操作系统生成,用来通知程序某个事件已经发生,例如:非法操作、外部中断、定时器溢出等。
原型如下:
void (*signal(int signum, void (*handler)(int)))(int);
两个参数:
- signum:表示要设置的信号编号
- handler:表示要设置的信号处理函数,可以是一个函数指针,一般来说是我们自己写的一个函数,将signum所表示的信号重新实现
注意事项
- 可重入性:信号处理函数应该是可重入的,即它们应该避免使用全局状态和执行非原子操作。
- 限制:在信号处理函数中,只有少数几个函数是安全可调用的(通常称为异步信全函数)。例如,大多数系统调用和库函数都不应该在信号处理函数中调用。
举例:这是一个将2号信号进行更改,原本应该执行的功能被我修改成了handler函数内的内容
#include<iostream>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
std::cout<< "获得一个" << signo << "号信号" << std::endl;
exit(1);
}
int main()
{
signal(2,handler);
return 0;
}
恢复默认
如果在开发过程中,忘记了自己之前对某个信号的执行更改,可以将signal函数的第二个参数传入宏:SIG_DFL来使得信号执行它默认的功能
signal(2,SIG_DFL);
信号忽略
在程序开发中,如果想要忽略某个信号,可以直接将signal函数的第二个参数传参宏:SIG_IGN
signal(2,SIG_IGN);
信号的管理
为了高效地处理多个信号,因此要先描述,再组织 , Linux提供了信号集的概念,使得可以将多个信号组合在一起进行处理。
信号集 sigset_t
sigset_t是一个用于表示信号集的数据类型,它通常定义在<signal.h>头文件中。
原型:
typedef struct sigset_t {
unsigned long sig[_NSIG / sizeof(long)];
} sigset_t;
其中,_NSIG是一个宏,表示系统中定义的信号总数。sigset_t类型的信号集用于保存一个或多个信号的集合,可以通过位运算来操控信号集。
对信号集的操作
- 要创建一个空的信号集,可以使用以下代码:
sigset_t empty_set;
sigemptyset(&empty_set);
这样就有了一个新的信号集,这将清除empty_set中的所有信号位,使其成为一个空集。
- 创建一个全满的信号集:
sigset_t full_set;
sigfillset(&full_set);
这样就可以创建一个全满的信号集,并且将full_set中的所有信号位设置为1,表示设置了所有可能的信号。
- 设置或查询进程的信号前景,就好像设置权限掩码一样,对信号这些信号在进程处于等待状态(如在 select、poll、epoll_wait 等系统调用中)时会被优先处理:
void sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
其中,how参数表示拷贝的方式,可以是SIG_BLOCK、SIG_UNBLOCK或SIG_SETMASK。set参数指向要拷贝的信号集,oldset参数用于接收旧的信号集。
- 添加信号集:
void sigaddset(sigset_t *set, int signum);
该函数将signum信号的位添加到set信号集中。
- 清除信号集:
void sigdelset(sigset_t *set, int signum);
该函数将signum信号的位从set信号集中删除。
- 检查信号是否存在于信号集中:
int sigismember(const sigset_t *set, int signum);
返回 1:如果指定的信号 signum 在信号集 set 中。
返回 0:如果指定的信号 signum 不在信号集 set 中。
返回 -1:如果发生错误(例如指定的信号编号无效),并且设置全局变量 errno
- 查询当前进程等待处理的信号集合,也就是查询处于pending状态的信号集合.这个函数可以帮助进程了解有哪些信号已经被生成但尚未被处理。
int sigpending(sigset_t *set);
当 sigpending 函数被调用时,它会将当前进程的信号等待集合(即那些已经被生成但尚未被处理或阻塞的信号)复制到 set 指向的 sigset_t 变量中。这样,进程就可以通过检查 set 中的信号位来确定有哪些信号需要处理。
下面是一个代码示例,我们首先使用 sigprocmask 函数屏蔽了 SIGINT 信号。然后,我们让进程睡眠5秒钟,以便有足够的时间让用户按下 Ctrl+C。在睡眠之后,我们调用 sigpending 函数来查询当前进程等待的信号。如果 SIGINT 信号在等待队列中,我们将打印出相应的信息。最后,我们使用 sigprocmask 函数恢复到旧的信号掩码,以便进程可以正常处理信号。
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
int main() {
sigset_t pending_mask, empty_mask, old_mask;
// 创建一个信号集合,用于存储 `SIGINT`
sigemptyset(&empty_mask);
sigaddset(&empty_mask, SIGINT); // 添加 SIGINT 到屏蔽集合
// 阻止 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &empty_mask, &old_mask) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT 信号已被屏蔽,现在我将进入睡眠状态。\n");
// 睡眠一段时间,以便有机会生成 SIGINT 信号
sleep(5);
// 查询当前进程等待的信号
if (sigpending(&pending_mask) == -1) {
perror("sigpending");
return 1;
}
// 打印等待的信号
if (sigismember(&pending_mask, SIGINT)) {
printf("SIGINT 信号正在等待处理。\n");
} else {
printf("没有信号在等待处理。\n");
}
// 恢复旧的信号掩码
if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
perror("sigprocmask");
return 1;
}
return 0;
}
信号的捕捉过程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
但是自定义信号处理函数是我们自己写的,是存在于用户空间的,因此这里涉及到一个用户到内核的转换问题,因为用户并不具有所有的操作权限,下面的图片可以很清楚的表达出信号捕捉过程中用户和内核态的转化过程
图片来自必应搜索